summaryrefslogtreecommitdiffstats
path: root/browser/components
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components')
-rw-r--r--browser/components/BrowserContentHandler.sys.mjs51
-rw-r--r--browser/components/BrowserGlue.sys.mjs102
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_sessionRestore.js2
-rw-r--r--browser/components/aboutlogins/tests/browser/head.js2
-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.css106
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.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/templates/OnboardingMessage/WhatsNewMessage.schema.json73
-rw-r--r--browser/components/asrouter/content/asrouter-admin.bundle.js52
-rw-r--r--browser/components/asrouter/docs/targeting-attributes.md11
-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.mjs9
-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/MomentsPageHub.sys.mjs2
-rw-r--r--browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs100
-rw-r--r--browser/components/asrouter/modules/PanelTestProvider.sys.mjs128
-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_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/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/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.mjs129
-rw-r--r--browser/components/backup/content/debug.html46
-rw-r--r--browser/components/backup/content/debug.js59
-rw-r--r--browser/components/backup/docs/backup-resources.rst18
-rw-r--r--browser/components/backup/docs/index.rst1
-rw-r--r--browser/components/backup/jar.mn9
-rw-r--r--browser/components/backup/metrics.yaml276
-rw-r--r--browser/components/backup/moz.build10
-rw-r--r--browser/components/backup/resources/AddonsBackupResource.sys.mjs100
-rw-r--r--browser/components/backup/resources/BackupResource.sys.mjs83
-rw-r--r--browser/components/backup/resources/CookiesBackupResource.sys.mjs25
-rw-r--r--browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs53
-rw-r--r--browser/components/backup/resources/FormHistoryBackupResource.sys.mjs25
-rw-r--r--browser/components/backup/resources/MiscDataBackupResource.sys.mjs101
-rw-r--r--browser/components/backup/resources/PlacesBackupResource.sys.mjs91
-rw-r--r--browser/components/backup/resources/PreferencesBackupResource.sys.mjs98
-rw-r--r--browser/components/backup/resources/SessionStoreBackupResource.sys.mjs53
-rw-r--r--browser/components/backup/tests/xpcshell/head.js167
-rw-r--r--browser/components/backup/tests/xpcshell/test_BackupResource.js (renamed from browser/components/backup/tests/xpcshell/test_BrowserResource.js)26
-rw-r--r--browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js113
-rw-r--r--browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js226
-rw-r--r--browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js132
-rw-r--r--browser/components/backup/tests/xpcshell/test_createBackup.js74
-rw-r--r--browser/components/backup/tests/xpcshell/test_measurements.js547
-rw-r--r--browser/components/backup/tests/xpcshell/xpcshell.toml14
-rw-r--r--browser/components/contentanalysis/content/ContentAnalysis.sys.mjs99
-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.mjs4
-rw-r--r--browser/components/customizableui/CustomizeMode.sys.mjs20
-rw-r--r--browser/components/customizableui/content/panelUI.js43
-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_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_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_switch_to_customize_mode.js2
-rw-r--r--browser/components/customizableui/test/browser_synced_tabs_menu.js6
-rw-r--r--browser/components/customizableui/test/head.js12
-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/enterprisepolicies/Policies.sys.mjs91
-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.json16
-rw-r--r--browser/components/enterprisepolicies/tests/browser/browser.toml2
-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.js115
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js6
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js54
-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/parent/ext-browser.js8
-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-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.toml10
-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.js6
-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_onCommand.js14
-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_icons.js6
-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.js4
-rw-r--r--browser/components/firefoxview/HistoryController.mjs188
-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.mjs2
-rw-r--r--browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs17
-rw-r--r--browser/components/firefoxview/firefoxview.css19
-rw-r--r--browser/components/firefoxview/firefoxview.html12
-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-tab-list.css22
-rw-r--r--browser/components/firefoxview/fxview-tab-list.mjs689
-rw-r--r--browser/components/firefoxview/fxview-tab-row.css178
-rw-r--r--browser/components/firefoxview/helpers.mjs17
-rw-r--r--browser/components/firefoxview/history.css13
-rw-r--r--browser/components/firefoxview/history.mjs224
-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.mjs42
-rw-r--r--browser/components/firefoxview/recentlyclosed.mjs16
-rw-r--r--browser/components/firefoxview/syncedtabs.mjs387
-rw-r--r--browser/components/firefoxview/tests/browser/browser.toml8
-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_history_firefoxview.js15
-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.js7
-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.html5
-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/messagepreview.js14
-rw-r--r--browser/components/migration/.eslintrc.js5
-rw-r--r--browser/components/migration/FileMigrators.sys.mjs5
-rw-r--r--browser/components/migration/MigratorBase.sys.mjs10
-rw-r--r--browser/components/migration/content/migration-wizard.mjs9
-rw-r--r--browser/components/newtab/.eslintrc.js8
-rw-r--r--browser/components/newtab/common/Actions.mjs (renamed from browser/components/newtab/common/Actions.sys.mjs)10
-rw-r--r--browser/components/newtab/common/Reducers.sys.mjs15
-rw-r--r--browser/components/newtab/content-src/activity-stream.jsx5
-rw-r--r--browser/components/newtab/content-src/components/Base/Base.jsx150
-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.jsx15
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx4
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss4
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx11
-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.jsx11
-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/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/WallpapersSection/WallpapersSection.jsx100
-rw-r--r--browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss87
-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)2
-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.scss12
-rw-r--r--browser/components/newtab/css/activity-stream-linux.css150
-rw-r--r--browser/components/newtab/css/activity-stream-mac.css150
-rw-r--r--browser/components/newtab/css/activity-stream-windows.css150
-rw-r--r--browser/components/newtab/data/content/activity-stream.bundle.js942
-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.js22
-rw-r--r--browser/components/newtab/lib/AboutPreferences.sys.mjs2
-rw-r--r--browser/components/newtab/lib/ActivityStream.sys.mjs30
-rw-r--r--browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs2
-rw-r--r--browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs39
-rw-r--r--browser/components/newtab/lib/DownloadsManager.sys.mjs2
-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.mjs2
-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.mjs49
-rw-r--r--browser/components/newtab/lib/TopSitesFeed.sys.mjs2
-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/metrics.yaml31
-rw-r--r--browser/components/newtab/test/browser/browser_as_load_location.js2
-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.jsx2
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx5
-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.jsx22
-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.js5
-rw-r--r--browser/components/newtab/test/unit/lib/ActivityStream.test.js2
-rw-r--r--browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js7
-rw-r--r--browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js30
-rw-r--r--browser/components/newtab/test/unit/lib/DownloadsManager.test.js2
-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.js2
-rw-r--r--browser/components/newtab/test/xpcshell/test_TelemetryFeed.js56
-rw-r--r--browser/components/newtab/test/xpcshell/test_TopSitesFeed.js2
-rw-r--r--browser/components/newtab/test/xpcshell/test_WallpaperFeed.js115
-rw-r--r--browser/components/newtab/test/xpcshell/xpcshell.toml2
-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/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.js4
-rw-r--r--browser/components/places/tests/browser/browser_sidebarpanels_click.js8
-rw-r--r--browser/components/places/tests/browser/head.js2
-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/preferences.js26
-rw-r--r--browser/components/preferences/preferences.xhtml4
-rw-r--r--browser/components/preferences/privacy.inc.xhtml27
-rw-r--r--browser/components/preferences/privacy.js23
-rw-r--r--browser/components/preferences/sync.inc.xhtml39
-rw-r--r--browser/components/preferences/tests/browser.toml6
-rw-r--r--browser/components/preferences/tests/browser_applications_selection.js20
-rw-r--r--browser/components/preferences/tests/browser_contentblocking.js2
-rw-r--r--browser/components/preferences/tests/browser_privacy_dnsoverhttps.js162
-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/test/browser/browser_oa_private_browsing_window.js2
-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_ui.js2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js4
-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_dynamical_window_rounding.js2
-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_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.html2
-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.js6
-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.mjs151
-rw-r--r--browser/components/screenshots/ScreenshotsUtils.sys.mjs5
-rw-r--r--browser/components/screenshots/content/screenshots.css21
-rw-r--r--browser/components/screenshots/content/screenshots.html22
-rw-r--r--browser/components/screenshots/content/screenshots.js132
-rw-r--r--browser/components/screenshots/overlay/overlay.css41
-rw-r--r--browser/components/screenshots/screenshots-buttons.css3
-rw-r--r--browser/components/screenshots/screenshots-buttons.js31
-rw-r--r--browser/components/screenshots/tests/browser/browser.toml4
-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_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_test_toggle_pref.js2
-rw-r--r--browser/components/screenshots/tests/browser/browser_test_element_picker.js4
-rw-r--r--browser/components/screenshots/tests/browser/browser_test_selection_size_text.js86
-rw-r--r--browser/components/screenshots/tests/browser/head.js50
-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.mjs860
-rw-r--r--browser/components/search/metrics.yaml104
-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.toml23
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js15
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js13
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js35
-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.js141
-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.js12
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js60
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js21
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js19
-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.py89
-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/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.mjs4
-rw-r--r--browser/components/sessionstore/SessionFile.sys.mjs68
-rw-r--r--browser/components/sessionstore/SessionSaver.sys.mjs2
-rw-r--r--browser/components/sessionstore/SessionStartup.sys.mjs29
-rw-r--r--browser/components/sessionstore/SessionStore.sys.mjs415
-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/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.build4
-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/shell/HeadlessShell.sys.mjs2
-rw-r--r--browser/components/shell/ShellService.sys.mjs18
-rw-r--r--browser/components/shell/content/setDesktopBackground.js2
-rw-r--r--browser/components/shell/nsIWindowsShellService.idl12
-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/tests/browser/browser_exposure_telemetry.js2
-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.js744
-rw-r--r--browser/components/sidebar/jar.mn9
-rw-r--r--browser/components/sidebar/sidebar-history.html34
-rw-r--r--browser/components/sidebar/sidebar-history.mjs201
-rw-r--r--browser/components/sidebar/sidebar-launcher.css34
-rw-r--r--browser/components/sidebar/sidebar-launcher.mjs169
-rw-r--r--browser/components/sidebar/sidebar-page.mjs45
-rw-r--r--browser/components/sidebar/sidebar-syncedtabs.html45
-rw-r--r--browser/components/sidebar/sidebar-syncedtabs.mjs191
-rw-r--r--browser/components/sidebar/sidebar.css27
-rw-r--r--browser/components/sidebar/sidebar.ftl26
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs2
-rw-r--r--browser/components/storybook/.storybook/main.js9
-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.storybook.stories.md2
-rw-r--r--browser/components/storybook/stories/fxview-tab-list.stories.mjs2
-rw-r--r--browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs2
-rw-r--r--browser/components/syncedtabs/TabListView.sys.mjs2
-rw-r--r--browser/components/tabpreview/jar.mn2
-rw-r--r--browser/components/tabpreview/tab-preview-panel.mjs174
-rw-r--r--browser/components/tabpreview/tabpreview.css53
-rw-r--r--browser/components/tabpreview/tabpreview.mjs237
-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/touchbar/MacTouchBar.sys.mjs2
-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.js91
-rw-r--r--browser/components/translations/content/selectTranslationsPanel.inc.xhtml172
-rw-r--r--browser/components/translations/content/selectTranslationsPanel.js895
-rw-r--r--browser/components/translations/moz.build2
-rw-r--r--browser/components/translations/tests/browser/browser.toml38
-rw-r--r--browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js201
-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_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.js12
-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.js14
-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_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_open_to_idle_state.js61
-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.js71
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js71
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js71
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js71
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js97
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js98
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js86
-rw-r--r--browser/components/translations/tests/browser/head.js1036
-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_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/UrlbarController.sys.mjs121
-rw-r--r--browser/components/urlbar/UrlbarInput.sys.mjs29
-rw-r--r--browser/components/urlbar/UrlbarPrefs.sys.mjs20
-rw-r--r--browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs4
-rw-r--r--browser/components/urlbar/UrlbarProviderAutofill.sys.mjs2
-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.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs4
-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.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs4
-rw-r--r--browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs12
-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.mjs6
-rw-r--r--browser/components/urlbar/UrlbarProvidersManager.sys.mjs8
-rw-r--r--browser/components/urlbar/UrlbarUtils.sys.mjs32
-rw-r--r--browser/components/urlbar/UrlbarView.sys.mjs10
-rw-r--r--browser/components/urlbar/docs/dynamic-result-types.rst6
-rw-r--r--browser/components/urlbar/metrics.yaml38
-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.mjs9
-rw-r--r--browser/components/urlbar/private/YelpSuggestions.sys.mjs10
-rw-r--r--browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs17
-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.toml14
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js2
-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_copy_during_load.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_less_common_selection_manipulations.js288
-rw-r--r--browser/components/urlbar/tests/browser/browser_locationBarCommand.js2
-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_raceWithTabs.js4
-rw-r--r--browser/components/urlbar/tests/browser/browser_result_menu.js12
-rw-r--r--browser/components/urlbar/tests/browser/browser_stop_pending.js4
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js4
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml8
-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_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.js8
-rw-r--r--browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs17
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js37
-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.js5
-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/head.js41
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/head.js10
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js2
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js2
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_weather.js2
-rw-r--r--browser/components/urlbar/tests/unit/test_exposure.js11
-rw-r--r--browser/components/urlbar/tests/unit/test_l10nCache.js2
702 files changed, 20741 insertions, 9490 deletions
diff --git a/browser/components/BrowserContentHandler.sys.mjs b/browser/components/BrowserContentHandler.sys.mjs
index ca7cf4d2c4..247f33c8b0 100644
--- a/browser/components/BrowserContentHandler.sys.mjs
+++ b/browser/components/BrowserContentHandler.sys.mjs
@@ -438,7 +438,7 @@ function openBrowserWindow(
});
}
-function openPreferences(cmdLine, extraArgs) {
+function openPreferences(cmdLine) {
openBrowserWindow(cmdLine, lazy.gSystemPrincipal, "about:preferences");
}
@@ -1244,24 +1244,12 @@ nsDefaultCommandLineHandler.prototype = {
async function handleNotification() {
let { tagWasHandled } = await alertService.handleWindowsTag(tag);
- // If the tag was not handled via callback, then the notification was
- // from a prior instance of the application and we need to handle
- // fallback behavior.
- if (!tagWasHandled) {
- console.info(
- `Completing Windows notification (tag=${JSON.stringify(
- tag
- )}, notificationData=${notificationData})`
+ try {
+ notificationData = JSON.parse(notificationData);
+ } catch (e) {
+ console.error(
+ `Failed to parse (notificationData=${notificationData}) for Windows notification (tag=${tag})`
);
- try {
- notificationData = JSON.parse(notificationData);
- } catch (e) {
- console.error(
- `Completing Windows notification (tag=${JSON.stringify(
- tag
- )}, failed to parse (notificationData=${notificationData})`
- );
- }
}
// This is awkward: the relaunch data set by the caller is _wrapped_
@@ -1275,11 +1263,7 @@ nsDefaultCommandLineHandler.prototype = {
);
} catch (e) {
console.error(
- `Completing Windows notification (tag=${JSON.stringify(
- tag
- )}, failed to parse (opaqueRelaunchData=${
- notificationData.opaqueRelaunchData
- })`
+ `Failed to parse (opaqueRelaunchData=${notificationData.opaqueRelaunchData}) for Windows notification (tag=${tag})`
);
}
}
@@ -1298,9 +1282,16 @@ nsDefaultCommandLineHandler.prototype = {
// window to perform the action in.
let winForAction;
- if (notificationData?.launchUrl && !opaqueRelaunchData) {
- // Unprivileged Web Notifications contain a launch URL and are handled
- // slightly differently than privileged notifications with actions.
+ if (
+ !tagWasHandled &&
+ notificationData?.launchUrl &&
+ !opaqueRelaunchData
+ ) {
+ // Unprivileged Web Notifications contain a launch URL and are
+ // handled slightly differently than privileged notifications with
+ // actions. If the tag was not handled, then the notification was
+ // from a prior instance of the application and we need to handle
+ // fallback behavior.
let { uri, principal } = resolveURIInternal(
cmdLine,
notificationData.launchUrl
@@ -1347,6 +1338,14 @@ nsDefaultCommandLineHandler.prototype = {
});
}
+ // Note: at time of writing `opaqueRelaunchData` was only used by the
+ // Messaging System; if present it could be inferred that the message
+ // originated from the Messaging System. The Messaging System did not
+ // act on Windows 8 style notification callbacks, so there was no risk
+ // of duplicating behavior. If a non-Messaging System consumer is
+ // modified to populate `opaqueRelaunchData` or the Messaging System
+ // modified to use the callback directly, we will need to revisit
+ // this assumption.
if (opaqueRelaunchData && winForAction) {
// Without dispatch, `OPEN_URL` with `where: "tab"` does not work on relaunch.
Services.tm.dispatchToMainThread(() => {
diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs
index f4ea0c87a3..52f4a77d82 100644
--- a/browser/components/BrowserGlue.sys.mjs
+++ b/browser/components/BrowserGlue.sys.mjs
@@ -27,9 +27,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs",
+ ContentRelevancyManager:
+ "resource://gre/modules/ContentRelevancyManager.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
- Corroborate: "resource://gre/modules/Corroborate.sys.mjs",
DAPTelemetrySender: "resource://gre/modules/DAPTelemetrySender.sys.mjs",
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
Discovery: "resource:///modules/Discovery.sys.mjs",
@@ -80,8 +81,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs",
ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
- SearchSERPDomainToCategoriesMap:
- "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs",
SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
@@ -223,6 +223,22 @@ let JSPROCESSACTORS = {
* available at https://firefox-source-docs.mozilla.org/dom/ipc/jsactors.html
*/
let JSWINDOWACTORS = {
+ Megalist: {
+ parent: {
+ esModuleURI: "resource://gre/actors/MegalistParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://gre/actors/MegalistChild.sys.mjs",
+ events: {
+ DOMContentLoaded: {},
+ },
+ },
+ includeChrome: true,
+ matches: ["chrome://global/content/megalist/megalist.html"],
+ allFrames: true,
+ enablePreference: "browser.megalist.enabled",
+ },
+
AboutLogins: {
parent: {
esModuleURI: "resource:///actors/AboutLoginsParent.sys.mjs",
@@ -2111,7 +2127,7 @@ BrowserGlue.prototype = {
() => lazy.BrowserUsageTelemetry.uninit(),
() => lazy.SearchSERPTelemetry.uninit(),
- () => lazy.SearchSERPDomainToCategoriesMap.uninit(),
+ () => lazy.SearchSERPCategorization.uninit(),
() => lazy.Interactions.uninit(),
() => lazy.PageDataService.uninit(),
() => lazy.PageThumbs.uninit(),
@@ -2341,7 +2357,7 @@ BrowserGlue.prototype = {
_badgeIcon();
}
- lazy.RemoteSettings(STUDY_ADDON_COLLECTION_KEY).on("sync", async event => {
+ lazy.RemoteSettings(STUDY_ADDON_COLLECTION_KEY).on("sync", async () => {
Services.prefs.setBoolPref(PREF_ION_NEW_STUDIES_AVAILABLE, true);
});
@@ -2436,7 +2452,7 @@ BrowserGlue.prototype = {
lazy.Sanitizer.onStartup();
this._maybeShowRestoreSessionInfoBar();
this._scheduleStartupIdleTasks();
- this._lateTasksIdleObserver = (idleService, topic, data) => {
+ this._lateTasksIdleObserver = (idleService, topic) => {
if (topic == "idle") {
idleService.removeIdleObserver(
this._lateTasksIdleObserver,
@@ -2662,7 +2678,19 @@ BrowserGlue.prototype = {
AppConstants.platform == "win") &&
Services.prefs.getBoolPref("browser.firefoxbridge.enabled", false),
task: async () => {
- await lazy.FirefoxBridgeExtensionUtils.ensureRegistered();
+ let profileService = Cc[
+ "@mozilla.org/toolkit/profile-service;1"
+ ].getService(Ci.nsIToolkitProfileService);
+ if (
+ profileService.defaultProfile &&
+ profileService.currentProfile == profileService.defaultProfile
+ ) {
+ await lazy.FirefoxBridgeExtensionUtils.ensureRegistered();
+ } else {
+ lazy.log.debug(
+ "FirefoxBridgeExtensionUtils failed to register due to non-default current profile."
+ );
+ }
},
},
@@ -3082,9 +3110,16 @@ BrowserGlue.prototype = {
},
{
- name: "SearchSERPDomainToCategoriesMap.init",
+ name: "SearchSERPCategorization.init",
task: () => {
- lazy.SearchSERPDomainToCategoriesMap.init().catch(console.error);
+ lazy.SearchSERPCategorization.init();
+ },
+ },
+
+ {
+ name: "ContentRelevancyManager.init",
+ task: () => {
+ lazy.ContentRelevancyManager.init();
},
},
@@ -3193,12 +3228,6 @@ BrowserGlue.prototype = {
lazy.RemoteSecuritySettings.init();
},
- function CorroborateInit() {
- if (Services.prefs.getBoolPref("corroborator.enabled", false)) {
- lazy.Corroborate.init().catch(console.error);
- }
- },
-
function BrowserUsageTelemetryReportProfileCount() {
lazy.BrowserUsageTelemetry.reportProfileCount();
},
@@ -3699,7 +3728,7 @@ BrowserGlue.prototype = {
"account-connection-connected",
]);
- let clickCallback = (subject, topic, data) => {
+ let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
@@ -3740,7 +3769,7 @@ BrowserGlue.prototype = {
_migrateUI() {
// Use an increasing number to keep track of the current migration state.
// Completely unrelated to the current Firefox release number.
- const UI_VERSION = 143;
+ const UI_VERSION = 144;
const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
if (!Services.prefs.prefHasUserValue("browser.migration.version")) {
@@ -4374,6 +4403,23 @@ BrowserGlue.prototype = {
}
}
+ if (currentUIVersion < 144) {
+ // TerminatorTelemetry was removed in bug 1879136. Before it was removed,
+ // the ShutdownDuration.json file would be written to disk at shutdown
+ // so that the next launch of the browser could read it in and send
+ // shutdown performance measurements.
+ //
+ // Unfortunately, this mechanism and its measurements were fairly
+ // unreliable, so they were removed.
+ for (const filename of [
+ "ShutdownDuration.json",
+ "ShutdownDuration.json.tmp",
+ ]) {
+ const filePath = PathUtils.join(PathUtils.profileDir, filename);
+ IOUtils.remove(filePath, { ignoreAbsent: true }).catch(console.error);
+ }
+ }
+
// Update the migration version.
Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
},
@@ -4462,14 +4508,6 @@ BrowserGlue.prototype = {
// Check the default branch as enterprise policies can set prefs there.
const defaultPrefs = Services.prefs.getDefaultBranch("");
- if (
- !defaultPrefs.getBoolPref(
- "browser.messaging-system.whatsNewPanel.enabled",
- true
- )
- ) {
- return "no-whatsNew";
- }
if (!defaultPrefs.getBoolPref("browser.aboutwelcome.enabled", true)) {
return "no-welcome";
}
@@ -4712,7 +4750,7 @@ BrowserGlue.prototype = {
}
const title = await lazy.accountsL10n.formatValue(titleL10nId);
- const clickCallback = (obsSubject, obsTopic, obsData) => {
+ const clickCallback = (obsSubject, obsTopic) => {
if (obsTopic == "alertclickcallback") {
win.gBrowser.selectedTab = firstTab;
}
@@ -4751,7 +4789,7 @@ BrowserGlue.prototype = {
tab = win.gBrowser.addWebTab(url);
}
tab.attention = true;
- let clickCallback = (subject, topic, data) => {
+ let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
@@ -4780,7 +4818,7 @@ BrowserGlue.prototype = {
: { id: "account-connection-connected-with-noname" },
]);
- let clickCallback = async (subject, topic, data) => {
+ let clickCallback = async (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
@@ -4815,7 +4853,7 @@ BrowserGlue.prototype = {
"account-connection-disconnected",
]);
- let clickCallback = (subject, topic, data) => {
+ let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
@@ -4870,7 +4908,7 @@ BrowserGlue.prototype = {
const TOGGLE_ENABLED_PREF =
"media.videocontrols.picture-in-picture.video-toggle.enabled";
- const observe = (subject, topic, data) => {
+ const observe = (subject, topic) => {
const enabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF, false);
Services.telemetry.scalarSet("pictureinpicture.toggle_enabled", enabled);
@@ -6475,11 +6513,11 @@ export var AboutHomeStartupCache = {
/** nsICacheEntryOpenCallback **/
- onCacheEntryCheck(aEntry) {
+ onCacheEntryCheck() {
return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
},
- onCacheEntryAvailable(aEntry, aNew, aResult) {
+ onCacheEntryAvailable(aEntry) {
this.log.trace("Cache entry is available.");
this._cacheEntry = aEntry;
diff --git a/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js b/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js
index 5ab03f9867..86e754084b 100644
--- a/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js
+++ b/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js
@@ -35,7 +35,7 @@ add_task(async function () {
createLazyBrowser: true,
});
- Assert.equal(lazyTab.linkedPanel, "", "Tab is lazy");
+ Assert.equal(lazyTab.linkedPanel, null, "Tab is lazy");
let tabLoaded = new Promise(resolve => {
gBrowser.addTabsProgressListener({
async onLocationChange(aBrowser) {
diff --git a/browser/components/aboutlogins/tests/browser/head.js b/browser/components/aboutlogins/tests/browser/head.js
index 2aec0e632a..82d3cf2062 100644
--- a/browser/components/aboutlogins/tests/browser/head.js
+++ b/browser/components/aboutlogins/tests/browser/head.js
@@ -131,7 +131,7 @@ add_setup(async function setup_head() {
return;
}
if (msg.errorMessage.includes("Can't find profile directory.")) {
- // Ignore error messages for no profile found in old XULStore.jsm
+ // Ignore error messages for no profile found in old XULStore.sys.mjs
return;
}
if (msg.errorMessage.includes("Error reading typed URL history")) {
diff --git a/browser/components/aboutwelcome/.eslintrc.js b/browser/components/aboutwelcome/.eslintrc.js
index 1168a8e840..0b8e1cc676 100644
--- a/browser/components/aboutwelcome/.eslintrc.js
+++ b/browser/components/aboutwelcome/.eslintrc.js
@@ -80,8 +80,6 @@ module.exports = {
},
],
rules: {
- "fetch-options/no-fetch-credentials": "error",
-
"react/jsx-boolean-value": ["error", "always"],
"react/jsx-key": "error",
"react/jsx-no-bind": [
diff --git a/browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs b/browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs
index 7b32161e3b..258eff36ef 100644
--- a/browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs
+++ b/browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs
@@ -70,7 +70,7 @@ class AboutWelcomeObserver {
this.win.addEventListener("unload", this.onWindowClose, { once: true });
}
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
switch (aTopic) {
case "quit-application":
this.terminateReason = AWTerminate.APP_SHUT_DOWN;
diff --git a/browser/components/aboutwelcome/content-src/aboutwelcome.scss b/browser/components/aboutwelcome/content-src/aboutwelcome.scss
index 9174fe2439..07bfcd2c96 100644
--- a/browser/components/aboutwelcome/content-src/aboutwelcome.scss
+++ b/browser/components/aboutwelcome/content-src/aboutwelcome.scss
@@ -1139,6 +1139,38 @@ html {
&[no-rdm] {
width: 800px;
+
+ &[reverse-split] {
+ flex-direction: row-reverse;
+
+ .section-main {
+ .main-content {
+ border-radius: inherit;
+ }
+
+ margin: auto;
+ margin-inline-end: 0;
+ border-radius: 8px 0 0 8px;
+
+ &:dir(rtl) {
+ border-radius: 0 8px 8px 0;
+ margin: auto;
+ margin-inline-end: 0;
+ }
+ }
+
+ .section-secondary {
+ margin: auto;
+ margin-inline-start: 0;
+ border-radius: 0 8px 8px 0;
+
+ &:dir(rtl) {
+ border-radius: 8px 0 0 8px;
+ margin: auto;
+ margin-inline-start: 0;
+ }
+ }
+ }
}
}
@@ -1372,6 +1404,107 @@ html {
outline: 2px solid var(--in-content-primary-button-background);
}
+ // newtab wallpaper specific styles
+ &.wallpaper {
+ justify-content: center;
+ gap: 10px;
+
+ &:hover, &:focus-within {
+ outline: none;
+ }
+
+ .theme {
+ flex: unset;
+ width: unset;
+ transition: var(--transition);
+
+ &:has(.input:focus) {
+ outline: 2px solid var(--in-content-primary-button-background);
+ outline-offset: 2px;
+ }
+
+ .icon {
+ width: 116px;
+ height: 86px;
+ border-radius: 8px;
+ box-shadow: 0 1px 2px 0 #3A394433;
+
+ &:hover {
+ filter: brightness(45%);
+ }
+
+ // dark theme wallpapers
+ &.dark-landscape {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif');
+ }
+
+ &.dark-beach {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif');
+ }
+
+ &.dark-color {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif');
+ }
+
+ &.dark-mountain {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif');
+ }
+
+ &.dark-panda {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif');
+ }
+
+ &.dark-sky {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif');
+ }
+
+ // light theme wallpapers
+ &.light-beach {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif');
+ }
+
+ &.light-color {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif');
+ }
+
+ &.light-landscape {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif');
+ }
+
+ &.light-mountain {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif');
+ }
+
+ &.light-panda {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif');
+ }
+
+ &.light-sky {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif');
+ }
+ }
+ }
+
+ .dark {
+ display: none;
+ }
+
+ .text {
+ display: none;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ .light {
+ display: none;
+ }
+
+ .dark {
+ display: block;
+ }
+ }
+
+ }
+
.theme {
align-items: center;
display: flex;
@@ -1405,13 +1538,17 @@ html {
transform: scaleX(-1);
}
- &:focus,
+ &:focus-visible,
&:active,
&.selected {
outline: 2px solid var(--in-content-primary-button-background);
outline-offset: 2px;
}
+ &.selected {
+ outline-color: var(--color-accent-primary-active);
+ }
+
&.light {
background-image: url('resource://builtin-themes/light/icon.svg');
}
diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx
index 034055bf3d..3ccbd71f40 100644
--- a/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx
+++ b/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx
@@ -463,9 +463,21 @@ export class WelcomeScreen extends React.PureComponent {
action.theme === "<event>"
? event.currentTarget.value
: this.props.initialTheme || action.theme;
-
this.props.setActiveTheme(themeToUse);
- window.AWSelectTheme(themeToUse);
+ if (props.content.tiles?.category?.type === "wallpaper") {
+ const theme = themeToUse.split("-")?.[1];
+ let actionWallpaper = { ...props.content.tiles.category.action };
+ actionWallpaper.data.actions.forEach(async wpAction => {
+ if (wpAction.data.pref.name?.includes("dark")) {
+ wpAction.data.pref.value = `dark-${theme}`;
+ } else {
+ wpAction.data.pref.value = `light-${theme}`;
+ }
+ await AboutWelcomeUtils.handleUserAction(actionWallpaper);
+ });
+ } else {
+ window.AWSelectTheme(themeToUse);
+ }
}
// If the action has persistActiveTheme: true, we set the initial theme to the currently active theme
diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx
index 59771e4e48..b6e1ffa6b5 100644
--- a/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx
+++ b/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx
@@ -570,32 +570,39 @@ export class ProtonScreen extends React.PureComponent {
</div>
) : null}
- <div className="main-content-inner">
- <div className={`welcome-text ${content.title_style || ""}`}>
- {content.title ? this.renderTitle(content) : null}
+ <div
+ className="main-content-inner"
+ style={{
+ justifyContent: content.split_content_justify_content,
+ }}
+ >
+ {content.title || content.subtitle ? (
+ <div className={`welcome-text ${content.title_style || ""}`}>
+ {content.title ? this.renderTitle(content) : null}
- {content.subtitle ? (
- <Localized text={content.subtitle}>
- <h2
- data-l10n-args={JSON.stringify({
- "addon-name": this.props.addonName,
- ...this.props.appAndSystemLocaleInfo?.displayNames,
- })}
- aria-flowto={
- this.props.messageId?.includes("FEATURE_TOUR")
- ? "steps"
- : ""
- }
+ {content.subtitle ? (
+ <Localized text={content.subtitle}>
+ <h2
+ data-l10n-args={JSON.stringify({
+ "addon-name": this.props.addonName,
+ ...this.props.appAndSystemLocaleInfo?.displayNames,
+ })}
+ aria-flowto={
+ this.props.messageId?.includes("FEATURE_TOUR")
+ ? "steps"
+ : ""
+ }
+ />
+ </Localized>
+ ) : null}
+ {content.cta_paragraph ? (
+ <CTAParagraph
+ content={content.cta_paragraph}
+ handleAction={this.props.handleAction}
/>
- </Localized>
- ) : null}
- {content.cta_paragraph ? (
- <CTAParagraph
- content={content.cta_paragraph}
- handleAction={this.props.handleAction}
- />
- ) : null}
- </div>
+ ) : null}
+ </div>
+ ) : null}
{content.video_container ? (
<OnboardingVideo
content={content.video_container}
diff --git a/browser/components/aboutwelcome/content-src/components/Themes.jsx b/browser/components/aboutwelcome/content-src/components/Themes.jsx
index 0ee986f982..e430ecf3aa 100644
--- a/browser/components/aboutwelcome/content-src/components/Themes.jsx
+++ b/browser/components/aboutwelcome/content-src/components/Themes.jsx
@@ -6,28 +6,29 @@ import React from "react";
import { Localized } from "./MSLocalized";
export const Themes = props => {
+ const category = props.content.tiles?.category?.type;
return (
<div className="tiles-theme-container">
<div>
- <fieldset className="tiles-theme-section">
+ <fieldset className={`tiles-theme-section ${category}`}>
<Localized text={props.content.subtitle}>
<legend className="sr-only" />
</Localized>
{props.content.tiles.data.map(
- ({ theme, label, tooltip, description }) => (
+ ({ theme, label, tooltip, description, type }) => (
<Localized
key={theme + label}
text={typeof tooltip === "object" ? tooltip : {}}
>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
- <label className="theme" title={theme + label}>
+ <label className={`theme ${type}`} title={theme + label}>
<Localized
text={typeof description === "object" ? description : {}}
>
<input
type="radio"
value={theme}
- name="theme"
+ name={category === "wallpaper" ? theme : "theme"}
checked={theme === props.activeTheme}
className="sr-only input"
onClick={props.handleAction}
diff --git a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js
index 0d96257677..11a398e960 100644
--- a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js
+++ b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js
@@ -560,7 +560,22 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
if (action.theme) {
let themeToUse = action.theme === "<event>" ? event.currentTarget.value : this.props.initialTheme || action.theme;
this.props.setActiveTheme(themeToUse);
- window.AWSelectTheme(themeToUse);
+ if (props.content.tiles?.category?.type === "wallpaper") {
+ const theme = themeToUse.split("-")?.[1];
+ let actionWallpaper = {
+ ...props.content.tiles.category.action
+ };
+ actionWallpaper.data.actions.forEach(async wpAction => {
+ if (wpAction.data.pref.name?.includes("dark")) {
+ wpAction.data.pref.value = `dark-${theme}`;
+ } else {
+ wpAction.data.pref.value = `light-${theme}`;
+ }
+ await _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleUserAction(actionWallpaper);
+ });
+ } else {
+ window.AWSelectTheme(themeToUse);
+ }
}
// If the action has persistActiveTheme: true, we set the initial theme to the currently active theme
@@ -1214,8 +1229,11 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
alt: "",
role: "presentation"
})) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
- className: "main-content-inner"
- }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
+ className: "main-content-inner",
+ style: {
+ justifyContent: content.split_content_justify_content
+ }
+ }, content.title || content.subtitle ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: `welcome-text ${content.title_style || ""}`
}, content.title ? this.renderTitle(content) : null, content.subtitle ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: content.subtitle
@@ -1228,7 +1246,7 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
})) : null, content.cta_paragraph ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_CTAParagraph__WEBPACK_IMPORTED_MODULE_8__.CTAParagraph, {
content: content.cta_paragraph,
handleAction: this.props.handleAction
- }) : null), content.video_container ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_OnboardingVideo__WEBPACK_IMPORTED_MODULE_10__.OnboardingVideo, {
+ }) : null) : null, content.video_container ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_OnboardingVideo__WEBPACK_IMPORTED_MODULE_10__.OnboardingVideo, {
content: content.video_container,
handleAction: this.props.handleAction
}) : null, this.renderContentTiles(), this.renderLanguageSwitcher(), content.above_button_content ? this.renderOrderedContent(content.above_button_content) : null, !hideStepsIndicator && aboveButtonStepsIndicator ? this.renderStepsIndicator() : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(ProtonScreenActionButtons, {
@@ -1448,10 +1466,11 @@ __webpack_require__.r(__webpack_exports__);
const Themes = props => {
+ const category = props.content.tiles?.category?.type;
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "tiles-theme-container"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("fieldset", {
- className: "tiles-theme-section"
+ className: `tiles-theme-section ${category}`
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: props.content.subtitle
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("legend", {
@@ -1460,19 +1479,20 @@ const Themes = props => {
theme,
label,
tooltip,
- description
+ description,
+ type
}) => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
key: theme + label,
text: typeof tooltip === "object" ? tooltip : {}
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
- className: "theme",
+ className: `theme ${type}`,
title: theme + label
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: typeof description === "object" ? description : {}
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", {
type: "radio",
value: theme,
- name: "theme",
+ name: category === "wallpaper" ? theme : "theme",
checked: theme === props.activeTheme,
className: "sr-only input",
onClick: props.handleAction
diff --git a/browser/components/aboutwelcome/content/aboutwelcome.css b/browser/components/aboutwelcome/content/aboutwelcome.css
index aa0445e0ef..4e86b29e5d 100644
--- a/browser/components/aboutwelcome/content/aboutwelcome.css
+++ b/browser/components/aboutwelcome/content/aboutwelcome.css
@@ -1896,6 +1896,32 @@ html {
.onboardingContainer .screen[pos=split][no-rdm] {
width: 800px;
}
+ .onboardingContainer .screen[pos=split][no-rdm][reverse-split] {
+ flex-direction: row-reverse;
+ }
+ .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-main {
+ margin: auto;
+ margin-inline-end: 0;
+ border-radius: 8px 0 0 8px;
+ }
+ .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-main .main-content {
+ border-radius: inherit;
+ }
+ .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-main:dir(rtl) {
+ border-radius: 0 8px 8px 0;
+ margin: auto;
+ margin-inline-end: 0;
+ }
+ .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-secondary {
+ margin: auto;
+ margin-inline-start: 0;
+ border-radius: 0 8px 8px 0;
+ }
+ .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-secondary:dir(rtl) {
+ border-radius: 8px 0 0 8px;
+ margin: auto;
+ margin-inline-start: 0;
+ }
}
@media only screen and (height <= 650px) and (800px <= width <= 990px) {
.onboardingContainer .screen[pos=split] .section-main .secondary-cta.top {
@@ -2091,6 +2117,81 @@ html {
border-radius: 8px;
outline: 2px solid var(--in-content-primary-button-background);
}
+.onboardingContainer .tiles-theme-section.wallpaper {
+ justify-content: center;
+ gap: 10px;
+}
+.onboardingContainer .tiles-theme-section.wallpaper:hover, .onboardingContainer .tiles-theme-section.wallpaper:focus-within {
+ outline: none;
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme {
+ flex: unset;
+ width: unset;
+ transition: var(--transition);
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme:has(.input:focus) {
+ outline: 2px solid var(--in-content-primary-button-background);
+ outline-offset: 2px;
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon {
+ width: 116px;
+ height: 86px;
+ border-radius: 8px;
+ box-shadow: 0 1px 2px 0 rgba(58, 57, 68, 0.2);
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon:hover {
+ filter: brightness(45%);
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-landscape {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-beach {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-color {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-mountain {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-panda {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-sky {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-beach {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-color {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-landscape {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-mountain {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-panda {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-sky {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif");
+}
+.onboardingContainer .tiles-theme-section.wallpaper .dark {
+ display: none;
+}
+.onboardingContainer .tiles-theme-section.wallpaper .text {
+ display: none;
+}
+@media (prefers-color-scheme: dark) {
+ .onboardingContainer .tiles-theme-section.wallpaper .light {
+ display: none;
+ }
+ .onboardingContainer .tiles-theme-section.wallpaper .dark {
+ display: block;
+ }
+}
.onboardingContainer .tiles-theme-section .theme {
align-items: center;
display: flex;
@@ -2121,10 +2222,13 @@ html {
.onboardingContainer .tiles-theme-section .theme .icon:dir(rtl) {
transform: scaleX(-1);
}
-.onboardingContainer .tiles-theme-section .theme .icon:focus, .onboardingContainer .tiles-theme-section .theme .icon:active, .onboardingContainer .tiles-theme-section .theme .icon.selected {
+.onboardingContainer .tiles-theme-section .theme .icon:focus-visible, .onboardingContainer .tiles-theme-section .theme .icon:active, .onboardingContainer .tiles-theme-section .theme .icon.selected {
outline: 2px solid var(--in-content-primary-button-background);
outline-offset: 2px;
}
+.onboardingContainer .tiles-theme-section .theme .icon.selected {
+ outline-color: var(--color-accent-primary-active);
+}
.onboardingContainer .tiles-theme-section .theme .icon.light {
background-image: url("resource://builtin-themes/light/icon.svg");
}
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js
index 3081688a0c..f3bea5b499 100644
--- a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js
@@ -249,7 +249,7 @@ add_task(async function test_aboutwelcome_with_title_styles() {
{
"font-weight": "276",
"font-size": "36px",
- animation: "50s linear 0s infinite normal none running shine",
+ animation: "50s linear infinite shine",
"letter-spacing": "normal",
}
);
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js
index c9180ddf2d..308c27a427 100644
--- a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js
@@ -32,7 +32,7 @@ add_task(async function test_add_and_remove_toolbar_button() {
});
// Open newtab
let win = await BrowserTestUtils.openNewBrowserWindow();
- win.BrowserOpenTab();
+ win.BrowserCommands.openTab();
ok(win, "browser exists");
// Try to add the button. It shouldn't add because the pref is false
await AWToolbarButton.maybeAddSetupButton();
diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx
index 9b452d5c6b..ed0260bf30 100644
--- a/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx
+++ b/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx
@@ -671,4 +671,27 @@ describe("MultiStageAboutWelcomeProton module", () => {
assert.isTrue(wrapper.find("migration-wizard").exists());
});
});
+
+ describe("Custom main content inner custom justify content", () => {
+ const SCREEN_PROPS = {
+ content: {
+ title: "test title",
+ position: "split",
+ split_content_justify_content: "flex-start",
+ },
+ };
+
+ it("should render split screen with custom justify-content", async () => {
+ const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />);
+ assert.ok(wrapper.exists());
+ assert.equal(wrapper.find("main").prop("pos"), "split");
+ assert.exists(wrapper.find(".main-content-inner"));
+ assert.ok(
+ wrapper
+ .find(".main-content-inner")
+ .prop("style")
+ .justifyContent.includes("flex-start")
+ );
+ });
+ });
});
diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx
index b4593a45f3..2fb897125d 100644
--- a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx
+++ b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx
@@ -383,6 +383,72 @@ describe("MultiStageAboutWelcome module", () => {
);
});
});
+
+ describe("Wallpaper screen", () => {
+ let WALLPAPER_SCREEN_PROPS;
+ beforeEach(() => {
+ WALLPAPER_SCREEN_PROPS = {
+ content: {
+ title: "test title",
+ subtitle: "test subtitle",
+ tiles: {
+ type: "theme",
+ category: {
+ type: "wallpaper",
+ action: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "test-dark",
+ },
+ },
+ },
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "test-light",
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ action: {
+ theme: "<event>",
+ },
+ data: [
+ {
+ theme: "mountain",
+ type: "light",
+ },
+ ],
+ },
+ primary_button: {
+ action: {},
+ label: "test button",
+ },
+ },
+ navigate: sandbox.stub(),
+ setActiveTheme: sandbox.stub(),
+ };
+ sandbox.stub(AboutWelcomeUtils, "handleUserAction").resolves();
+ });
+ it("should handle wallpaper click", () => {
+ const wrapper = mount(<WelcomeScreen {...WALLPAPER_SCREEN_PROPS} />);
+ const wallpaperOptions = wrapper.find(
+ ".tiles-theme-section .theme input[name='mountain']"
+ );
+ wallpaperOptions.simulate("click");
+ assert.calledTwice(AboutWelcomeUtils.handleUserAction);
+ });
+ });
+
describe("#handleAction", () => {
let SCREEN_PROPS;
let TEST_ACTION;
diff --git a/browser/components/aboutwelcome/tests/unit/unit-entry.js b/browser/components/aboutwelcome/tests/unit/unit-entry.js
index fb70eeb843..3da6964c53 100644
--- a/browser/components/aboutwelcome/tests/unit/unit-entry.js
+++ b/browser/components/aboutwelcome/tests/unit/unit-entry.js
@@ -97,8 +97,8 @@ const TEST_GLOBAL = {
JSWindowActorParent,
JSWindowActorChild,
AboutReaderParent: {
- addMessageListener: (messageName, listener) => {},
- removeMessageListener: (messageName, listener) => {},
+ addMessageListener: (_messageName, _listener) => {},
+ removeMessageListener: (_messageName, _listener) => {},
},
AboutWelcomeTelemetry: class {
submitGleanPingForPing() {}
@@ -281,8 +281,8 @@ const TEST_GLOBAL = {
},
dump() {},
EveryWindow: {
- registerCallback: (id, init, uninit) => {},
- unregisterCallback: id => {},
+ registerCallback: (_id, _init, _uninit) => {},
+ unregisterCallback: _id => {},
},
setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window),
@@ -402,7 +402,7 @@ const TEST_GLOBAL = {
},
urlFormatter: { formatURL: str => str, formatURLPref: str => str },
mm: {
- addMessageListener: (msg, cb) => this.receiveMessage(),
+ addMessageListener: (_msg, _cb) => this.receiveMessage(),
removeMessageListener() {},
},
obs: {
@@ -412,7 +412,7 @@ const TEST_GLOBAL = {
},
telemetry: {
setEventRecordingEnabled: () => {},
- recordEvent: eventDetails => {},
+ recordEvent: _eventDetails => {},
scalarSet: () => {},
keyedScalarAdd: () => {},
},
@@ -570,7 +570,7 @@ const TEST_GLOBAL = {
finish: () => {},
},
Sampling: {
- ratioSample(seed, ratios) {
+ ratioSample(_seed, _ratios) {
return Promise.resolve(0);
},
},
diff --git a/browser/components/asrouter/.eslintrc.js b/browser/components/asrouter/.eslintrc.js
index ef5bc81b68..b2a647e42d 100644
--- a/browser/components/asrouter/.eslintrc.js
+++ b/browser/components/asrouter/.eslintrc.js
@@ -63,8 +63,6 @@ module.exports = {
},
],
rules: {
- "fetch-options/no-fetch-credentials": "error",
-
"react/jsx-boolean-value": ["error", "always"],
"react/jsx-key": "error",
"react/jsx-no-bind": [
diff --git a/browser/components/asrouter/actors/ASRouterChild.sys.mjs b/browser/components/asrouter/actors/ASRouterChild.sys.mjs
index 2096d92bb3..95f625e2b5 100644
--- a/browser/components/asrouter/actors/ASRouterChild.sys.mjs
+++ b/browser/components/asrouter/actors/ASRouterChild.sys.mjs
@@ -11,9 +11,7 @@
// eslint-disable-next-line mozilla/use-static-import
const { MESSAGE_TYPE_LIST, MESSAGE_TYPE_HASH: msg } =
- ChromeUtils.importESModule(
- "resource:///modules/asrouter/ActorConstants.sys.mjs"
- );
+ ChromeUtils.importESModule("resource:///modules/asrouter/ActorConstants.mjs");
const VALID_TYPES = new Set(MESSAGE_TYPE_LIST);
@@ -103,8 +101,6 @@ export class ASRouterChild extends JSWindowActorChild {
case msg.DISABLE_PROVIDER:
case msg.ENABLE_PROVIDER:
case msg.EXPIRE_QUERY_CACHE:
- case msg.FORCE_WHATSNEW_PANEL:
- case msg.CLOSE_WHATSNEW_PANEL:
case msg.FORCE_PRIVATE_BROWSING_WINDOW:
case msg.IMPRESSION:
case msg.RESET_PROVIDER_PREF:
diff --git a/browser/components/asrouter/bin/import-rollouts.js b/browser/components/asrouter/bin/import-rollouts.js
index d29a31a068..bb5c17d9ae 100644
--- a/browser/components/asrouter/bin/import-rollouts.js
+++ b/browser/components/asrouter/bin/import-rollouts.js
@@ -126,10 +126,6 @@ async function getMessageValidators(skipValidation) {
"./content-src/templates/OnboardingMessage/UpdateAction.schema.json",
{ common: true }
),
- whatsnew_panel_message: await getValidator(
- "./content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json",
- { common: true }
- ),
feature_callout: await getValidator(
// For now, Feature Callout and Spotlight share a common schema
"./content-src/templates/OnboardingMessage/Spotlight.schema.json",
diff --git a/browser/components/asrouter/content-src/asrouter-utils.mjs b/browser/components/asrouter/content-src/asrouter-utils.mjs
index 989d864e71..3789158547 100644
--- a/browser/components/asrouter/content-src/asrouter-utils.mjs
+++ b/browser/components/asrouter/content-src/asrouter-utils.mjs
@@ -2,10 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system
-import { MESSAGE_TYPE_HASH as msg } from "../modules/ActorConstants.sys.mjs";
-// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system
-import { actionCreators as ac } from "../../newtab/common/Actions.sys.mjs";
+import { MESSAGE_TYPE_HASH as msg } from "../modules/ActorConstants.mjs";
+import { actionCreators as ac } from "../../newtab/common/Actions.mjs";
export const ASRouterUtils = {
addListener(listener) {
diff --git a/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx b/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
index befce707ef..32d1614307 100644
--- a/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
+++ b/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
@@ -15,6 +15,18 @@ const Row = props => (
</tr>
);
+// Convert a UTF-8 string to a string in which only one byte of each
+// 16-bit unit is occupied. This is necessary to comply with `btoa` API constraints.
+export function toBinary(string) {
+ const codeUnits = new Uint16Array(string.length);
+ for (let i = 0; i < codeUnits.length; i++) {
+ codeUnits[i] = string.charCodeAt(i);
+ }
+ return btoa(
+ String.fromCharCode(...Array.from(new Uint8Array(codeUnits.buffer)))
+ );
+}
+
function relativeTime(timestamp) {
if (!timestamp) {
return "";
@@ -531,7 +543,9 @@ export class ASRouterAdminInner extends React.PureComponent {
{aboutMessagePreviewSupported ? (
<CopyButton
transformer={text =>
- `about:messagepreview?json=${encodeURIComponent(btoa(text))}`
+ `about:messagepreview?json=${encodeURIComponent(
+ toBinary(text)
+ )}`
}
label="Share"
copiedLabel="Copied!"
diff --git a/browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json b/browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json
index 9de01052f7..5fe86f9617 100644
--- a/browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json
+++ b/browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json
@@ -10,7 +10,9 @@
"const": "multi"
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/MultiMessage"
@@ -68,7 +70,9 @@
"type": "object"
}
},
- "required": ["type"],
+ "required": [
+ "type"
+ ],
"additionalProperties": true
},
"requireInteraction": {
@@ -116,24 +120,37 @@
"type": "object"
}
},
- "required": ["type"],
+ "required": [
+ "type"
+ ],
"additionalProperties": true
}
},
- "required": ["action", "title"],
+ "required": [
+ "action",
+ "title"
+ ],
"additionalProperties": true
}
}
},
"additionalProperties": true,
- "required": ["title", "body"]
+ "required": [
+ "title",
+ "body"
+ ]
},
"template": {
"type": "string",
"const": "toast_notification"
}
},
- "required": ["content", "targeting", "template", "trigger"],
+ "required": [
+ "content",
+ "targeting",
+ "template",
+ "trigger"
+ ],
"additionalProperties": true
},
"Message": {
@@ -154,7 +171,9 @@
"template": {
"type": "string",
"description": "Which messaging template this message is using.",
- "enum": ["toast_notification"]
+ "enum": [
+ "toast_notification"
+ ]
},
"frequency": {
"type": "object",
@@ -184,7 +203,10 @@
"maximum": 100
}
},
- "required": ["period", "cap"]
+ "required": [
+ "period",
+ "cap"
+ ]
}
}
}
@@ -224,7 +246,9 @@
}
}
},
- "required": ["id"]
+ "required": [
+ "id"
+ ]
},
"provider": {
"description": "An identifier for the provider of this message, such as \"cfr\" or \"preview\".",
@@ -233,8 +257,14 @@
},
"additionalProperties": true,
"dependentRequired": {
- "content": ["id", "template"],
- "template": ["id", "content"]
+ "content": [
+ "id",
+ "template"
+ ],
+ "template": [
+ "id",
+ "content"
+ ]
}
},
"localizedText": {
@@ -245,7 +275,9 @@
"type": "string"
}
},
- "required": ["string_id"]
+ "required": [
+ "string_id"
+ ]
},
"localizableText": {
"description": "Either a raw string or an object containing the string_id of the localized text",
@@ -272,10 +304,14 @@
"properties": {
"template": {
"type": "string",
- "enum": ["toast_notification"]
+ "enum": [
+ "toast_notification"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/ToastNotification"
@@ -299,7 +335,10 @@
}
}
},
- "required": ["template", "messages"]
+ "required": [
+ "template",
+ "messages"
+ ]
}
}
}
diff --git a/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json b/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json
index fbabb109f8..dd4ce4776d 100644
--- a/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json
+++ b/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json
@@ -10,7 +10,9 @@
"const": "multi"
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/MultiMessage"
@@ -41,7 +43,9 @@
"layout": {
"type": "string",
"description": "Describes how content should be displayed.",
- "enum": ["chiclet_open_url"]
+ "enum": [
+ "chiclet_open_url"
+ ]
},
"bucket_id": {
"type": "string",
@@ -66,11 +70,17 @@
"where": {
"description": "Should it open in a new tab or the current tab",
"type": "string",
- "enum": ["current", "tabshifted"]
+ "enum": [
+ "current",
+ "tabshifted"
+ ]
}
},
"additionalProperties": true,
- "required": ["url", "where"]
+ "required": [
+ "url",
+ "where"
+ ]
}
},
"additionalProperties": true,
@@ -87,7 +97,10 @@
"const": "cfr_urlbar_chiclet"
}
},
- "required": ["targeting", "trigger"]
+ "required": [
+ "targeting",
+ "trigger"
+ ]
},
"ExtensionDoorhanger": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
@@ -162,10 +175,14 @@
"description": "Text for button tooltip used to provide information about the doorhanger."
}
},
- "required": ["tooltiptext"]
+ "required": [
+ "tooltiptext"
+ ]
}
},
- "required": ["attributes"]
+ "required": [
+ "attributes"
+ ]
},
{
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizedText"
@@ -185,7 +202,10 @@
"learn_more": {
"type": "string",
"description": "Last part of the path in the SUMO URL to the support page with the information about the doorhanger.",
- "examples": ["extensionpromotions", "extensionrecommendations"]
+ "examples": [
+ "extensionpromotions",
+ "extensionrecommendations"
+ ]
},
"heading_text": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
@@ -236,7 +256,12 @@
"description": "Link that offers more information related to the addon."
}
},
- "required": ["title", "author", "icon", "amo_url"]
+ "required": [
+ "title",
+ "author",
+ "icon",
+ "amo_url"
+ ]
},
"text": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
@@ -255,7 +280,9 @@
}
}
},
- "required": ["steps"]
+ "required": [
+ "steps"
+ ]
},
"buttons": {
"description": "The label and functionality for the buttons in the pop-over.",
@@ -281,11 +308,16 @@
"description": "A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label."
}
},
- "required": ["accesskey"],
+ "required": [
+ "accesskey"
+ ],
"description": "Button attributes."
}
},
- "required": ["value", "attributes"]
+ "required": [
+ "value",
+ "attributes"
+ ]
},
{
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizedText"
@@ -341,11 +373,16 @@
"description": "A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label."
}
},
- "required": ["accesskey"],
+ "required": [
+ "accesskey"
+ ],
"description": "Button attributes."
}
},
- "required": ["value", "attributes"]
+ "required": [
+ "value",
+ "attributes"
+ ]
},
{
"properties": {
@@ -360,7 +397,9 @@
]
}
},
- "required": ["string_id"]
+ "required": [
+ "string_id"
+ ]
}
],
"description": "Id of localized string or message override."
@@ -417,16 +456,25 @@
}
},
"then": {
- "required": ["category", "notification_text"]
+ "required": [
+ "category",
+ "notification_text"
+ ]
}
},
"template": {
"type": "string",
- "enum": ["cfr_doorhanger", "milestone_message"]
+ "enum": [
+ "cfr_doorhanger",
+ "milestone_message"
+ ]
}
},
"additionalProperties": true,
- "required": ["targeting", "trigger"],
+ "required": [
+ "targeting",
+ "trigger"
+ ],
"$defs": {
"plainText": {
"description": "Plain text (no HTML allowed)",
@@ -457,7 +505,10 @@
"type": {
"type": "string",
"description": "Should the message be global (persisted across tabs) or local (disappear when switching to a different tab).",
- "enum": ["global", "tab"]
+ "enum": [
+ "global",
+ "tab"
+ ]
},
"text": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
@@ -497,7 +548,9 @@
"type": "object"
}
},
- "required": ["type"],
+ "required": [
+ "type"
+ ],
"additionalProperties": true
},
"supportPage": {
@@ -505,13 +558,19 @@
"description": "A page title on SUMO to link to"
}
},
- "required": ["label", "action"],
+ "required": [
+ "label",
+ "action"
+ ],
"additionalProperties": true
}
}
},
"additionalProperties": true,
- "required": ["text", "buttons"]
+ "required": [
+ "text",
+ "buttons"
+ ]
},
"template": {
"type": "string",
@@ -519,7 +578,10 @@
}
},
"additionalProperties": true,
- "required": ["targeting", "trigger"],
+ "required": [
+ "targeting",
+ "trigger"
+ ],
"$defs": {
"plainText": {
"description": "Plain text (no HTML allowed)",
@@ -587,12 +649,22 @@
"promoType": {
"type": "string",
"description": "Promo type used to determine if promo should show to a given user",
- "enum": ["FOCUS", "VPN", "PIN", "COOKIE_BANNERS", "OTHER"]
+ "enum": [
+ "FOCUS",
+ "VPN",
+ "PIN",
+ "COOKIE_BANNERS",
+ "OTHER"
+ ]
},
"promoSectionStyle": {
"type": "string",
"description": "Sets the position of the promo section. Possible values are: top, below-search, bottom. Default bottom.",
- "enum": ["top", "below-search", "bottom"]
+ "enum": [
+ "top",
+ "below-search",
+ "bottom"
+ ]
},
"promoTitle": {
"type": "string",
@@ -624,16 +696,23 @@
"type": "object"
}
},
- "required": ["type"],
+ "required": [
+ "type"
+ ],
"additionalProperties": true
}
},
- "required": ["action"]
+ "required": [
+ "action"
+ ]
},
"promoLinkType": {
"type": "string",
"description": "Type of promo link type. Possible values: link, button. Default is link.",
- "enum": ["link", "button"]
+ "enum": [
+ "link",
+ "button"
+ ]
},
"promoImageLarge": {
"type": "string",
@@ -655,10 +734,14 @@
"const": true
}
},
- "required": ["promoEnabled"]
+ "required": [
+ "promoEnabled"
+ ]
},
"then": {
- "required": ["promoButton"]
+ "required": [
+ "promoButton"
+ ]
}
},
{
@@ -668,20 +751,28 @@
"const": true
}
},
- "required": ["infoEnabled"]
+ "required": [
+ "infoEnabled"
+ ]
},
"then": {
- "required": ["infoLinkText"],
+ "required": [
+ "infoLinkText"
+ ],
"if": {
"properties": {
"infoTitleEnabled": {
"const": true
}
},
- "required": ["infoTitleEnabled"]
+ "required": [
+ "infoTitleEnabled"
+ ]
},
"then": {
- "required": ["infoTitle"]
+ "required": [
+ "infoTitle"
+ ]
}
}
}
@@ -693,7 +784,9 @@
}
},
"additionalProperties": true,
- "required": ["targeting"]
+ "required": [
+ "targeting"
+ ]
},
"Spotlight": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
@@ -763,11 +856,16 @@
"template": {
"type": "string",
"description": "Specify whether the surface is shown as a Spotlight modal or an in-surface Feature Callout dialog",
- "enum": ["spotlight", "feature_callout"]
+ "enum": [
+ "spotlight",
+ "feature_callout"
+ ]
}
},
"additionalProperties": true,
- "required": ["targeting"]
+ "required": [
+ "targeting"
+ ]
},
"ToastNotification": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
@@ -818,7 +916,9 @@
"type": "object"
}
},
- "required": ["type"],
+ "required": [
+ "type"
+ ],
"additionalProperties": true
},
"requireInteraction": {
@@ -866,24 +966,37 @@
"type": "object"
}
},
- "required": ["type"],
+ "required": [
+ "type"
+ ],
"additionalProperties": true
}
},
- "required": ["action", "title"],
+ "required": [
+ "action",
+ "title"
+ ],
"additionalProperties": true
}
}
},
"additionalProperties": true,
- "required": ["title", "body"]
+ "required": [
+ "title",
+ "body"
+ ]
},
"template": {
"type": "string",
"const": "toast_notification"
}
},
- "required": ["content", "targeting", "template", "trigger"],
+ "required": [
+ "content",
+ "targeting",
+ "template",
+ "trigger"
+ ],
"additionalProperties": true
},
"ToolbarBadgeMessage": {
@@ -912,7 +1025,9 @@
}
},
"additionalProperties": true,
- "required": ["id"],
+ "required": [
+ "id"
+ ],
"description": "Optional action to take in addition to showing the notification"
},
"delay": {
@@ -925,7 +1040,9 @@
}
},
"additionalProperties": true,
- "required": ["target"]
+ "required": [
+ "target"
+ ]
},
"template": {
"type": "string",
@@ -933,7 +1050,9 @@
}
},
"additionalProperties": true,
- "required": ["targeting"]
+ "required": [
+ "targeting"
+ ]
},
"UpdateAction": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
@@ -973,101 +1092,25 @@
},
"additionalProperties": true,
"description": "Optional action to take in addition to showing the notification",
- "required": ["id", "data"]
- }
- },
- "additionalProperties": true,
- "required": ["action"]
- },
- "template": {
- "type": "string",
- "const": "update_action"
- }
- },
- "required": ["targeting"]
- },
- "WhatsNewMessage": {
- "$schema": "https://json-schema.org/draft/2019-09/schema",
- "$id": "file:///WhatsNewMessage.schema.json",
- "title": "WhatsNewMessage",
- "description": "A template for the messages that appear in the What's New panel.",
- "allOf": [
- {
- "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/Message"
- }
- ],
- "type": "object",
- "properties": {
- "content": {
- "type": "object",
- "properties": {
- "layout": {
- "description": "Different message layouts",
- "enum": ["tracking-protections"]
- },
- "bucket_id": {
- "type": "string",
- "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting."
- },
- "published_date": {
- "type": "integer",
- "description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published."
- },
- "title": {
- "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
- "description": "Id of localized string or message override of What's New message title"
- },
- "subtitle": {
- "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
- "description": "Id of localized string or message override of What's New message subtitle"
- },
- "body": {
- "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
- "description": "Id of localized string or message override of What's New message body"
- },
- "link_text": {
- "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
- "description": "(optional) Id of localized string or message override of What's New message link text"
- },
- "cta_url": {
- "description": "Target URL for the What's New message.",
- "type": "string",
- "format": "moz-url-format"
- },
- "cta_type": {
- "description": "Type of url open action",
- "enum": ["OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PROTECTION_REPORT"]
- },
- "cta_where": {
- "description": "How to open the cta: new window, tab, focused, unfocused.",
- "enum": ["current", "tabshifted", "tab", "save", "window"]
- },
- "icon_url": {
- "description": "(optional) URL for the What's New message icon.",
- "type": "string",
- "format": "uri"
- },
- "icon_alt": {
- "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
- "description": "Alt text for image."
+ "required": [
+ "id",
+ "data"
+ ]
}
},
"additionalProperties": true,
"required": [
- "published_date",
- "title",
- "body",
- "cta_url",
- "bucket_id"
+ "action"
]
},
"template": {
"type": "string",
- "const": "whatsnew_panel_message"
+ "const": "update_action"
}
},
- "required": ["order"],
- "additionalProperties": true
+ "required": [
+ "targeting"
+ ]
},
"Message": {
"type": "object",
@@ -1097,8 +1140,7 @@
"feature_callout",
"toast_notification",
"toolbar_badge",
- "update_action",
- "whatsnew_panel_message"
+ "update_action"
]
},
"frequency": {
@@ -1129,7 +1171,10 @@
"maximum": 100
}
},
- "required": ["period", "cap"]
+ "required": [
+ "period",
+ "cap"
+ ]
}
}
}
@@ -1169,7 +1214,9 @@
}
}
},
- "required": ["id"]
+ "required": [
+ "id"
+ ]
},
"provider": {
"description": "An identifier for the provider of this message, such as \"cfr\" or \"preview\".",
@@ -1178,8 +1225,14 @@
},
"additionalProperties": true,
"dependentRequired": {
- "content": ["id", "template"],
- "template": ["id", "content"]
+ "content": [
+ "id",
+ "template"
+ ],
+ "template": [
+ "id",
+ "content"
+ ]
}
},
"localizedText": {
@@ -1190,7 +1243,9 @@
"type": "string"
}
},
- "required": ["string_id"]
+ "required": [
+ "string_id"
+ ]
},
"localizableText": {
"description": "Either a raw string or an object containing the string_id of the localized text",
@@ -1217,10 +1272,14 @@
"properties": {
"template": {
"type": "string",
- "enum": ["cfr_urlbar_chiclet"]
+ "enum": [
+ "cfr_urlbar_chiclet"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/CFRUrlbarChiclet"
@@ -1232,10 +1291,15 @@
"properties": {
"template": {
"type": "string",
- "enum": ["cfr_doorhanger", "milestone_message"]
+ "enum": [
+ "cfr_doorhanger",
+ "milestone_message"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/ExtensionDoorhanger"
@@ -1247,10 +1311,14 @@
"properties": {
"template": {
"type": "string",
- "enum": ["infobar"]
+ "enum": [
+ "infobar"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/InfoBar"
@@ -1262,10 +1330,14 @@
"properties": {
"template": {
"type": "string",
- "enum": ["pb_newtab"]
+ "enum": [
+ "pb_newtab"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/NewtabPromoMessage"
@@ -1277,10 +1349,15 @@
"properties": {
"template": {
"type": "string",
- "enum": ["spotlight", "feature_callout"]
+ "enum": [
+ "spotlight",
+ "feature_callout"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/Spotlight"
@@ -1292,10 +1369,14 @@
"properties": {
"template": {
"type": "string",
- "enum": ["toast_notification"]
+ "enum": [
+ "toast_notification"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/ToastNotification"
@@ -1307,10 +1388,14 @@
"properties": {
"template": {
"type": "string",
- "enum": ["toolbar_badge"]
+ "enum": [
+ "toolbar_badge"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/ToolbarBadgeMessage"
@@ -1322,29 +1407,18 @@
"properties": {
"template": {
"type": "string",
- "enum": ["update_action"]
+ "enum": [
+ "update_action"
+ ]
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/UpdateAction"
}
- },
- {
- "if": {
- "type": "object",
- "properties": {
- "template": {
- "type": "string",
- "enum": ["whatsnew_panel_message"]
- }
- },
- "required": ["template"]
- },
- "then": {
- "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/WhatsNewMessage"
- }
}
]
},
@@ -1364,7 +1438,10 @@
}
}
},
- "required": ["template", "messages"]
+ "required": [
+ "template",
+ "messages"
+ ]
}
}
}
diff --git a/browser/components/asrouter/content-src/schemas/make-schemas.py b/browser/components/asrouter/content-src/schemas/make-schemas.py
index f66490f23a..1f677cab28 100755
--- a/browser/components/asrouter/content-src/schemas/make-schemas.py
+++ b/browser/components/asrouter/content-src/schemas/make-schemas.py
@@ -83,9 +83,6 @@ SCHEMAS = [
"UpdateAction": (
SCHEMA_DIR / "OnboardingMessage" / "UpdateAction.schema.json"
),
- "WhatsNewMessage": (
- SCHEMA_DIR / "OnboardingMessage" / "WhatsNewMessage.schema.json"
- ),
},
bundle_common=True,
test_corpus={
diff --git a/browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json b/browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json
deleted file mode 100644
index 26e795d068..0000000000
--- a/browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json
+++ /dev/null
@@ -1,73 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2019-09/schema",
- "$id": "file:///WhatsNewMessage.schema.json",
- "title": "WhatsNewMessage",
- "description": "A template for the messages that appear in the What's New panel.",
- "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }],
- "type": "object",
- "properties": {
- "content": {
- "type": "object",
- "properties": {
- "layout": {
- "description": "Different message layouts",
- "enum": ["tracking-protections"]
- },
- "bucket_id": {
- "type": "string",
- "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting."
- },
- "published_date": {
- "type": "integer",
- "description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published."
- },
- "title": {
- "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
- "description": "Id of localized string or message override of What's New message title"
- },
- "subtitle": {
- "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
- "description": "Id of localized string or message override of What's New message subtitle"
- },
- "body": {
- "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
- "description": "Id of localized string or message override of What's New message body"
- },
- "link_text": {
- "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
- "description": "(optional) Id of localized string or message override of What's New message link text"
- },
- "cta_url": {
- "description": "Target URL for the What's New message.",
- "type": "string",
- "format": "moz-url-format"
- },
- "cta_type": {
- "description": "Type of url open action",
- "enum": ["OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PROTECTION_REPORT"]
- },
- "cta_where": {
- "description": "How to open the cta: new window, tab, focused, unfocused.",
- "enum": ["current", "tabshifted", "tab", "save", "window"]
- },
- "icon_url": {
- "description": "(optional) URL for the What's New message icon.",
- "type": "string",
- "format": "uri"
- },
- "icon_alt": {
- "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
- "description": "Alt text for image."
- }
- },
- "additionalProperties": true,
- "required": ["published_date", "title", "body", "cta_url", "bucket_id"]
- },
- "template": {
- "type": "string",
- "const": "whatsnew_panel_message"
- }
- },
- "required": ["order"],
- "additionalProperties": true
-}
diff --git a/browser/components/asrouter/content/asrouter-admin.bundle.js b/browser/components/asrouter/content/asrouter-admin.bundle.js
index b38d551a17..e5a1e6dd6a 100644
--- a/browser/components/asrouter/content/asrouter-admin.bundle.js
+++ b/browser/components/asrouter/content/asrouter-admin.bundle.js
@@ -16,15 +16,13 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ ASRouterUtils: () => (/* binding */ ASRouterUtils)
/* harmony export */ });
-/* harmony import */ var _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var _newtab_common_Actions_sys_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
+/* harmony import */ var _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
+/* harmony import */ var _newtab_common_Actions_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system
-// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system
const ASRouterUtils = {
@@ -46,54 +44,54 @@ const ASRouterUtils = {
},
blockById(id, options) {
return ASRouterUtils.sendMessage({
- type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.BLOCK_MESSAGE_BY_ID,
+ type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.BLOCK_MESSAGE_BY_ID,
data: { id, ...options },
});
},
modifyMessageJson(content) {
return ASRouterUtils.sendMessage({
- type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.MODIFY_MESSAGE_JSON,
+ type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.MODIFY_MESSAGE_JSON,
data: { content },
});
},
executeAction(button_action) {
return ASRouterUtils.sendMessage({
- type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.USER_ACTION,
+ type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.USER_ACTION,
data: button_action,
});
},
unblockById(id) {
return ASRouterUtils.sendMessage({
- type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.UNBLOCK_MESSAGE_BY_ID,
+ type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.UNBLOCK_MESSAGE_BY_ID,
data: { id },
});
},
blockBundle(bundle) {
return ASRouterUtils.sendMessage({
- type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.BLOCK_BUNDLE,
+ type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.BLOCK_BUNDLE,
data: { bundle },
});
},
unblockBundle(bundle) {
return ASRouterUtils.sendMessage({
- type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.UNBLOCK_BUNDLE,
+ type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.UNBLOCK_BUNDLE,
data: { bundle },
});
},
overrideMessage(id) {
return ASRouterUtils.sendMessage({
- type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.OVERRIDE_MESSAGE,
+ type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.OVERRIDE_MESSAGE,
data: { id },
});
},
editState(key, value) {
return ASRouterUtils.sendMessage({
- type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.EDIT_STATE,
+ type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.EDIT_STATE,
data: { [key]: value },
});
},
sendTelemetry(ping) {
- return ASRouterUtils.sendMessage(_newtab_common_Actions_sys_mjs__WEBPACK_IMPORTED_MODULE_1__.actionCreators.ASRouterUserEvent(ping));
+ return ASRouterUtils.sendMessage(_newtab_common_Actions_mjs__WEBPACK_IMPORTED_MODULE_1__.actionCreators.ASRouterUserEvent(ping));
},
getPreviewEndpoint() {
return null;
@@ -124,7 +122,6 @@ const MESSAGE_TYPE_LIST = [
"PBNEWTAB_MESSAGE_REQUEST",
"DOORHANGER_TELEMETRY",
"TOOLBAR_BADGE_TELEMETRY",
- "TOOLBAR_PANEL_TELEMETRY",
"MOMENTS_PAGE_TELEMETRY",
"INFOBAR_TELEMETRY",
"SPOTLIGHT_TELEMETRY",
@@ -142,9 +139,7 @@ const MESSAGE_TYPE_LIST = [
"EVALUATE_JEXL_EXPRESSION",
"EXPIRE_QUERY_CACHE",
"FORCE_ATTRIBUTION",
- "FORCE_WHATSNEW_PANEL",
"FORCE_PRIVATE_BROWSING_WINDOW",
- "CLOSE_WHATSNEW_PANEL",
"OVERRIDE_MESSAGE",
"MODIFY_MESSAGE_JSON",
"RESET_PROVIDER_PREF",
@@ -181,6 +176,8 @@ __webpack_require__.r(__webpack_exports__);
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// This file is accessed from both content and system scopes.
+
const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser";
@@ -337,6 +334,7 @@ for (const type of [
"UPDATE_PINNED_SEARCH_SHORTCUTS",
"UPDATE_SEARCH_SHORTCUTS",
"UPDATE_SECTION_PREFS",
+ "WALLPAPERS_SET",
"WEBEXT_CLICK",
"WEBEXT_DISMISS",
]) {
@@ -550,8 +548,11 @@ function DiscoveryStreamLoadedContent(
return importContext === UI_CODE ? AlsoToMain(action) : action;
}
-function SetPref(name, value, importContext = globalImportContext) {
- const action = { type: actionTypes.SET_PREF, data: { name, value } };
+function SetPref(prefName, value, importContext = globalImportContext) {
+ const action = {
+ type: actionTypes.SET_PREF,
+ data: { name: prefName, value },
+ };
return importContext === UI_CODE ? AlsoToMain(action) : action;
}
@@ -961,7 +962,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ ToggleMessageJSON: () => (/* binding */ ToggleMessageJSON),
/* harmony export */ TogglePrefCheckbox: () => (/* binding */ TogglePrefCheckbox),
/* harmony export */ ToggleStoryButton: () => (/* binding */ ToggleStoryButton),
-/* harmony export */ renderASRouterAdmin: () => (/* binding */ renderASRouterAdmin)
+/* harmony export */ renderASRouterAdmin: () => (/* binding */ renderASRouterAdmin),
+/* harmony export */ toBinary: () => (/* binding */ toBinary)
/* harmony export */ });
/* harmony import */ var _asrouter_utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
@@ -985,6 +987,16 @@ function _extends() { _extends = Object.assign ? Object.assign.bind() : function
const Row = props => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement("tr", _extends({
className: "message-item"
}, props), props.children);
+
+// Convert a UTF-8 string to a string in which only one byte of each
+// 16-bit unit is occupied. This is necessary to comply with `btoa` API constraints.
+function toBinary(string) {
+ const codeUnits = new Uint16Array(string.length);
+ for (let i = 0; i < codeUnits.length; i++) {
+ codeUnits[i] = string.charCodeAt(i);
+ }
+ return btoa(String.fromCharCode(...Array.from(new Uint8Array(codeUnits.buffer))));
+}
function relativeTime(timestamp) {
if (!timestamp) {
return "";
@@ -1427,7 +1439,7 @@ class ASRouterAdminInner extends (react__WEBPACK_IMPORTED_MODULE_1___default().P
className: "button modify",
onClick: () => this.modifyJson(msg)
}, "Modify"), aboutMessagePreviewSupported ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(_CopyButton__WEBPACK_IMPORTED_MODULE_4__.CopyButton, {
- transformer: text => `about:messagepreview?json=${encodeURIComponent(btoa(text))}`,
+ transformer: text => `about:messagepreview?json=${encodeURIComponent(toBinary(text))}`,
label: "Share",
copiedLabel: "Copied!",
inputSelector: `#${msg.id}-textarea`,
diff --git a/browser/components/asrouter/docs/targeting-attributes.md b/browser/components/asrouter/docs/targeting-attributes.md
index 89c5a6b6c6..b0049a4f1b 100644
--- a/browser/components/asrouter/docs/targeting-attributes.md
+++ b/browser/components/asrouter/docs/targeting-attributes.md
@@ -44,7 +44,6 @@ Please note that some targeting attributes require stricter controls on the tele
* [isFxASignedIn](#isFxASignedIn)
* [isMajorUpgrade](#ismajorupgrade)
* [isRTAMO](#isrtamo)
-* [isWhatsNewPanelEnabled](#iswhatsnewpanelenabled)
* [launchOnLoginEnabled](#launchonloginenabled)
* [locale](#locale)
* [localeLanguageCode](#localelanguagecode)
@@ -630,16 +629,6 @@ Boolean pref that gets set the first time the user opens the FxA toolbar panel
declare const hasAccessedFxAPanel: boolean;
```
-### `isWhatsNewPanelEnabled`
-
-Boolean pref that controls if the What's New panel feature is enabled
-
-#### Definition
-
-```ts
-declare const isWhatsNewPanelEnabled: boolean;
-```
-
### `totalBlockedCount`
Total number of events from the content blocking database
diff --git a/browser/components/asrouter/modules/ASRouter.sys.mjs b/browser/components/asrouter/modules/ASRouter.sys.mjs
index e46c57f685..b36a9023e1 100644
--- a/browser/components/asrouter/modules/ASRouter.sys.mjs
+++ b/browser/components/asrouter/modules/ASRouter.sys.mjs
@@ -55,7 +55,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
Spotlight: "resource:///modules/asrouter/Spotlight.sys.mjs",
ToastNotification: "resource:///modules/asrouter/ToastNotification.sys.mjs",
ToolbarBadgeHub: "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs",
- ToolbarPanelHub: "resource:///modules/asrouter/ToolbarPanelHub.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetters(lazy, {
@@ -67,7 +66,7 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => {
);
return new Logger("ASRouter");
});
-import { actionCreators as ac } from "resource://activity-stream/common/Actions.sys.mjs";
+import { actionCreators as ac } from "resource://activity-stream/common/Actions.mjs";
import { MESSAGING_EXPERIMENTS_DEFAULT_FEATURES } from "resource:///modules/asrouter/MessagingExperimentConstants.sys.mjs";
import { CFRMessageProvider } from "resource:///modules/asrouter/CFRMessageProvider.sys.mjs";
import { OnboardingMessageProvider } from "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs";
@@ -620,7 +619,6 @@ export class _ASRouter {
this._onLocaleChanged = this._onLocaleChanged.bind(this);
this.isUnblockedMessage = this.isUnblockedMessage.bind(this);
this.unblockAll = this.unblockAll.bind(this);
- this.forceWNPanel = this.forceWNPanel.bind(this);
this._onExperimentEnrollmentsUpdated =
this._onExperimentEnrollmentsUpdated.bind(this);
this.forcePBWindow = this.forcePBWindow.bind(this);
@@ -995,10 +993,6 @@ export class _ASRouter {
unblockMessageById: this.unblockMessageById,
sendTelemetry: this.sendTelemetry,
});
- lazy.ToolbarPanelHub.init(this.waitForInitialized, {
- getMessages: this.handleMessageRequest,
- sendTelemetry: this.sendTelemetry,
- });
lazy.MomentsPageHub.init(this.waitForInitialized, {
handleMessageRequest: this.handleMessageRequest,
addImpression: this.addImpression,
@@ -1055,7 +1049,6 @@ export class _ASRouter {
lazy.ASRouterPreferences.removeListener(this.onPrefChange);
lazy.ASRouterPreferences.uninit();
- lazy.ToolbarPanelHub.uninit();
lazy.ToolbarBadgeHub.uninit();
lazy.MomentsPageHub.uninit();
@@ -1309,16 +1302,6 @@ export class _ASRouter {
return true;
}
- async _extraTemplateStrings(originalMessage) {
- let extraTemplateStrings;
- let localProvider = this._findProvider(originalMessage.provider);
- if (localProvider && localProvider.getExtraAttributes) {
- extraTemplateStrings = await localProvider.getExtraAttributes();
- }
-
- return extraTemplateStrings;
- }
-
_findProvider(providerID) {
return this._localProviders[
this.state.providers.find(i => i.id === providerID).localProvider
@@ -1346,11 +1329,6 @@ export class _ASRouter {
}
switch (message.template) {
- case "whatsnew_panel_message":
- if (force) {
- lazy.ToolbarPanelHub.forceShowMessage(browser, message);
- }
- break;
case "cfr_doorhanger":
case "milestone_message":
if (force) {
@@ -2005,29 +1983,6 @@ export class _ASRouter {
);
}
- async forceWNPanel(browser) {
- let win = browser.ownerGlobal;
- await lazy.ToolbarPanelHub.enableToolbarButton();
-
- win.PanelUI.showSubView(
- "PanelUI-whatsNew",
- win.document.getElementById("whats-new-menu-button")
- );
-
- let panel = win.document.getElementById("customizationui-widget-panel");
- // Set the attribute to keep the panel open
- panel.setAttribute("noautohide", true);
- }
-
- async closeWNPanel(browser) {
- let win = browser.ownerGlobal;
- let panel = win.document.getElementById("customizationui-widget-panel");
- // Set the attribute to allow the panel to close
- panel.setAttribute("noautohide", false);
- // Removing the button is enough to close the panel.
- await lazy.ToolbarPanelHub._hideToolbarButton(win);
- }
-
async _onExperimentEnrollmentsUpdated() {
const experimentProvider = this.state.providers.find(
p => p.id === "messaging-experiments"
diff --git a/browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs b/browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs
index c2f5fcd884..8aa4d7dbc9 100644
--- a/browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs
+++ b/browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs
@@ -4,7 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ASRouterPreferences } from "resource:///modules/asrouter/ASRouterPreferences.sys.mjs";
-import { MESSAGE_TYPE_HASH as msg } from "resource:///modules/asrouter/ActorConstants.sys.mjs";
+import { MESSAGE_TYPE_HASH as msg } from "resource:///modules/asrouter/ActorConstants.mjs";
export class ASRouterParentProcessMessageHandler {
constructor({
@@ -27,7 +27,6 @@ export class ASRouterParentProcessMessageHandler {
switch (type) {
case msg.INFOBAR_TELEMETRY:
case msg.TOOLBAR_BADGE_TELEMETRY:
- case msg.TOOLBAR_PANEL_TELEMETRY:
case msg.MOMENTS_PAGE_TELEMETRY:
case msg.DOORHANGER_TELEMETRY:
case msg.SPOTLIGHT_TELEMETRY:
@@ -128,12 +127,6 @@ export class ASRouterParentProcessMessageHandler {
case msg.FORCE_PRIVATE_BROWSING_WINDOW: {
return this._router.forcePBWindow(browser, data.message);
}
- case msg.FORCE_WHATSNEW_PANEL: {
- return this._router.forceWNPanel(browser);
- }
- case msg.CLOSE_WHATSNEW_PANEL: {
- return this._router.closeWNPanel(browser);
- }
case msg.MODIFY_MESSAGE_JSON: {
return this._router.routeCFRMessage(data.content, browser, data, true);
}
diff --git a/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs b/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs
index d76b303fc6..9773eda270 100644
--- a/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs
+++ b/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs
@@ -74,12 +74,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
- "isWhatsNewPanelEnabled",
- "browser.messaging-system.whatsNewPanel.enabled",
- false
-);
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
"hasAccessedFxAPanel",
"identity.fxaccounts.toolbar.accessed",
false
@@ -704,9 +698,6 @@ const TargetingGetters = {
get hasAccessedFxAPanel() {
return lazy.hasAccessedFxAPanel;
},
- get isWhatsNewPanelEnabled() {
- return lazy.isWhatsNewPanelEnabled;
- },
get userPrefs() {
return {
cfrFeatures: lazy.cfrFeaturesUserPref,
diff --git a/browser/components/asrouter/modules/ActorConstants.sys.mjs b/browser/components/asrouter/modules/ActorConstants.mjs
index 4c996552ab..c1c18e006e 100644
--- a/browser/components/asrouter/modules/ActorConstants.sys.mjs
+++ b/browser/components/asrouter/modules/ActorConstants.mjs
@@ -12,7 +12,6 @@ export const MESSAGE_TYPE_LIST = [
"PBNEWTAB_MESSAGE_REQUEST",
"DOORHANGER_TELEMETRY",
"TOOLBAR_BADGE_TELEMETRY",
- "TOOLBAR_PANEL_TELEMETRY",
"MOMENTS_PAGE_TELEMETRY",
"INFOBAR_TELEMETRY",
"SPOTLIGHT_TELEMETRY",
@@ -30,9 +29,7 @@ export const MESSAGE_TYPE_LIST = [
"EVALUATE_JEXL_EXPRESSION",
"EXPIRE_QUERY_CACHE",
"FORCE_ATTRIBUTION",
- "FORCE_WHATSNEW_PANEL",
"FORCE_PRIVATE_BROWSING_WINDOW",
- "CLOSE_WHATSNEW_PANEL",
"OVERRIDE_MESSAGE",
"MODIFY_MESSAGE_JSON",
"RESET_PROVIDER_PREF",
diff --git a/browser/components/asrouter/modules/MomentsPageHub.sys.mjs b/browser/components/asrouter/modules/MomentsPageHub.sys.mjs
index 84fee3b517..3a59e9d450 100644
--- a/browser/components/asrouter/modules/MomentsPageHub.sys.mjs
+++ b/browser/components/asrouter/modules/MomentsPageHub.sys.mjs
@@ -165,7 +165,7 @@ export class _MomentsPageHub {
}
/**
- * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate
+ * MomentsPageHub - singleton instance of _MomentsPageHub that can initiate
* message requests and render messages.
*/
export const MomentsPageHub = new _MomentsPageHub();
diff --git a/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs b/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
index ceded6b755..3cfbbb3f34 100644
--- a/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
+++ b/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
@@ -1210,6 +1210,106 @@ const BASE_MESSAGES = () => [
id: "defaultBrowserCheck",
},
},
+ {
+ id: "SET_DEFAULT_BROWSER_GUIDANCE_NOTIFICATION_WIN10",
+ template: "toast_notification",
+ content: {
+ title: {
+ string_id: "default-browser-guidance-notification-title",
+ },
+ body: {
+ string_id:
+ "default-browser-guidance-notification-body-instruction-win10",
+ },
+ launch_action: {
+ type: "OPEN_URL",
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/win-set-firefox-default-browser",
+ where: "tabshifted",
+ },
+ },
+ requireInteraction: true,
+ actions: [
+ {
+ action: "info-page",
+ title: {
+ string_id: "default-browser-guidance-notification-info-page",
+ },
+ launch_action: {
+ type: "OPEN_URL",
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/win-set-firefox-default-browser",
+ where: "tabshifted",
+ },
+ },
+ },
+ {
+ action: "dismiss",
+ title: {
+ string_id: "default-browser-guidance-notification-dismiss",
+ },
+ windowsSystemActivationType: true,
+ },
+ ],
+ tag: "set-default-guidance-notification",
+ },
+ // Both Windows 10 and 11 return `os.windowsVersion == 10.0`. We limit to
+ // only Windows 10 with `os.windowsBuildNumber < 22000`. We need this due to
+ // Windows 10 and 11 having substantively different UX for Windows Settings.
+ targeting:
+ "os.isWindows && os.windowsVersion >= 10.0 && os.windowsBuildNumber < 22000",
+ trigger: { id: "deeplinkedToWindowsSettingsUI" },
+ },
+ {
+ id: "SET_DEFAULT_BROWSER_GUIDANCE_NOTIFICATION_WIN11",
+ template: "toast_notification",
+ content: {
+ title: {
+ string_id: "default-browser-guidance-notification-title",
+ },
+ body: {
+ string_id:
+ "default-browser-guidance-notification-body-instruction-win11",
+ },
+ launch_action: {
+ type: "OPEN_URL",
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/win-set-firefox-default-browser",
+ where: "tabshifted",
+ },
+ },
+ requireInteraction: true,
+ actions: [
+ {
+ action: "info-page",
+ title: {
+ string_id: "default-browser-guidance-notification-info-page",
+ },
+ launch_action: {
+ type: "OPEN_URL",
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/win-set-firefox-default-browser",
+ where: "tabshifted",
+ },
+ },
+ },
+ {
+ action: "dismiss",
+ title: {
+ string_id: "default-browser-guidance-notification-dismiss",
+ },
+ windowsSystemActivationType: true,
+ },
+ ],
+ tag: "set-default-guidance-notification",
+ },
+ // Both Windows 10 and 11 return `os.windowsVersion == 10.0`. We limit to
+ // only Windows 11 with `os.windowsBuildNumber >= 22000`. We need this due to
+ // Windows 10 and 11 having substantively different UX for Windows Settings.
+ targeting:
+ "os.isWindows && os.windowsVersion >= 10.0 && os.windowsBuildNumber >= 22000",
+ trigger: { id: "deeplinkedToWindowsSettingsUI" },
+ },
];
// Eventually, move Feature Callout messages to their own provider
diff --git a/browser/components/asrouter/modules/PanelTestProvider.sys.mjs b/browser/components/asrouter/modules/PanelTestProvider.sys.mjs
index 7a7ff1e1fc..5180e2e6a2 100644
--- a/browser/components/asrouter/modules/PanelTestProvider.sys.mjs
+++ b/browser/components/asrouter/modules/PanelTestProvider.sys.mjs
@@ -20,134 +20,6 @@ const MESSAGES = () => [
trigger: { id: "momentsUpdate" },
},
{
- id: "WHATS_NEW_FINGERPRINTER_COUNTER_ALT",
- template: "whatsnew_panel_message",
- order: 6,
- content: {
- bucket_id: "WHATS_NEW_72",
- published_date: 1574776601000,
- title: "Title",
- icon_url:
- "chrome://activity-stream/content/data/content/assets/protection-report-icon.png",
- icon_alt: { string_id: "cfr-badge-reader-label-newfeature" },
- body: "Message body",
- link_text: "Click here",
- cta_url: "about:blank",
- cta_type: "OPEN_PROTECTION_REPORT",
- },
- targeting: `firefoxVersion >= 72`,
- trigger: { id: "whatsNewPanelOpened" },
- },
- {
- id: "WHATS_NEW_70_1",
- template: "whatsnew_panel_message",
- order: 3,
- content: {
- bucket_id: "WHATS_NEW_70_1",
- published_date: 1560969794394,
- title: "Protection Is Our Focus",
- icon_url:
- "chrome://activity-stream/content/data/content/assets/whatsnew-send-icon.png",
- icon_alt: "Firefox Send Logo",
- body: "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.",
- cta_url: "https://blog.mozilla.org/",
- cta_type: "OPEN_URL",
- },
- targeting: `firefoxVersion > 69`,
- trigger: { id: "whatsNewPanelOpened" },
- },
- {
- id: "WHATS_NEW_70_2",
- template: "whatsnew_panel_message",
- order: 1,
- content: {
- bucket_id: "WHATS_NEW_70_1",
- published_date: 1560969794394,
- title: "Another thing new in Firefox 70",
- body: "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.",
- link_text: "Learn more on our blog",
- cta_url: "https://blog.mozilla.org/",
- cta_type: "OPEN_URL",
- },
- targeting: `firefoxVersion > 69`,
- trigger: { id: "whatsNewPanelOpened" },
- },
- {
- id: "WHATS_NEW_SEARCH_SHORTCUTS_84",
- template: "whatsnew_panel_message",
- order: 2,
- content: {
- bucket_id: "WHATS_NEW_SEARCH_SHORTCUTS_84",
- published_date: 1560969794394,
- title: "Title",
- icon_url: "chrome://global/skin/icons/check.svg",
- icon_alt: "",
- body: "Message content",
- cta_url:
- "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/search-shortcuts",
- cta_type: "OPEN_URL",
- link_text: "Click here",
- },
- targeting: "firefoxVersion >= 84",
- trigger: {
- id: "whatsNewPanelOpened",
- },
- },
- {
- id: "WHATS_NEW_PIONEER_82",
- template: "whatsnew_panel_message",
- order: 1,
- content: {
- bucket_id: "WHATS_NEW_PIONEER_82",
- published_date: 1603152000000,
- title: "Put your data to work for a better internet",
- body: "Contribute your data to Mozilla's Pioneer program to help researchers understand pressing technology issues like misinformation, data privacy, and ethical AI.",
- cta_url: "about:blank",
- cta_where: "tab",
- cta_type: "OPEN_ABOUT_PAGE",
- link_text: "Join Pioneer",
- },
- targeting: "firefoxVersion >= 82",
- trigger: {
- id: "whatsNewPanelOpened",
- },
- },
- {
- id: "WHATS_NEW_MEDIA_SESSION_82",
- template: "whatsnew_panel_message",
- order: 3,
- content: {
- bucket_id: "WHATS_NEW_MEDIA_SESSION_82",
- published_date: 1603152000000,
- title: "Title",
- body: "Message content",
- cta_url:
- "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/media-keyboard-control",
- cta_type: "OPEN_URL",
- link_text: "Click here",
- },
- targeting: "firefoxVersion >= 82",
- trigger: {
- id: "whatsNewPanelOpened",
- },
- },
- {
- id: "WHATS_NEW_69_1",
- template: "whatsnew_panel_message",
- order: 1,
- content: {
- bucket_id: "WHATS_NEW_69_1",
- published_date: 1557346235089,
- title: "Something new in Firefox 69",
- body: "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.",
- link_text: "Learn more on our blog",
- cta_url: "https://blog.mozilla.org/",
- cta_type: "OPEN_URL",
- },
- targeting: `firefoxVersion > 68`,
- trigger: { id: "whatsNewPanelOpened" },
- },
- {
id: "PERSONALIZED_CFR_MESSAGE",
template: "cfr_doorhanger",
groups: ["cfr"],
diff --git a/browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs b/browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs
index 57fd104f19..36f7ca5005 100644
--- a/browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs
+++ b/browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs
@@ -10,7 +10,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
- ToolbarPanelHub: "resource:///modules/asrouter/ToolbarPanelHub.sys.mjs",
});
let notificationsByWindow = new WeakMap();
@@ -19,9 +18,6 @@ export class _ToolbarBadgeHub {
constructor() {
this.id = "toolbar-badge-hub";
this.state = {};
- this.prefs = {
- WHATSNEW_TOOLBAR_PANEL: "browser.messaging-system.whatsNewPanel.enabled",
- };
this.removeAllNotifications = this.removeAllNotifications.bind(this);
this.removeToolbarNotification = this.removeToolbarNotification.bind(this);
this.addToolbarNotification = this.addToolbarNotification.bind(this);
@@ -62,34 +58,12 @@ export class _ToolbarBadgeHub {
triggerId: "toolbarBadgeUpdate",
template: "toolbar_badge",
});
- // Listen for pref changes that could trigger new badges
- Services.prefs.addObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this);
- }
-
- observe(aSubject, aTopic, aPrefName) {
- switch (aPrefName) {
- case this.prefs.WHATSNEW_TOOLBAR_PANEL:
- this.messageRequest({
- triggerId: "toolbarBadgeUpdate",
- template: "toolbar_badge",
- });
- break;
- }
}
maybeInsertFTL(win) {
win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
}
- executeAction({ id }) {
- switch (id) {
- case "show-whatsnew-button":
- lazy.ToolbarPanelHub.enableToolbarButton();
- lazy.ToolbarPanelHub.enableAppmenuButton();
- break;
- }
- }
-
_clearBadgeTimeout() {
if (this.state.showBadgeTimeoutId) {
lazy.clearTimeout(this.state.showBadgeTimeoutId);
@@ -153,9 +127,6 @@ export class _ToolbarBadgeHub {
addToolbarNotification(win, message) {
const document = win.browser.ownerDocument;
- if (message.content.action) {
- this.executeAction({ ...message.content.action, message_id: message.id });
- }
let toolbarbutton = document.getElementById(message.content.target);
if (toolbarbutton) {
const badge = toolbarbutton.querySelector(".toolbarbutton-badge");
@@ -211,12 +182,6 @@ export class _ToolbarBadgeHub {
}
registerBadgeToAllWindows(message) {
- if (message.template === "update_action") {
- this.executeAction({ ...message.content.action, message_id: message.id });
- // No badge to set only an action to execute
- return;
- }
-
lazy.EveryWindow.registerCallback(
this.id,
win => {
@@ -297,7 +262,6 @@ export class _ToolbarBadgeHub {
this.state = {};
this._initialized = false;
notificationsByWindow = new WeakMap();
- Services.prefs.removeObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this);
}
}
diff --git a/browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs b/browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs
deleted file mode 100644
index 519bca8a89..0000000000
--- a/browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs
+++ /dev/null
@@ -1,544 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-const lazy = {};
-
-// We use importESModule here instead of static import so that
-// the Karma test environment won't choke on this module. This
-// is because the Karma test environment already stubs out
-// XPCOMUtils. That environment overrides importESModule to be a no-op
-// (which can't be done for a static import statement).
-
-// eslint-disable-next-line mozilla/use-static-import
-const { XPCOMUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/XPCOMUtils.sys.mjs"
-);
-
-ChromeUtils.defineESModuleGetters(lazy, {
- EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
- PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
- PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
- SpecialMessageActions:
- "resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
- RemoteL10n: "resource:///modules/asrouter/RemoteL10n.sys.mjs",
-});
-
-XPCOMUtils.defineLazyServiceGetter(
- lazy,
- "TrackingDBService",
- "@mozilla.org/tracking-db-service;1",
- "nsITrackingDBService"
-);
-
-const idToTextMap = new Map([
- [Ci.nsITrackingDBService.TRACKERS_ID, "trackerCount"],
- [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookieCount"],
- [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominerCount"],
- [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinterCount"],
- [Ci.nsITrackingDBService.SOCIAL_ID, "socialCount"],
-]);
-
-const WHATSNEW_ENABLED_PREF = "browser.messaging-system.whatsNewPanel.enabled";
-const PROTECTIONS_PANEL_INFOMSG_PREF =
- "browser.protections_panel.infoMessage.seen";
-
-const TOOLBAR_BUTTON_ID = "whats-new-menu-button";
-const APPMENU_BUTTON_ID = "appMenu-whatsnew-button";
-
-const BUTTON_STRING_ID = "cfr-whatsnew-button";
-const WHATS_NEW_PANEL_SELECTOR = "PanelUI-whatsNew-message-container";
-
-export class _ToolbarPanelHub {
- constructor() {
- this.triggerId = "whatsNewPanelOpened";
- this._showAppmenuButton = this._showAppmenuButton.bind(this);
- this._hideAppmenuButton = this._hideAppmenuButton.bind(this);
- this._showToolbarButton = this._showToolbarButton.bind(this);
- this._hideToolbarButton = this._hideToolbarButton.bind(this);
-
- this.state = {};
- this._initialized = false;
- }
-
- async init(waitForInitialized, { getMessages, sendTelemetry }) {
- if (this._initialized) {
- return;
- }
-
- this._initialized = true;
- this._getMessages = getMessages;
- this._sendTelemetry = sendTelemetry;
- // Wait for ASRouter messages to become available in order to know
- // if we can show the What's New panel
- await waitForInitialized;
- // Enable the application menu button so that the user can access
- // the panel outside of the toolbar button
- await this.enableAppmenuButton();
-
- this.state = {
- protectionPanelMessageSeen: Services.prefs.getBoolPref(
- PROTECTIONS_PANEL_INFOMSG_PREF,
- false
- ),
- };
- }
-
- uninit() {
- this._initialized = false;
- lazy.EveryWindow.unregisterCallback(TOOLBAR_BUTTON_ID);
- lazy.EveryWindow.unregisterCallback(APPMENU_BUTTON_ID);
- }
-
- get messages() {
- return this._getMessages({
- template: "whatsnew_panel_message",
- triggerId: "whatsNewPanelOpened",
- returnAll: true,
- });
- }
-
- toggleWhatsNewPref(event) {
- // Checkbox onclick handler gets called before the checkbox state gets toggled,
- // so we have to call it with the opposite value.
- let newValue = !event.target.checked;
- Services.prefs.setBoolPref(WHATSNEW_ENABLED_PREF, newValue);
-
- this.sendUserEventTelemetry(
- event.target.ownerGlobal,
- "WNP_PREF_TOGGLE",
- // Message id is not applicable in this case, the notification state
- // is not related to a particular message
- { id: "n/a" },
- { value: { prefValue: newValue } }
- );
- }
-
- maybeInsertFTL(win) {
- win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
- win.MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
- win.MozXULElement.insertFTLIfNeeded("toolkit/branding/accounts.ftl");
- }
-
- maybeLoadCustomElement(win) {
- if (!win.customElements.get("remote-text")) {
- Services.scriptloader.loadSubScript(
- "resource://activity-stream/data/custom-elements/paragraph.js",
- win
- );
- }
- }
-
- // Turns on the Appmenu (hamburger menu) button for all open windows and future windows.
- async enableAppmenuButton() {
- if ((await this.messages).length) {
- lazy.EveryWindow.registerCallback(
- APPMENU_BUTTON_ID,
- this._showAppmenuButton,
- this._hideAppmenuButton
- );
- }
- }
-
- // Removes the button from the Appmenu.
- // Only used in tests.
- disableAppmenuButton() {
- lazy.EveryWindow.unregisterCallback(APPMENU_BUTTON_ID);
- }
-
- // Turns on the Toolbar button for all open windows and future windows.
- async enableToolbarButton() {
- if ((await this.messages).length) {
- lazy.EveryWindow.registerCallback(
- TOOLBAR_BUTTON_ID,
- this._showToolbarButton,
- this._hideToolbarButton
- );
- }
- }
-
- // When the panel is hidden we want to run some cleanup
- _onPanelHidden(win) {
- const panelContainer = win.document.getElementById(
- "customizationui-widget-panel"
- );
- // When the panel is hidden we want to remove any toolbar buttons that
- // might have been added as an entry point to the panel
- const removeToolbarButton = () => {
- lazy.EveryWindow.unregisterCallback(TOOLBAR_BUTTON_ID);
- };
- if (!panelContainer) {
- return;
- }
- panelContainer.addEventListener("popuphidden", removeToolbarButton, {
- once: true,
- });
- }
-
- // Newer messages first and use `order` field to decide between messages
- // with the same timestamp
- _sortWhatsNewMessages(m1, m2) {
- // Sort by published_date in descending order.
- if (m1.content.published_date === m2.content.published_date) {
- // Ascending order
- return m1.order - m2.order;
- }
- if (m1.content.published_date > m2.content.published_date) {
- return -1;
- }
- return 1;
- }
-
- // Render what's new messages into the panel.
- async renderMessages(win, doc, containerId, options = {}) {
- // Set the checked status of the footer checkbox
- let value = Services.prefs.getBoolPref(WHATSNEW_ENABLED_PREF);
- let checkbox = win.document.getElementById("panelMenu-toggleWhatsNew");
-
- checkbox.checked = value;
-
- this.maybeLoadCustomElement(win);
- const messages =
- (options.force && options.messages) ||
- (await this.messages).sort(this._sortWhatsNewMessages);
- const container = lazy.PanelMultiView.getViewNode(doc, containerId);
-
- if (messages) {
- // Targeting attribute state might have changed making new messages
- // available and old messages invalid, we need to refresh
- this.removeMessages(win, containerId);
- let previousDate = 0;
- // Get and store any variable part of the message content
- this.state.contentArguments = await this._contentArguments();
- for (let message of messages) {
- container.appendChild(
- this._createMessageElements(win, doc, message, previousDate)
- );
- previousDate = message.content.published_date;
- }
- }
-
- this._onPanelHidden(win);
-
- // Panel impressions are not associated with one particular message
- // but with a set of messages. We concatenate message ids and send them
- // back for every impression.
- const eventId = {
- id: messages
- .map(({ id }) => id)
- .sort()
- .join(","),
- };
- // Check `mainview` attribute to determine if the panel is shown as a
- // subview (inside the application menu) or as a toolbar dropdown.
- // https://searchfox.org/mozilla-central/rev/07f7390618692fa4f2a674a96b9b677df3a13450/browser/components/customizableui/PanelMultiView.jsm#1268
- const mainview = win.PanelUI.whatsNewPanel.hasAttribute("mainview");
- this.sendUserEventTelemetry(win, "IMPRESSION", eventId, {
- value: { view: mainview ? "toolbar_dropdown" : "application_menu" },
- });
- }
-
- removeMessages(win, containerId) {
- const doc = win.document;
- const messageNodes = lazy.PanelMultiView.getViewNode(
- doc,
- containerId
- ).querySelectorAll(".whatsNew-message");
- for (const messageNode of messageNodes) {
- messageNode.remove();
- }
- }
-
- /**
- * Dispatch the action defined in the message and user telemetry event.
- */
- _dispatchUserAction(win, message) {
- let url;
- try {
- // Set platform specific path variables for SUMO articles
- url = Services.urlFormatter.formatURL(message.content.cta_url);
- } catch (e) {
- console.error(e);
- url = message.content.cta_url;
- }
- lazy.SpecialMessageActions.handleAction(
- {
- type: message.content.cta_type,
- data: {
- args: url,
- where: message.content.cta_where || "tabshifted",
- },
- },
- win.browser
- );
-
- this.sendUserEventTelemetry(win, "CLICK", message);
- }
-
- /**
- * Attach event listener to dispatch message defined action.
- */
- _attachCommandListener(win, element, message) {
- // Add event listener for `mouseup` not to overlap with the
- // `mousedown` & `click` events dispatched from PanelMultiView.sys.mjs
- // https://searchfox.org/mozilla-central/rev/7531325c8660cfa61bf71725f83501028178cbb9/browser/components/customizableui/PanelMultiView.jsm#1830-1837
- element.addEventListener("mouseup", () => {
- this._dispatchUserAction(win, message);
- });
- element.addEventListener("keyup", e => {
- if (e.key === "Enter" || e.key === " ") {
- this._dispatchUserAction(win, message);
- }
- });
- }
-
- _createMessageElements(win, doc, message, previousDate) {
- const { content } = message;
- const messageEl = lazy.RemoteL10n.createElement(doc, "div");
- messageEl.classList.add("whatsNew-message");
-
- // Only render date if it is different from the one rendered before.
- if (content.published_date !== previousDate) {
- messageEl.appendChild(
- lazy.RemoteL10n.createElement(doc, "p", {
- classList: "whatsNew-message-date",
- content: new Date(content.published_date).toLocaleDateString(
- "default",
- {
- month: "long",
- day: "numeric",
- year: "numeric",
- }
- ),
- })
- );
- }
-
- const wrapperEl = lazy.RemoteL10n.createElement(doc, "div");
- wrapperEl.doCommand = () => this._dispatchUserAction(win, message);
- wrapperEl.classList.add("whatsNew-message-body");
- messageEl.appendChild(wrapperEl);
-
- if (content.icon_url) {
- wrapperEl.classList.add("has-icon");
- const iconEl = lazy.RemoteL10n.createElement(doc, "img");
- iconEl.src = content.icon_url;
- iconEl.classList.add("whatsNew-message-icon");
- if (content.icon_alt && content.icon_alt.string_id) {
- doc.l10n.setAttributes(iconEl, content.icon_alt.string_id);
- } else {
- iconEl.setAttribute("alt", content.icon_alt);
- }
- wrapperEl.appendChild(iconEl);
- }
-
- wrapperEl.appendChild(this._createMessageContent(win, doc, content));
-
- if (content.link_text) {
- const anchorEl = lazy.RemoteL10n.createElement(doc, "a", {
- classList: "text-link",
- content: content.link_text,
- });
- anchorEl.doCommand = () => this._dispatchUserAction(win, message);
- wrapperEl.appendChild(anchorEl);
- }
-
- // Attach event listener on entire message container
- this._attachCommandListener(win, messageEl, message);
-
- return messageEl;
- }
-
- /**
- * Return message title (optional subtitle) and body
- */
- _createMessageContent(win, doc, content) {
- const wrapperEl = new win.DocumentFragment();
-
- wrapperEl.appendChild(
- lazy.RemoteL10n.createElement(doc, "h2", {
- classList: "whatsNew-message-title",
- content: content.title,
- attributes: this.state.contentArguments,
- })
- );
-
- wrapperEl.appendChild(
- lazy.RemoteL10n.createElement(doc, "p", {
- content: content.body,
- classList: "whatsNew-message-content",
- attributes: this.state.contentArguments,
- })
- );
-
- return wrapperEl;
- }
-
- _createHeroElement(win, doc, message) {
- this.maybeLoadCustomElement(win);
-
- const messageEl = lazy.RemoteL10n.createElement(doc, "div");
- messageEl.setAttribute("id", "protections-popup-message");
- messageEl.classList.add("whatsNew-hero-message");
- const wrapperEl = lazy.RemoteL10n.createElement(doc, "div");
- wrapperEl.classList.add("whatsNew-message-body");
- messageEl.appendChild(wrapperEl);
-
- wrapperEl.appendChild(
- lazy.RemoteL10n.createElement(doc, "h2", {
- classList: "whatsNew-message-title",
- content: message.content.title,
- })
- );
- wrapperEl.appendChild(
- lazy.RemoteL10n.createElement(doc, "p", {
- classList: "protections-popup-content",
- content: message.content.body,
- })
- );
-
- if (message.content.link_text) {
- let linkEl = lazy.RemoteL10n.createElement(doc, "a", {
- classList: "text-link",
- content: message.content.link_text,
- });
- linkEl.disabled = true;
- wrapperEl.appendChild(linkEl);
- this._attachCommandListener(win, linkEl, message);
- } else {
- this._attachCommandListener(win, wrapperEl, message);
- }
-
- return messageEl;
- }
-
- async _contentArguments() {
- const { defaultEngine } = Services.search;
- // Between now and 6 weeks ago
- const dateTo = new Date();
- const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000);
- const eventsByDate = await lazy.TrackingDBService.getEventsByDateRange(
- dateFrom,
- dateTo
- );
- // Make sure we set all types of possible values to 0 because they might
- // be referenced by fluent strings
- let totalEvents = { blockedCount: 0 };
- for (let blockedType of idToTextMap.values()) {
- totalEvents[blockedType] = 0;
- }
- // Count all events in the past 6 weeks. Returns an object with:
- // `blockedCount` total number of blocked resources
- // {tracker|cookie|social...} breakdown by event type as defined by `idToTextMap`
- totalEvents = eventsByDate.reduce((acc, day) => {
- const type = day.getResultByName("type");
- const count = day.getResultByName("count");
- acc[idToTextMap.get(type)] = (acc[idToTextMap.get(type)] || 0) + count;
- acc.blockedCount += count;
- return acc;
- }, totalEvents);
- return {
- // Keys need to match variable names used in asrouter.ftl
- // `earliestDate` will be either 6 weeks ago or when tracking recording
- // started. Whichever is more recent.
- earliestDate: Math.max(
- new Date(await lazy.TrackingDBService.getEarliestRecordedDate()),
- dateFrom
- ),
- ...totalEvents,
- // Passing in `undefined` as string for the Fluent variable name
- // in order to match and select the message that does not require
- // the variable.
- searchEngineName: defaultEngine ? defaultEngine.name : "undefined",
- };
- }
-
- async _showAppmenuButton(win) {
- this.maybeInsertFTL(win);
- await this._showElement(
- win.browser.ownerDocument,
- APPMENU_BUTTON_ID,
- BUTTON_STRING_ID
- );
- }
-
- _hideAppmenuButton(win, windowClosed) {
- // No need to do something if the window is going away
- if (!windowClosed) {
- this._hideElement(win.browser.ownerDocument, APPMENU_BUTTON_ID);
- }
- }
-
- _showToolbarButton(win) {
- const document = win.browser.ownerDocument;
- this.maybeInsertFTL(win);
- return this._showElement(document, TOOLBAR_BUTTON_ID, BUTTON_STRING_ID);
- }
-
- _hideToolbarButton(win) {
- this._hideElement(win.browser.ownerDocument, TOOLBAR_BUTTON_ID);
- }
-
- _showElement(document, id, string_id) {
- const el = lazy.PanelMultiView.getViewNode(document, id);
- document.l10n.setAttributes(el, string_id);
- el.hidden = false;
- }
-
- _hideElement(document, id) {
- const el = lazy.PanelMultiView.getViewNode(document, id);
- if (el) {
- el.hidden = true;
- }
- }
-
- _sendPing(ping) {
- this._sendTelemetry({
- type: "TOOLBAR_PANEL_TELEMETRY",
- data: { action: "whats-new-panel_user_event", ...ping },
- });
- }
-
- sendUserEventTelemetry(win, event, message, options = {}) {
- // Only send pings for non private browsing windows
- if (
- win &&
- !lazy.PrivateBrowsingUtils.isBrowserPrivate(
- win.ownerGlobal.gBrowser.selectedBrowser
- )
- ) {
- this._sendPing({
- message_id: message.id,
- event,
- event_context: options.value,
- });
- }
- }
-
- /**
- * @param {object} [browser] MessageChannel target argument as a response to a
- * user action. No message is shown if undefined.
- * @param {object[]} messages Messages selected from devtools page
- */
- forceShowMessage(browser, messages) {
- if (!browser) {
- return;
- }
- const win = browser.ownerGlobal;
- const doc = browser.ownerDocument;
- this.removeMessages(win, WHATS_NEW_PANEL_SELECTOR);
- this.renderMessages(win, doc, WHATS_NEW_PANEL_SELECTOR, {
- force: true,
- messages: Array.isArray(messages) ? messages : [messages],
- });
- win.PanelUI.panel.addEventListener("popuphidden", event =>
- this.removeMessages(event.target.ownerGlobal, WHATS_NEW_PANEL_SELECTOR)
- );
- }
-}
-
-/**
- * ToolbarPanelHub - singleton instance of _ToolbarPanelHub that can initiate
- * message requests and render messages.
- */
-export const ToolbarPanelHub = new _ToolbarPanelHub();
diff --git a/browser/components/asrouter/moz.build b/browser/components/asrouter/moz.build
index 558ccbeb9b..cf45186619 100644
--- a/browser/components/asrouter/moz.build
+++ b/browser/components/asrouter/moz.build
@@ -15,7 +15,7 @@ FINAL_TARGET_FILES.actors += [
]
EXTRA_JS_MODULES.asrouter += [
- "modules/ActorConstants.sys.mjs",
+ "modules/ActorConstants.mjs",
"modules/ASRouter.sys.mjs",
"modules/ASRouterDefaultConfig.sys.mjs",
"modules/ASRouterNewTabHook.sys.mjs",
@@ -38,7 +38,6 @@ EXTRA_JS_MODULES.asrouter += [
"modules/Spotlight.sys.mjs",
"modules/ToastNotification.sys.mjs",
"modules/ToolbarBadgeHub.sys.mjs",
- "modules/ToolbarPanelHub.sys.mjs",
]
BROWSER_CHROME_MANIFESTS += [
@@ -57,7 +56,6 @@ TESTING_JS_MODULES += [
"content-src/templates/OnboardingMessage/Spotlight.schema.json",
"content-src/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json",
"content-src/templates/OnboardingMessage/UpdateAction.schema.json",
- "content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json",
"content-src/templates/PBNewtab/NewtabPromoMessage.schema.json",
"content-src/templates/ToastNotification/ToastNotification.schema.json",
"tests/InflightAssetsMessageProvider.sys.mjs",
diff --git a/browser/components/asrouter/package.json b/browser/components/asrouter/package.json
index 17bc8f7364..26c09392c2 100644
--- a/browser/components/asrouter/package.json
+++ b/browser/components/asrouter/package.json
@@ -60,6 +60,7 @@
"testmc:lint": "npm run lint",
"testmc:build": "npm run bundle:admin",
"testmc:unit": "karma start karma.mc.config.js",
+ "testmc:import": "npm run import-rollouts",
"tddmc": "karma start karma.mc.config.js --tdd",
"debugcoverage": "open logs/coverage/lcov-report/index.html",
"lint": "npm-run-all lint:*",
diff --git a/browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs b/browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs
index e92b210c12..adb14ecc38 100644
--- a/browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs
+++ b/browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs
@@ -10,58 +10,10 @@ export const InflightAssetsMessageProvider = {
getMessages() {
return [
{
- id: "MILESTONE_MESSAGE",
- groups: ["cfr"],
- content: {
- anchor_id: "tracking-protection-icon-box",
- bucket_id: "CFR_MILESTONE_MESSAGE",
- buttons: {
- primary: {
- action: {
- type: "OPEN_PROTECTION_REPORT",
- },
- event: "PROTECTION",
- label: {
- string_id: "cfr-doorhanger-milestone-ok-button",
- },
- },
- secondary: [
- {
- label: {
- string_id: "cfr-doorhanger-milestone-close-button",
- },
- action: {
- type: "CANCEL",
- },
- event: "DISMISS",
- },
- ],
- },
- category: "cfrFeatures",
- heading_text: {
- string_id: "cfr-doorhanger-milestone-heading",
- },
- layout: "short_message",
- notification_text: "",
- skip_address_bar_notifier: true,
- text: "",
- },
- frequency: {
- lifetime: 7,
- },
- targeting:
- "pageLoad >= 4 && firefoxVersion < 87 && userPrefs.cfrFeatures",
- template: "milestone_message",
- trigger: {
- id: "contentBlocking",
- params: ["ContentBlockingMilestone"],
- },
- },
- {
id: "MILESTONE_MESSAGE_87",
groups: ["cfr"],
content: {
- anchor_id: "tracking-protection-icon-box",
+ anchor_id: "tracking-protection-icon-container",
bucket_id: "CFR_MILESTONE_MESSAGE",
buttons: {
primary: {
@@ -98,7 +50,7 @@ export const InflightAssetsMessageProvider = {
lifetime: 7,
},
targeting:
- "pageLoad >= 4 && firefoxVersion >= 87 && userPrefs.cfrFeatures",
+ "pageLoad >= 4 && firefoxVersion >= 115 && firefoxVersion < 121 && userPrefs.cfrFeatures",
template: "milestone_message",
trigger: {
id: "contentBlocking",
@@ -275,66 +227,6 @@ export const InflightAssetsMessageProvider = {
],
},
},
- {
- id: "WNP_MOMENTS_12",
- groups: ["moments-pages"],
- content: {
- action: {
- data: {
- expire: 1640908800000,
- url: "https://www.mozilla.org/firefox/welcome/12",
- },
- id: "moments-wnp",
- },
- bucket_id: "WNP_MOMENTS_12",
- },
- targeting:
- 'localeLanguageCode == "en" && region in ["DE", "AT", "BE", "CA", "FR", "IE", "IT", "MY", "NL", "NZ", "SG", "CH", "US", "GB", "ES"] && (addonsInfo.addons|keys intersect ["@testpilot-containers"])|length == 1 && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\'|preferenceValue && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\'|preferenceValue',
- template: "update_action",
- trigger: {
- id: "momentsUpdate",
- },
- },
- {
- id: "WNP_MOMENTS_13",
- groups: ["moments-pages"],
- content: {
- action: {
- data: {
- expire: 1640908800000,
- url: "https://www.mozilla.org/firefox/welcome/13",
- },
- id: "moments-wnp",
- },
- bucket_id: "WNP_MOMENTS_13",
- },
- targeting:
- '(localeLanguageCode in ["en", "de", "fr", "nl", "it", "ms"] || locale == "es-ES") && region in ["DE", "AT", "BE", "CA", "FR", "IE", "IT", "MY", "NL", "NZ", "SG", "CH", "US", "GB", "ES"] && (addonsInfo.addons|keys intersect ["@testpilot-containers"])|length == 0 && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\'|preferenceValue && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\'|preferenceValue',
- template: "update_action",
- trigger: {
- id: "momentsUpdate",
- },
- },
- {
- id: "WNP_MOMENTS_14",
- groups: ["moments-pages"],
- content: {
- action: {
- data: {
- expire: 1668470400000,
- url: "https://www.mozilla.org/firefox/welcome/14",
- },
- id: "moments-wnp",
- },
- bucket_id: "WNP_MOMENTS_14",
- },
- targeting:
- 'localeLanguageCode in ["en", "de", "fr"] && region in ["AT", "BE", "CA", "CH", "DE", "ES", "FI", "FR", "GB", "IE", "IT", "MY", "NL", "NZ", "SE", "SG", "US"] && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\'|preferenceValue && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\'|preferenceValue',
- template: "update_action",
- trigger: {
- id: "momentsUpdate",
- },
- },
];
},
};
diff --git a/browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs b/browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs
index 5bfbec9557..f2c94f7de6 100644
--- a/browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs
+++ b/browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs
@@ -12,6 +12,427 @@ export const NimbusRolloutMessageProvider = {
getMessages() {
return [
{
+ // Nimbus slug: device-migration-q4-spotlights-remaining-population-esr:treatment (message 1 of 3)
+ // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population-esr/summary#treatment
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ groups: ["eco"],
+ content: {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ modal: "tab",
+ screens: [
+ {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_BACKUP",
+ content: {
+ logo: {
+ height: "152px",
+ imageURL:
+ "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/c92a41e4-82cf-4ad5-8480-04a138bfb3cd.png",
+ },
+ title: {
+ fontSize: "24px",
+ string_id: "device-migration-fxa-spotlight-heavy-user-header",
+ letterSpacing: 0,
+ },
+ subtitle: {
+ fontSize: "15px",
+ string_id: "device-migration-fxa-spotlight-heavy-user-body",
+ lineHeight: "1.4",
+ marginBlock: "8px 20px",
+ letterSpacing: 0,
+ paddingInline: "20px",
+ },
+ dismiss_button: {
+ action: {
+ navigate: true,
+ },
+ },
+ primary_button: {
+ label: {
+ string_id:
+ "device-migration-fxa-spotlight-heavy-user-primary-button",
+ paddingBlock: "4px",
+ paddingInline: "16px",
+ },
+ action: {
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=dont-forget-to-backup&entrypoint=device-migration-spotlight-experiment-v2",
+ where: "tabshifted",
+ },
+ type: "OPEN_URL",
+ navigate: true,
+ },
+ },
+ },
+ },
+ ],
+ backdrop: "transparent",
+ template: "multistage",
+ transitions: true,
+ },
+ trigger: {
+ id: "defaultBrowserCheck",
+ },
+ template: "spotlight",
+ frequency: {
+ lifetime: 1,
+ },
+ targeting:
+ "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && (((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35)",
+ },
+ {
+ // Nimbus slug: device-migration-q4-spotlights-remaining-population-esr:treatment (message 2 of 3)
+ // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population-esr/summary#treatment
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ groups: ["eco"],
+ content: {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ modal: "tab",
+ screens: [
+ {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_PEACE",
+ content: {
+ logo: {
+ height: "133px",
+ imageURL:
+ "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/4a56d3ed-98c8-4a33-b853-b2cf7646efd8.png",
+ marginBlock: "22px -10px",
+ },
+ title: {
+ fontSize: "24px",
+ string_id:
+ "device-migration-fxa-spotlight-older-device-header",
+ letterSpacing: 0,
+ },
+ subtitle: {
+ fontSize: "15px",
+ string_id: "device-migration-fxa-spotlight-older-device-body",
+ lineHeight: "1.4",
+ marginBlock: "8px 20px",
+ letterSpacing: 0,
+ paddingInline: "40px",
+ },
+ dismiss_button: {
+ action: {
+ navigate: true,
+ },
+ },
+ primary_button: {
+ label: {
+ string_id:
+ "device-migration-fxa-spotlight-older-device-primary-button",
+ marginBlock: "0 22px",
+ paddingBlock: "4px",
+ paddingInline: "16px",
+ },
+ action: {
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=peace-of-mind&entrypoint=device-migration-spotlight-experiment-v2",
+ where: "tabshifted",
+ },
+ type: "OPEN_URL",
+ navigate: true,
+ },
+ },
+ },
+ },
+ ],
+ backdrop: "transparent",
+ template: "multistage",
+ transitions: true,
+ },
+ trigger: {
+ id: "defaultBrowserCheck",
+ },
+ template: "spotlight",
+ frequency: {
+ lifetime: 1,
+ },
+ targeting:
+ "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && !(((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35) && os.isWindows && os.windowsVersion >= 6.1 && os.windowsBuildNumber < 22000",
+ },
+ {
+ // Nimbus slug: device-migration-q4-spotlights-remaining-population-esr:treatment (message 3 of 3)
+ // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population-esr/summary#treatment
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ groups: ["eco"],
+ content: {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ modal: "tab",
+ screens: [
+ {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_NEW_DEVICE",
+ content: {
+ logo: {
+ height: "149px",
+ imageURL:
+ "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/a43cd9cc-e8b2-477c-92f2-345557370de1.svg",
+ },
+ title: {
+ fontSize: "24px",
+ string_id:
+ "device-migration-fxa-spotlight-getting-new-device-header-2",
+ letterSpacing: 0,
+ },
+ subtitle: {
+ fontSize: "15px",
+ string_id:
+ "device-migration-fxa-spotlight-getting-new-device-body-2",
+ lineHeight: "1.4",
+ marginBlock: "8px 20px",
+ letterSpacing: 0,
+ paddingInline: "40px",
+ },
+ dismiss_button: {
+ action: {
+ navigate: true,
+ },
+ },
+ primary_button: {
+ label: {
+ string_id:
+ "device-migration-fxa-spotlight-getting-new-device-primary-button",
+ paddingBlock: "4px",
+ paddingInline: "16px",
+ },
+ action: {
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=new-device-in-your-future&entrypoint=device-migration-spotlight-experiment-v2",
+ where: "tabshifted",
+ },
+ type: "OPEN_URL",
+ navigate: true,
+ },
+ },
+ },
+ },
+ ],
+ backdrop: "transparent",
+ template: "multistage",
+ transitions: true,
+ },
+ trigger: {
+ id: "defaultBrowserCheck",
+ },
+ template: "spotlight",
+ frequency: {
+ lifetime: 1,
+ },
+ targeting:
+ "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && !(((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35) && !(os.isWindows && os.windowsVersion >= 6.1 && os.windowsBuildNumber < 22000)",
+ },
+ {
+ // Nimbus slug: device-migration-q4-spotlights-remaining-population:treatment (message 1 of 3)
+ // Version range: 122+
+ // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population/summary#treatment
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ groups: ["eco"],
+ content: {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ modal: "tab",
+ screens: [
+ {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_BACKUP",
+ content: {
+ logo: {
+ height: "152px",
+ imageURL:
+ "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/c92a41e4-82cf-4ad5-8480-04a138bfb3cd.png",
+ },
+ title: {
+ fontSize: "24px",
+ string_id: "device-migration-fxa-spotlight-heavy-user-header",
+ letterSpacing: 0,
+ },
+ subtitle: {
+ fontSize: "15px",
+ string_id: "device-migration-fxa-spotlight-heavy-user-body",
+ lineHeight: "1.4",
+ marginBlock: "8px 20px",
+ letterSpacing: 0,
+ paddingInline: "20px",
+ },
+ dismiss_button: {
+ action: {
+ navigate: true,
+ },
+ },
+ primary_button: {
+ label: {
+ string_id:
+ "device-migration-fxa-spotlight-heavy-user-primary-button",
+ paddingBlock: "4px",
+ paddingInline: "16px",
+ },
+ action: {
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=dont-forget-to-backup&entrypoint=device-migration-spotlight-experiment-v2",
+ where: "tabshifted",
+ },
+ type: "OPEN_URL",
+ navigate: true,
+ },
+ },
+ },
+ },
+ ],
+ backdrop: "transparent",
+ template: "multistage",
+ transitions: true,
+ },
+ trigger: {
+ id: "defaultBrowserCheck",
+ },
+ template: "spotlight",
+ frequency: {
+ lifetime: 1,
+ },
+ targeting:
+ "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && (((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35)",
+ },
+ {
+ // Nimbus slug: device-migration-q4-spotlights-remaining-population:treatment (message 2 of 3)
+ // Version range: 122+
+ // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population/summary#treatment
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ groups: ["eco"],
+ content: {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ modal: "tab",
+ screens: [
+ {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_PEACE",
+ content: {
+ logo: {
+ height: "133px",
+ imageURL:
+ "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/4a56d3ed-98c8-4a33-b853-b2cf7646efd8.png",
+ marginBlock: "22px -10px",
+ },
+ title: {
+ fontSize: "24px",
+ string_id:
+ "device-migration-fxa-spotlight-older-device-header",
+ letterSpacing: 0,
+ },
+ subtitle: {
+ fontSize: "15px",
+ string_id: "device-migration-fxa-spotlight-older-device-body",
+ lineHeight: "1.4",
+ marginBlock: "8px 20px",
+ letterSpacing: 0,
+ paddingInline: "40px",
+ },
+ dismiss_button: {
+ action: {
+ navigate: true,
+ },
+ },
+ primary_button: {
+ label: {
+ string_id:
+ "device-migration-fxa-spotlight-older-device-primary-button",
+ marginBlock: "0 22px",
+ paddingBlock: "4px",
+ paddingInline: "16px",
+ },
+ action: {
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=peace-of-mind&entrypoint=device-migration-spotlight-experiment-v2",
+ where: "tabshifted",
+ },
+ type: "OPEN_URL",
+ navigate: true,
+ },
+ },
+ },
+ },
+ ],
+ backdrop: "transparent",
+ template: "multistage",
+ transitions: true,
+ },
+ trigger: {
+ id: "defaultBrowserCheck",
+ },
+ template: "spotlight",
+ frequency: {
+ lifetime: 1,
+ },
+ targeting:
+ "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && !(((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35) && os.isWindows && os.windowsVersion >= 6.1 && os.windowsBuildNumber < 22000",
+ },
+ {
+ // Nimbus slug: device-migration-q4-spotlights-remaining-population:treatment (message 3 of 3)
+ // Version range: 122+
+ // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population/summary#treatment
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ groups: ["eco"],
+ content: {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT",
+ modal: "tab",
+ screens: [
+ {
+ id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_NEW_DEVICE",
+ content: {
+ logo: {
+ height: "149px",
+ imageURL:
+ "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/a43cd9cc-e8b2-477c-92f2-345557370de1.svg",
+ },
+ title: {
+ fontSize: "24px",
+ string_id:
+ "device-migration-fxa-spotlight-getting-new-device-header-2",
+ letterSpacing: 0,
+ },
+ subtitle: {
+ fontSize: "15px",
+ string_id:
+ "device-migration-fxa-spotlight-getting-new-device-body-2",
+ lineHeight: "1.4",
+ marginBlock: "8px 20px",
+ letterSpacing: 0,
+ paddingInline: "40px",
+ },
+ dismiss_button: {
+ action: {
+ navigate: true,
+ },
+ },
+ primary_button: {
+ label: {
+ string_id:
+ "device-migration-fxa-spotlight-getting-new-device-primary-button",
+ paddingBlock: "4px",
+ paddingInline: "16px",
+ },
+ action: {
+ data: {
+ args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=new-device-in-your-future&entrypoint=device-migration-spotlight-experiment-v2",
+ where: "tabshifted",
+ },
+ type: "OPEN_URL",
+ navigate: true,
+ },
+ },
+ },
+ },
+ ],
+ backdrop: "transparent",
+ template: "multistage",
+ transitions: true,
+ },
+ trigger: {
+ id: "defaultBrowserCheck",
+ },
+ template: "spotlight",
+ frequency: {
+ lifetime: 1,
+ },
+ targeting:
+ "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && !(((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35) && !(os.isWindows && os.windowsVersion >= 6.1 && os.windowsBuildNumber < 22000)",
+ },
+ {
// Nimbus slug: fox-doodle-set-to-default-early-day-user-de-fr-it-treatment-a-rollout:treatment-a
// Version range: 116+
// Recipe: https://experimenter.services.mozilla.com/nimbus/fox-doodle-set-to-default-early-day-user-de-fr-it-treatment-a-rollout/summary#treatment-a
diff --git a/browser/components/asrouter/tests/browser/browser.toml b/browser/components/asrouter/tests/browser/browser.toml
index 7bed40373d..60ce42dfd8 100644
--- a/browser/components/asrouter/tests/browser/browser.toml
+++ b/browser/components/asrouter/tests/browser/browser.toml
@@ -21,6 +21,11 @@ skip-if = ["os == 'linux' && bits == 64 && !debug"] # Bug 1643036
["browser_asrouter_infobar.js"]
+["browser_asrouter_keyboard_cfr.js"]
+https_first_disabled = true
+
+["browser_asrouter_milestone_message_cfr.js"]
+
["browser_asrouter_momentspagehub.js"]
tags = "remote-settings"
diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js b/browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js
index 19fcb63131..d22605d589 100644
--- a/browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js
+++ b/browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js
@@ -20,7 +20,7 @@ const { RemoteSettings } = ChromeUtils.importESModule(
);
// This pref is used to override the Remote Settings server URL in tests.
-// See SERVER_URL in services/settings/Utils.jsm for more details.
+// See SERVER_URL in services/settings/Utils.sys.mjs for more details.
const RS_SERVER_PREF = "services.settings.server";
const FLUENT_CONTENT = "asrouter-test-string = Test Test Test\n";
diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js b/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js
index e29771c24f..1979c81a79 100644
--- a/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js
+++ b/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js
@@ -115,14 +115,6 @@ function checkCFRAddonsElements(notification) {
);
}
-function checkCFRTrackingProtectionMilestone(notification) {
- Assert.ok(notification.hidden === false, "Panel should be visible");
- Assert.ok(
- notification.getAttribute("data-notification-category") === "short_message",
- "Panel have correct data attribute"
- );
-}
-
function clearNotifications() {
for (let notification of PopupNotifications._currentNotifications) {
notification.remove();
@@ -498,59 +490,6 @@ add_task(async function test_cfr_addon_install() {
Services.fog.testResetFOG();
});
-add_task(
- async function test_cfr_tracking_protection_milestone_notification_remove() {
- await SpecialPowers.pushPrefEnv({
- set: [
- ["browser.contentblocking.cfr-milestone.milestone-achieved", 1000],
- [
- "browser.newtabpage.activity-stream.asrouter.providers.cfr",
- `{"id":"cfr","enabled":true,"type":"local","localProvider":"CFRMessageProvider","updateCycleInMs":3600000}`,
- ],
- ],
- });
-
- // addRecommendation checks that scheme starts with http and host matches
- let browser = gBrowser.selectedBrowser;
- BrowserTestUtils.startLoadingURIString(browser, "http://example.com/");
- await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
-
- const showPanel = BrowserTestUtils.waitForEvent(
- PopupNotifications.panel,
- "popupshown"
- );
-
- Services.obs.notifyObservers(
- {
- wrappedJSObject: {
- event: "ContentBlockingMilestone",
- },
- },
- "SiteProtection:ContentBlockingMilestone"
- );
-
- await showPanel;
-
- const notification = document.getElementById(
- "contextual-feature-recommendation-notification"
- );
-
- checkCFRTrackingProtectionMilestone(notification);
-
- Assert.ok(notification.secondaryButton);
- let hidePanel = BrowserTestUtils.waitForEvent(
- PopupNotifications.panel,
- "popuphidden"
- );
-
- notification.secondaryButton.click();
- await hidePanel;
- await SpecialPowers.popPrefEnv();
- clearNotifications();
- Services.fog.testResetFOG();
- }
-);
-
add_task(async function test_cfr_addon_and_features_show() {
// addRecommendation checks that scheme starts with http and host matches
let browser = gBrowser.selectedBrowser;
@@ -747,62 +686,6 @@ add_task(async function test_providerNames() {
}
});
-add_task(async function test_cfr_notification_keyboard() {
- // addRecommendation checks that scheme starts with http and host matches
- const browser = gBrowser.selectedBrowser;
- BrowserTestUtils.startLoadingURIString(browser, "http://example.com/");
- await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
-
- const response = await trigger_cfr_panel(browser, "example.com");
- Assert.ok(
- response,
- "Should return true if addRecommendation checks were successful"
- );
-
- // Open the panel with the keyboard.
- // Toolbar buttons aren't always focusable; toolbar keyboard navigation
- // makes them focusable on demand. Therefore, we must force focus.
- const button = document.getElementById("contextual-feature-recommendation");
- button.setAttribute("tabindex", "-1");
- button.focus();
- button.removeAttribute("tabindex");
-
- let focused = BrowserTestUtils.waitForEvent(
- PopupNotifications.panel,
- "focus",
- true
- );
- EventUtils.synthesizeKey(" ");
- await focused;
- Assert.ok(true, "Focus inside panel after button pressed");
-
- let hidden = BrowserTestUtils.waitForEvent(
- PopupNotifications.panel,
- "popuphidden"
- );
- EventUtils.synthesizeKey("KEY_Escape");
- await hidden;
- Assert.ok(true, "Panel hidden after Escape pressed");
-
- const showPanel = BrowserTestUtils.waitForEvent(
- PopupNotifications.panel,
- "popupshown"
- );
- // Need to dismiss the notification to clear the RecommendationMap
- document.getElementById("contextual-feature-recommendation").click();
- await showPanel;
-
- const hidePanel = BrowserTestUtils.waitForEvent(
- PopupNotifications.panel,
- "popuphidden"
- );
- document
- .getElementById("contextual-feature-recommendation-notification")
- .button.click();
- await hidePanel;
- Services.fog.testResetFOG();
-});
-
add_task(function test_updateCycleForProviders() {
Services.prefs
.getChildList("browser.newtabpage.activity-stream.asrouter.providers.")
diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_keyboard_cfr.js b/browser/components/asrouter/tests/browser/browser_asrouter_keyboard_cfr.js
new file mode 100644
index 0000000000..c3dfc0b0bb
--- /dev/null
+++ b/browser/components/asrouter/tests/browser/browser_asrouter_keyboard_cfr.js
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-disable @microsoft/sdl/no-insecure-url */
+function clearNotifications() {
+ for (let notification of PopupNotifications._currentNotifications) {
+ notification.remove();
+ }
+
+ // Clicking the primary action also removes the notification
+ Assert.equal(
+ PopupNotifications._currentNotifications.length,
+ 0,
+ "Should have removed the notification"
+ );
+}
+
+add_setup(async function () {
+ // Store it in order to restore to the original value
+ const { _fetchLatestAddonVersion } = CFRPageActions;
+ // Prevent fetching the real addon url and making a network request
+ CFRPageActions._fetchLatestAddonVersion = () => "http://example.com";
+ Services.fog.testResetFOG();
+
+ registerCleanupFunction(() => {
+ CFRPageActions._fetchLatestAddonVersion = _fetchLatestAddonVersion;
+ clearNotifications();
+ CFRPageActions.clearRecommendations();
+ });
+});
+
+add_task(async function test_cfr_notification_keyboard() {
+ // addRecommendation checks that scheme starts with http and host matches
+ const browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
+
+ clearNotifications();
+
+ let recommendation = {
+ template: "cfr_doorhanger",
+ groups: ["mochitest-group"],
+ content: {
+ layout: "addon_recommendation",
+ category: "cfrAddons",
+ anchor_id: "page-action-buttons",
+ icon_class: "cfr-doorhanger-medium-icon",
+ skip_address_bar_notifier: false,
+ heading_text: "Sample Mochitest",
+ icon: "chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg",
+ icon_dark_theme:
+ "chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg",
+ info_icon: {
+ label: { attributes: { tooltiptext: "Why am I seeing this" } },
+ sumo_path: "extensionrecommendations",
+ },
+ addon: {
+ id: "addon-id",
+ title: "Addon name",
+ icon: "chrome://browser/skin/addons/addon-install-downloading.svg",
+ author: "Author name",
+ amo_url: "https://example.com",
+ rating: "4.5",
+ users: "1.1M",
+ },
+ text: "Mochitest",
+ buttons: {
+ primary: {
+ label: {
+ value: "OK",
+ attributes: { accesskey: "O" },
+ },
+ action: {
+ type: "CANCEL",
+ data: {},
+ },
+ },
+ secondary: [
+ {
+ label: {
+ value: "Cancel",
+ attributes: { accesskey: "C" },
+ },
+ action: {
+ type: "CANCEL",
+ },
+ },
+ ],
+ },
+ },
+ };
+
+ recommendation.content.notification_text = new String("Mochitest"); // eslint-disable-line
+ recommendation.content.notification_text.attributes = {
+ tooltiptext: "Mochitest tooltip",
+ "a11y-announcement": "Mochitest announcement",
+ };
+
+ const response = await CFRPageActions.addRecommendation(
+ gBrowser.selectedBrowser,
+ "example.com",
+ recommendation,
+ // Use the real AS dispatch method to trigger real notifications
+ ASRouter.dispatchCFRAction
+ );
+
+ Assert.ok(
+ response,
+ "Should return true if addRecommendation checks were successful"
+ );
+
+ // Open the panel with the keyboard.
+ // Toolbar buttons aren't always focusable; toolbar keyboard navigation
+ // makes them focusable on demand. Therefore, we must force focus.
+ const button = document.getElementById("contextual-feature-recommendation");
+ button.setAttribute("tabindex", "-1");
+
+ let buttonFocused = BrowserTestUtils.waitForEvent(button, "focus");
+ button.focus();
+ await buttonFocused;
+
+ Assert.ok(true, "Focus page action button");
+
+ let focused = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "focus",
+ true
+ );
+
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ Assert.ok(true, "Focus inside panel after button pressed");
+
+ button.removeAttribute("tabindex");
+
+ let hidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ await hidden;
+ Assert.ok(true, "Panel hidden after Escape pressed");
+
+ const showPanel = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ // Need to dismiss the notification to clear the RecommendationMap
+ document.getElementById("contextual-feature-recommendation").click();
+ await showPanel;
+
+ const hidePanel = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ document
+ .getElementById("contextual-feature-recommendation-notification")
+ .button.click();
+ await hidePanel;
+ Services.fog.testResetFOG();
+});
diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_milestone_message_cfr.js b/browser/components/asrouter/tests/browser/browser_asrouter_milestone_message_cfr.js
new file mode 100644
index 0000000000..6585963d6f
--- /dev/null
+++ b/browser/components/asrouter/tests/browser/browser_asrouter_milestone_message_cfr.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-disable @microsoft/sdl/no-insecure-url */
+function checkCFRTrackingProtectionMilestone(notification) {
+ Assert.ok(notification.hidden === false, "Panel should be visible");
+ Assert.ok(
+ notification.getAttribute("data-notification-category") === "short_message",
+ "Panel have correct data attribute"
+ );
+}
+
+function clearNotifications() {
+ for (let notification of PopupNotifications._currentNotifications) {
+ notification.remove();
+ }
+
+ // Clicking the primary action also removes the notification
+ Assert.equal(
+ PopupNotifications._currentNotifications.length,
+ 0,
+ "Should have removed the notification"
+ );
+}
+
+add_task(
+ async function test_cfr_tracking_protection_milestone_notification_remove() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.cfr-milestone.milestone-achieved", 1000],
+ [
+ "browser.newtabpage.activity-stream.asrouter.providers.cfr",
+ `{"id":"cfr","enabled":true,"type":"local","localProvider":"CFRMessageProvider","updateCycleInMs":3600000}`,
+ ],
+ ],
+ });
+
+ // addRecommendation checks that scheme starts with http and host matches
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
+
+ const showPanel = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: {
+ event: "ContentBlockingMilestone",
+ },
+ },
+ "SiteProtection:ContentBlockingMilestone"
+ );
+
+ await showPanel;
+
+ const notification = document.getElementById(
+ "contextual-feature-recommendation-notification"
+ );
+
+ checkCFRTrackingProtectionMilestone(notification);
+
+ Assert.ok(notification.secondaryButton);
+ let hidePanel = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ notification.secondaryButton.click();
+ await hidePanel;
+ await SpecialPowers.popPrefEnv();
+ clearNotifications();
+ Services.fog.testResetFOG();
+ }
+);
diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js b/browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js
index f752d01116..aea702bb61 100644
--- a/browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js
+++ b/browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js
@@ -14,11 +14,11 @@ const { ASRouter } = ChromeUtils.importESModule(
const HOMEPAGE_OVERRIDE_PREF = "browser.startup.homepage_override.once";
add_task(async function test_with_rs_messages() {
- // Force the WNPanel provider cache to 0 by modifying updateCycleInMs
+ // Force the cfr provider cache to 0 by modifying updateCycleInMs
await SpecialPowers.pushPrefEnv({
set: [
[
- "browser.newtabpage.activity-stream.asrouter.providers.whats-new-panel",
+ "browser.newtabpage.activity-stream.asrouter.providers.cfr",
`{"id":"cfr","enabled":true,"type":"remote-settings","collection":"cfr","updateCycleInMs":0}`,
],
],
diff --git a/browser/components/asrouter/tests/unit/ASRouter.test.js b/browser/components/asrouter/tests/unit/ASRouter.test.js
index 7df1449a14..1f5899fce1 100644
--- a/browser/components/asrouter/tests/unit/ASRouter.test.js
+++ b/browser/components/asrouter/tests/unit/ASRouter.test.js
@@ -48,7 +48,6 @@ describe("ASRouter", () => {
let fakeAttributionCode;
let fakeTargetingContext;
let FakeToolbarBadgeHub;
- let FakeToolbarPanelHub;
let FakeMomentsPageHub;
let ASRouterTargeting;
let screenImpressions;
@@ -151,7 +150,6 @@ describe("ASRouter", () => {
cfr: "",
"message-groups": "",
"messaging-experiments": "",
- "whats-new-panel": "",
},
totalBookmarksCount: {},
firefoxVersion: 80,
@@ -159,7 +157,6 @@ describe("ASRouter", () => {
needsUpdate: {},
hasPinnedTabs: false,
hasAccessedFxAPanel: false,
- isWhatsNewPanelEnabled: true,
userPrefs: {
cfrFeatures: true,
cfrAddons: true,
@@ -203,12 +200,6 @@ describe("ASRouter", () => {
writeAttributionFile: () => Promise.resolve(),
getCachedAttributionData: sinon.stub(),
};
- FakeToolbarPanelHub = {
- init: sandbox.stub(),
- uninit: sandbox.stub(),
- forceShowMessage: sandbox.stub(),
- enableToolbarButton: sandbox.stub(),
- };
FakeToolbarBadgeHub = {
init: sandbox.stub(),
uninit: sandbox.stub(),
@@ -252,7 +243,6 @@ describe("ASRouter", () => {
PanelTestProvider,
MacAttribution: { applicationPath: "" },
ToolbarBadgeHub: FakeToolbarBadgeHub,
- ToolbarPanelHub: FakeToolbarPanelHub,
MomentsPageHub: FakeMomentsPageHub,
KintoHttpClient: class {
bucket() {
@@ -354,7 +344,6 @@ describe("ASRouter", () => {
// ASRouter init called in `beforeEach` block above
assert.calledOnce(FakeToolbarBadgeHub.init);
- assert.calledOnce(FakeToolbarPanelHub.init);
assert.calledOnce(FakeMomentsPageHub.init);
assert.calledWithExactly(
@@ -370,15 +359,6 @@ describe("ASRouter", () => {
);
assert.calledWithExactly(
- FakeToolbarPanelHub.init,
- Router.waitForInitialized,
- {
- getMessages: Router.handleMessageRequest,
- sendTelemetry: Router.sendTelemetry,
- }
- );
-
- assert.calledWithExactly(
FakeMomentsPageHub.init,
Router.waitForInitialized,
{
@@ -678,25 +658,10 @@ describe("ASRouter", () => {
sandbox.stub(CFRPageActions, "addRecommendation");
browser = {};
});
- it("should route whatsnew_panel_message message to the right hub", () => {
- Router.routeCFRMessage(
- { template: "whatsnew_panel_message" },
- browser,
- "",
- true
- );
-
- assert.calledOnce(FakeToolbarPanelHub.forceShowMessage);
- assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
- assert.notCalled(CFRPageActions.addRecommendation);
- assert.notCalled(CFRPageActions.forceRecommendation);
- assert.notCalled(FakeMomentsPageHub.executeAction);
- });
it("should route moments messages to the right hub", () => {
Router.routeCFRMessage({ template: "update_action" }, browser, "", true);
assert.calledOnce(FakeMomentsPageHub.executeAction);
- assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(CFRPageActions.forceRecommendation);
@@ -705,7 +670,6 @@ describe("ASRouter", () => {
Router.routeCFRMessage({ template: "toolbar_badge" }, browser);
assert.calledOnce(FakeToolbarBadgeHub.registerBadgeNotificationListener);
- assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeMomentsPageHub.executeAction);
@@ -721,7 +685,6 @@ describe("ASRouter", () => {
assert.calledOnce(CFRPageActions.addRecommendation);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
- assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
it("should route cfr_doorhanger message to the right hub force = false", () => {
@@ -733,7 +696,6 @@ describe("ASRouter", () => {
);
assert.calledOnce(CFRPageActions.addRecommendation);
- assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeMomentsPageHub.executeAction);
@@ -742,7 +704,6 @@ describe("ASRouter", () => {
Router.routeCFRMessage({ template: "cfr_doorhanger" }, browser, {}, true);
assert.calledOnce(CFRPageActions.forceRecommendation);
- assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(FakeMomentsPageHub.executeAction);
@@ -759,7 +720,6 @@ describe("ASRouter", () => {
const { args } = CFRPageActions.addRecommendation.firstCall;
// Host should be null
assert.isNull(args[1]);
- assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeMomentsPageHub.executeAction);
@@ -773,7 +733,6 @@ describe("ASRouter", () => {
);
assert.calledOnce(CFRPageActions.forceRecommendation);
- assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(FakeMomentsPageHub.executeAction);
@@ -786,7 +745,6 @@ describe("ASRouter", () => {
true
);
- assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
@@ -961,7 +919,6 @@ describe("ASRouter", () => {
type: "local",
enabled: true,
messages: [
- "whatsnew_panel_message",
"cfr_doorhanger",
"toolbar_badge",
"update_action",
@@ -1272,43 +1229,6 @@ describe("ASRouter", () => {
Router.state.messageImpressions
);
});
- it("should return all unblocked messages that match the template, trigger if returnAll=true", async () => {
- const message1 = {
- provider: "whats_new",
- id: "1",
- template: "whatsnew_panel_message",
- trigger: { id: "whatsNewPanelOpened" },
- groups: ["whats_new"],
- };
- const message2 = {
- provider: "whats_new",
- id: "2",
- template: "whatsnew_panel_message",
- trigger: { id: "whatsNewPanelOpened" },
- groups: ["whats_new"],
- };
- const message3 = {
- provider: "whats_new",
- id: "3",
- template: "badge",
- groups: ["whats_new"],
- };
- ASRouterTargeting.findMatchingMessage.callsFake(() => [
- message2,
- message1,
- ]);
- await Router.setState({
- messages: [message3, message2, message1],
- providers: [{ id: "whats_new" }],
- });
- const result = await Router.handleMessageRequest({
- template: "whatsnew_panel_message",
- triggerId: "whatsNewPanelOpened",
- returnAll: true,
- });
-
- assert.deepEqual(result, [message2, message1]);
- });
it("should forward trigger param info", async () => {
const trigger = {
triggerId: "foo",
@@ -1854,33 +1774,6 @@ describe("ASRouter", () => {
});
});
- describe("#forceWNPanel", () => {
- let browser = {
- ownerGlobal: {
- document: new Document(),
- PanelUI: {
- showSubView: sinon.stub(),
- panel: {
- setAttribute: sinon.stub(),
- },
- },
- },
- };
- let fakePanel = {
- setAttribute: sinon.stub(),
- };
- sinon
- .stub(browser.ownerGlobal.document, "getElementById")
- .returns(fakePanel);
-
- it("should call enableToolbarButton", async () => {
- await Router.forceWNPanel(browser);
- assert.calledOnce(FakeToolbarPanelHub.enableToolbarButton);
- assert.calledOnce(browser.ownerGlobal.PanelUI.showSubView);
- assert.calledWith(fakePanel.setAttribute, "noautohide", true);
- });
- });
-
describe("_triggerHandler", () => {
it("should call #sendTriggerMessage with the correct trigger", () => {
const getter = sandbox.stub();
diff --git a/browser/components/asrouter/tests/unit/ASRouterChild.test.js b/browser/components/asrouter/tests/unit/ASRouterChild.test.js
index b73e56d510..c6533e073d 100644
--- a/browser/components/asrouter/tests/unit/ASRouterChild.test.js
+++ b/browser/components/asrouter/tests/unit/ASRouterChild.test.js
@@ -1,6 +1,6 @@
/*eslint max-nested-callbacks: ["error", 10]*/
import { ASRouterChild } from "actors/ASRouterChild.sys.mjs";
-import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.sys.mjs";
+import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.mjs";
describe("ASRouterChild", () => {
let asRouterChild = null;
@@ -24,7 +24,6 @@ describe("ASRouterChild", () => {
msg.DISABLE_PROVIDER,
msg.ENABLE_PROVIDER,
msg.EXPIRE_QUERY_CACHE,
- msg.FORCE_WHATSNEW_PANEL,
msg.IMPRESSION,
msg.RESET_PROVIDER_PREF,
msg.SET_PROVIDER_USER_PREF,
diff --git a/browser/components/asrouter/tests/unit/ASRouterParent.test.js b/browser/components/asrouter/tests/unit/ASRouterParent.test.js
index 0358b1261c..e65d7db825 100644
--- a/browser/components/asrouter/tests/unit/ASRouterParent.test.js
+++ b/browser/components/asrouter/tests/unit/ASRouterParent.test.js
@@ -1,5 +1,5 @@
import { ASRouterParent } from "actors/ASRouterParent.sys.mjs";
-import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.sys.mjs";
+import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.mjs";
describe("ASRouterParent", () => {
let asRouterParent = null;
diff --git a/browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js b/browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js
index 7bfec3e099..6a965c5689 100644
--- a/browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js
+++ b/browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js
@@ -1,6 +1,6 @@
import { ASRouterParentProcessMessageHandler } from "modules/ASRouterParentProcessMessageHandler.sys.mjs";
import { _ASRouter } from "modules/ASRouter.sys.mjs";
-import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.sys.mjs";
+import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.mjs";
describe("ASRouterParentProcessMessageHandler", () => {
let handler = null;
@@ -14,8 +14,6 @@ describe("ASRouterParentProcessMessageHandler", () => {
"addImpression",
"evaluateExpression",
"forceAttribution",
- "forceWNPanel",
- "closeWNPanel",
"forcePBWindow",
"resetGroupsState",
"resetMessageState",
@@ -122,7 +120,6 @@ describe("ASRouterParentProcessMessageHandler", () => {
[
msg.AS_ROUTER_TELEMETRY_USER_EVENT,
msg.TOOLBAR_BADGE_TELEMETRY,
- msg.TOOLBAR_PANEL_TELEMETRY,
msg.MOMENTS_PAGE_TELEMETRY,
msg.DOORHANGER_TELEMETRY,
].forEach(type => {
@@ -309,28 +306,6 @@ describe("ASRouterParentProcessMessageHandler", () => {
assert.calledOnce(config.router.forceAttribution);
});
});
- describe("FORCE_WHATSNEW_PANEL action", () => {
- it("default calls forceWNPanel", () => {
- handler.handleMessage(
- msg.FORCE_WHATSNEW_PANEL,
- {},
- { browser: { ownerGlobal: {} } }
- );
- assert.calledOnce(config.router.forceWNPanel);
- assert.calledWith(config.router.forceWNPanel, { ownerGlobal: {} });
- });
- });
- describe("CLOSE_WHATSNEW_PANEL action", () => {
- it("default calls closeWNPanel", () => {
- handler.handleMessage(
- msg.CLOSE_WHATSNEW_PANEL,
- {},
- { browser: { ownerGlobal: {} } }
- );
- assert.calledOnce(config.router.closeWNPanel);
- assert.calledWith(config.router.closeWNPanel, { ownerGlobal: {} });
- });
- });
describe("FORCE_PRIVATE_BROWSING_WINDOW action", () => {
it("default calls forcePBWindow", () => {
handler.handleMessage(
diff --git a/browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js b/browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js
index 3e91b657bc..cfeac77025 100644
--- a/browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js
+++ b/browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js
@@ -1,10 +1,6 @@
import { _ToolbarBadgeHub } from "modules/ToolbarBadgeHub.sys.mjs";
import { GlobalOverrider } from "test/unit/utils";
import { OnboardingMessageProvider } from "modules/OnboardingMessageProvider.sys.mjs";
-import {
- _ToolbarPanelHub,
- ToolbarPanelHub,
-} from "modules/ToolbarPanelHub.sys.mjs";
describe("ToolbarBadgeHub", () => {
let sandbox;
@@ -13,7 +9,6 @@ describe("ToolbarBadgeHub", () => {
let fakeSendTelemetry;
let isBrowserPrivateStub;
let fxaMessage;
- let whatsnewMessage;
let fakeElement;
let globals;
let everyWindowStub;
@@ -36,28 +31,6 @@ describe("ToolbarBadgeHub", () => {
const onboardingMsgs =
await OnboardingMessageProvider.getUntranslatedMessages();
fxaMessage = onboardingMsgs.find(({ id }) => id === "FXA_ACCOUNTS_BADGE");
- whatsnewMessage = {
- id: `WHATS_NEW_BADGE_71`,
- template: "toolbar_badge",
- content: {
- delay: 1000,
- target: "whats-new-menu-button",
- action: { id: "show-whatsnew-button" },
- badgeDescription: { string_id: "cfr-badge-reader-label-newfeature" },
- },
- priority: 1,
- trigger: { id: "toolbarBadgeUpdate" },
- frequency: {
- // Makes it so that we track impressions for this message while at the
- // same time it can have unlimited impressions
- lifetime: Infinity,
- },
- // Never saw this message or saw it in the past 4 days or more recent
- targeting: `isWhatsNewPanelEnabled &&
- (!messageImpressions['WHATS_NEW_BADGE_71'] ||
- (messageImpressions['WHATS_NEW_BADGE_71']|length >= 1 &&
- currentDate|date - messageImpressions['WHATS_NEW_BADGE_71'][0] <= 4 * 24 * 3600 * 1000))`,
- };
fakeElement = {
classList: {
add: sandbox.stub(),
@@ -93,7 +66,6 @@ describe("ToolbarBadgeHub", () => {
setStringPrefStub = sandbox.stub();
requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn());
globals.set({
- ToolbarPanelHub,
requestIdleCallback: requestIdleCallbackStub,
EveryWindow: everyWindowStub,
PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub },
@@ -139,16 +111,6 @@ describe("ToolbarBadgeHub", () => {
assert.calledTwice(instance.messageRequest);
});
- it("should add a pref observer", async () => {
- await instance.init(sandbox.stub().resolves(), {});
-
- assert.calledOnce(addObserverStub);
- assert.calledWithExactly(
- addObserverStub,
- instance.prefs.WHATSNEW_TOOLBAR_PANEL,
- instance
- );
- });
});
describe("#uninit", () => {
beforeEach(async () => {
@@ -164,16 +126,6 @@ describe("ToolbarBadgeHub", () => {
assert.calledOnce(clearTimeoutStub);
assert.calledWithExactly(clearTimeoutStub, 2);
});
- it("should remove the pref observer", () => {
- instance.uninit();
-
- assert.calledOnce(removeObserverStub);
- assert.calledWithExactly(
- removeObserverStub,
- instance.prefs.WHATSNEW_TOOLBAR_PANEL,
- instance
- );
- });
});
describe("messageRequest", () => {
let handleMessageRequestStub;
@@ -293,66 +245,6 @@ describe("ToolbarBadgeHub", () => {
instance.removeAllNotifications
);
});
- it("should execute actions if they exist", () => {
- sandbox.stub(instance, "executeAction");
- instance.addToolbarNotification(target, whatsnewMessage);
-
- assert.calledOnce(instance.executeAction);
- assert.calledWithExactly(instance.executeAction, {
- ...whatsnewMessage.content.action,
- message_id: whatsnewMessage.id,
- });
- });
- it("should create a description element", () => {
- sandbox.stub(instance, "executeAction");
- instance.addToolbarNotification(target, whatsnewMessage);
-
- assert.calledOnce(fakeDocument.createElement);
- assert.calledWithExactly(fakeDocument.createElement, "span");
- });
- it("should set description id to element and to button", () => {
- sandbox.stub(instance, "executeAction");
- instance.addToolbarNotification(target, whatsnewMessage);
-
- assert.calledWithExactly(
- fakeElement.setAttribute,
- "id",
- "toolbarbutton-notification-description"
- );
- assert.calledWithExactly(
- fakeElement.setAttribute,
- "aria-labelledby",
- `toolbarbutton-notification-description ${whatsnewMessage.content.target}`
- );
- });
- it("should attach fluent id to description", () => {
- sandbox.stub(instance, "executeAction");
- instance.addToolbarNotification(target, whatsnewMessage);
-
- assert.calledOnce(fakeDocument.l10n.setAttributes);
- assert.calledWithExactly(
- fakeDocument.l10n.setAttributes,
- fakeElement,
- whatsnewMessage.content.badgeDescription.string_id
- );
- });
- it("should add an impression for the message", () => {
- instance.addToolbarNotification(target, whatsnewMessage);
-
- assert.calledOnce(instance._addImpression);
- assert.calledWithExactly(instance._addImpression, whatsnewMessage);
- });
- it("should send an impression ping", async () => {
- sandbox.stub(instance, "sendUserEventTelemetry");
- instance.addToolbarNotification(target, whatsnewMessage);
-
- assert.calledOnce(instance.sendUserEventTelemetry);
- assert.calledWithExactly(
- instance.sendUserEventTelemetry,
- "IMPRESSION",
- whatsnewMessage
- );
- });
});
describe("registerBadgeNotificationListener", () => {
let msg_no_delay;
@@ -410,44 +302,6 @@ describe("ToolbarBadgeHub", () => {
assert.calledOnce(everyWindowStub.unregisterCallback);
assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id);
});
- it("should only call executeAction for 'update_action' messages", () => {
- const stub = sandbox.stub(instance, "executeAction");
- const updateActionMsg = { ...msg_no_delay, template: "update_action" };
-
- instance.registerBadgeNotificationListener(updateActionMsg);
-
- assert.notCalled(everyWindowStub.registerCallback);
- assert.calledOnce(stub);
- });
- });
- describe("executeAction", () => {
- let blockMessageByIdStub;
- beforeEach(async () => {
- blockMessageByIdStub = sandbox.stub();
- await instance.init(sandbox.stub().resolves(), {
- blockMessageById: blockMessageByIdStub,
- });
- });
- it("should call ToolbarPanelHub.enableToolbarButton", () => {
- const stub = sandbox.stub(
- _ToolbarPanelHub.prototype,
- "enableToolbarButton"
- );
-
- instance.executeAction({ id: "show-whatsnew-button" });
-
- assert.calledOnce(stub);
- });
- it("should call ToolbarPanelHub.enableAppmenuButton", () => {
- const stub = sandbox.stub(
- _ToolbarPanelHub.prototype,
- "enableAppmenuButton"
- );
-
- instance.executeAction({ id: "show-whatsnew-button" });
-
- assert.calledOnce(stub);
- });
});
describe("removeToolbarNotification", () => {
it("should remove the notification", () => {
@@ -629,24 +483,4 @@ describe("ToolbarBadgeHub", () => {
assert.propertyVal(ping.data, "event", "CLICK");
});
});
- describe("#observe", () => {
- it("should make a message request when the whats new pref is changed", () => {
- sandbox.stub(instance, "messageRequest");
-
- instance.observe("", "", instance.prefs.WHATSNEW_TOOLBAR_PANEL);
-
- assert.calledOnce(instance.messageRequest);
- assert.calledWithExactly(instance.messageRequest, {
- template: "toolbar_badge",
- triggerId: "toolbarBadgeUpdate",
- });
- });
- it("should not react to other pref changes", () => {
- sandbox.stub(instance, "messageRequest");
-
- instance.observe("", "", "foo");
-
- assert.notCalled(instance.messageRequest);
- });
- });
});
diff --git a/browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js b/browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js
deleted file mode 100644
index 1755f62308..0000000000
--- a/browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js
+++ /dev/null
@@ -1,760 +0,0 @@
-import { _ToolbarPanelHub } from "modules/ToolbarPanelHub.sys.mjs";
-import { GlobalOverrider } from "test/unit/utils";
-import { PanelTestProvider } from "modules/PanelTestProvider.sys.mjs";
-
-describe("ToolbarPanelHub", () => {
- let globals;
- let sandbox;
- let instance;
- let everyWindowStub;
- let fakeDocument;
- let fakeWindow;
- let fakeElementById;
- let fakeElementByTagName;
- let createdCustomElements = [];
- let eventListeners = {};
- let addObserverStub;
- let removeObserverStub;
- let getBoolPrefStub;
- let setBoolPrefStub;
- let waitForInitializedStub;
- let isBrowserPrivateStub;
- let fakeSendTelemetry;
- let getEarliestRecordedDateStub;
- let getEventsByDateRangeStub;
- let defaultSearchStub;
- let scriptloaderStub;
- let fakeRemoteL10n;
- let getViewNodeStub;
-
- beforeEach(async () => {
- sandbox = sinon.createSandbox();
- globals = new GlobalOverrider();
- instance = new _ToolbarPanelHub();
- waitForInitializedStub = sandbox.stub().resolves();
- fakeElementById = {
- setAttribute: sandbox.stub(),
- removeAttribute: sandbox.stub(),
- querySelector: sandbox.stub().returns(null),
- querySelectorAll: sandbox.stub().returns([]),
- appendChild: sandbox.stub(),
- addEventListener: sandbox.stub(),
- hasAttribute: sandbox.stub(),
- toggleAttribute: sandbox.stub(),
- remove: sandbox.stub(),
- removeChild: sandbox.stub(),
- };
- fakeElementByTagName = {
- setAttribute: sandbox.stub(),
- removeAttribute: sandbox.stub(),
- querySelector: sandbox.stub().returns(null),
- querySelectorAll: sandbox.stub().returns([]),
- appendChild: sandbox.stub(),
- addEventListener: sandbox.stub(),
- hasAttribute: sandbox.stub(),
- toggleAttribute: sandbox.stub(),
- remove: sandbox.stub(),
- removeChild: sandbox.stub(),
- };
- fakeDocument = {
- getElementById: sandbox.stub().returns(fakeElementById),
- getElementsByTagName: sandbox.stub().returns(fakeElementByTagName),
- querySelector: sandbox.stub().returns({}),
- createElement: tagName => {
- const element = {
- tagName,
- classList: {},
- addEventListener: (ev, fn) => {
- eventListeners[ev] = fn;
- },
- appendChild: sandbox.stub(),
- setAttribute: sandbox.stub(),
- textContent: "",
- };
- element.classList.add = sandbox.stub();
- element.classList.includes = className =>
- element.classList.add.firstCall.args[0] === className;
- createdCustomElements.push(element);
- return element;
- },
- l10n: {
- translateElements: sandbox.stub(),
- translateFragment: sandbox.stub(),
- formatMessages: sandbox.stub().resolves([{}]),
- setAttributes: sandbox.stub(),
- },
- };
- fakeWindow = {
- // eslint-disable-next-line object-shorthand
- DocumentFragment: function () {
- return fakeElementById;
- },
- document: fakeDocument,
- browser: {
- ownerDocument: fakeDocument,
- },
- MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
- ownerGlobal: {
- openLinkIn: sandbox.stub(),
- gBrowser: "gBrowser",
- },
- PanelUI: {
- panel: fakeElementById,
- whatsNewPanel: fakeElementById,
- },
- customElements: { get: sandbox.stub() },
- };
- everyWindowStub = {
- registerCallback: sandbox.stub(),
- unregisterCallback: sandbox.stub(),
- };
- scriptloaderStub = { loadSubScript: sandbox.stub() };
- addObserverStub = sandbox.stub();
- removeObserverStub = sandbox.stub();
- getBoolPrefStub = sandbox.stub();
- setBoolPrefStub = sandbox.stub();
- fakeSendTelemetry = sandbox.stub();
- isBrowserPrivateStub = sandbox.stub();
- getEarliestRecordedDateStub = sandbox.stub().returns(
- // A random date that's not the current timestamp
- new Date() - 500
- );
- getEventsByDateRangeStub = sandbox.stub().returns([]);
- getViewNodeStub = sandbox.stub().returns(fakeElementById);
- defaultSearchStub = { defaultEngine: { name: "DDG" } };
- fakeRemoteL10n = {
- l10n: {},
- reloadL10n: sandbox.stub(),
- createElement: sandbox
- .stub()
- .callsFake((doc, el) => fakeDocument.createElement(el)),
- };
- globals.set({
- EveryWindow: everyWindowStub,
- Services: {
- ...Services,
- prefs: {
- addObserver: addObserverStub,
- removeObserver: removeObserverStub,
- getBoolPref: getBoolPrefStub,
- setBoolPref: setBoolPrefStub,
- },
- search: defaultSearchStub,
- scriptloader: scriptloaderStub,
- },
- PrivateBrowsingUtils: {
- isBrowserPrivate: isBrowserPrivateStub,
- },
- TrackingDBService: {
- getEarliestRecordedDate: getEarliestRecordedDateStub,
- getEventsByDateRange: getEventsByDateRangeStub,
- },
- SpecialMessageActions: {
- handleAction: sandbox.stub(),
- },
- RemoteL10n: fakeRemoteL10n,
- PanelMultiView: {
- getViewNode: getViewNodeStub,
- },
- });
- });
- afterEach(() => {
- instance.uninit();
- sandbox.restore();
- globals.restore();
- eventListeners = {};
- createdCustomElements = [];
- });
- it("should create an instance", () => {
- assert.ok(instance);
- });
- it("should enableAppmenuButton() on init() just once", async () => {
- instance.enableAppmenuButton = sandbox.stub();
-
- await instance.init(waitForInitializedStub, { getMessages: () => {} });
- await instance.init(waitForInitializedStub, { getMessages: () => {} });
-
- assert.calledOnce(instance.enableAppmenuButton);
-
- instance.uninit();
-
- await instance.init(waitForInitializedStub, { getMessages: () => {} });
-
- assert.calledTwice(instance.enableAppmenuButton);
- });
- it("should unregisterCallback on uninit()", () => {
- instance.uninit();
- assert.calledTwice(everyWindowStub.unregisterCallback);
- });
- describe("#maybeLoadCustomElement", () => {
- it("should not load customElements a second time", () => {
- instance.maybeLoadCustomElement({ customElements: new Map() });
- instance.maybeLoadCustomElement({
- customElements: new Map([["remote-text", true]]),
- });
-
- assert.calledOnce(scriptloaderStub.loadSubScript);
- });
- });
- describe("#toggleWhatsNewPref", () => {
- it("should call Services.prefs.setBoolPref() with the opposite value", () => {
- let checkbox = {};
- let event = { target: checkbox };
- // checkbox starts false
- checkbox.checked = false;
-
- // toggling the checkbox to set the value to true;
- // Preferences.set() gets called before the checkbox changes,
- // so we have to call it with the opposite value.
- instance.toggleWhatsNewPref(event);
-
- assert.calledOnce(setBoolPrefStub);
- assert.calledWith(
- setBoolPrefStub,
- "browser.messaging-system.whatsNewPanel.enabled",
- !checkbox.checked
- );
- });
- it("should report telemetry with the opposite value", () => {
- let sendUserEventTelemetryStub = sandbox.stub(
- instance,
- "sendUserEventTelemetry"
- );
- let event = {
- target: { checked: true, ownerGlobal: fakeWindow },
- };
-
- instance.toggleWhatsNewPref(event);
-
- assert.calledOnce(sendUserEventTelemetryStub);
- const { args } = sendUserEventTelemetryStub.firstCall;
- assert.equal(args[1], "WNP_PREF_TOGGLE");
- assert.propertyVal(args[3].value, "prefValue", false);
- });
- });
- describe("#enableAppmenuButton", () => {
- it("should registerCallback on enableAppmenuButton() if there are messages", async () => {
- await instance.init(waitForInitializedStub, {
- getMessages: sandbox.stub().resolves([{}, {}]),
- });
- // init calls `enableAppmenuButton`
- everyWindowStub.registerCallback.resetHistory();
-
- await instance.enableAppmenuButton();
-
- assert.calledOnce(everyWindowStub.registerCallback);
- assert.calledWithExactly(
- everyWindowStub.registerCallback,
- "appMenu-whatsnew-button",
- sinon.match.func,
- sinon.match.func
- );
- });
- it("should not registerCallback on enableAppmenuButton() if there are no messages", async () => {
- instance.init(waitForInitializedStub, {
- getMessages: sandbox.stub().resolves([]),
- });
- // init calls `enableAppmenuButton`
- everyWindowStub.registerCallback.resetHistory();
-
- await instance.enableAppmenuButton();
-
- assert.notCalled(everyWindowStub.registerCallback);
- });
- });
- describe("#disableAppmenuButton", () => {
- it("should call the unregisterCallback", () => {
- assert.notCalled(everyWindowStub.unregisterCallback);
-
- instance.disableAppmenuButton();
-
- assert.calledOnce(everyWindowStub.unregisterCallback);
- assert.calledWithExactly(
- everyWindowStub.unregisterCallback,
- "appMenu-whatsnew-button"
- );
- });
- });
- describe("#enableToolbarButton", () => {
- it("should registerCallback on enableToolbarButton if messages.length", async () => {
- await instance.init(waitForInitializedStub, {
- getMessages: sandbox.stub().resolves([{}, {}]),
- });
- // init calls `enableAppmenuButton`
- everyWindowStub.registerCallback.resetHistory();
-
- await instance.enableToolbarButton();
-
- assert.calledOnce(everyWindowStub.registerCallback);
- assert.calledWithExactly(
- everyWindowStub.registerCallback,
- "whats-new-menu-button",
- sinon.match.func,
- sinon.match.func
- );
- });
- it("should not registerCallback on enableToolbarButton if no messages", async () => {
- await instance.init(waitForInitializedStub, {
- getMessages: sandbox.stub().resolves([]),
- });
-
- await instance.enableToolbarButton();
-
- assert.notCalled(everyWindowStub.registerCallback);
- });
- });
- describe("Show/Hide functions", () => {
- it("should unhide appmenu button on _showAppmenuButton()", async () => {
- await instance._showAppmenuButton(fakeWindow);
-
- assert.equal(fakeElementById.hidden, false);
- });
- it("should hide appmenu button on _hideAppmenuButton()", () => {
- instance._hideAppmenuButton(fakeWindow);
- assert.equal(fakeElementById.hidden, true);
- });
- it("should not do anything if the window is closed", () => {
- instance._hideAppmenuButton(fakeWindow, true);
- assert.notCalled(global.PanelMultiView.getViewNode);
- });
- it("should not throw if the element does not exist", () => {
- let fn = instance._hideAppmenuButton.bind(null, {
- browser: { ownerDocument: {} },
- });
- getViewNodeStub.returns(undefined);
- assert.doesNotThrow(fn);
- });
- it("should unhide toolbar button on _showToolbarButton()", async () => {
- await instance._showToolbarButton(fakeWindow);
-
- assert.equal(fakeElementById.hidden, false);
- });
- it("should hide toolbar button on _hideToolbarButton()", () => {
- instance._hideToolbarButton(fakeWindow);
- assert.equal(fakeElementById.hidden, true);
- });
- });
- describe("#renderMessages", () => {
- let getMessagesStub;
- beforeEach(() => {
- getMessagesStub = sandbox.stub();
- instance.init(waitForInitializedStub, {
- getMessages: getMessagesStub,
- sendTelemetry: fakeSendTelemetry,
- });
- });
- it("should have correct state", async () => {
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
-
- getMessagesStub.returns(messages);
- const ev1 = sandbox.stub();
- ev1.withArgs("type").returns(1); // tracker
- ev1.withArgs("count").returns(4);
- const ev2 = sandbox.stub();
- ev2.withArgs("type").returns(4); // fingerprinter
- ev2.withArgs("count").returns(3);
- getEventsByDateRangeStub.returns([
- { getResultByName: ev1 },
- { getResultByName: ev2 },
- ]);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- assert.propertyVal(instance.state.contentArguments, "trackerCount", 4);
- assert.propertyVal(
- instance.state.contentArguments,
- "fingerprinterCount",
- 3
- );
- });
- it("should render messages to the panel on renderMessages()", async () => {
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
- messages[0].content.link_text = { string_id: "link_text_id" };
-
- getMessagesStub.returns(messages);
- const ev1 = sandbox.stub();
- ev1.withArgs("type").returns(1); // tracker
- ev1.withArgs("count").returns(4);
- const ev2 = sandbox.stub();
- ev2.withArgs("type").returns(4); // fingerprinter
- ev2.withArgs("count").returns(3);
- getEventsByDateRangeStub.returns([
- { getResultByName: ev1 },
- { getResultByName: ev2 },
- ]);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- for (let message of messages) {
- assert.ok(
- fakeRemoteL10n.createElement.args.find(
- ([, , args]) => args && args.classList === "whatsNew-message-title"
- )
- );
- if (message.content.layout === "tracking-protections") {
- assert.ok(
- fakeRemoteL10n.createElement.args.find(
- ([, , args]) =>
- args && args.classList === "whatsNew-message-subtitle"
- )
- );
- }
- if (message.id === "WHATS_NEW_FINGERPRINTER_COUNTER_72") {
- assert.ok(
- fakeRemoteL10n.createElement.args.find(
- ([, el, args]) => el === "h2" && args.content === 3
- )
- );
- }
- assert.ok(
- fakeRemoteL10n.createElement.args.find(
- ([, , args]) =>
- args && args.classList === "whatsNew-message-content"
- )
- );
- }
- // Call the click handler to make coverage happy.
- eventListeners.mouseup();
- assert.calledOnce(global.SpecialMessageActions.handleAction);
- });
- it("should clear previous messages on 2nd renderMessages()", async () => {
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
- const removeStub = sandbox.stub();
- fakeElementById.querySelectorAll.onCall(0).returns([]);
- fakeElementById.querySelectorAll
- .onCall(1)
- .returns([{ remove: removeStub }, { remove: removeStub }]);
-
- getMessagesStub.returns(messages);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- assert.calledTwice(removeStub);
- });
- it("should sort based on order field value", async () => {
- const messages = (await PanelTestProvider.getMessages()).filter(
- m =>
- m.template === "whatsnew_panel_message" &&
- m.content.published_date === 1560969794394
- );
-
- messages.forEach(m => (m.content.title = m.order));
-
- getMessagesStub.returns(messages);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- // Select the title elements that are supposed to be set to the same
- // value as the `order` field of the message
- const titleEls = fakeRemoteL10n.createElement.args
- .filter(
- ([, , args]) => args && args.classList === "whatsNew-message-title"
- )
- .map(([, , args]) => args.content);
- assert.deepEqual(titleEls, [1, 2, 3]);
- });
- it("should accept string for image attributes", async () => {
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.id === "WHATS_NEW_70_1"
- );
- getMessagesStub.returns(messages);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- const imageEl = createdCustomElements.find(el => el.tagName === "img");
- assert.calledOnce(imageEl.setAttribute);
- assert.calledWithExactly(
- imageEl.setAttribute,
- "alt",
- "Firefox Send Logo"
- );
- });
- it("should set state values as data-attribute", async () => {
- const message = (await PanelTestProvider.getMessages()).find(
- m => m.template === "whatsnew_panel_message"
- );
- getMessagesStub.returns([message]);
- instance.state.contentArguments = { foo: "foo", bar: "bar" };
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- const [, , args] = fakeRemoteL10n.createElement.args.find(
- ([, , elArgs]) => elArgs && elArgs.attributes
- );
- assert.ok(args);
- // Currently this.state.contentArguments has 8 different entries
- assert.lengthOf(Object.keys(args.attributes), 8);
- assert.equal(
- args.attributes.searchEngineName,
- defaultSearchStub.defaultEngine.name
- );
- });
- it("should only render unique dates (no duplicates)", async () => {
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
- const uniqueDates = [
- ...new Set(messages.map(m => m.content.published_date)),
- ];
- getMessagesStub.returns(messages);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- const dateElements = fakeRemoteL10n.createElement.args.filter(
- ([, el, args]) =>
- el === "p" && args.classList === "whatsNew-message-date"
- );
- assert.lengthOf(dateElements, uniqueDates.length);
- });
- it("should listen for panelhidden and remove the toolbar button", async () => {
- getMessagesStub.returns([]);
- fakeDocument.getElementById
- .withArgs("customizationui-widget-panel")
- .returns(null);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- assert.notCalled(fakeElementById.addEventListener);
- });
- it("should attach doCommand cbs that handle user actions", async () => {
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
- getMessagesStub.returns(messages);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- const messageEl = createdCustomElements.find(
- el =>
- el.tagName === "div" && el.classList.includes("whatsNew-message-body")
- );
- const anchorEl = createdCustomElements.find(el => el.tagName === "a");
-
- assert.notCalled(global.SpecialMessageActions.handleAction);
-
- messageEl.doCommand();
- anchorEl.doCommand();
-
- assert.calledTwice(global.SpecialMessageActions.handleAction);
- });
- it("should listen for panelhidden and remove the toolbar button", async () => {
- getMessagesStub.returns([]);
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- assert.calledOnce(fakeElementById.addEventListener);
- assert.calledWithExactly(
- fakeElementById.addEventListener,
- "popuphidden",
- sinon.match.func,
- {
- once: true,
- }
- );
- const [, cb] = fakeElementById.addEventListener.firstCall.args;
-
- assert.notCalled(everyWindowStub.unregisterCallback);
-
- cb();
-
- assert.calledOnce(everyWindowStub.unregisterCallback);
- assert.calledWithExactly(
- everyWindowStub.unregisterCallback,
- "whats-new-menu-button"
- );
- });
- describe("#IMPRESSION", () => {
- it("should dispatch a IMPRESSION for messages", async () => {
- // means panel is triggered from the toolbar button
- fakeElementById.hasAttribute.returns(true);
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
- getMessagesStub.returns(messages);
- const spy = sandbox.spy(instance, "sendUserEventTelemetry");
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- assert.calledOnce(spy);
- assert.calledOnce(fakeSendTelemetry);
- assert.propertyVal(
- spy.firstCall.args[2],
- "id",
- messages
- .map(({ id }) => id)
- .sort()
- .join(",")
- );
- });
- it("should dispatch a CLICK for clicking a message", async () => {
- // means panel is triggered from the toolbar button
- fakeElementById.hasAttribute.returns(true);
- // Force to render the message
- fakeElementById.querySelector.returns(null);
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
- getMessagesStub.returns([messages[0]]);
- const spy = sandbox.spy(instance, "sendUserEventTelemetry");
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- assert.calledOnce(spy);
- assert.calledOnce(fakeSendTelemetry);
-
- spy.resetHistory();
-
- // Message click event listener cb
- eventListeners.mouseup();
-
- assert.calledOnce(spy);
- assert.calledWithExactly(spy, fakeWindow, "CLICK", messages[0]);
- });
- it("should dispatch a IMPRESSION with toolbar_dropdown", async () => {
- // means panel is triggered from the toolbar button
- fakeElementById.hasAttribute.returns(true);
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
- getMessagesStub.resolves(messages);
- const spy = sandbox.spy(instance, "sendUserEventTelemetry");
- const panelPingId = messages
- .map(({ id }) => id)
- .sort()
- .join(",");
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- assert.calledOnce(spy);
- assert.calledWithExactly(
- spy,
- fakeWindow,
- "IMPRESSION",
- {
- id: panelPingId,
- },
- {
- value: {
- view: "toolbar_dropdown",
- },
- }
- );
- assert.calledOnce(fakeSendTelemetry);
- const {
- args: [dispatchPayload],
- } = fakeSendTelemetry.lastCall;
- assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY");
- assert.propertyVal(dispatchPayload.data, "message_id", panelPingId);
- assert.deepEqual(dispatchPayload.data.event_context, {
- view: "toolbar_dropdown",
- });
- });
- it("should dispatch a IMPRESSION with application_menu", async () => {
- // means panel is triggered as a subview in the application menu
- fakeElementById.hasAttribute.returns(false);
- const messages = (await PanelTestProvider.getMessages()).filter(
- m => m.template === "whatsnew_panel_message"
- );
- getMessagesStub.resolves(messages);
- const spy = sandbox.spy(instance, "sendUserEventTelemetry");
- const panelPingId = messages
- .map(({ id }) => id)
- .sort()
- .join(",");
-
- await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
-
- assert.calledOnce(spy);
- assert.calledWithExactly(
- spy,
- fakeWindow,
- "IMPRESSION",
- {
- id: panelPingId,
- },
- {
- value: {
- view: "application_menu",
- },
- }
- );
- assert.calledOnce(fakeSendTelemetry);
- const {
- args: [dispatchPayload],
- } = fakeSendTelemetry.lastCall;
- assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY");
- assert.propertyVal(dispatchPayload.data, "message_id", panelPingId);
- assert.deepEqual(dispatchPayload.data.event_context, {
- view: "application_menu",
- });
- });
- });
- describe("#forceShowMessage", () => {
- const panelSelector = "PanelUI-whatsNew-message-container";
- let removeMessagesSpy;
- let renderMessagesStub;
- let addEventListenerStub;
- let messages;
- let browser;
- beforeEach(async () => {
- messages = (await PanelTestProvider.getMessages()).find(
- m => m.id === "WHATS_NEW_70_1"
- );
- removeMessagesSpy = sandbox.spy(instance, "removeMessages");
- renderMessagesStub = sandbox.spy(instance, "renderMessages");
- addEventListenerStub = fakeElementById.addEventListener;
- browser = { ownerGlobal: fakeWindow, ownerDocument: fakeDocument };
- fakeElementById.querySelectorAll.returns([fakeElementById]);
- });
- it("should call removeMessages when forcing a message to show", () => {
- instance.forceShowMessage(browser, messages);
-
- assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector);
- });
- it("should call renderMessages when forcing a message to show", () => {
- instance.forceShowMessage(browser, messages);
-
- assert.calledOnce(renderMessagesStub);
- assert.calledWithExactly(
- renderMessagesStub,
- fakeWindow,
- fakeDocument,
- panelSelector,
- {
- force: true,
- messages: Array.isArray(messages) ? messages : [messages],
- }
- );
- });
- it("should cleanup after the panel is hidden when forcing a message to show", () => {
- instance.forceShowMessage(browser, messages);
-
- assert.calledOnce(addEventListenerStub);
- assert.calledWithExactly(
- addEventListenerStub,
- "popuphidden",
- sinon.match.func
- );
-
- const [, cb] = addEventListenerStub.firstCall.args;
- // Reset the call count from the first `forceShowMessage` call
- removeMessagesSpy.resetHistory();
- cb({ target: { ownerGlobal: fakeWindow } });
-
- assert.calledOnce(removeMessagesSpy);
- assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector);
- });
- it("should exit gracefully if called before a browser exists", () => {
- instance.forceShowMessage(null, messages);
- assert.neverCalledWith(removeMessagesSpy, fakeWindow, panelSelector);
- });
- });
- });
-});
diff --git a/browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx b/browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx
index 46d5704107..c5b0d09b39 100644
--- a/browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx
+++ b/browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx
@@ -1,4 +1,7 @@
-import { ASRouterAdminInner } from "content-src/components/ASRouterAdmin/ASRouterAdmin";
+import {
+ ASRouterAdminInner,
+ toBinary,
+} from "content-src/components/ASRouterAdmin/ASRouterAdmin";
import { ASRouterUtils } from "content-src/asrouter-utils";
import { GlobalOverrider } from "test/unit/utils";
import React from "react";
@@ -259,4 +262,43 @@ describe("ASRouterAdmin", () => {
});
});
});
+ describe("toBinary", () => {
+ // Bringing the 'fromBinary' function over from
+ // messagepreview to prove it works
+ function fromBinary(encoded) {
+ const binary = atob(decodeURIComponent(encoded));
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < bytes.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ return String.fromCharCode(...new Uint16Array(bytes.buffer));
+ }
+
+ it("correctly encodes a latin string", () => {
+ const testString = "Hi I am a test string";
+ const expectedResult =
+ "SABpACAASQAgAGEAbQAgAGEAIAB0AGUAcwB0ACAAcwB0AHIAaQBuAGcA";
+
+ const encodedResult = toBinary(testString);
+
+ assert.equal(encodedResult, expectedResult);
+
+ const decodedResult = fromBinary(encodedResult);
+
+ assert.equal(decodedResult, testString);
+ });
+
+ it("correctly encodes a non-latin string", () => {
+ const nonLatinString = "тестовое сообщение";
+ const expectedResult = "QgQ1BEEEQgQ+BDIEPgQ1BCAAQQQ+BD4EMQRJBDUEPQQ4BDUE";
+
+ const encodedResult = toBinary("тестовое сообщение");
+
+ assert.equal(encodedResult, expectedResult);
+
+ const decodedResult = fromBinary(encodedResult);
+
+ assert.equal(decodedResult, nonLatinString);
+ });
+ });
});
diff --git a/browser/components/asrouter/tests/unit/unit-entry.js b/browser/components/asrouter/tests/unit/unit-entry.js
index f2046a81cb..2464b02c58 100644
--- a/browser/components/asrouter/tests/unit/unit-entry.js
+++ b/browser/components/asrouter/tests/unit/unit-entry.js
@@ -14,7 +14,7 @@ import FxMSCommonSchema from "../../content-src/schemas/FxMSCommon.schema.json";
import {
MESSAGE_TYPE_LIST,
MESSAGE_TYPE_HASH,
-} from "modules/ActorConstants.sys.mjs";
+} from "modules/ActorConstants.mjs";
enzyme.configure({ adapter: new Adapter() });
diff --git a/browser/components/asrouter/tests/xpcshell/head.js b/browser/components/asrouter/tests/xpcshell/head.js
index 0c6cec1ac8..fa361a00b9 100644
--- a/browser/components/asrouter/tests/xpcshell/head.js
+++ b/browser/components/asrouter/tests/xpcshell/head.js
@@ -81,10 +81,6 @@ async function makeValidators() {
"resource://testing-common/UpdateAction.schema.json",
{ common: true }
),
- whatsnew_panel_message: await schemaValidatorFor(
- "resource://testing-common/WhatsNewMessage.schema.json",
- { common: true }
- ),
feature_callout: await schemaValidatorFor(
// For now, Feature Callout and Spotlight share a common schema
"resource://testing-common/Spotlight.schema.json",
diff --git a/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js b/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js
index 3523355659..7e9892a595 100644
--- a/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js
+++ b/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js
@@ -22,7 +22,6 @@ add_task(async function test_PanelTestProvider() {
cfr_doorhanger: 1,
milestone_message: 0,
update_action: 1,
- whatsnew_panel_message: 7,
spotlight: 3,
feature_callout: 1,
pb_newtab: 2,
diff --git a/browser/components/asrouter/yamscripts.yml b/browser/components/asrouter/yamscripts.yml
index de16c269a4..e52063c911 100644
--- a/browser/components/asrouter/yamscripts.yml
+++ b/browser/components/asrouter/yamscripts.yml
@@ -19,6 +19,7 @@ scripts:
lint: =>lint
build: =>bundle:admin
unit: karma start karma.mc.config.js
+ import: =>import-rollouts
tddmc: karma start karma.mc.config.js --tdd
diff --git a/browser/components/backup/.eslintrc.js b/browser/components/backup/.eslintrc.js
deleted file mode 100644
index 9aafb4a214..0000000000
--- a/browser/components/backup/.eslintrc.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-module.exports = {
- extends: ["plugin:mozilla/require-jsdoc"],
-};
diff --git a/browser/components/backup/BackupResources.sys.mjs b/browser/components/backup/BackupResources.sys.mjs
index 276fabefdf..ce7f53b10d 100644
--- a/browser/components/backup/BackupResources.sys.mjs
+++ b/browser/components/backup/BackupResources.sys.mjs
@@ -2,14 +2,28 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-// Remove this import after BackupResource is referenced elsewhere.
-// eslint-disable-next-line no-unused-vars
-import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
-
/**
* Classes exported here are registered as a resource that can be
* backed up and restored in the BackupService.
*
* They must extend the BackupResource base class.
*/
-export {};
+import { AddonsBackupResource } from "resource:///modules/backup/AddonsBackupResource.sys.mjs";
+import { CookiesBackupResource } from "resource:///modules/backup/CookiesBackupResource.sys.mjs";
+import { CredentialsAndSecurityBackupResource } from "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs";
+import { FormHistoryBackupResource } from "resource:///modules/backup/FormHistoryBackupResource.sys.mjs";
+import { MiscDataBackupResource } from "resource:///modules/backup/MiscDataBackupResource.sys.mjs";
+import { PlacesBackupResource } from "resource:///modules/backup/PlacesBackupResource.sys.mjs";
+import { PreferencesBackupResource } from "resource:///modules/backup/PreferencesBackupResource.sys.mjs";
+import { SessionStoreBackupResource } from "resource:///modules/backup/SessionStoreBackupResource.sys.mjs";
+
+export {
+ AddonsBackupResource,
+ CookiesBackupResource,
+ CredentialsAndSecurityBackupResource,
+ FormHistoryBackupResource,
+ MiscDataBackupResource,
+ PlacesBackupResource,
+ PreferencesBackupResource,
+ SessionStoreBackupResource,
+};
diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs
index 853f4768ce..3521f315fd 100644
--- a/browser/components/backup/BackupService.sys.mjs
+++ b/browser/components/backup/BackupService.sys.mjs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import * as BackupResources from "resource:///modules/backup/BackupResources.sys.mjs";
+import * as DefaultBackupResources from "resource:///modules/backup/BackupResources.sys.mjs";
const lazy = {};
@@ -37,6 +37,13 @@ export class BackupService {
#resources = new Map();
/**
+ * True if a backup is currently in progress.
+ *
+ * @type {boolean}
+ */
+ #backupInProgress = false;
+
+ /**
* Returns a reference to a BackupService singleton. If this is the first time
* that this getter is accessed, this causes the BackupService singleton to be
* be instantiated.
@@ -48,27 +55,130 @@ export class BackupService {
if (this.#instance) {
return this.#instance;
}
- this.#instance = new BackupService(BackupResources);
+ this.#instance = new BackupService(DefaultBackupResources);
this.#instance.takeMeasurements();
return this.#instance;
}
/**
+ * Returns a reference to the BackupService singleton. If the singleton has
+ * not been initialized, an error is thrown.
+ *
+ * @static
+ * @returns {BackupService}
+ */
+ static get() {
+ if (!this.#instance) {
+ throw new Error("BackupService not initialized");
+ }
+ return this.#instance;
+ }
+
+ /**
* Create a BackupService instance.
*
- * @param {object} [backupResources=BackupResources] - Object containing BackupResource classes to associate with this service.
+ * @param {object} [backupResources=DefaultBackupResources] - Object containing BackupResource classes to associate with this service.
*/
- constructor(backupResources = BackupResources) {
+ constructor(backupResources = DefaultBackupResources) {
lazy.logConsole.debug("Instantiated");
for (const resourceName in backupResources) {
- let resource = BackupResources[resourceName];
+ let resource = backupResources[resourceName];
this.#resources.set(resource.key, resource);
}
}
/**
+ * Create a backup of the user's profile.
+ *
+ * @param {object} [options]
+ * Options for the backup.
+ * @param {string} [options.profilePath=PathUtils.profileDir]
+ * The path to the profile to backup. By default, this is the current
+ * profile.
+ * @returns {Promise<undefined>}
+ */
+ async createBackup({ profilePath = PathUtils.profileDir } = {}) {
+ // createBackup does not allow re-entry or concurrent backups.
+ if (this.#backupInProgress) {
+ lazy.logConsole.warn("Backup attempt already in progress");
+ return;
+ }
+
+ this.#backupInProgress = true;
+
+ try {
+ lazy.logConsole.debug(`Creating backup for profile at ${profilePath}`);
+
+ // First, check to see if a `backups` directory already exists in the
+ // profile.
+ let backupDirPath = PathUtils.join(profilePath, "backups");
+ lazy.logConsole.debug("Creating backups folder");
+
+ // ignoreExisting: true is the default, but we're being explicit that it's
+ // okay if this folder already exists.
+ await IOUtils.makeDirectory(backupDirPath, { ignoreExisting: true });
+
+ let stagingPath = await this.#prepareStagingFolder(backupDirPath);
+
+ // Perform the backup for each resource.
+ for (let resourceClass of this.#resources.values()) {
+ try {
+ lazy.logConsole.debug(
+ `Backing up resource with key ${resourceClass.key}. ` +
+ `Requires encryption: ${resourceClass.requiresEncryption}`
+ );
+ let resourcePath = PathUtils.join(stagingPath, resourceClass.key);
+ await IOUtils.makeDirectory(resourcePath);
+
+ // `backup` on each BackupResource should return us a ManifestEntry
+ // that we eventually write to a JSON manifest file, but for now,
+ // we're just going to log it.
+ let manifestEntry = await new resourceClass().backup(
+ resourcePath,
+ profilePath
+ );
+ lazy.logConsole.debug(
+ `Backup of resource with key ${resourceClass.key} completed`,
+ manifestEntry
+ );
+ } catch (e) {
+ lazy.logConsole.error(
+ `Failed to backup resource: ${resourceClass.key}`,
+ e
+ );
+ }
+ }
+ } finally {
+ this.#backupInProgress = false;
+ }
+ }
+
+ /**
+ * Constructs the staging folder for the backup in the passed in backup
+ * folder. If a pre-existing staging folder exists, it will be cleared out.
+ *
+ * @param {string} backupDirPath
+ * The path to the backup folder.
+ * @returns {Promise<string>}
+ * The path to the empty staging folder.
+ */
+ async #prepareStagingFolder(backupDirPath) {
+ let stagingPath = PathUtils.join(backupDirPath, "staging");
+ lazy.logConsole.debug("Checking for pre-existing staging folder");
+ if (await IOUtils.exists(stagingPath)) {
+ // A pre-existing staging folder exists. A previous backup attempt must
+ // have failed or been interrupted. We'll clear it out.
+ lazy.logConsole.warn("A pre-existing staging folder exists. Clearing.");
+ await IOUtils.remove(stagingPath, { recursive: true });
+ }
+ await IOUtils.makeDirectory(stagingPath);
+
+ return stagingPath;
+ }
+
+ /**
* Take measurements of the current profile state for Telemetry.
*
* @returns {Promise<undefined>}
@@ -97,7 +207,14 @@ export class BackupService {
// Measure the size of each file we are going to backup.
for (let resourceClass of this.#resources.values()) {
- await new resourceClass().measure(PathUtils.profileDir);
+ try {
+ await new resourceClass().measure(PathUtils.profileDir);
+ } catch (e) {
+ lazy.logConsole.error(
+ `Failed to measure for resource: ${resourceClass.key}`,
+ e
+ );
+ }
}
}
}
diff --git a/browser/components/backup/content/debug.html b/browser/components/backup/content/debug.html
new file mode 100644
index 0000000000..5d6517cf2a
--- /dev/null
+++ b/browser/components/backup/content/debug.html
@@ -0,0 +1,46 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Profile backup debug tool</title>
+
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ </head>
+ <body>
+ <header>
+ <h1>Profile backup debug tool</h1>
+ </header>
+
+ <main>
+ <section>
+ <h2>State</h2>
+ <ol>
+ <li>
+ <input
+ type="checkbox"
+ preference="browser.backup.enabled"
+ />BackupService component enabled
+ </li>
+ <li>
+ <input
+ type="checkbox"
+ preference="browser.backup.log"
+ />BackupService debug logging enabled
+ </li>
+ </ol>
+ </section>
+ <section id="controls">
+ <h2>Controls</h2>
+ <button id="create-backup">Create backup</button>
+ <button id="open-backup-folder">Open backups folder</button>
+ </section>
+ </main>
+
+ <script src="chrome://global/content/preferencesBindings.js"></script>
+ <script src="chrome://browser/content/backup/debug.js"></script>
+ </body>
+</html>
diff --git a/browser/components/backup/content/debug.js b/browser/components/backup/content/debug.js
new file mode 100644
index 0000000000..fd673818c0
--- /dev/null
+++ b/browser/components/backup/content/debug.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+Preferences.addAll([
+ { id: "browser.backup.enabled", type: "bool" },
+ { id: "browser.backup.log", type: "bool" },
+]);
+
+const { BackupService } = ChromeUtils.importESModule(
+ "resource:///modules/backup/BackupService.sys.mjs"
+);
+
+let DebugUI = {
+ init() {
+ let controls = document.querySelector("#controls");
+ controls.addEventListener("click", this);
+ },
+
+ handleEvent(event) {
+ let target = event.target;
+ if (HTMLButtonElement.isInstance(event.target)) {
+ this.onButtonClick(target);
+ }
+ },
+
+ async onButtonClick(button) {
+ switch (button.id) {
+ case "create-backup": {
+ let service = BackupService.get();
+ button.disabled = true;
+ await service.createBackup();
+ button.disabled = false;
+ break;
+ }
+ case "open-backup-folder": {
+ let backupsDir = PathUtils.join(PathUtils.profileDir, "backups");
+
+ let nsLocalFile = Components.Constructor(
+ "@mozilla.org/file/local;1",
+ "nsIFile",
+ "initWithPath"
+ );
+
+ if (await IOUtils.exists(backupsDir)) {
+ new nsLocalFile(backupsDir).reveal();
+ } else {
+ alert("backups folder doesn't exist yet");
+ }
+
+ break;
+ }
+ }
+ },
+};
+
+DebugUI.init();
diff --git a/browser/components/backup/docs/backup-resources.rst b/browser/components/backup/docs/backup-resources.rst
new file mode 100644
index 0000000000..4ead0d316d
--- /dev/null
+++ b/browser/components/backup/docs/backup-resources.rst
@@ -0,0 +1,18 @@
+================================
+Backup Resources Reference
+================================
+
+A ``BackupResource`` is the base class used to represent a group of data within
+a user profile that is logical to backup together. For example, the
+``PlacesBackupResource`` represents both the ``places.sqlite`` SQLite database,
+as well as the ``favicons.sqlite`` database. The ``AddonsBackupResource``
+represents not only the preferences for various addons, but also the XPI files
+that those addons are defined in.
+
+Each ``BackupResource`` subclass is registered for use by the
+``BackupService`` by adding it to the default set of exported classes in the
+``BackupResources`` module in ``BackupResources.sys.mjs``.
+
+.. js:autoclass:: BackupResource
+ :members:
+ :private-members:
diff --git a/browser/components/backup/docs/index.rst b/browser/components/backup/docs/index.rst
index 1e201f8f1c..db9995dad2 100644
--- a/browser/components/backup/docs/index.rst
+++ b/browser/components/backup/docs/index.rst
@@ -11,3 +11,4 @@ into a single file that can be easily restored from.
:maxdepth: 3
backup-service
+ backup-resources
diff --git a/browser/components/backup/jar.mn b/browser/components/backup/jar.mn
new file mode 100644
index 0000000000..7800962486
--- /dev/null
+++ b/browser/components/backup/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+browser.jar:
+#ifdef NIGHTLY_BUILD
+ content/browser/backup/debug.html (content/debug.html)
+ content/browser/backup/debug.js (content/debug.js)
+#endif
diff --git a/browser/components/backup/metrics.yaml b/browser/components/backup/metrics.yaml
index 6d6a16a178..cf6f95ee75 100644
--- a/browser/components/backup/metrics.yaml
+++ b/browser/components/backup/metrics.yaml
@@ -28,3 +28,279 @@ browser.backup:
- mconley@mozilla.com
expires: never
telemetry_mirror: BROWSER_BACKUP_PROF_D_DISK_SPACE
+
+ places_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total file size of the places.sqlite db located in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_PLACES_SIZE
+
+ favicons_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total file size of the favicons.sqlite db located in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_FAVICONS_SIZE
+
+ credentials_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of logins, payment method, and form autofill related files
+ in the current profile directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_CREDENTIALS_DATA_SIZE
+
+ security_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of files needed for NSS initialization parameters and security
+ certificate settings in the current profile directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_SECURITY_DATA_SIZE
+
+ preferences_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of files relating to user preferences and permissions in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883739
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883739
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_PREFERENCES_SIZE
+
+ misc_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of files for telemetry, site storage, media device origin mapping,
+ chrome privileged IndexedDB databases, and Mozilla Accounts in the current profile directory,
+ rounded to the nearest tenth kilobyte.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883747
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887746
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883747
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887746
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_MISC_DATA_SIZE
+
+ cookies_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total file size of the cookies.sqlite db located in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_COOKIES_SIZE
+
+ form_history_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The file size of the formhistory.sqlite db located in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_FORM_HISTORY_SIZE
+
+ session_store_backups_directory_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of the session store backups directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_SESSION_STORE_BACKUPS_DIRECTORY_SIZE
+
+ session_store_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The size of uncompressed session store json, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_SESSION_STORE_SIZE
+
+ extensions_json_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total file size of the current profiles extensions metadata files,
+ rounded to the nearest 10 kilobytes.
+ Files included are:
+ - extensions.json
+ - extension-settings.json
+ - extension-preferences.json
+ - addonStartup.json.lz4
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_JSON_SIZE
+
+ extension_store_permissions_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The file size of the current profiles extension-store-permissions/data.safe.bin
+ file, rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_EXTENSION_STORE_PERMISSIONS_DATA_SIZE
+
+ storage_sync_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The file size of the current profiles storage-sync-v2.sqlite db,
+ rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_STORAGE_SYNC_SIZE
+
+ browser_extension_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of the current profiles storage.local legacy JSON backend
+ in the browser-extension-data directory, rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_BROWSER_EXTENSION_DATA_SIZE
+
+ extensions_xpi_directory_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of the current profiles extensions directory,
+ rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_XPI_DIRECTORY_SIZE
+
+ extensions_storage_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of all extensions storage directories,
+ rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_STORAGE_SIZE
diff --git a/browser/components/backup/moz.build b/browser/components/backup/moz.build
index 0ea7d66b7d..be548ce81f 100644
--- a/browser/components/backup/moz.build
+++ b/browser/components/backup/moz.build
@@ -7,6 +7,8 @@
with Files("**"):
BUG_COMPONENT = ("Firefox", "Profiles")
+JAR_MANIFESTS += ["jar.mn"]
+
SPHINX_TREES["docs"] = "docs"
XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
@@ -14,5 +16,13 @@ XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
EXTRA_JS_MODULES.backup += [
"BackupResources.sys.mjs",
"BackupService.sys.mjs",
+ "resources/AddonsBackupResource.sys.mjs",
"resources/BackupResource.sys.mjs",
+ "resources/CookiesBackupResource.sys.mjs",
+ "resources/CredentialsAndSecurityBackupResource.sys.mjs",
+ "resources/FormHistoryBackupResource.sys.mjs",
+ "resources/MiscDataBackupResource.sys.mjs",
+ "resources/PlacesBackupResource.sys.mjs",
+ "resources/PreferencesBackupResource.sys.mjs",
+ "resources/SessionStoreBackupResource.sys.mjs",
]
diff --git a/browser/components/backup/resources/AddonsBackupResource.sys.mjs b/browser/components/backup/resources/AddonsBackupResource.sys.mjs
new file mode 100644
index 0000000000..83b97ed2f2
--- /dev/null
+++ b/browser/components/backup/resources/AddonsBackupResource.sys.mjs
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+/**
+ * Backup for addons and extensions files and data.
+ */
+export class AddonsBackupResource extends BackupResource {
+ static get key() {
+ return "addons";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ // Report the total size of the extension json files.
+ const jsonFiles = [
+ "extensions.json",
+ "extension-settings.json",
+ "extension-preferences.json",
+ "addonStartup.json.lz4",
+ ];
+ let extensionsJsonSize = 0;
+ for (const filePath of jsonFiles) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ extensionsJsonSize += resourceSize;
+ }
+ }
+ Glean.browserBackup.extensionsJsonSize.set(extensionsJsonSize);
+
+ // Report the size of permissions store data, if present.
+ let extensionStorePermissionsDataPath = PathUtils.join(
+ profilePath,
+ "extension-store-permissions",
+ "data.safe.bin"
+ );
+ let extensionStorePermissionsDataSize = await BackupResource.getFileSize(
+ extensionStorePermissionsDataPath
+ );
+ if (Number.isInteger(extensionStorePermissionsDataSize)) {
+ Glean.browserBackup.extensionStorePermissionsDataSize.set(
+ extensionStorePermissionsDataSize
+ );
+ }
+
+ // Report the size of extensions storage sync database.
+ let storageSyncPath = PathUtils.join(profilePath, "storage-sync-v2.sqlite");
+ let storageSyncSize = await BackupResource.getFileSize(storageSyncPath);
+ Glean.browserBackup.storageSyncSize.set(storageSyncSize);
+
+ // Report the total size of XPI files in the extensions directory.
+ let extensionsXpiDirectoryPath = PathUtils.join(profilePath, "extensions");
+ let extensionsXpiDirectorySize = await BackupResource.getDirectorySize(
+ extensionsXpiDirectoryPath,
+ {
+ shouldExclude: (filePath, fileType) =>
+ fileType !== "regular" || !filePath.endsWith(".xpi"),
+ }
+ );
+ Glean.browserBackup.extensionsXpiDirectorySize.set(
+ extensionsXpiDirectorySize
+ );
+
+ // Report the total size of the browser extension data.
+ let browserExtensionDataPath = PathUtils.join(
+ profilePath,
+ "browser-extension-data"
+ );
+ let browserExtensionDataSize = await BackupResource.getDirectorySize(
+ browserExtensionDataPath
+ );
+ Glean.browserBackup.browserExtensionDataSize.set(browserExtensionDataSize);
+
+ // Report the size of all moz-extension IndexedDB databases.
+ let defaultStoragePath = PathUtils.join(profilePath, "storage", "default");
+ let extensionsStorageSize = await BackupResource.getDirectorySize(
+ defaultStoragePath,
+ {
+ shouldExclude: (filePath, _fileType, parentPath) => {
+ if (
+ parentPath == defaultStoragePath &&
+ !PathUtils.filename(filePath).startsWith("moz-extension")
+ ) {
+ return true;
+ }
+ return false;
+ },
+ }
+ );
+ if (Number.isInteger(extensionsStorageSize)) {
+ Glean.browserBackup.extensionsStorageSize.set(extensionsStorageSize);
+ }
+ }
+}
diff --git a/browser/components/backup/resources/BackupResource.sys.mjs b/browser/components/backup/resources/BackupResource.sys.mjs
index bde3f0669c..d851eb5199 100644
--- a/browser/components/backup/resources/BackupResource.sys.mjs
+++ b/browser/components/backup/resources/BackupResource.sys.mjs
@@ -3,7 +3,19 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// Convert from bytes to kilobytes (not kibibytes).
-const BYTES_IN_KB = 1000;
+export const BYTES_IN_KB = 1000;
+
+/**
+ * Convert bytes to the nearest 10th kilobyte to make the measurements fuzzier.
+ *
+ * @param {number} bytes - size in bytes.
+ * @returns {number} - size in kilobytes rounded to the nearest 10th kilobyte.
+ */
+export function bytesToFuzzyKilobytes(bytes) {
+ let sizeInKb = Math.ceil(bytes / BYTES_IN_KB);
+ let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
+ return Math.max(nearestTenthKb, 1);
+}
/**
* An abstract class representing a set of data within a user profile
@@ -23,6 +35,21 @@ export class BackupResource {
}
/**
+ * This must be overridden to return a boolean indicating whether the
+ * resource requires encryption when being backed up. Encryption should be
+ * required for particularly sensitive data, such as passwords / credentials,
+ * cookies, or payment methods. If you're not sure, talk to someone from the
+ * Privacy team.
+ *
+ * @type {boolean}
+ */
+ static get requiresEncryption() {
+ throw new Error(
+ "BackupResource::requiresEncryption needs to be overridden."
+ );
+ }
+
+ /**
* Get the size of a file.
*
* @param {string} filePath - path to a file.
@@ -40,21 +67,25 @@ export class BackupResource {
return null;
}
- let sizeInKb = Math.ceil(size / BYTES_IN_KB);
- // Make the measurement fuzzier by rounding to the nearest 10kb.
- let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
+ let nearestTenthKb = bytesToFuzzyKilobytes(size);
- return Math.max(nearestTenthKb, 1);
+ return nearestTenthKb;
}
/**
* Get the total size of a directory.
*
* @param {string} directoryPath - path to a directory.
+ * @param {object} options - A set of additional optional parameters.
+ * @param {Function} [options.shouldExclude] - an optional callback which based on file path and file type should return true
+ * if the file should be excluded from the computed directory size.
* @returns {Promise<number|null>} - the size of all descendants of the directory in kilobytes, or null if the
* directory does not exist, the path is not a directory or the size is unknown.
*/
- static async getDirectorySize(directoryPath) {
+ static async getDirectorySize(
+ directoryPath,
+ { shouldExclude = () => false } = {}
+ ) {
if (!(await IOUtils.exists(directoryPath))) {
return null;
}
@@ -75,15 +106,20 @@ export class BackupResource {
childFilePath
);
+ if (shouldExclude(childFilePath, childType, directoryPath)) {
+ continue;
+ }
+
if (childSize >= 0) {
- let sizeInKb = Math.ceil(childSize / BYTES_IN_KB);
- // Make the measurement fuzzier by rounding to the nearest 10kb.
- let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
- size += Math.max(nearestTenthKb, 1);
+ let nearestTenthKb = bytesToFuzzyKilobytes(childSize);
+
+ size += nearestTenthKb;
}
if (childType == "directory") {
- let childDirectorySize = await this.getDirectorySize(childFilePath);
+ let childDirectorySize = await this.getDirectorySize(childFilePath, {
+ shouldExclude,
+ });
if (Number.isInteger(childDirectorySize)) {
size += childDirectorySize;
}
@@ -106,4 +142,29 @@ export class BackupResource {
async measure(profilePath) {
throw new Error("BackupResource::measure needs to be overridden.");
}
+
+ /**
+ * Perform a safe copy of the resource(s) and write them into the backup
+ * database. The Promise should resolve with an object that can be serialized
+ * to JSON, as it will be written to the manifest file. This same object will
+ * be deserialized and passed to restore() when restoring the backup. This
+ * object can be null if no additional information is needed to restore the
+ * backup.
+ *
+ * @param {string} stagingPath
+ * The path to the staging folder where copies of the datastores for this
+ * BackupResource should be written to.
+ * @param {string} [profilePath=null]
+ * This is null if the backup is being run on the currently running user
+ * profile. If, however, the backup is being run on a different user profile
+ * (for example, it's being run from a BackgroundTask on a user profile that
+ * just shut down, or during test), then this is a string set to that user
+ * profile path.
+ *
+ * @returns {Promise<object|null>}
+ */
+ // eslint-disable-next-line no-unused-vars
+ async backup(stagingPath, profilePath = null) {
+ throw new Error("BackupResource::backup must be overridden");
+ }
}
diff --git a/browser/components/backup/resources/CookiesBackupResource.sys.mjs b/browser/components/backup/resources/CookiesBackupResource.sys.mjs
new file mode 100644
index 0000000000..8b988fd532
--- /dev/null
+++ b/browser/components/backup/resources/CookiesBackupResource.sys.mjs
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+/**
+ * Class representing Cookies database within a user profile.
+ */
+export class CookiesBackupResource extends BackupResource {
+ static get key() {
+ return "cookies";
+ }
+
+ static get requiresEncryption() {
+ return true;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ let cookiesDBPath = PathUtils.join(profilePath, "cookies.sqlite");
+ let cookiesSize = await BackupResource.getFileSize(cookiesDBPath);
+
+ Glean.browserBackup.cookiesSize.set(cookiesSize);
+ }
+}
diff --git a/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs
new file mode 100644
index 0000000000..89069de826
--- /dev/null
+++ b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+/**
+ * Class representing files needed for logins, payment methods and form autofill within a user profile.
+ */
+export class CredentialsAndSecurityBackupResource extends BackupResource {
+ static get key() {
+ return "credentials_and_security";
+ }
+
+ static get requiresEncryption() {
+ return true;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ const securityFiles = ["cert9.db", "pkcs11.txt"];
+ let securitySize = 0;
+
+ for (let filePath of securityFiles) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ securitySize += resourceSize;
+ }
+ }
+
+ Glean.browserBackup.securityDataSize.set(securitySize);
+
+ const credentialsFiles = [
+ "key4.db",
+ "logins.json",
+ "logins-backup.json",
+ "autofill-profiles.json",
+ "credentialstate.sqlite",
+ "signedInUser.json",
+ ];
+ let credentialsSize = 0;
+
+ for (let filePath of credentialsFiles) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ credentialsSize += resourceSize;
+ }
+ }
+
+ Glean.browserBackup.credentialsDataSize.set(credentialsSize);
+ }
+}
diff --git a/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs
new file mode 100644
index 0000000000..cb314eb34d
--- /dev/null
+++ b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+/**
+ * Class representing Form history database within a user profile.
+ */
+export class FormHistoryBackupResource extends BackupResource {
+ static get key() {
+ return "formhistory";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ let formHistoryDBPath = PathUtils.join(profilePath, "formhistory.sqlite");
+ let formHistorySize = await BackupResource.getFileSize(formHistoryDBPath);
+
+ Glean.browserBackup.formHistorySize.set(formHistorySize);
+ }
+}
diff --git a/browser/components/backup/resources/MiscDataBackupResource.sys.mjs b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs
new file mode 100644
index 0000000000..97224f0e31
--- /dev/null
+++ b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+/**
+ * Class representing miscellaneous files for telemetry, site storage,
+ * media device origin mapping, chrome privileged IndexedDB databases,
+ * and Mozilla Accounts within a user profile.
+ */
+export class MiscDataBackupResource extends BackupResource {
+ static get key() {
+ return "miscellaneous";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async backup(stagingPath, profilePath = PathUtils.profileDir) {
+ const files = [
+ "times.json",
+ "enumerate_devices.txt",
+ "SiteSecurityServiceState.bin",
+ ];
+
+ for (let fileName of files) {
+ let sourcePath = PathUtils.join(profilePath, fileName);
+ let destPath = PathUtils.join(stagingPath, fileName);
+ if (await IOUtils.exists(sourcePath)) {
+ await IOUtils.copy(sourcePath, destPath, { recursive: true });
+ }
+ }
+
+ const sqliteDatabases = ["protections.sqlite"];
+
+ for (let fileName of sqliteDatabases) {
+ let sourcePath = PathUtils.join(profilePath, fileName);
+ let destPath = PathUtils.join(stagingPath, fileName);
+ let connection;
+
+ try {
+ connection = await lazy.Sqlite.openConnection({
+ path: sourcePath,
+ readOnly: true,
+ });
+
+ await connection.backup(destPath);
+ } finally {
+ await connection.close();
+ }
+ }
+
+ // Bug 1890585 - we don't currently have the ability to copy the
+ // chrome-privileged IndexedDB databases under storage/permanent/chrome, so
+ // we'll just skip that for now.
+
+ return null;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ const files = [
+ "times.json",
+ "enumerate_devices.txt",
+ "protections.sqlite",
+ "SiteSecurityServiceState.bin",
+ ];
+
+ let fullSize = 0;
+
+ for (let filePath of files) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ fullSize += resourceSize;
+ }
+ }
+
+ let chromeIndexedDBDirPath = PathUtils.join(
+ profilePath,
+ "storage",
+ "permanent",
+ "chrome"
+ );
+ let chromeIndexedDBDirSize = await BackupResource.getDirectorySize(
+ chromeIndexedDBDirPath
+ );
+ if (Number.isInteger(chromeIndexedDBDirSize)) {
+ fullSize += chromeIndexedDBDirSize;
+ }
+
+ Glean.browserBackup.miscDataSize.set(fullSize);
+ }
+}
diff --git a/browser/components/backup/resources/PlacesBackupResource.sys.mjs b/browser/components/backup/resources/PlacesBackupResource.sys.mjs
new file mode 100644
index 0000000000..1955406f51
--- /dev/null
+++ b/browser/components/backup/resources/PlacesBackupResource.sys.mjs
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isBrowsingHistoryEnabled",
+ "places.history.enabled",
+ true
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isSanitizeOnShutdownEnabled",
+ "privacy.sanitize.sanitizeOnShutdown",
+ false
+);
+
+/**
+ * Class representing Places database related files within a user profile.
+ */
+export class PlacesBackupResource extends BackupResource {
+ static get key() {
+ return "places";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async backup(stagingPath, profilePath = PathUtils.profileDir) {
+ const sqliteDatabases = ["places.sqlite", "favicons.sqlite"];
+ let canBackupHistory =
+ !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing &&
+ !lazy.isSanitizeOnShutdownEnabled &&
+ lazy.isBrowsingHistoryEnabled;
+
+ /**
+ * Do not backup places.sqlite and favicons.sqlite if users have history disabled, want history cleared on shutdown or are using permanent private browsing mode.
+ * Instead, export all existing bookmarks to a compressed JSON file that we can read when restoring the backup.
+ */
+ if (!canBackupHistory) {
+ let bookmarksBackupFile = PathUtils.join(
+ stagingPath,
+ "bookmarks.jsonlz4"
+ );
+ await lazy.BookmarkJSONUtils.exportToFile(bookmarksBackupFile, {
+ compress: true,
+ });
+ return { bookmarksOnly: true };
+ }
+
+ for (let fileName of sqliteDatabases) {
+ let sourcePath = PathUtils.join(profilePath, fileName);
+ let destPath = PathUtils.join(stagingPath, fileName);
+ let connection;
+
+ try {
+ connection = await lazy.Sqlite.openConnection({
+ path: sourcePath,
+ readOnly: true,
+ });
+
+ await connection.backup(destPath);
+ } finally {
+ await connection.close();
+ }
+ }
+ return null;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ let placesDBPath = PathUtils.join(profilePath, "places.sqlite");
+ let faviconsDBPath = PathUtils.join(profilePath, "favicons.sqlite");
+ let placesDBSize = await BackupResource.getFileSize(placesDBPath);
+ let faviconsDBSize = await BackupResource.getFileSize(faviconsDBPath);
+
+ Glean.browserBackup.placesSize.set(placesDBSize);
+ Glean.browserBackup.faviconsSize.set(faviconsDBSize);
+ }
+}
diff --git a/browser/components/backup/resources/PreferencesBackupResource.sys.mjs b/browser/components/backup/resources/PreferencesBackupResource.sys.mjs
new file mode 100644
index 0000000000..012c0bf91e
--- /dev/null
+++ b/browser/components/backup/resources/PreferencesBackupResource.sys.mjs
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs";
+
+/**
+ * Class representing files that modify preferences and permissions within a user profile.
+ */
+export class PreferencesBackupResource extends BackupResource {
+ static get key() {
+ return "preferences";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async backup(stagingPath, profilePath = PathUtils.profileDir) {
+ // These are files that can be simply copied into the staging folder using
+ // IOUtils.copy.
+ const simpleCopyFiles = [
+ "xulstore.json",
+ "containers.json",
+ "handlers.json",
+ "search.json.mozlz4",
+ "user.js",
+ "chrome",
+ ];
+
+ for (let fileName of simpleCopyFiles) {
+ let sourcePath = PathUtils.join(profilePath, fileName);
+ let destPath = PathUtils.join(stagingPath, fileName);
+ if (await IOUtils.exists(sourcePath)) {
+ await IOUtils.copy(sourcePath, destPath, { recursive: true });
+ }
+ }
+
+ const sqliteDatabases = ["permissions.sqlite", "content-prefs.sqlite"];
+
+ for (let fileName of sqliteDatabases) {
+ let sourcePath = PathUtils.join(profilePath, fileName);
+ let destPath = PathUtils.join(stagingPath, fileName);
+ let connection;
+
+ try {
+ connection = await Sqlite.openConnection({
+ path: sourcePath,
+ });
+
+ await connection.backup(destPath);
+ } finally {
+ await connection.close();
+ }
+ }
+
+ // prefs.js is a special case - we have a helper function to flush the
+ // current prefs state to disk off of the main thread.
+ let prefsDestPath = PathUtils.join(stagingPath, "prefs.js");
+ let prefsDestFile = await IOUtils.getFile(prefsDestPath);
+ await Services.prefs.backupPrefFile(prefsDestFile);
+
+ return null;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ const files = [
+ "prefs.js",
+ "xulstore.json",
+ "permissions.sqlite",
+ "content-prefs.sqlite",
+ "containers.json",
+ "handlers.json",
+ "search.json.mozlz4",
+ "user.js",
+ ];
+ let fullSize = 0;
+
+ for (let filePath of files) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ fullSize += resourceSize;
+ }
+ }
+
+ const chromeDirectoryPath = PathUtils.join(profilePath, "chrome");
+ let chromeDirectorySize = await BackupResource.getDirectorySize(
+ chromeDirectoryPath
+ );
+ if (Number.isInteger(chromeDirectorySize)) {
+ fullSize += chromeDirectorySize;
+ }
+
+ Glean.browserBackup.preferencesSize.set(fullSize);
+ }
+}
diff --git a/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs
new file mode 100644
index 0000000000..fa5dcca848
--- /dev/null
+++ b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import {
+ BackupResource,
+ bytesToFuzzyKilobytes,
+} from "resource:///modules/backup/BackupResource.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+
+/**
+ * Class representing Session store related files within a user profile.
+ */
+export class SessionStoreBackupResource extends BackupResource {
+ static get key() {
+ return "sessionstore";
+ }
+
+ static get requiresEncryption() {
+ // Session store data does not require encryption, but if encryption is
+ // disabled, then session cookies will be cleared from the backup before
+ // writing it to the disk.
+ return false;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ // Get the current state of the session store JSON and
+ // measure it's uncompressed size.
+ let sessionStoreJson = lazy.SessionStore.getCurrentState(true);
+ let sessionStoreSize = new TextEncoder().encode(
+ JSON.stringify(sessionStoreJson)
+ ).byteLength;
+ let sessionStoreNearestTenthKb = bytesToFuzzyKilobytes(sessionStoreSize);
+
+ Glean.browserBackup.sessionStoreSize.set(sessionStoreNearestTenthKb);
+
+ let sessionStoreBackupsDirectoryPath = PathUtils.join(
+ profilePath,
+ "sessionstore-backups"
+ );
+ let sessionStoreBackupsDirectorySize =
+ await BackupResource.getDirectorySize(sessionStoreBackupsDirectoryPath);
+
+ Glean.browserBackup.sessionStoreBackupsDirectorySize.set(
+ sessionStoreBackupsDirectorySize
+ );
+ }
+}
diff --git a/browser/components/backup/tests/xpcshell/head.js b/browser/components/backup/tests/xpcshell/head.js
new file mode 100644
index 0000000000..2402870a13
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/head.js
@@ -0,0 +1,167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { BackupService } = ChromeUtils.importESModule(
+ "resource:///modules/backup/BackupService.sys.mjs"
+);
+
+const { BackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/BackupResource.sys.mjs"
+);
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const { Sqlite } = ChromeUtils.importESModule(
+ "resource://gre/modules/Sqlite.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const BYTES_IN_KB = 1000;
+
+do_get_profile();
+
+/**
+ * Some fake backup resource classes to test with.
+ */
+class FakeBackupResource1 extends BackupResource {
+ static get key() {
+ return "fake1";
+ }
+ static get requiresEncryption() {
+ return false;
+ }
+}
+
+/**
+ * Another fake backup resource class to test with.
+ */
+class FakeBackupResource2 extends BackupResource {
+ static get key() {
+ return "fake2";
+ }
+ static get requiresEncryption() {
+ return true;
+ }
+}
+
+/**
+ * Yet another fake backup resource class to test with.
+ */
+class FakeBackupResource3 extends BackupResource {
+ static get key() {
+ return "fake3";
+ }
+ static get requiresEncryption() {
+ return false;
+ }
+}
+
+/**
+ * Create a file of a given size in kilobytes.
+ *
+ * @param {string} path the path where the file will be created.
+ * @param {number} sizeInKB size file in Kilobytes.
+ * @returns {Promise<undefined>}
+ */
+async function createKilobyteSizedFile(path, sizeInKB) {
+ let bytes = new Uint8Array(sizeInKB * BYTES_IN_KB);
+ await IOUtils.write(path, bytes);
+}
+
+/**
+ * @typedef {object} TestFileObject
+ * @property {(string|Array.<string>)} path
+ * The relative path of the file. It can be a string or an array of strings
+ * in the event that directories need to be created. For example, this is
+ * an array of valid TestFileObjects.
+ *
+ * [
+ * { path: "file1.txt" },
+ * { path: ["dir1", "file2.txt"] },
+ * { path: ["dir2", "dir3", "file3.txt"], sizeInKB: 25 },
+ * { path: "file4.txt" },
+ * ]
+ *
+ * @property {number} [sizeInKB=10]
+ * The size of the created file in kilobytes. Defaults to 10.
+ */
+
+/**
+ * Easily creates a series of test files and directories under parentPath.
+ *
+ * @param {string} parentPath
+ * The path to the parent directory where the files will be created.
+ * @param {TestFileObject[]} testFilesArray
+ * An array of TestFileObjects describing what test files to create within
+ * the parentPath.
+ * @see TestFileObject
+ * @returns {Promise<undefined>}
+ */
+async function createTestFiles(parentPath, testFilesArray) {
+ for (let { path, sizeInKB } of testFilesArray) {
+ if (Array.isArray(path)) {
+ // Make a copy of the array of path elements, chopping off the last one.
+ // We'll assume the unchopped items are directories, and make sure they
+ // exist first.
+ let folders = path.slice(0, -1);
+ await IOUtils.getDirectory(PathUtils.join(parentPath, ...folders));
+ }
+
+ if (sizeInKB === undefined) {
+ sizeInKB = 10;
+ }
+
+ // This little piece of cleverness coerces a string into an array of one
+ // if path is a string, or just leaves it alone if it's already an array.
+ let filePath = PathUtils.join(parentPath, ...[].concat(path));
+ await createKilobyteSizedFile(filePath, sizeInKB);
+ }
+}
+
+/**
+ * Checks that files exist within a particular folder. The filesize is not
+ * checked.
+ *
+ * @param {string} parentPath
+ * The path to the parent directory where the files should exist.
+ * @param {TestFileObject[]} testFilesArray
+ * An array of TestFileObjects describing what test files to search for within
+ * parentPath.
+ * @see TestFileObject
+ * @returns {Promise<undefined>}
+ */
+async function assertFilesExist(parentPath, testFilesArray) {
+ for (let { path } of testFilesArray) {
+ let copiedFileName = PathUtils.join(parentPath, ...[].concat(path));
+ Assert.ok(
+ await IOUtils.exists(copiedFileName),
+ `${copiedFileName} should exist in the staging folder`
+ );
+ }
+}
+
+/**
+ * Remove a file or directory at a path if it exists and files are unlocked.
+ *
+ * @param {string} path path to remove.
+ */
+async function maybeRemovePath(path) {
+ try {
+ await IOUtils.remove(path, { ignoreAbsent: true, recursive: true });
+ } catch (error) {
+ // Sometimes remove() throws when the file is not unlocked soon
+ // enough.
+ if (error.name != "NS_ERROR_FILE_IS_LOCKED") {
+ // Ignoring any errors, as the temp folder will be cleaned up.
+ console.error(error);
+ }
+ }
+}
diff --git a/browser/components/backup/tests/xpcshell/test_BrowserResource.js b/browser/components/backup/tests/xpcshell/test_BackupResource.js
index 23c8e077a5..6623f4cd77 100644
--- a/browser/components/backup/tests/xpcshell/test_BrowserResource.js
+++ b/browser/components/backup/tests/xpcshell/test_BackupResource.js
@@ -3,16 +3,12 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
-const { BackupResource } = ChromeUtils.importESModule(
+const { bytesToFuzzyKilobytes } = ChromeUtils.importESModule(
"resource:///modules/backup/BackupResource.sys.mjs"
);
const EXPECTED_KILOBYTES_FOR_XULSTORE = 1;
-add_setup(() => {
- do_get_profile();
-});
-
/**
* Tests that BackupService.getFileSize will get the size of a file in kilobytes.
*/
@@ -35,7 +31,7 @@ add_task(async function test_getFileSize() {
});
/**
- * Tests that BackupService.getFileSize will get the total size of all the files in a directory and it's children in kilobytes.
+ * Tests that BackupService.getDirectorySize will get the total size of all the files in a directory and it's children in kilobytes.
*/
add_task(async function test_getDirectorySize() {
let file = do_get_file("data/test_xulstore.json");
@@ -61,3 +57,21 @@ add_task(async function test_getDirectorySize() {
await IOUtils.remove(testDir, { recursive: true });
});
+
+/**
+ * Tests that bytesToFuzzyKilobytes will convert bytes to kilobytes
+ * and round up to the nearest tenth kilobyte.
+ */
+add_task(async function test_bytesToFuzzyKilobytes() {
+ let largeSize = bytesToFuzzyKilobytes(1234000);
+
+ Assert.equal(
+ largeSize,
+ 1230,
+ "1234 bytes is rounded up to the nearest tenth kilobyte, 1230"
+ );
+
+ let smallSize = bytesToFuzzyKilobytes(3);
+
+ Assert.equal(smallSize, 1, "Sizes under 10 kilobytes return 1 kilobyte");
+});
diff --git a/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js
new file mode 100644
index 0000000000..e57dd50cd3
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { MiscDataBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/MiscDataBackupResource.sys.mjs"
+);
+
+/**
+ * Tests that we can measure miscellaneous files in the profile directory.
+ */
+add_task(async function test_measure() {
+ Services.fog.testResetFOG();
+
+ const EXPECTED_MISC_KILOBYTES_SIZE = 241;
+ const tempDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-measurement-test"
+ );
+
+ const mockFiles = [
+ { path: "times.json", sizeInKB: 5 },
+ { path: "enumerate_devices.txt", sizeInKB: 1 },
+ { path: "protections.sqlite", sizeInKB: 100 },
+ { path: "SiteSecurityServiceState.bin", sizeInKB: 10 },
+ { path: ["storage", "permanent", "chrome", "123ABC.sqlite"], sizeInKB: 40 },
+ { path: ["storage", "permanent", "chrome", "456DEF.sqlite"], sizeInKB: 40 },
+ {
+ path: ["storage", "permanent", "chrome", "mockIDBDir", "890HIJ.sqlite"],
+ sizeInKB: 40,
+ },
+ ];
+
+ await createTestFiles(tempDir, mockFiles);
+
+ let miscDataBackupResource = new MiscDataBackupResource();
+ await miscDataBackupResource.measure(tempDir);
+
+ let measurement = Glean.browserBackup.miscDataSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.misc_data_size",
+ measurement,
+ "Glean and telemetry measurements for misc data should be equal"
+ );
+ Assert.equal(
+ measurement,
+ EXPECTED_MISC_KILOBYTES_SIZE,
+ "Should have collected the correct glean measurement for misc files"
+ );
+
+ await maybeRemovePath(tempDir);
+});
+
+add_task(async function test_backup() {
+ let sandbox = sinon.createSandbox();
+
+ let miscDataBackupResource = new MiscDataBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-staging-test"
+ );
+
+ const simpleCopyFiles = [
+ { path: "times.json" },
+ { path: "enumerate_devices.txt" },
+ { path: "SiteSecurityServiceState.bin" },
+ ];
+ await createTestFiles(sourcePath, simpleCopyFiles);
+
+ // We have no need to test that Sqlite.sys.mjs's backup method is working -
+ // this is something that is tested in Sqlite's own tests. We can just make
+ // sure that it's being called using sinon. Unfortunately, we cannot do the
+ // same thing with IOUtils.copy, as its methods are not stubbable.
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ await miscDataBackupResource.backup(stagingPath, sourcePath);
+
+ await assertFilesExist(stagingPath, simpleCopyFiles);
+
+ // Next, we'll make sure that the Sqlite connection had `backup` called on it
+ // with the right arguments.
+ Assert.ok(
+ fakeConnection.backup.calledOnce,
+ "Called backup the expected number of times for all connections"
+ );
+ Assert.ok(
+ fakeConnection.backup.firstCall.calledWith(
+ PathUtils.join(stagingPath, "protections.sqlite")
+ ),
+ "Called backup on the protections.sqlite Sqlite connection"
+ );
+
+ // Bug 1890585 - we don't currently have the ability to copy the
+ // chrome-privileged IndexedDB databases under storage/permanent/chrome, so
+ // we'll just skip testing that for now.
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js
new file mode 100644
index 0000000000..de97281372
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PlacesBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/PlacesBackupResource.sys.mjs"
+);
+const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+);
+
+const HISTORY_ENABLED_PREF = "places.history.enabled";
+const SANITIZE_ON_SHUTDOWN_PREF = "privacy.sanitize.sanitizeOnShutdown";
+
+registerCleanupFunction(() => {
+ /**
+ * Even though test_backup_no_saved_history clears user prefs too,
+ * clear them here as well in case that test fails and we don't
+ * reach the end of the test, which handles the cleanup.
+ */
+ Services.prefs.clearUserPref(HISTORY_ENABLED_PREF);
+ Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF);
+});
+
+/**
+ * Tests that we can measure Places DB related files in the profile directory.
+ */
+add_task(async function test_measure() {
+ Services.fog.testResetFOG();
+
+ const EXPECTED_PLACES_DB_SIZE = 5240;
+ const EXPECTED_FAVICONS_DB_SIZE = 5240;
+
+ // Create resource files in temporary directory
+ const tempDir = PathUtils.tempDir;
+ let tempPlacesDBPath = PathUtils.join(tempDir, "places.sqlite");
+ let tempFaviconsDBPath = PathUtils.join(tempDir, "favicons.sqlite");
+ await createKilobyteSizedFile(tempPlacesDBPath, EXPECTED_PLACES_DB_SIZE);
+ await createKilobyteSizedFile(tempFaviconsDBPath, EXPECTED_FAVICONS_DB_SIZE);
+
+ let placesBackupResource = new PlacesBackupResource();
+ await placesBackupResource.measure(tempDir);
+
+ let placesMeasurement = Glean.browserBackup.placesSize.testGetValue();
+ let faviconsMeasurement = Glean.browserBackup.faviconsSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Compare glean vs telemetry measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.places_size",
+ placesMeasurement,
+ "Glean and telemetry measurements for places.sqlite should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.favicons_size",
+ faviconsMeasurement,
+ "Glean and telemetry measurements for favicons.sqlite should be equal"
+ );
+
+ // Compare glean measurements vs actual file sizes
+ Assert.equal(
+ placesMeasurement,
+ EXPECTED_PLACES_DB_SIZE,
+ "Should have collected the correct glean measurement for places.sqlite"
+ );
+ Assert.equal(
+ faviconsMeasurement,
+ EXPECTED_FAVICONS_DB_SIZE,
+ "Should have collected the correct glean measurement for favicons.sqlite"
+ );
+
+ await maybeRemovePath(tempPlacesDBPath);
+ await maybeRemovePath(tempFaviconsDBPath);
+});
+
+/**
+ * Tests that the backup method correctly copies places.sqlite and
+ * favicons.sqlite from the profile directory into the staging directory.
+ */
+add_task(async function test_backup() {
+ let sandbox = sinon.createSandbox();
+
+ let placesBackupResource = new PlacesBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-staging-test"
+ );
+
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ await placesBackupResource.backup(stagingPath, sourcePath);
+
+ Assert.ok(
+ fakeConnection.backup.calledTwice,
+ "Backup should have been called twice"
+ );
+ Assert.ok(
+ fakeConnection.backup.firstCall.calledWith(
+ PathUtils.join(stagingPath, "places.sqlite")
+ ),
+ "places.sqlite should have been backed up first"
+ );
+ Assert.ok(
+ fakeConnection.backup.secondCall.calledWith(
+ PathUtils.join(stagingPath, "favicons.sqlite")
+ ),
+ "favicons.sqlite should have been backed up second"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that the backup method correctly creates a compressed bookmarks JSON file when users
+ * don't want history saved, even on shutdown.
+ */
+add_task(async function test_backup_no_saved_history() {
+ let sandbox = sinon.createSandbox();
+
+ let placesBackupResource = new PlacesBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-staging-test"
+ );
+
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ /**
+ * First verify that remember history pref alone affects backup file type for places,
+ * despite sanitize on shutdown pref value.
+ */
+ Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, false);
+ Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, false);
+
+ await placesBackupResource.backup(stagingPath, sourcePath);
+
+ Assert.ok(
+ fakeConnection.backup.notCalled,
+ "No sqlite connections should have been made with remember history disabled"
+ );
+ await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]);
+ await IOUtils.remove(PathUtils.join(stagingPath, "bookmarks.jsonlz4"));
+
+ /**
+ * Now verify that the sanitize shutdown pref alone affects backup file type for places,
+ * even if the user is okay with remembering history while browsing.
+ */
+ Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, true);
+
+ fakeConnection.backup.resetHistory();
+ await placesBackupResource.backup(stagingPath, sourcePath);
+
+ Assert.ok(
+ fakeConnection.backup.notCalled,
+ "No sqlite connections should have been made with sanitize shutdown enabled"
+ );
+ await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]);
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+ Services.prefs.clearUserPref(HISTORY_ENABLED_PREF);
+ Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF);
+});
+
+/**
+ * Tests that the backup method correctly creates a compressed bookmarks JSON file when
+ * permanent private browsing mode is enabled.
+ */
+add_task(async function test_backup_private_browsing() {
+ let sandbox = sinon.createSandbox();
+
+ let placesBackupResource = new PlacesBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-staging-test"
+ );
+
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+ sandbox.stub(PrivateBrowsingUtils, "permanentPrivateBrowsing").value(true);
+
+ await placesBackupResource.backup(stagingPath, sourcePath);
+
+ Assert.ok(
+ fakeConnection.backup.notCalled,
+ "No sqlite connections should have been made with permanent private browsing enabled"
+ );
+ await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]);
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js
new file mode 100644
index 0000000000..6845431bb8
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PreferencesBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/PreferencesBackupResource.sys.mjs"
+);
+
+/**
+ * Test that the measure method correctly collects the disk-sizes of things that
+ * the PreferencesBackupResource is meant to back up.
+ */
+add_task(async function test_measure() {
+ Services.fog.testResetFOG();
+
+ const EXPECTED_PREFERENCES_KILOBYTES_SIZE = 415;
+ const tempDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PreferencesBackupResource-measure-test"
+ );
+ const mockFiles = [
+ { path: "prefs.js", sizeInKB: 20 },
+ { path: "xulstore.json", sizeInKB: 1 },
+ { path: "permissions.sqlite", sizeInKB: 100 },
+ { path: "content-prefs.sqlite", sizeInKB: 260 },
+ { path: "containers.json", sizeInKB: 1 },
+ { path: "handlers.json", sizeInKB: 1 },
+ { path: "search.json.mozlz4", sizeInKB: 1 },
+ { path: "user.js", sizeInKB: 2 },
+ { path: ["chrome", "userChrome.css"], sizeInKB: 5 },
+ { path: ["chrome", "userContent.css"], sizeInKB: 5 },
+ { path: ["chrome", "css", "mockStyles.css"], sizeInKB: 5 },
+ ];
+
+ await createTestFiles(tempDir, mockFiles);
+
+ let preferencesBackupResource = new PreferencesBackupResource();
+
+ await preferencesBackupResource.measure(tempDir);
+
+ let measurement = Glean.browserBackup.preferencesSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.preferences_size",
+ measurement,
+ "Glean and telemetry measurements for preferences data should be equal"
+ );
+ Assert.equal(
+ measurement,
+ EXPECTED_PREFERENCES_KILOBYTES_SIZE,
+ "Should have collected the correct glean measurement for preferences files"
+ );
+
+ await maybeRemovePath(tempDir);
+});
+
+/**
+ * Test that the backup method correctly copies items from the profile directory
+ * into the staging directory.
+ */
+add_task(async function test_backup() {
+ let sandbox = sinon.createSandbox();
+
+ let preferencesBackupResource = new PreferencesBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PreferencesBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PreferencesBackupResource-staging-test"
+ );
+
+ const simpleCopyFiles = [
+ { path: "xulstore.json" },
+ { path: "containers.json" },
+ { path: "handlers.json" },
+ { path: "search.json.mozlz4" },
+ { path: "user.js" },
+ { path: ["chrome", "userChrome.css"] },
+ { path: ["chrome", "userContent.css"] },
+ { path: ["chrome", "childFolder", "someOtherStylesheet.css"] },
+ ];
+ await createTestFiles(sourcePath, simpleCopyFiles);
+
+ // We have no need to test that Sqlite.sys.mjs's backup method is working -
+ // this is something that is tested in Sqlite's own tests. We can just make
+ // sure that it's being called using sinon. Unfortunately, we cannot do the
+ // same thing with IOUtils.copy, as its methods are not stubbable.
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ await preferencesBackupResource.backup(stagingPath, sourcePath);
+
+ await assertFilesExist(stagingPath, simpleCopyFiles);
+
+ // Next, we'll make sure that the Sqlite connection had `backup` called on it
+ // with the right arguments.
+ Assert.ok(
+ fakeConnection.backup.calledTwice,
+ "Called backup the expected number of times for all connections"
+ );
+ Assert.ok(
+ fakeConnection.backup.firstCall.calledWith(
+ PathUtils.join(stagingPath, "permissions.sqlite")
+ ),
+ "Called backup on the permissions.sqlite Sqlite connection"
+ );
+ Assert.ok(
+ fakeConnection.backup.secondCall.calledWith(
+ PathUtils.join(stagingPath, "content-prefs.sqlite")
+ ),
+ "Called backup on the content-prefs.sqlite Sqlite connection"
+ );
+
+ // And we'll make sure that preferences were properly written out.
+ Assert.ok(
+ await IOUtils.exists(PathUtils.join(stagingPath, "prefs.js")),
+ "prefs.js should exist in the staging folder"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_createBackup.js b/browser/components/backup/tests/xpcshell/test_createBackup.js
new file mode 100644
index 0000000000..fcace695ef
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_createBackup.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that calling BackupService.createBackup will call backup on each
+ * registered BackupResource, and that each BackupResource will have a folder
+ * created for them to write into.
+ */
+add_task(async function test_createBackup() {
+ let sandbox = sinon.createSandbox();
+ sandbox
+ .stub(FakeBackupResource1.prototype, "backup")
+ .resolves({ fake1: "hello from 1" });
+ sandbox
+ .stub(FakeBackupResource2.prototype, "backup")
+ .rejects(new Error("Some failure to backup"));
+ sandbox
+ .stub(FakeBackupResource3.prototype, "backup")
+ .resolves({ fake3: "hello from 3" });
+
+ let bs = new BackupService({
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ });
+
+ let fakeProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "createBackupTest"
+ );
+
+ await bs.createBackup({ profilePath: fakeProfilePath });
+
+ // For now, we expect a staging folder to exist under the fakeProfilePath,
+ // and we should find a folder for each fake BackupResource.
+ let stagingPath = PathUtils.join(fakeProfilePath, "backups", "staging");
+ Assert.ok(await IOUtils.exists(stagingPath), "Staging folder exists");
+
+ for (let backupResourceClass of [
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ ]) {
+ let expectedResourceFolder = PathUtils.join(
+ stagingPath,
+ backupResourceClass.key
+ );
+ Assert.ok(
+ await IOUtils.exists(expectedResourceFolder),
+ `BackupResource staging folder exists for ${backupResourceClass.key}`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.backup.calledOnce,
+ `Backup was called for ${backupResourceClass.key}`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.backup.calledWith(
+ expectedResourceFolder,
+ fakeProfilePath
+ ),
+ `Backup was passed the right paths for ${backupResourceClass.key}`
+ );
+ }
+
+ // After createBackup is more fleshed out, we're going to want to make sure
+ // that we're writing the manifest file and that it contains the expected
+ // ManifestEntry objects, and that the staging folder was successfully
+ // renamed with the current date.
+ await IOUtils.remove(fakeProfilePath, { recursive: true });
+
+ sandbox.restore();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_measurements.js b/browser/components/backup/tests/xpcshell/test_measurements.js
index e5726126b2..0dece6b370 100644
--- a/browser/components/backup/tests/xpcshell/test_measurements.js
+++ b/browser/components/backup/tests/xpcshell/test_measurements.js
@@ -3,22 +3,59 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
-const { BackupService } = ChromeUtils.importESModule(
- "resource:///modules/backup/BackupService.sys.mjs"
+const { CredentialsAndSecurityBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs"
+);
+const { AddonsBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/AddonsBackupResource.sys.mjs"
+);
+const { CookiesBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/CookiesBackupResource.sys.mjs"
+);
+
+const { FormHistoryBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/FormHistoryBackupResource.sys.mjs"
);
-const { TelemetryTestUtils } = ChromeUtils.importESModule(
- "resource://testing-common/TelemetryTestUtils.sys.mjs"
+const { SessionStoreBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/SessionStoreBackupResource.sys.mjs"
);
add_setup(() => {
- do_get_profile();
// FOG needs to be initialized in order for data to flow.
Services.fog.initializeFOG();
Services.telemetry.clearScalars();
});
/**
+ * Tests that calling `BackupService.takeMeasurements` will call the measure
+ * method of all registered BackupResource classes.
+ */
+add_task(async function test_takeMeasurements() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(FakeBackupResource1.prototype, "measure").resolves();
+ sandbox
+ .stub(FakeBackupResource2.prototype, "measure")
+ .rejects(new Error("Some failure to measure"));
+
+ let bs = new BackupService({ FakeBackupResource1, FakeBackupResource2 });
+ await bs.takeMeasurements();
+
+ for (let backupResourceClass of [FakeBackupResource1, FakeBackupResource2]) {
+ Assert.ok(
+ backupResourceClass.prototype.measure.calledOnce,
+ "Measure was called"
+ );
+ Assert.ok(
+ backupResourceClass.prototype.measure.calledWith(PathUtils.profileDir),
+ "Measure was called with the profile directory argument"
+ );
+ }
+
+ sandbox.restore();
+});
+
+/**
* Tests that we can measure the disk space available in the profile directory.
*/
add_task(async function test_profDDiskSpace() {
@@ -38,3 +75,503 @@ add_task(async function test_profDDiskSpace() {
"device"
);
});
+
+/**
+ * Tests that we can measure credentials related files in the profile directory.
+ */
+add_task(async function test_credentialsAndSecurityBackupResource() {
+ Services.fog.testResetFOG();
+
+ const EXPECTED_CREDENTIALS_KILOBYTES_SIZE = 413;
+ const EXPECTED_SECURITY_KILOBYTES_SIZE = 231;
+
+ // Create resource files in temporary directory
+ const tempDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CredentialsAndSecurityBackupResource-measurement-test"
+ );
+
+ const mockFiles = [
+ // Set up credentials files
+ { path: "key4.db", sizeInKB: 300 },
+ { path: "logins.json", sizeInKB: 1 },
+ { path: "logins-backup.json", sizeInKB: 1 },
+ { path: "autofill-profiles.json", sizeInKB: 1 },
+ { path: "credentialstate.sqlite", sizeInKB: 100 },
+ { path: "signedInUser.json", sizeInKB: 5 },
+ // Set up security files
+ { path: "cert9.db", sizeInKB: 230 },
+ { path: "pkcs11.txt", sizeInKB: 1 },
+ ];
+
+ await createTestFiles(tempDir, mockFiles);
+
+ let credentialsAndSecurityBackupResource =
+ new CredentialsAndSecurityBackupResource();
+ await credentialsAndSecurityBackupResource.measure(tempDir);
+
+ let credentialsMeasurement =
+ Glean.browserBackup.credentialsDataSize.testGetValue();
+ let securityMeasurement = Glean.browserBackup.securityDataSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Credentials measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.credentials_data_size",
+ credentialsMeasurement,
+ "Glean and telemetry measurements for credentials data should be equal"
+ );
+
+ Assert.equal(
+ credentialsMeasurement,
+ EXPECTED_CREDENTIALS_KILOBYTES_SIZE,
+ "Should have collected the correct glean measurement for credentials files"
+ );
+
+ // Security measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.security_data_size",
+ securityMeasurement,
+ "Glean and telemetry measurements for security data should be equal"
+ );
+ Assert.equal(
+ securityMeasurement,
+ EXPECTED_SECURITY_KILOBYTES_SIZE,
+ "Should have collected the correct glean measurement for security files"
+ );
+
+ // Cleanup
+ await maybeRemovePath(tempDir);
+});
+
+/**
+ * Tests that we can measure the Cookies db in a profile directory.
+ */
+add_task(async function test_cookiesBackupResource() {
+ const EXPECTED_COOKIES_DB_SIZE = 1230;
+
+ Services.fog.testResetFOG();
+
+ // Create resource files in temporary directory
+ let tempDir = PathUtils.tempDir;
+ let tempCookiesDBPath = PathUtils.join(tempDir, "cookies.sqlite");
+ await createKilobyteSizedFile(tempCookiesDBPath, EXPECTED_COOKIES_DB_SIZE);
+
+ let cookiesBackupResource = new CookiesBackupResource();
+ await cookiesBackupResource.measure(tempDir);
+
+ let cookiesMeasurement = Glean.browserBackup.cookiesSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Compare glean vs telemetry measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.cookies_size",
+ cookiesMeasurement,
+ "Glean and telemetry measurements for cookies.sqlite should be equal"
+ );
+
+ // Compare glean measurements vs actual file sizes
+ Assert.equal(
+ cookiesMeasurement,
+ EXPECTED_COOKIES_DB_SIZE,
+ "Should have collected the correct glean measurement for cookies.sqlite"
+ );
+
+ await maybeRemovePath(tempCookiesDBPath);
+});
+
+/**
+ * Tests that we can measure the Form History db in a profile directory.
+ */
+add_task(async function test_formHistoryBackupResource() {
+ const EXPECTED_FORM_HISTORY_DB_SIZE = 500;
+
+ Services.fog.testResetFOG();
+
+ // Create resource files in temporary directory
+ let tempDir = PathUtils.tempDir;
+ let tempFormHistoryDBPath = PathUtils.join(tempDir, "formhistory.sqlite");
+ await createKilobyteSizedFile(
+ tempFormHistoryDBPath,
+ EXPECTED_FORM_HISTORY_DB_SIZE
+ );
+
+ let formHistoryBackupResource = new FormHistoryBackupResource();
+ await formHistoryBackupResource.measure(tempDir);
+
+ let formHistoryMeasurement =
+ Glean.browserBackup.formHistorySize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Compare glean vs telemetry measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.form_history_size",
+ formHistoryMeasurement,
+ "Glean and telemetry measurements for formhistory.sqlite should be equal"
+ );
+
+ // Compare glean measurements vs actual file sizes
+ Assert.equal(
+ formHistoryMeasurement,
+ EXPECTED_FORM_HISTORY_DB_SIZE,
+ "Should have collected the correct glean measurement for formhistory.sqlite"
+ );
+
+ await IOUtils.remove(tempFormHistoryDBPath);
+});
+
+/**
+ * Tests that we can measure the Session Store JSON and backups directory.
+ */
+add_task(async function test_sessionStoreBackupResource() {
+ const EXPECTED_KILOBYTES_FOR_BACKUPS_DIR = 1000;
+ Services.fog.testResetFOG();
+
+ // Create the sessionstore-backups directory.
+ let tempDir = PathUtils.tempDir;
+ let sessionStoreBackupsPath = PathUtils.join(
+ tempDir,
+ "sessionstore-backups",
+ "restore.jsonlz4"
+ );
+ await createKilobyteSizedFile(
+ sessionStoreBackupsPath,
+ EXPECTED_KILOBYTES_FOR_BACKUPS_DIR
+ );
+
+ let sessionStoreBackupResource = new SessionStoreBackupResource();
+ await sessionStoreBackupResource.measure(tempDir);
+
+ let sessionStoreBackupsDirectoryMeasurement =
+ Glean.browserBackup.sessionStoreBackupsDirectorySize.testGetValue();
+ let sessionStoreMeasurement =
+ Glean.browserBackup.sessionStoreSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Compare glean vs telemetry measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.session_store_backups_directory_size",
+ sessionStoreBackupsDirectoryMeasurement,
+ "Glean and telemetry measurements for session store backups directory should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.session_store_size",
+ sessionStoreMeasurement,
+ "Glean and telemetry measurements for session store should be equal"
+ );
+
+ // Compare glean measurements vs actual file sizes
+ Assert.equal(
+ sessionStoreBackupsDirectoryMeasurement,
+ EXPECTED_KILOBYTES_FOR_BACKUPS_DIR,
+ "Should have collected the correct glean measurement for the sessionstore-backups directory"
+ );
+
+ // Session store measurement is from `getCurrentState`, so exact size is unknown.
+ Assert.greater(
+ sessionStoreMeasurement,
+ 0,
+ "Should have collected a measurement for the session store"
+ );
+
+ await IOUtils.remove(sessionStoreBackupsPath);
+});
+
+/**
+ * Tests that we can measure the size of all the addons & extensions data.
+ */
+add_task(async function test_AddonsBackupResource() {
+ Services.fog.testResetFOG();
+ Services.telemetry.clearScalars();
+
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON = 250;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE = 500;
+ const EXPECTED_KILOBYTES_FOR_STORAGE_SYNC = 50;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_A = 600;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_B = 400;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C = 150;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_DIRECTORY = 1000;
+ const EXPECTED_KILOBYTES_FOR_EXTENSION_DATA = 100;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE = 200;
+
+ let tempDir = PathUtils.tempDir;
+
+ // Create extensions json files (all the same size).
+ const extensionsFilePath = PathUtils.join(tempDir, "extensions.json");
+ await createKilobyteSizedFile(
+ extensionsFilePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON
+ );
+ const extensionSettingsFilePath = PathUtils.join(
+ tempDir,
+ "extension-settings.json"
+ );
+ await createKilobyteSizedFile(
+ extensionSettingsFilePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON
+ );
+ const extensionsPrefsFilePath = PathUtils.join(
+ tempDir,
+ "extension-preferences.json"
+ );
+ await createKilobyteSizedFile(
+ extensionsPrefsFilePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON
+ );
+ const addonStartupFilePath = PathUtils.join(tempDir, "addonStartup.json.lz4");
+ await createKilobyteSizedFile(
+ addonStartupFilePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON
+ );
+
+ // Create the extension store permissions data file.
+ let extensionStorePermissionsDataSize = PathUtils.join(
+ tempDir,
+ "extension-store-permissions",
+ "data.safe.bin"
+ );
+ await createKilobyteSizedFile(
+ extensionStorePermissionsDataSize,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE
+ );
+
+ // Create the storage sync database file.
+ let storageSyncPath = PathUtils.join(tempDir, "storage-sync-v2.sqlite");
+ await createKilobyteSizedFile(
+ storageSyncPath,
+ EXPECTED_KILOBYTES_FOR_STORAGE_SYNC
+ );
+
+ // Create the extensions directory with XPI files.
+ let extensionsXpiAPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "extension-b.xpi"
+ );
+ let extensionsXpiBPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "extension-a.xpi"
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiAPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_A
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiBPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_B
+ );
+ // Should be ignored.
+ let extensionsXpiStagedPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "staged",
+ "staged-test-extension.xpi"
+ );
+ let extensionsXpiTrashPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "trash",
+ "trashed-test-extension.xpi"
+ );
+ let extensionsXpiUnpackedPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "unpacked-extension.xpi",
+ "manifest.json"
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiStagedPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiTrashPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiUnpackedPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C
+ );
+
+ // Create the browser extension data directory.
+ let browserExtensionDataPath = PathUtils.join(
+ tempDir,
+ "browser-extension-data",
+ "test-file"
+ );
+ await createKilobyteSizedFile(
+ browserExtensionDataPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSION_DATA
+ );
+
+ // Create the extensions storage directory.
+ let extensionsStoragePath = PathUtils.join(
+ tempDir,
+ "storage",
+ "default",
+ "moz-extension+++test-extension-id",
+ "idb",
+ "data.sqlite"
+ );
+ // Other storage files that should not be counted.
+ let otherStoragePath = PathUtils.join(
+ tempDir,
+ "storage",
+ "default",
+ "https+++accounts.firefox.com",
+ "ls",
+ "data.sqlite"
+ );
+
+ await createKilobyteSizedFile(
+ extensionsStoragePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE
+ );
+ await createKilobyteSizedFile(
+ otherStoragePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE
+ );
+
+ // Measure all the extensions data.
+ let extensionsBackupResource = new AddonsBackupResource();
+ await extensionsBackupResource.measure(tempDir);
+
+ let extensionsJsonSizeMeasurement =
+ Glean.browserBackup.extensionsJsonSize.testGetValue();
+ Assert.equal(
+ extensionsJsonSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON * 4, // There are 4 equally sized files.
+ "Should have collected the correct measurement of the total size of all extensions JSON files"
+ );
+
+ let extensionStorePermissionsDataSizeMeasurement =
+ Glean.browserBackup.extensionStorePermissionsDataSize.testGetValue();
+ Assert.equal(
+ extensionStorePermissionsDataSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE,
+ "Should have collected the correct measurement of the size of the extension store permissions data"
+ );
+
+ let storageSyncSizeMeasurement =
+ Glean.browserBackup.storageSyncSize.testGetValue();
+ Assert.equal(
+ storageSyncSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_STORAGE_SYNC,
+ "Should have collected the correct measurement of the size of the storage sync database"
+ );
+
+ let extensionsXpiDirectorySizeMeasurement =
+ Glean.browserBackup.extensionsXpiDirectorySize.testGetValue();
+ Assert.equal(
+ extensionsXpiDirectorySizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_DIRECTORY,
+ "Should have collected the correct measurement of the size 2 equally sized XPI files in the extensions directory"
+ );
+
+ let browserExtensionDataSizeMeasurement =
+ Glean.browserBackup.browserExtensionDataSize.testGetValue();
+ Assert.equal(
+ browserExtensionDataSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSION_DATA,
+ "Should have collected the correct measurement of the size of the browser extension data directory"
+ );
+
+ let extensionsStorageSizeMeasurement =
+ Glean.browserBackup.extensionsStorageSize.testGetValue();
+ Assert.equal(
+ extensionsStorageSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE,
+ "Should have collected the correct measurement of all the extensions storage"
+ );
+
+ // Compare glean vs telemetry measurements
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.extensions_json_size",
+ extensionsJsonSizeMeasurement,
+ "Glean and telemetry measurements for extensions JSON should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.extension_store_permissions_data_size",
+ extensionStorePermissionsDataSizeMeasurement,
+ "Glean and telemetry measurements for extension store permissions data should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.storage_sync_size",
+ storageSyncSizeMeasurement,
+ "Glean and telemetry measurements for storage sync database should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.extensions_xpi_directory_size",
+ extensionsXpiDirectorySizeMeasurement,
+ "Glean and telemetry measurements for extensions directory should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.browser_extension_data_size",
+ browserExtensionDataSizeMeasurement,
+ "Glean and telemetry measurements for browser extension data should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.extensions_storage_size",
+ extensionsStorageSizeMeasurement,
+ "Glean and telemetry measurements for extensions storage should be equal"
+ );
+
+ await maybeRemovePath(tempDir);
+});
+
+/**
+ * Tests that we can handle the extension store permissions data not existing.
+ */
+add_task(
+ async function test_AddonsBackupResource_no_extension_store_permissions_data() {
+ Services.fog.testResetFOG();
+
+ let tempDir = PathUtils.tempDir;
+
+ let extensionsBackupResource = new AddonsBackupResource();
+ await extensionsBackupResource.measure(tempDir);
+
+ let extensionStorePermissionsDataSizeMeasurement =
+ Glean.browserBackup.extensionStorePermissionsDataSize.testGetValue();
+ Assert.equal(
+ extensionStorePermissionsDataSizeMeasurement,
+ null,
+ "Should NOT have collected a measurement for the missing data"
+ );
+ }
+);
+
+/**
+ * Tests that we can handle a profile with no moz-extension IndexedDB databases.
+ */
+add_task(
+ async function test_AddonsBackupResource_no_extension_storage_databases() {
+ Services.fog.testResetFOG();
+
+ let tempDir = PathUtils.tempDir;
+
+ let extensionsBackupResource = new AddonsBackupResource();
+ await extensionsBackupResource.measure(tempDir);
+
+ let extensionsStorageSizeMeasurement =
+ Glean.browserBackup.extensionsStorageSize.testGetValue();
+ Assert.equal(
+ extensionsStorageSizeMeasurement,
+ null,
+ "Should NOT have collected a measurement for the missing data"
+ );
+ }
+);
diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml
index fb6dcd6846..07e517f1f2 100644
--- a/browser/components/backup/tests/xpcshell/xpcshell.toml
+++ b/browser/components/backup/tests/xpcshell/xpcshell.toml
@@ -1,8 +1,20 @@
[DEFAULT]
+head = "head.js"
firefox-appdir = "browser"
skip-if = ["os == 'android'"]
+prefs = [
+ "browser.backup.log=true",
+]
-["test_BrowserResource.js"]
+["test_BackupResource.js"]
support-files = ["data/test_xulstore.json"]
+["test_MiscDataBackupResource.js"]
+
+["test_PlacesBackupResource.js"]
+
+["test_PreferencesBackupResource.js"]
+
+["test_createBackup.js"]
+
["test_measurements.js"]
diff --git a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs
index 34f132a539..c710f098cb 100644
--- a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs
+++ b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs
@@ -42,6 +42,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
"A DLP agent"
);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "showBlockedResult",
+ "browser.contentanalysis.show_blocked_result",
+ true
+);
+
/**
* A class that groups browsing contexts by their top-level one.
* This is necessary because if there may be a subframe that
@@ -236,7 +243,7 @@ export const ContentAnalysis = {
},
// nsIObserver
- async observe(aSubj, aTopic, aData) {
+ async observe(aSubj, aTopic, _aData) {
switch (aTopic) {
case "quit-application-requested": {
let pendingRequests =
@@ -293,10 +300,10 @@ export const ContentAnalysis = {
);
return;
}
- const operation = request.analysisType;
+ const analysisType = request.analysisType;
// For operations that block browser interaction, show the "slow content analysis"
// dialog faster
- let slowTimeoutMs = this._shouldShowBlockingNotification(operation)
+ let slowTimeoutMs = this._shouldShowBlockingNotification(analysisType)
? this._SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS
: this._SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS;
let browsingContext = request.windowGlobalParent?.browsingContext;
@@ -326,7 +333,7 @@ export const ContentAnalysis = {
timer: lazy.setTimeout(() => {
this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, {
notification: this._showSlowCAMessage(
- operation,
+ analysisType,
request,
resourceNameOrOperationType,
browsingContext
@@ -338,7 +345,7 @@ export const ContentAnalysis = {
});
}
break;
- case "dlp-response":
+ case "dlp-response": {
const request = aSubj.QueryInterface(Ci.nsIContentAnalysisResponse);
// Cancels timer or slow message UI,
// if present, and possibly presents the CA verdict.
@@ -372,12 +379,14 @@ export const ContentAnalysis = {
windowAndResourceNameOrOperationType.resourceNameOrOperationType,
windowAndResourceNameOrOperationType.browsingContext,
request.requestToken,
- responseResult
+ responseResult,
+ request.cancelError
);
this._showAnotherPendingDialog(
windowAndResourceNameOrOperationType.browsingContext
);
break;
+ }
}
},
@@ -441,7 +450,10 @@ export const ContentAnalysis = {
}
if (this._SHOW_NOTIFICATIONS) {
- const notification = new aBrowsingContext.topChromeWindow.Notification(
+ let topWindow =
+ aBrowsingContext.topChromeWindow ??
+ aBrowsingContext.embedderWindowGlobal.browsingContext.topChromeWindow;
+ const notification = new topWindow.Notification(
this.l10n.formatValueSync("contentanalysis-notification-title"),
{
body: aMessage,
@@ -460,10 +472,10 @@ export const ContentAnalysis = {
return null;
},
- _shouldShowBlockingNotification(aOperation) {
+ _shouldShowBlockingNotification(aAnalysisType) {
return !(
- aOperation == Ci.nsIContentAnalysisRequest.eFileDownloaded ||
- aOperation == Ci.nsIContentAnalysisRequest.ePrint
+ aAnalysisType == Ci.nsIContentAnalysisRequest.eFileDownloaded ||
+ aAnalysisType == Ci.nsIContentAnalysisRequest.ePrint
);
},
@@ -479,6 +491,9 @@ export const ContentAnalysis = {
case Ci.nsIContentAnalysisRequest.eDroppedText:
l10nId = "contentanalysis-operationtype-dropped-text";
break;
+ case Ci.nsIContentAnalysisRequest.eOperationPrint:
+ l10nId = "contentanalysis-operationtype-print";
+ break;
}
if (!l10nId) {
console.error(
@@ -587,10 +602,14 @@ export const ContentAnalysis = {
case Ci.nsIContentAnalysisRequest.eDroppedText:
l10nId = "contentanalysis-slow-agent-dialog-body-dropped-text";
break;
+ case Ci.nsIContentAnalysisRequest.eOperationPrint:
+ l10nId = "contentanalysis-slow-agent-dialog-body-print";
+ break;
}
if (!l10nId) {
console.error(
- "Unknown operationTypeForDisplay: " + aResourceNameOrOperationType
+ "Unknown operationTypeForDisplay: ",
+ aResourceNameOrOperationType
);
return "";
}
@@ -655,7 +674,8 @@ export const ContentAnalysis = {
aResourceNameOrOperationType,
aBrowsingContext,
aRequestToken,
- aCAResult
+ aCAResult,
+ aRequestCancelError
) {
let message = null;
let timeoutMs = 0;
@@ -676,7 +696,7 @@ export const ContentAnalysis = {
);
timeoutMs = this._RESULT_NOTIFICATION_FAST_TIMEOUT_MS;
break;
- case Ci.nsIContentAnalysisResponse.eWarn:
+ case Ci.nsIContentAnalysisResponse.eWarn: {
const result = await Services.prompt.asyncConfirmEx(
aBrowsingContext,
Ci.nsIPromptService.MODAL_TYPE_TAB,
@@ -704,7 +724,12 @@ export const ContentAnalysis = {
const allow = result.get("buttonNumClicked") === 0;
lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow);
return null;
+ }
case Ci.nsIContentAnalysisResponse.eBlock:
+ if (!lazy.showBlockedResult) {
+ // Don't show anything
+ return null;
+ }
message = await this.l10n.formatValue("contentanalysis-block-message", {
content: this._getResourceNameFromNameOrOperationType(
aResourceNameOrOperationType
@@ -713,13 +738,51 @@ export const ContentAnalysis = {
timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
break;
case Ci.nsIContentAnalysisResponse.eUnspecified:
- message = await this.l10n.formatValue("contentanalysis-error-message", {
- content: this._getResourceNameFromNameOrOperationType(
- aResourceNameOrOperationType
- ),
- });
+ message = await this.l10n.formatValue(
+ "contentanalysis-unspecified-error-message",
+ {
+ agent: lazy.agentName,
+ content: this._getResourceNameFromNameOrOperationType(
+ aResourceNameOrOperationType
+ ),
+ }
+ );
timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
break;
+ case Ci.nsIContentAnalysisResponse.eCanceled:
+ {
+ let messageId;
+ switch (aRequestCancelError) {
+ case Ci.nsIContentAnalysisResponse.eUserInitiated:
+ console.error(
+ "Got unexpected cancel response with eUserInitiated"
+ );
+ return null;
+ case Ci.nsIContentAnalysisResponse.eNoAgent:
+ messageId = "contentanalysis-no-agent-connected-message";
+ break;
+ case Ci.nsIContentAnalysisResponse.eInvalidAgentSignature:
+ messageId = "contentanalysis-invalid-agent-signature-message";
+ break;
+ case Ci.nsIContentAnalysisResponse.eErrorOther:
+ messageId = "contentanalysis-unspecified-error-message";
+ break;
+ default:
+ console.error(
+ "Unexpected CA cancelError value: " + aRequestCancelError
+ );
+ messageId = "contentanalysis-unspecified-error-message";
+ break;
+ }
+ message = await this.l10n.formatValue(messageId, {
+ agent: lazy.agentName,
+ content: this._getResourceNameFromNameOrOperationType(
+ aResourceNameOrOperationType
+ ),
+ });
+ timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
+ }
+ break;
default:
throw new Error("Unexpected CA result value: " + aCAResult);
}
diff --git a/browser/components/contextualidentity/test/browser/browser_eme.js b/browser/components/contextualidentity/test/browser/browser_eme.js
index 5b0cd8a940..1c7432382f 100644
--- a/browser/components/contextualidentity/test/browser/browser_eme.js
+++ b/browser/components/contextualidentity/test/browser/browser_eme.js
@@ -121,7 +121,7 @@ add_task(async function test() {
// Insert the media key.
await new Promise(resolve => {
- session.addEventListener("message", function (event) {
+ session.addEventListener("message", function () {
session
.update(aKeyInfo.keyObj)
.then(() => {
diff --git a/browser/components/contextualidentity/test/browser/browser_favicon.js b/browser/components/contextualidentity/test/browser/browser_favicon.js
index 8d29aff28f..c4c615077a 100644
--- a/browser/components/contextualidentity/test/browser/browser_favicon.js
+++ b/browser/components/contextualidentity/test/browser/browser_favicon.js
@@ -20,7 +20,7 @@ function getIconFile() {
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON,
},
- function (inputStream, status) {
+ function (inputStream) {
let size = inputStream.available();
gFaviconData = NetUtil.readInputStreamToString(inputStream, size);
resolve();
diff --git a/browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js b/browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js
index 24a4c51118..10b8474072 100644
--- a/browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js
+++ b/browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js
@@ -108,7 +108,7 @@ async function setupEMEKey(browser) {
// Insert the EME key.
await new Promise(resolve => {
- session.addEventListener("message", function (event) {
+ session.addEventListener("message", function () {
session
.update(aKeyInfo.keyObj)
.then(() => {
diff --git a/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js b/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js
index 79975fff8c..204c8eccbf 100644
--- a/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js
+++ b/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js
@@ -86,11 +86,11 @@ function OpenCacheEntry(key, where, flags, lci) {
CacheListener.prototype = {
QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
- onCacheEntryCheck(entry) {
+ onCacheEntryCheck() {
return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
},
- onCacheEntryAvailable(entry, isnew, status) {
+ onCacheEntryAvailable() {
resolve();
},
@@ -311,7 +311,7 @@ async function test_storage_cleared() {
let storeRequest = store.get(1);
await new Promise(done => {
- storeRequest.onsuccess = event => {
+ storeRequest.onsuccess = () => {
let res = storeRequest.result;
Assert.equal(
res.userContext,
diff --git a/browser/components/contextualidentity/test/browser/browser_guessusercontext.js b/browser/components/contextualidentity/test/browser/browser_guessusercontext.js
index 69461e67b5..90ccba9ca4 100644
--- a/browser/components/contextualidentity/test/browser/browser_guessusercontext.js
+++ b/browser/components/contextualidentity/test/browser/browser_guessusercontext.js
@@ -93,7 +93,7 @@ add_task(async function test() {
openURIFromExternal(HOST_EXAMPLE.spec + "?new");
is(
gBrowser.selectedTab.getAttribute("usercontextid"),
- "",
+ null,
"opener flow with default user context ID forced by pref"
);
});
diff --git a/browser/components/contextualidentity/test/browser/browser_middleClick.js b/browser/components/contextualidentity/test/browser/browser_middleClick.js
index 9b9fbcb737..d6c0feb77e 100644
--- a/browser/components/contextualidentity/test/browser/browser_middleClick.js
+++ b/browser/components/contextualidentity/test/browser/browser_middleClick.js
@@ -24,7 +24,7 @@ add_task(async function () {
});
info("Synthesize a mouse click and wait for a new tab...");
- let newTab = await new Promise((resolve, reject) => {
+ let newTab = await new Promise(resolve => {
gBrowser.tabContainer.addEventListener(
"TabOpen",
function (openEvent) {
diff --git a/browser/components/contextualidentity/test/browser/browser_serviceworkers.js b/browser/components/contextualidentity/test/browser/browser_serviceworkers.js
index 4f42c55d73..e40737669e 100644
--- a/browser/components/contextualidentity/test/browser/browser_serviceworkers.js
+++ b/browser/components/contextualidentity/test/browser/browser_serviceworkers.js
@@ -111,7 +111,7 @@ function promiseUnregister(info) {
ok(aState, "ServiceWorkerRegistration exists");
resolve();
},
- unregisterFailed(aState) {
+ unregisterFailed() {
ok(false, "unregister should succeed");
},
},
diff --git a/browser/components/contextualidentity/test/browser/browser_windowName.js b/browser/components/contextualidentity/test/browser/browser_windowName.js
index 5ba2cc0e0a..256f84a8f6 100644
--- a/browser/components/contextualidentity/test/browser/browser_windowName.js
+++ b/browser/components/contextualidentity/test/browser/browser_windowName.js
@@ -24,7 +24,7 @@ add_task(async function test() {
});
let browser1 = gBrowser.getBrowserForTab(tab1);
await BrowserTestUtils.browserLoaded(browser1);
- await SpecialPowers.spawn(browser1, [], function (opts) {
+ await SpecialPowers.spawn(browser1, [], function () {
content.window.name = "tab-1";
});
@@ -34,7 +34,7 @@ add_task(async function test() {
});
let browser2 = gBrowser.getBrowserForTab(tab2);
await BrowserTestUtils.browserLoaded(browser2);
- await SpecialPowers.spawn(browser2, [], function (opts) {
+ await SpecialPowers.spawn(browser2, [], function () {
content.window.name = "tab-2";
});
diff --git a/browser/components/contextualidentity/test/browser/file_set_storages.html b/browser/components/contextualidentity/test/browser/file_set_storages.html
index 96c46f9062..16a16d8691 100644
--- a/browser/components/contextualidentity/test/browser/file_set_storages.html
+++ b/browser/components/contextualidentity/test/browser/file_set_storages.html
@@ -25,7 +25,7 @@
store.createIndex("userContext", "userContext", { unique: false });
};
- request.onsuccess = event => {
+ request.onsuccess = () => {
let db = request.result;
let transaction = db.transaction(["obj"], "readwrite");
let store = transaction.objectStore("obj");
diff --git a/browser/components/controlcenter/content/protectionsPanel.inc.xhtml b/browser/components/controlcenter/content/protectionsPanel.inc.xhtml
index 707105f520..29e98c2bb2 100644
--- a/browser/components/controlcenter/content/protectionsPanel.inc.xhtml
+++ b/browser/components/controlcenter/content/protectionsPanel.inc.xhtml
@@ -36,8 +36,8 @@
</box>
<toolbarseparator></toolbarseparator>
- <html:div id="messaging-system-message-container" disabled="true">
- <!-- Messaging System Messages will render in this container -->
+ <html:div id="info-message-container" disabled="true">
+ <!-- Info message will render in this container -->
</html:div>
</vbox>
diff --git a/browser/components/customizableui/CustomizableUI.sys.mjs b/browser/components/customizableui/CustomizableUI.sys.mjs
index 5b09402dc1..9f9bbf37dc 100644
--- a/browser/components/customizableui/CustomizableUI.sys.mjs
+++ b/browser/components/customizableui/CustomizableUI.sys.mjs
@@ -1454,7 +1454,7 @@ var CustomizableUIInternal = {
}
},
- onCustomizeEnd(aWindow) {
+ onCustomizeEnd() {
this._clearPreviousUIState();
},
@@ -6215,7 +6215,7 @@ class OverflowableToolbar {
* nsIObserver implementation starts here.
*/
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
// This nsIObserver method allows us to defer initialization until after
// this window has finished painting and starting up.
if (
diff --git a/browser/components/customizableui/CustomizableWidgets.sys.mjs b/browser/components/customizableui/CustomizableWidgets.sys.mjs
index ab95e8e7db..1f37af7963 100644
--- a/browser/components/customizableui/CustomizableWidgets.sys.mjs
+++ b/browser/components/customizableui/CustomizableWidgets.sys.mjs
@@ -155,7 +155,7 @@ export const CustomizableWidgets = [
panelview.panelMultiView.addEventListener("PanelMultiViewHidden", this);
window.addEventListener("unload", this);
},
- onViewHiding(event) {
+ onViewHiding() {
lazy.log.debug("History view is being hidden!");
},
onPanelMultiViewHidden(event) {
@@ -175,7 +175,7 @@ export const CustomizableWidgets = [
}
panelMultiView.removeEventListener("PanelMultiViewHidden", this);
},
- onWindowUnload(event) {
+ onWindowUnload() {
if (this._panelMenuView) {
delete this._panelMenuView;
}
diff --git a/browser/components/customizableui/CustomizeMode.sys.mjs b/browser/components/customizableui/CustomizeMode.sys.mjs
index 5f6d01d833..7b4ee373be 100644
--- a/browser/components/customizableui/CustomizeMode.sys.mjs
+++ b/browser/components/customizableui/CustomizeMode.sys.mjs
@@ -28,7 +28,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs",
- SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
@@ -63,14 +62,14 @@ var gTab;
function closeGlobalTab() {
let win = gTab.ownerGlobal;
if (win.gBrowser.browsers.length == 1) {
- win.BrowserOpenTab();
+ win.BrowserCommands.openTab();
}
win.gBrowser.removeTab(gTab, { animate: true });
gTab = null;
}
var gTabsProgressListener = {
- onLocationChange(aBrowser, aWebProgress, aRequest, aLocation, aFlags) {
+ onLocationChange(aBrowser, aWebProgress, aRequest, aLocation) {
// Tear down customize mode when the customize mode tab loads some other page.
// Customize mode will be re-entered if "about:blank" is loaded again, so
// don't tear down in this case.
@@ -221,7 +220,6 @@ CustomizeMode.prototype = {
gTab = aTab;
gTab.setAttribute("customizemode", "true");
- lazy.SessionStore.persistTabAttribute("customizemode");
if (gTab.linkedPanel) {
gTab.linkedBrowser.stop();
@@ -663,7 +661,7 @@ CustomizeMode.prototype = {
});
},
- async addToToolbar(aNode, aReason) {
+ async addToToolbar(aNode) {
aNode = this._getCustomizableChildForNode(aNode);
if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
aNode = aNode.firstElementChild;
@@ -1282,15 +1280,15 @@ CustomizeMode.prototype = {
this._onUIChange();
},
- onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) {
+ onWidgetMoved() {
this._onUIChange();
},
- onWidgetAdded(aWidgetId, aArea, aPosition) {
+ onWidgetAdded() {
this._onUIChange();
},
- onWidgetRemoved(aWidgetId, aArea) {
+ onWidgetRemoved() {
this._onUIChange();
},
@@ -1649,7 +1647,7 @@ CustomizeMode.prototype = {
delete this.paletteDragHandler;
},
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
switch (aTopic) {
case "nsPref:changed":
this._updateResetButton();
@@ -2329,7 +2327,7 @@ CustomizeMode.prototype = {
}
},
- _setGridDragActive(aDragOverNode, aDraggedItem, aValue) {
+ _setGridDragActive(aDragOverNode, aDraggedItem) {
let targetArea = this._getCustomizableParent(aDragOverNode);
let draggedWrapper = this.$("wrapper-" + aDraggedItem.id);
let originArea = this._getCustomizableParent(draggedWrapper);
@@ -2428,7 +2426,7 @@ CustomizeMode.prototype = {
return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(","));
},
- _getDragOverNode(aEvent, aAreaElement, aAreaType, aDraggedItemId) {
+ _getDragOverNode(aEvent, aAreaElement, aAreaType) {
let expectedParent =
CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement;
if (!expectedParent.contains(aEvent.target)) {
diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js
index f99560bd42..cb32085fd7 100644
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -6,7 +6,6 @@ ChromeUtils.defineESModuleGetters(this, {
AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
- ToolbarPanelHub: "resource:///modules/asrouter/ToolbarPanelHub.jsm",
});
/**
@@ -167,9 +166,6 @@ const PanelUI = {
this.menuButton.removeEventListener("mousedown", this);
this.menuButton.removeEventListener("keypress", this);
CustomizableUI.removeListener(this);
- if (this.whatsNewPanel) {
- this.whatsNewPanel.removeEventListener("ViewShowing", this);
- }
},
/**
@@ -303,11 +299,6 @@ const PanelUI = {
case "activate":
this.updateNotifications();
break;
- case "ViewShowing":
- if (aEvent.target == this.whatsNewPanel) {
- this.onWhatsNewPanelShowing();
- }
- break;
}
},
@@ -412,7 +403,6 @@ const PanelUI = {
return;
}
- this.ensureWhatsNewInitialized(viewNode);
this.ensurePanicViewInitialized(viewNode);
let container = aAnchor.closest("panelmultiview");
@@ -497,24 +487,6 @@ const PanelUI = {
},
/**
- * Sets up the event listener for when the What's New panel is shown.
- *
- * @param {panelview} panelView The What's New panelview.
- */
- ensureWhatsNewInitialized(panelView) {
- if (panelView.id != "PanelUI-whatsNew" || panelView._initialized) {
- return;
- }
-
- if (!this.whatsNewPanel) {
- this.whatsNewPanel = panelView;
- }
-
- panelView._initialized = true;
- panelView.addEventListener("ViewShowing", this);
- },
-
- /**
* Adds FTL before appending the panic view markup to the main DOM.
*
* @param {panelview} panelView The Panic View panelview.
@@ -533,17 +505,6 @@ const PanelUI = {
},
/**
- * When the What's New panel is showing, we fetch the messages to show.
- */
- onWhatsNewPanelShowing() {
- ToolbarPanelHub.renderMessages(
- window,
- document,
- "PanelUI-whatsNew-message-container"
- );
- },
-
- /**
* NB: The enable- and disableSingleSubviewPanelAnimations methods only
* affect the hiding/showing animations of single-subview panels (tempPanel
* in the showSubView method).
@@ -568,7 +529,7 @@ const PanelUI = {
}
},
- onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) {
+ onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
if (aContainer == this.overflowFixedList) {
this.updateOverflowStatus();
}
@@ -601,7 +562,7 @@ const PanelUI = {
}
},
- _onHelpViewShow(aEvent) {
+ _onHelpViewShow() {
// Call global menu setup function
buildHelpMenu();
diff --git a/browser/components/customizableui/test/browser_1087303_button_fullscreen.js b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js
index f67e81b892..42f9b58370 100644
--- a/browser/components/customizableui/test/browser_1087303_button_fullscreen.js
+++ b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js
@@ -44,7 +44,7 @@ function promiseFullscreenChange() {
reject("Fullscreen change did not happen within " + 20000 + "ms");
}, 20000);
- function onFullscreenChange(event) {
+ function onFullscreenChange() {
clearTimeout(timeoutId);
window.removeEventListener("fullscreen", onFullscreenChange, true);
info("Fullscreen event received");
diff --git a/browser/components/customizableui/test/browser_1087303_button_preferences.js b/browser/components/customizableui/test/browser_1087303_button_preferences.js
index 7db48341cb..86bc89f48e 100644
--- a/browser/components/customizableui/test/browser_1087303_button_preferences.js
+++ b/browser/components/customizableui/test/browser_1087303_button_preferences.js
@@ -47,7 +47,7 @@ function waitForPageLoad(aTab) {
reject("Page didn't load within " + 20000 + "ms");
}, 20000);
- async function onTabLoad(event) {
+ async function onTabLoad() {
clearTimeout(timeoutId);
aTab.linkedBrowser.removeEventListener("load", onTabLoad, true);
info("Tab event received: load");
diff --git a/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js b/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js
index 89b86dba20..5d44cb1664 100644
--- a/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js
+++ b/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js
@@ -21,7 +21,7 @@ add_task(async function test_PanelMultiView_toggle_with_other_popup() {
gBrowser,
url: TEST_URL,
},
- async function (browser) {
+ async function () {
// 1. Open the main menu.
await gCUITestUtils.openMainMenu();
diff --git a/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js b/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js
index 346608dc99..b174d2bccf 100644
--- a/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js
+++ b/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js
@@ -34,7 +34,7 @@ add_task(async function () {
"Should not be in fullscreen sizemode before we enter fullscreen."
);
- BrowserFullScreen();
+ BrowserCommands.fullScreen();
await TestUtils.waitForCondition(() => isFullscreenSizeMode());
ok(
fullscreenButton.checked,
@@ -62,7 +62,7 @@ add_task(async function () {
await endCustomizing();
- BrowserFullScreen();
+ BrowserCommands.fullScreen();
fullscreenButton = document.getElementById("fullscreen-button");
await TestUtils.waitForCondition(() => !isFullscreenSizeMode());
ok(
diff --git a/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js
index 0cf9a93341..8f2dc87e19 100644
--- a/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js
+++ b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js
@@ -124,18 +124,17 @@ add_task(async function disabled_button_in_panel() {
button.remove();
});
-registerCleanupFunction(function () {
+registerCleanupFunction(async function () {
if (button && button.parentNode) {
button.remove();
}
if (menuButton && menuButton.parentNode) {
menuButton.remove();
}
- // Sadly this isn't task.jsm-enabled, so we can't wait for this to happen. But we should
- // definitely close it here and hope it won't interfere with other tests.
- // Of course, all the tests are meant to do this themselves, but if they fail...
if (isOverflowOpen()) {
+ let panelHiddenPromise = promiseOverflowHidden(window);
PanelUI.overflowPanel.hidePopup();
+ await panelHiddenPromise;
}
CustomizableUI.reset();
});
diff --git a/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js b/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js
index cc8842a3e8..42daab891f 100644
--- a/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js
+++ b/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js
@@ -21,7 +21,7 @@ add_task(async function () {
let privateWindow = null;
let observerWindowOpened = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
if (aTopic == "domwindowopened") {
privateWindow = aSubject;
privateWindow.addEventListener(
diff --git a/browser/components/customizableui/test/browser_947914_button_newWindow.js b/browser/components/customizableui/test/browser_947914_button_newWindow.js
index 591d13191e..910dd2a179 100644
--- a/browser/components/customizableui/test/browser_947914_button_newWindow.js
+++ b/browser/components/customizableui/test/browser_947914_button_newWindow.js
@@ -21,7 +21,7 @@ add_task(async function () {
let newWindow = null;
let observerWindowOpened = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
if (aTopic == "domwindowopened") {
newWindow = aSubject;
newWindow.addEventListener(
diff --git a/browser/components/customizableui/test/browser_947914_button_zoomReset.js b/browser/components/customizableui/test/browser_947914_button_zoomReset.js
index 7dc8299b28..c97e2f17d1 100644
--- a/browser/components/customizableui/test/browser_947914_button_zoomReset.js
+++ b/browser/components/customizableui/test/browser_947914_button_zoomReset.js
@@ -12,7 +12,7 @@ add_task(async function () {
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "http://example.com", waitForLoad: true },
- async function (browser) {
+ async function () {
CustomizableUI.addWidgetToArea(
"zoom-controls",
CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
diff --git a/browser/components/customizableui/test/browser_972267_customizationchange_events.js b/browser/components/customizableui/test/browser_972267_customizationchange_events.js
index 7d27b94136..fdd7236d65 100644
--- a/browser/components/customizableui/test/browser_972267_customizationchange_events.js
+++ b/browser/components/customizableui/test/browser_972267_customizationchange_events.js
@@ -11,7 +11,7 @@ add_task(async function () {
let otherToolbox = newWindow.gNavToolbox;
let handlerCalledCount = 0;
- let handler = ev => {
+ let handler = () => {
handlerCalledCount++;
};
diff --git a/browser/components/customizableui/test/browser_customization_context_menus.js b/browser/components/customizableui/test/browser_customization_context_menus.js
index 526b3abd1b..3f4c94fb72 100644
--- a/browser/components/customizableui/test/browser_customization_context_menus.js
+++ b/browser/components/customizableui/test/browser_customization_context_menus.js
@@ -171,8 +171,8 @@ add_task(async function urlbar_context() {
let contextMenu = document.getElementById("toolbar-context-menu");
let shownPromise = popupShown(contextMenu);
let urlBarContainer = document.getElementById("urlbar-container");
- // Need to make sure not to click within an edit field.
- EventUtils.synthesizeMouse(urlBarContainer, 100, 1, {
+ // This clicks in the urlbar container margin, to avoid hitting the urlbar field.
+ EventUtils.synthesizeMouse(urlBarContainer, -2, 4, {
type: "contextmenu",
button: 2,
});
@@ -549,7 +549,7 @@ add_task(async function custom_context_menus() {
await startCustomizing();
is(
widget.getAttribute("context"),
- "",
+ null,
"Should not have own context menu in the toolbar now that we're customizing."
);
is(
@@ -562,7 +562,7 @@ add_task(async function custom_context_menus() {
simulateItemDrag(widget, panel);
is(
widget.getAttribute("context"),
- "",
+ null,
"Should not have own context menu when in the panel."
);
is(
@@ -577,7 +577,7 @@ add_task(async function custom_context_menus() {
);
is(
widget.getAttribute("context"),
- "",
+ null,
"Should not have own context menu when back in toolbar because we're still customizing."
);
is(
diff --git a/browser/components/customizableui/test/browser_editcontrols_update.js b/browser/components/customizableui/test/browser_editcontrols_update.js
index 9f064e521a..1276606779 100644
--- a/browser/components/customizableui/test/browser_editcontrols_update.js
+++ b/browser/components/customizableui/test/browser_editcontrols_update.js
@@ -29,7 +29,7 @@ function expectCommandUpdate(count, testWindow = window) {
supportsCommand(cmd) {
return cmd == "cmd_delete";
},
- isCommandEnabled(cmd) {
+ isCommandEnabled() {
if (!count) {
ok(false, "unexpected update");
reject();
diff --git a/browser/components/customizableui/test/browser_open_in_lazy_tab.js b/browser/components/customizableui/test/browser_open_in_lazy_tab.js
index c18de67698..696bfde69b 100644
--- a/browser/components/customizableui/test/browser_open_in_lazy_tab.js
+++ b/browser/components/customizableui/test/browser_open_in_lazy_tab.js
@@ -9,7 +9,7 @@ add_task(async function open_customize_mode_in_lazy_tab() {
});
gCustomizeMode.setTab(tab);
- is(tab.linkedPanel, "", "Tab should be lazy");
+ is(tab.linkedPanel, null, "Tab should be lazy");
let title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle", [
document.getElementById("bundle_brand").getString("brandShortName"),
diff --git a/browser/components/customizableui/test/browser_panelUINotifications.js b/browser/components/customizableui/test/browser_panelUINotifications.js
index 818fcbad39..d5f2cc0450 100644
--- a/browser/components/customizableui/test/browser_panelUINotifications.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -14,7 +14,7 @@ add_task(async function testMainActionCalled() {
url: "about:blank",
};
- await BrowserTestUtils.withNewTab(options, function (browser) {
+ await BrowserTestUtils.withNewTab(options, function () {
is(
PanelUI.notificationPanel.state,
"closed",
@@ -77,7 +77,7 @@ add_task(async function testSecondaryActionWorkflow() {
url: "about:blank",
};
- await BrowserTestUtils.withNewTab(options, async function (browser) {
+ await BrowserTestUtils.withNewTab(options, async function () {
is(
PanelUI.notificationPanel.state,
"closed",
@@ -167,7 +167,7 @@ add_task(async function testDownloadingBadge() {
url: "about:blank",
};
- await BrowserTestUtils.withNewTab(options, async function (browser) {
+ await BrowserTestUtils.withNewTab(options, async function () {
let mainActionCalled = false;
let mainAction = {
callback: () => {
@@ -225,7 +225,7 @@ add_task(async function testDownloadingBadge() {
* then we display any other badges that are remaining.
*/
add_task(async function testInteractionWithBadges() {
- await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
// Remove the fxa toolbar button from the navbar to ensure the notification
// is displayed on the app menu button.
let { CustomizableUI } = ChromeUtils.importESModule(
@@ -328,7 +328,7 @@ add_task(async function testInteractionWithBadges() {
* This tests that adding a badge will not dismiss any existing doorhangers.
*/
add_task(async function testAddingBadgeWhileDoorhangerIsShowing() {
- await BrowserTestUtils.withNewTab("about:blank", function (browser) {
+ await BrowserTestUtils.withNewTab("about:blank", function () {
is(
PanelUI.notificationPanel.state,
"closed",
@@ -468,7 +468,7 @@ add_task(async function testMultipleBadges() {
* Tests that non-badges also operate like a stack.
*/
add_task(async function testMultipleNonBadges() {
- await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
is(
PanelUI.notificationPanel.state,
"closed",
diff --git a/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js
index 853c39e89f..d90f928ed9 100644
--- a/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js
@@ -20,7 +20,7 @@ function waitForDocshellActivated() {
content.document,
"visibilitychange",
true /* capture */,
- aEvent => {
+ () => {
return content.browsingContext.isActive;
}
);
diff --git a/browser/components/customizableui/test/browser_panelUINotifications_modals.js b/browser/components/customizableui/test/browser_panelUINotifications_modals.js
index 87be14fcee..a3aa6d058a 100644
--- a/browser/components/customizableui/test/browser_panelUINotifications_modals.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications_modals.js
@@ -8,10 +8,6 @@ const { AppMenuNotifications } = ChromeUtils.importESModule(
);
add_task(async function testModals() {
- await SpecialPowers.pushPrefEnv({
- set: [["prompts.windowPromptSubDialog", true]],
- });
-
is(
PanelUI.notificationPanel.state,
"closed",
diff --git a/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
index fd75763857..edda165692 100644
--- a/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
@@ -15,7 +15,7 @@ add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
url: "about:blank",
};
- await BrowserTestUtils.withNewTab(options, async function (browser) {
+ await BrowserTestUtils.withNewTab(options, async function () {
let win = await BrowserTestUtils.openNewBrowserWindow();
await SimpleTest.promiseFocus(win);
let mainActionCalled = false;
@@ -95,7 +95,7 @@ add_task(
url: "about:blank",
};
- await BrowserTestUtils.withNewTab(options, async function (browser) {
+ await BrowserTestUtils.withNewTab(options, async function () {
let win = await BrowserTestUtils.openNewBrowserWindow();
await SimpleTest.promiseFocus(win);
AppMenuNotifications.showNotification("update-manual", { callback() {} });
@@ -140,7 +140,7 @@ add_task(
url: "about:blank",
};
- await BrowserTestUtils.withNewTab(options, async function (browser) {
+ await BrowserTestUtils.withNewTab(options, async function () {
let win = await BrowserTestUtils.openNewBrowserWindow();
await SimpleTest.promiseFocus(win);
AppMenuNotifications.showNotification("update-manual", { callback() {} });
diff --git a/browser/components/customizableui/test/browser_switch_to_customize_mode.js b/browser/components/customizableui/test/browser_switch_to_customize_mode.js
index 55e80d3517..e3988cb41e 100644
--- a/browser/components/customizableui/test/browser_switch_to_customize_mode.js
+++ b/browser/components/customizableui/test/browser_switch_to_customize_mode.js
@@ -18,7 +18,7 @@ add_task(async function () {
await finishedCustomizing;
let startedCount = 0;
- let handler = e => startedCount++;
+ let handler = () => startedCount++;
gNavToolbox.addEventListener("customizationstarting", handler);
await startCustomizing();
CustomizableUI.removeWidgetFromArea("stop-reload-button");
diff --git a/browser/components/customizableui/test/browser_synced_tabs_menu.js b/browser/components/customizableui/test/browser_synced_tabs_menu.js
index ff60167fea..33c8f6a845 100644
--- a/browser/components/customizableui/test/browser_synced_tabs_menu.js
+++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -40,7 +40,7 @@ function updateTabsPanel() {
return promiseTabsUpdated;
}
-// This is the mock we use for SyncedTabs.jsm - tests may override various
+// This is the mock we use for SyncedTabs.sys.mjs - tests may override various
// functions.
let mockedInternal = {
get isConfiguredToSyncTabs() {
@@ -378,7 +378,7 @@ add_task(async function () {
// There is a single node saying there's no tabs for the client.
node = node.nextElementSibling;
is(node.nodeName, "label", "node is a label");
- is(node.getAttribute("itemtype"), "", "node is neither a tab nor a client");
+ is(node.getAttribute("itemtype"), null, "node is neither a tab nor a client");
node = node.nextElementSibling;
is(node, null, "no more siblings");
@@ -514,7 +514,7 @@ add_task(async function () {
return promise;
}
- showMoreButton = checkTabsPage(25, "Show More Tabs");
+ showMoreButton = checkTabsPage(25, "Show more tabs");
await clickShowMoreButton();
checkTabsPage(77, null);
diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js
index f8c0d02a12..bc1e88ed61 100644
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -267,7 +267,7 @@ function openAndLoadWindow(aOptions, aWaitForDelayedStartup = false) {
return new Promise(resolve => {
let win = OpenBrowserWindow(aOptions);
if (aWaitForDelayedStartup) {
- Services.obs.addObserver(function onDS(aSubject, aTopic, aData) {
+ Services.obs.addObserver(function onDS(aSubject) {
if (aSubject != win) {
return;
}
@@ -309,7 +309,7 @@ function promisePanelElementShown(win, aPanel) {
let timeoutId = win.setTimeout(() => {
reject("Panel did not show within 20 seconds.");
}, 20000);
- function onPanelOpen(e) {
+ function onPanelOpen() {
aPanel.removeEventListener("popupshown", onPanelOpen);
win.clearTimeout(timeoutId);
resolve();
@@ -328,7 +328,7 @@ function promisePanelElementHidden(win, aPanel) {
let timeoutId = win.setTimeout(() => {
reject("Panel did not hide within 20 seconds.");
}, 20000);
- function onPanelClose(e) {
+ function onPanelClose() {
aPanel.removeEventListener("popuphidden", onPanelClose);
win.clearTimeout(timeoutId);
executeSoon(resolve);
@@ -352,7 +352,7 @@ function subviewShown(aSubview) {
let timeoutId = win.setTimeout(() => {
reject("Subview (" + aSubview.id + ") did not show within 20 seconds.");
}, 20000);
- function onViewShown(e) {
+ function onViewShown() {
aSubview.removeEventListener("ViewShown", onViewShown);
win.clearTimeout(timeoutId);
resolve();
@@ -367,7 +367,7 @@ function subviewHidden(aSubview) {
let timeoutId = win.setTimeout(() => {
reject("Subview (" + aSubview.id + ") did not hide within 20 seconds.");
}, 20000);
- function onViewHiding(e) {
+ function onViewHiding() {
aSubview.removeEventListener("ViewHiding", onViewHiding);
win.clearTimeout(timeoutId);
resolve();
@@ -406,7 +406,7 @@ function promiseTabLoadEvent(aTab, aURL) {
* @return {Promise} resolved when the requisite mutation shows up.
*/
function promiseAttributeMutation(aNode, aAttribute, aFilterFn) {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
info("waiting for mutation of attribute '" + aAttribute + "'.");
let obs = new MutationObserver(mutations => {
for (let mut of mutations) {
diff --git a/browser/components/doh/DoHConfig.sys.mjs b/browser/components/doh/DoHConfig.sys.mjs
index 5d35940d55..f9ac5f0f40 100644
--- a/browser/components/doh/DoHConfig.sys.mjs
+++ b/browser/components/doh/DoHConfig.sys.mjs
@@ -196,7 +196,7 @@ export const DoHConfigController = {
return;
}
- Services.obs.addObserver(function obs(sub, top, data) {
+ Services.obs.addObserver(function obs() {
Services.obs.removeObserver(obs, lazy.Region.REGION_TOPIC);
updateRegionAndResolve();
}, lazy.Region.REGION_TOPIC);
diff --git a/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js b/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js
index cd4356ed3f..c41fa66abe 100644
--- a/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js
+++ b/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js
@@ -13,7 +13,7 @@ async function setPrefAndWaitForConfigFlush(pref, value) {
await configFlushedPromise;
}
-async function clearPrefAndWaitForConfigFlush(pref, value) {
+async function clearPrefAndWaitForConfigFlush(pref) {
let configFlushedPromise = DoHTestUtils.waitForConfigFlush();
Preferences.reset(pref);
await configFlushedPromise;
diff --git a/browser/components/enterprisepolicies/Policies.sys.mjs b/browser/components/enterprisepolicies/Policies.sys.mjs
index 41fc89957c..fd15c244cc 100644
--- a/browser/components/enterprisepolicies/Policies.sys.mjs
+++ b/browser/components/enterprisepolicies/Policies.sys.mjs
@@ -81,23 +81,23 @@ export var Policies = {
// Used for cleaning up policies.
// Use the same timing that you used for setting up the policy.
_cleanup: {
- onBeforeAddons(manager) {
+ onBeforeAddons() {
if (Cu.isInAutomation || isXpcshell) {
console.log("_cleanup from onBeforeAddons");
clearBlockedAboutPages();
}
},
- onProfileAfterChange(manager) {
+ onProfileAfterChange() {
if (Cu.isInAutomation || isXpcshell) {
console.log("_cleanup from onProfileAfterChange");
}
},
- onBeforeUIStartup(manager) {
+ onBeforeUIStartup() {
if (Cu.isInAutomation || isXpcshell) {
console.log("_cleanup from onBeforeUIStartup");
}
},
- onAllWindowsRestored(manager) {
+ onAllWindowsRestored() {
if (Cu.isInAutomation || isXpcshell) {
console.log("_cleanup from onAllWindowsRestored");
}
@@ -112,7 +112,7 @@ export var Policies = {
AllowedDomainsForApps: {
onBeforeAddons(manager, param) {
- Services.obs.addObserver(function (subject, topic, data) {
+ Services.obs.addObserver(function (subject) {
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
if (channel.URI.host.endsWith(".google.com")) {
channel.setRequestHeader("X-GoogApps-Allowed-Domains", param, true);
@@ -540,6 +540,15 @@ export var Policies = {
param.DenyUrlRegexList
);
}
+ if ("AgentName" in param) {
+ setAndLockPref("browser.contentanalysis.agent_name", param.AgentName);
+ }
+ if ("ClientSignature" in param) {
+ setAndLockPref(
+ "browser.contentanalysis.client_signature",
+ param.ClientSignature
+ );
+ }
let boolPrefs = [
["IsPerUser", "is_per_user"],
["ShowBlockedResult", "show_blocked_result"],
@@ -1802,6 +1811,8 @@ export var Policies = {
"places.",
"pref.",
"print.",
+ "privacy.userContext.enabled",
+ "privacy.userContext.ui.enabled",
"signon.",
"spellchecker.",
"toolkit.legacyUserProfileCustomizations.stylesheets",
@@ -1981,13 +1992,11 @@ export var Policies = {
onBeforeAddons(manager, param) {
if (param.Locked) {
manager.disallowFeature("changeProxySettings");
- lazy.ProxyPolicies.configureProxySettings(param, setAndLockPref);
- } else {
- lazy.ProxyPolicies.configureProxySettings(
- param,
- PoliciesUtils.setDefaultPref
- );
}
+ lazy.ProxyPolicies.configureProxySettings(
+ param,
+ PoliciesUtils.setDefaultPref
+ );
},
},
@@ -2023,6 +2032,13 @@ export var Policies = {
setAndLockPref("privacy.clearOnShutdown.sessions", param);
setAndLockPref("privacy.clearOnShutdown.siteSettings", param);
setAndLockPref("privacy.clearOnShutdown.offlineApps", param);
+ setAndLockPref(
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads",
+ param
+ );
+ setAndLockPref("privacy.clearOnShutdown_v2.cookiesAndStorage", param);
+ setAndLockPref("privacy.clearOnShutdown_v2.cache", param);
+ setAndLockPref("privacy.clearOnShutdown_v2.siteSettings", param);
} else {
let locked = true;
// Needed to preserve original behavior in perpetuity.
@@ -2042,12 +2058,22 @@ export var Policies = {
param.Cache,
locked
);
+ PoliciesUtils.setDefaultPref(
+ "privacy.clearOnShutdown_v2.cache",
+ param.Cache,
+ locked
+ );
} else {
PoliciesUtils.setDefaultPref(
"privacy.clearOnShutdown.cache",
false,
lockDefaultPrefs
);
+ PoliciesUtils.setDefaultPref(
+ "privacy.clearOnShutdown_v2.cache",
+ false,
+ lockDefaultPrefs
+ );
}
if ("Cookies" in param) {
PoliciesUtils.setDefaultPref(
@@ -2055,12 +2081,26 @@ export var Policies = {
param.Cookies,
locked
);
+
+ // We set cookiesAndStorage to follow lock and pref
+ // settings for cookies, and deprecate offlineApps
+ // and sessions in the new clear on shutdown dialog - Bug 1853996
+ PoliciesUtils.setDefaultPref(
+ "privacy.clearOnShutdown_v2.cookiesAndStorage",
+ param.Cookies,
+ locked
+ );
} else {
PoliciesUtils.setDefaultPref(
"privacy.clearOnShutdown.cookies",
false,
lockDefaultPrefs
);
+ PoliciesUtils.setDefaultPref(
+ "privacy.clearOnShutdown_v2.cookiesAndStorage",
+ false,
+ lockDefaultPrefs
+ );
}
if ("Downloads" in param) {
PoliciesUtils.setDefaultPref(
@@ -2094,12 +2134,26 @@ export var Policies = {
param.History,
locked
);
+
+ // We set historyFormDataAndDownloads to follow lock and pref
+ // settings for history, and deprecate formdata and downloads
+ // in the new clear on shutdown dialog - Bug 1853996
+ PoliciesUtils.setDefaultPref(
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads",
+ param.History,
+ locked
+ );
} else {
PoliciesUtils.setDefaultPref(
"privacy.clearOnShutdown.history",
false,
lockDefaultPrefs
);
+ PoliciesUtils.setDefaultPref(
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads",
+ false,
+ lockDefaultPrefs
+ );
}
if ("Sessions" in param) {
PoliciesUtils.setDefaultPref(
@@ -2120,6 +2174,11 @@ export var Policies = {
param.SiteSettings,
locked
);
+ PoliciesUtils.setDefaultPref(
+ "privacy.clearOnShutdown_v2.siteSettings",
+ param.SiteSettings,
+ locked
+ );
}
if ("OfflineApps" in param) {
PoliciesUtils.setDefaultPref(
@@ -2390,6 +2449,12 @@ export var Policies = {
},
},
+ TranslateEnabled: {
+ onBeforeAddons(manager, param) {
+ setAndLockPref("browser.translations.enable", param);
+ },
+ },
+
UserMessaging: {
onBeforeAddons(manager, param) {
if ("WhatsNew" in param) {
@@ -2790,7 +2855,7 @@ function clearBlockedAboutPages() {
gBlockedAboutPages = [];
}
-function blockAboutPage(manager, feature, neededOnContentProcess = false) {
+function blockAboutPage(manager, feature) {
addChromeURLBlocker();
gBlockedAboutPages.push(feature);
@@ -2826,7 +2891,7 @@ let ChromeURLBlockPolicy = {
}
return Ci.nsIContentPolicy.ACCEPT;
},
- shouldProcess(contentLocation, loadInfo) {
+ shouldProcess() {
return Ci.nsIContentPolicy.ACCEPT;
},
classDescription: "Policy Engine Content Policy",
diff --git a/browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs b/browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs
index 393b9bb85e..80968956ac 100644
--- a/browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs
+++ b/browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs
@@ -29,6 +29,22 @@ export var PROXY_TYPES_MAP = new Map([
["autoConfig", Ci.nsIProtocolProxyService.PROXYCONFIG_PAC],
]);
+let proxyPreferences = [
+ "network.proxy.type",
+ "network.proxy.autoconfig_url",
+ "network.proxy.socks_remote_dns",
+ "signon.autologin.proxy",
+ "network.proxy.socks_version",
+ "network.proxy.no_proxies_on",
+ "network.proxy.share_proxy_settings",
+ "network.proxy.http",
+ "network.proxy.http_port",
+ "network.proxy.ssl",
+ "network.proxy.ssl_port",
+ "network.proxy.socks",
+ "network.proxy.socks_port",
+];
+
export var ProxyPolicies = {
configureProxySettings(param, setPref) {
if (param.Mode) {
@@ -105,5 +121,13 @@ export var ProxyPolicies = {
if (param.SOCKSProxy) {
setProxyHostAndPort("socks", param.SOCKSProxy);
}
+
+ // All preferences should be locked regardless of whether or not a
+ // specific value was set.
+ if (param.Locked) {
+ for (let preference of proxyPreferences) {
+ Services.prefs.lockPref(preference);
+ }
+ }
},
};
diff --git a/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs b/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
index 81f7955f27..26bae7acd9 100644
--- a/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
+++ b/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
@@ -130,10 +130,10 @@ export let WebsiteFilter = {
}
return Ci.nsIContentPolicy.ACCEPT;
},
- shouldProcess(contentLocation, loadInfo) {
+ shouldProcess() {
return Ci.nsIContentPolicy.ACCEPT;
},
- observe(subject, topic, data) {
+ observe(subject) {
try {
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
if (
diff --git a/browser/components/enterprisepolicies/schemas/policies-schema.json b/browser/components/enterprisepolicies/schemas/policies-schema.json
index a1ccaed74f..3c578f2c4b 100644
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -253,6 +253,12 @@
"DenyUrlRegexList": {
"type": "string"
},
+ "AgentName": {
+ "type": "string"
+ },
+ "ClientSignature": {
+ "type": "string"
+ },
"IsPerUser": {
"type": "boolean"
},
@@ -662,6 +668,9 @@
"items": {
"type": "string"
}
+ },
+ "temporarily_allow_weak_signatures": {
+ "type": "boolean"
}
}
}
@@ -691,6 +700,9 @@
"default_area": {
"type": "string",
"enum": ["navbar", "menupanel"]
+ },
+ "temporarily_allow_weak_signatures": {
+ "type": "boolean"
}
}
}
@@ -1422,6 +1434,10 @@
"required": ["Title", "URL"]
},
+ "TranslateEnabled": {
+ "type": "boolean"
+ },
+
"UserMessaging": {
"type": "object",
"properties": {
diff --git a/browser/components/enterprisepolicies/tests/browser/browser.toml b/browser/components/enterprisepolicies/tests/browser/browser.toml
index 25ac681e5b..0517bb6557 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser.toml
+++ b/browser/components/enterprisepolicies/tests/browser/browser.toml
@@ -117,6 +117,8 @@ https_first_disabled = true
["browser_policy_support_menu.js"]
+["browser_policy_translateenabled.js"]
+
["browser_policy_usermessaging.js"]
["browser_policy_websitefilter.js"]
diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js
index 4921464782..2fc7892c8f 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js
@@ -58,8 +58,8 @@ add_task(async function test_pageinfo_permissions() {
"xr",
];
- await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) {
- let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab");
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function () {
+ let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN, "permTab");
await BrowserTestUtils.waitForEvent(pageInfo, "load");
for (let i = 0; i < permissions.length; i++) {
diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_translateenabled.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_translateenabled.js
new file mode 100644
index 0000000000..3658cf6388
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_translateenabled.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function setup() {
+ await setupPolicyEngineWithJson({
+ policies: {
+ TranslateEnabled: false,
+ },
+ });
+});
+
+add_task(async function test_translate_pref_disabled() {
+ is(
+ Services.prefs.getBoolPref("browser.translations.enable"),
+ false,
+ "The translations pref should be disabled when the enterprise policy is active."
+ );
+});
+
+add_task(async function test_translate_button_disabled() {
+ // Since testing will apply the policy after the browser has already started,
+ // we will need to open a new window to actually see changes from the policy
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ let appMenuButton = win.document.getElementById("PanelUI-menu-button");
+ let viewShown = BrowserTestUtils.waitForEvent(
+ win.PanelUI.mainView,
+ "ViewShown"
+ );
+
+ appMenuButton.click();
+ await viewShown;
+
+ let translateSiteButton = win.document.getElementById(
+ "appMenu-translate-button"
+ );
+
+ is(
+ translateSiteButton.hidden,
+ true,
+ "The app-menu translate button should be hidden when the enterprise policy is active."
+ );
+
+ is(
+ translateSiteButton.disabled,
+ true,
+ "The app-menu translate button should be disabled when the enterprise policy is active."
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js b/browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js
index 2f68436882..929f0470da 100644
--- a/browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js
+++ b/browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js
@@ -96,7 +96,7 @@ function waitForAboutDialog() {
var domwindow = aXULWindow.docShell.domWindow;
domwindow.addEventListener("load", aboutDialogOnLoad, true);
},
- onCloseWindow: aXULWindow => {},
+ onCloseWindow: () => {},
};
Services.wm.addListener(listener);
diff --git a/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js b/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js
index 27794aabbb..585218a016 100644
--- a/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js
+++ b/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js
@@ -73,7 +73,7 @@ async function testPageBlockedByPolicy(page, policyJSON) {
async browser => {
BrowserTestUtils.startLoadingURIString(browser, page);
await BrowserTestUtils.browserLoaded(browser, false, page, true);
- await SpecialPowers.spawn(browser, [page], async function (innerPage) {
+ await SpecialPowers.spawn(browser, [page], async function () {
ok(
content.document.documentURI.startsWith(
"about:neterror?e=blockedByPolicy"
diff --git a/browser/components/enterprisepolicies/tests/browser/head.js b/browser/components/enterprisepolicies/tests/browser/head.js
index bb08173aa9..dfa01fad0e 100644
--- a/browser/components/enterprisepolicies/tests/browser/head.js
+++ b/browser/components/enterprisepolicies/tests/browser/head.js
@@ -237,7 +237,7 @@ async function testPageBlockedByPolicy(page, policyJSON) {
async browser => {
BrowserTestUtils.startLoadingURIString(browser, page);
await BrowserTestUtils.browserLoaded(browser, false, page, true);
- await SpecialPowers.spawn(browser, [page], async function (innerPage) {
+ await SpecialPowers.spawn(browser, [page], async function () {
ok(
content.document.documentURI.startsWith(
"about:neterror?e=blockedByPolicy"
diff --git a/browser/components/enterprisepolicies/tests/xpcshell/head.js b/browser/components/enterprisepolicies/tests/xpcshell/head.js
index 8b81261538..3881760ed4 100644
--- a/browser/components/enterprisepolicies/tests/xpcshell/head.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/head.js
@@ -116,7 +116,7 @@ function checkUserPref(prefName, prefValue) {
);
}
-function checkClearPref(prefName, prefValue) {
+function checkClearPref(prefName) {
equal(
Services.prefs.prefHasUserValue(prefName),
false,
diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js
index ee329a65f8..22a6269cce 100644
--- a/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js
@@ -21,7 +21,7 @@ let themeID = "policytheme@mozilla.com";
let fileURL;
-add_task(async function setup() {
+add_setup(async function setup() {
await AddonTestUtils.promiseStartupManager();
let webExtensionFile = AddonTestUtils.createTempWebExtensionFile({
@@ -34,6 +34,10 @@ add_task(async function setup() {
},
});
+ server.registerFile(
+ "/data/amosigned-sha1only.xpi",
+ do_get_file("amosigned-sha1only.xpi")
+ );
server.registerFile("/data/policy_test.xpi", webExtensionFile);
fileURL = Services.io
.newFileURI(webExtensionFile)
@@ -289,3 +293,112 @@ add_task(async function test_addon_normalinstalled_file() {
await addon.uninstall();
});
+
+add_task(async function test_allow_weak_signatures() {
+ // Make sure weak signatures are restricted.
+ const resetWeakSignaturePref =
+ AddonTestUtils.setWeakSignatureInstallAllowed(false);
+
+ const id = "amosigned-xpi@tests.mozilla.org";
+ const perAddonSettings = {
+ installation_mode: "normal_installed",
+ install_url: BASE_URL + "/amosigned-sha1only.xpi",
+ };
+
+ info(
+ "Sanity check: expect install to fail if not allowed through enterprise policy settings"
+ );
+ await Promise.all([
+ AddonTestUtils.promiseInstallEvent("onDownloadFailed"),
+ setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ [id]: { ...perAddonSettings },
+ },
+ },
+ }),
+ ]);
+ let addon = await AddonManager.getAddonByID(id);
+ equal(addon, null, "Add-on not installed");
+
+ info(
+ "Expect install to be allowed through per-addon enterprise policy settings"
+ );
+ await Promise.all([
+ AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+ setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ [id]: {
+ ...perAddonSettings,
+ temporarily_allow_weak_signatures: true,
+ },
+ },
+ },
+ }),
+ ]);
+ addon = await AddonManager.getAddonByID(id);
+ notEqual(addon, null, "Add-on not installed");
+ await addon.uninstall();
+
+ info(
+ "Expect install to be allowed through global enterprise policy settings"
+ );
+ await Promise.all([
+ AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+ setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ "*": { temporarily_allow_weak_signatures: true },
+ [id]: { ...perAddonSettings },
+ },
+ },
+ }),
+ ]);
+ addon = await AddonManager.getAddonByID(id);
+ notEqual(addon, null, "Add-on installed");
+ await addon.uninstall();
+
+ info(
+ "Expect install to fail if allowed globally but disallowed by per-addon settings"
+ );
+ await Promise.all([
+ AddonTestUtils.promiseInstallEvent("onDownloadFailed"),
+ setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ "*": { temporarily_allow_weak_signatures: true },
+ [id]: {
+ ...perAddonSettings,
+ temporarily_allow_weak_signatures: false,
+ },
+ },
+ },
+ }),
+ ]);
+ addon = await AddonManager.getAddonByID(id);
+ equal(addon, null, "Add-on not installed");
+
+ info(
+ "Expect install to be allowed through per addon setting when globally disallowed"
+ );
+ await Promise.all([
+ AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+ setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ "*": { temporarily_allow_weak_signatures: false },
+ [id]: {
+ ...perAddonSettings,
+ temporarily_allow_weak_signatures: true,
+ },
+ },
+ },
+ }),
+ ]);
+ addon = await AddonManager.getAddonByID(id);
+ notEqual(addon, null, "Add-on installed");
+ await addon.uninstall();
+
+ resetWeakSignaturePref();
+});
diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js b/browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js
index 5908b2d35c..bfed2491be 100644
--- a/browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js
@@ -12,7 +12,7 @@ const REQ_LOC_CHANGE_EVENT = "intl:requested-locales-changed";
function promiseLocaleChanged(requestedLocale) {
return new Promise(resolve => {
let localeObserver = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
switch (aTopic) {
case REQ_LOC_CHANGE_EVENT:
let reqLocs = Services.locale.requestedLocales;
@@ -26,10 +26,10 @@ function promiseLocaleChanged(requestedLocale) {
});
}
-function promiseLocaleNotChanged(requestedLocale) {
+function promiseLocaleNotChanged() {
return new Promise(resolve => {
let localeObserver = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
switch (aTopic) {
case REQ_LOC_CHANGE_EVENT:
ok(false, "Locale should not change.");
diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js b/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js
index 82caee16a7..c0952d0627 100644
--- a/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js
@@ -227,6 +227,10 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.sessions": true,
"privacy.clearOnShutdown.siteSettings": true,
"privacy.clearOnShutdown.offlineApps": true,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": true,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": true,
+ "privacy.clearOnShutdown_v2.cache": true,
+ "privacy.clearOnShutdown_v2.siteSettings": true,
},
},
@@ -244,6 +248,10 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.sessions": false,
"privacy.clearOnShutdown.siteSettings": false,
"privacy.clearOnShutdown.offlineApps": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": false,
+ "privacy.clearOnShutdown_v2.siteSettings": false,
},
},
@@ -261,6 +269,9 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.formdata": false,
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": true,
},
},
@@ -278,6 +289,9 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.formdata": false,
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": true,
+ "privacy.clearOnShutdown_v2.cache": false,
},
},
@@ -295,6 +309,9 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.formdata": false,
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": false,
},
},
@@ -312,6 +329,9 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.formdata": true,
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": false,
},
},
@@ -329,6 +349,9 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.formdata": false,
"privacy.clearOnShutdown.history": true,
"privacy.clearOnShutdown.sessions": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": true,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": false,
},
},
@@ -346,6 +369,9 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.formdata": false,
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": true,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": false,
},
},
@@ -364,6 +390,10 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
"privacy.clearOnShutdown.siteSettings": true,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": false,
+ "privacy.clearOnShutdown_v2.siteSettings": true,
},
},
@@ -382,6 +412,9 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
"privacy.clearOnShutdown.offlineApps": true,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": false,
},
},
@@ -396,6 +429,7 @@ const POLICIES_TESTS = [
lockedPrefs: {
"privacy.sanitize.sanitizeOnShutdown": true,
"privacy.clearOnShutdown.cache": true,
+ "privacy.clearOnShutdown_v2.cache": true,
},
unlockedPrefs: {
"privacy.clearOnShutdown.cookies": false,
@@ -403,6 +437,8 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.formdata": false,
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
},
},
@@ -418,12 +454,15 @@ const POLICIES_TESTS = [
"privacy.sanitize.sanitizeOnShutdown": true,
"privacy.clearOnShutdown.cache": true,
"privacy.clearOnShutdown.cookies": false,
+ "privacy.clearOnShutdown_v2.cache": true,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
},
unlockedPrefs: {
"privacy.clearOnShutdown.downloads": false,
"privacy.clearOnShutdown.formdata": false,
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
},
},
@@ -442,6 +481,9 @@ const POLICIES_TESTS = [
"privacy.clearOnShutdown.formdata": false,
"privacy.clearOnShutdown.history": false,
"privacy.clearOnShutdown.sessions": false,
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false,
+ "privacy.clearOnShutdown_v2.cookiesAndStorage": false,
+ "privacy.clearOnShutdown_v2.cache": true,
},
},
@@ -1045,6 +1087,18 @@ const POLICIES_TESTS = [
"extensions.formautofill.creditCards.enabled": false,
},
},
+
+ // POLICY: Proxy - locking if no values are set
+ {
+ policies: {
+ Proxy: {
+ Locked: true,
+ },
+ },
+ lockedPrefs: {
+ "network.proxy.type": 5,
+ },
+ },
];
add_task(async function test_policy_simple_prefs() {
diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js b/browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js
index 0d246c850c..73755b1dbc 100644
--- a/browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js
@@ -32,7 +32,7 @@ add_task(async function test_policies_sorted() {
);
checkArrayIsSorted(
Object.keys(Policies),
- "Policies.jsm is alphabetically sorted."
+ "Policies.sys.mjs is alphabetically sorted."
);
});
diff --git a/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml b/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml
index 69dd3e5103..b21e0f9022 100644
--- a/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml
+++ b/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml
@@ -2,7 +2,10 @@
skip-if = ["os == 'android'"] # bug 1730213
firefox-appdir = "browser"
head = "head.js"
-support-files = ["policytest_v0.1.xpi"]
+support-files = [
+ "policytest_v0.1.xpi",
+ "../../../../../toolkit/mozapps/extensions/test/xpinstall/amosigned-sha1only.xpi"
+]
["test_3rdparty.js"]
diff --git a/browser/components/extensions/parent/ext-browser.js b/browser/components/extensions/parent/ext-browser.js
index 7b01d15101..d2f72d4f46 100644
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -67,8 +67,12 @@ global.openOptionsPage = extension => {
return Promise.reject({ message: "No browser window available" });
}
- if (extension.manifest.options_ui.open_in_tab) {
- window.switchToTabHavingURI(extension.manifest.options_ui.page, true, {
+ const { optionsPageProperties } = extension;
+ if (!optionsPageProperties) {
+ return Promise.reject({ message: "No options page" });
+ }
+ if (optionsPageProperties.open_in_tab) {
+ window.switchToTabHavingURI(optionsPageProperties.page, true, {
triggeringPrincipal: extension.principal,
});
return Promise.resolve();
diff --git a/browser/components/extensions/parent/ext-chrome-settings-overrides.js b/browser/components/extensions/parent/ext-chrome-settings-overrides.js
index 1fbb794b51..3d1b7d363e 100644
--- a/browser/components/extensions/parent/ext-chrome-settings-overrides.js
+++ b/browser/components/extensions/parent/ext-chrome-settings-overrides.js
@@ -56,7 +56,7 @@ ChromeUtils.defineLazyGetter(this, "homepagePopup", () => {
Services.prefs.addObserver(HOMEPAGE_PREF, async function prefObserver() {
Services.prefs.removeObserver(HOMEPAGE_PREF, prefObserver);
let loaded = waitForTabLoaded(tab);
- win.BrowserHome();
+ win.BrowserCommands.home();
await loaded;
// Manually trigger an event in case this is controlled again.
popup.open();
diff --git a/browser/components/extensions/parent/ext-commands.js b/browser/components/extensions/parent/ext-commands.js
index 328f05a802..5b2b5f11b2 100644
--- a/browser/components/extensions/parent/ext-commands.js
+++ b/browser/components/extensions/parent/ext-commands.js
@@ -13,8 +13,13 @@ ChromeUtils.defineESModuleGetters(this, {
this.commands = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
onCommand({ fire }) {
+ const { extension } = this;
+ const { tabManager } = extension;
+
let listener = (eventName, commandName) => {
- fire.async(commandName);
+ let nativeTab = tabTracker.activeTab;
+ tabManager.addActiveTabPermission(nativeTab);
+ fire.async(commandName, tabManager.convert(nativeTab));
};
this.on("command", listener);
return {
diff --git a/browser/components/extensions/parent/ext-devtools-panels.js b/browser/components/extensions/parent/ext-devtools-panels.js
index 6b83ea5dbb..4b88b91eab 100644
--- a/browser/components/extensions/parent/ext-devtools-panels.js
+++ b/browser/components/extensions/parent/ext-devtools-panels.js
@@ -104,20 +104,21 @@ class BaseDevToolsPanel {
/**
* Represents an addon devtools panel in the main process.
- *
- * @param {ExtensionChildProxyContext} context
- * A devtools extension proxy context running in a main process.
- * @param {object} options
- * @param {string} options.id
- * The id of the addon devtools panel.
- * @param {string} options.icon
- * The icon of the addon devtools panel.
- * @param {string} options.title
- * The title of the addon devtools panel.
- * @param {string} options.url
- * The url of the addon devtools panel, relative to the extension base URL.
*/
class ParentDevToolsPanel extends BaseDevToolsPanel {
+ /**
+ * @param {DevToolsExtensionPageContextParent} context
+ * A devtools extension proxy context running in a main process.
+ * @param {object} panelOptions
+ * @param {string} panelOptions.id
+ * The id of the addon devtools panel.
+ * @param {string} panelOptions.icon
+ * The icon of the addon devtools panel.
+ * @param {string} panelOptions.title
+ * The title of the addon devtools panel.
+ * @param {string} panelOptions.url
+ * The url of the addon devtools panel, relative to the extension base URL.
+ */
constructor(context, panelOptions) {
super(context, panelOptions);
@@ -339,16 +340,17 @@ class DevToolsSelectionObserver extends EventEmitter {
/**
* Represents an addon devtools inspector sidebar in the main process.
- *
- * @param {ExtensionChildProxyContext} context
- * A devtools extension proxy context running in a main process.
- * @param {object} options
- * @param {string} options.id
- * The id of the addon devtools sidebar.
- * @param {string} options.title
- * The title of the addon devtools sidebar.
*/
class ParentDevToolsInspectorSidebar extends BaseDevToolsPanel {
+ /**
+ * @param {DevToolsExtensionPageContextParent} context
+ * A devtools extension proxy context running in a main process.
+ * @param {object} panelOptions
+ * @param {string} panelOptions.id
+ * The id of the addon devtools sidebar.
+ * @param {string} panelOptions.title
+ * The title of the addon devtools sidebar.
+ */
constructor(context, panelOptions) {
super(context, panelOptions);
diff --git a/browser/components/extensions/parent/ext-tabs.js b/browser/components/extensions/parent/ext-tabs.js
index 128a42439b..4b8d296d67 100644
--- a/browser/components/extensions/parent/ext-tabs.js
+++ b/browser/components/extensions/parent/ext-tabs.js
@@ -1026,7 +1026,13 @@ this.tabs = class extends ExtensionAPIPersistent {
? windowTracker.getTopWindow(context)
: windowTracker.getWindow(windowId, context);
- let tab = tabManager.wrapTab(window.gBrowser.selectedTab);
+ let tab = tabManager.getWrapper(window.gBrowser.selectedTab);
+ if (
+ !extension.hasPermission("<all_urls>") &&
+ !tab.hasActiveTabPermission
+ ) {
+ throw new ExtensionError("Missing activeTab permission");
+ }
await tabListener.awaitTabReady(tab.nativeTab);
let zoom = window.ZoomManager.getZoomForBrowser(
diff --git a/browser/components/extensions/schemas/commands.json b/browser/components/extensions/schemas/commands.json
index 19e8e122f9..30942d0aab 100644
--- a/browser/components/extensions/schemas/commands.json
+++ b/browser/components/extensions/schemas/commands.json
@@ -104,6 +104,12 @@
{
"name": "command",
"type": "string"
+ },
+ {
+ "name": "tab",
+ "$ref": "tags.Tab",
+ "optional": true,
+ "description": "Details of the $(ref:tabs.Tab) where the command was activated."
}
]
},
diff --git a/browser/components/extensions/schemas/tabs.json b/browser/components/extensions/schemas/tabs.json
index ee7cd3dd93..55fccee0b5 100644
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -1198,8 +1198,8 @@
{
"name": "captureVisibleTab",
"type": "function",
- "description": "Captures an area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[&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..570a51eeb4 100644
--- a/browser/components/extensions/test/browser/browser.toml
+++ b/browser/components/extensions/test/browser/browser.toml
@@ -46,10 +46,13 @@ support-files = [
"empty.xpi",
"../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js",
"../../../../../toolkit/components/extensions/test/mochitest/redirection.sjs",
- "../../../../../toolkit/components/reader/test/readerModeNonArticle.html",
- "../../../../../toolkit/components/reader/test/readerModeArticle.html",
+ "../../../../../toolkit/components/reader/tests/browser/readerModeNonArticle.html",
+ "../../../../../toolkit/components/reader/tests/browser/readerModeArticle.html",
+]
+skip-if = [
+ "os == 'linux' && os_version == '18.04' && asan", # Bug 1721945 - Software WebRender
+ "os == 'linux' && os_version == '18.04' && tsan", # manifest runs too long
]
-skip-if = ["os == 'linux' && os_version == '18.04' && asan"] # Bug 1721945 - Software WebRender
["browser_AMBrowserExtensionsImport.js"]
@@ -700,7 +703,6 @@ tags = "fullscreen"
["browser_toolbar_prefers_color_scheme.js"]
["browser_unified_extensions.js"]
-fail-if = ["a11y_checks"] # Bug 1854460 clicked browser may not be accessible
["browser_unified_extensions_accessibility.js"]
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
index e5d315c5d2..a55952e610 100644
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -184,7 +184,11 @@ async function runTests(options) {
is(getListStyleImage(button), details.icon, "icon URL is correct");
is(button.getAttribute("tooltiptext"), title, "image title is correct");
is(button.getAttribute("label"), title, "image label is correct");
- is(button.getAttribute("badge"), details.badge, "badge text is correct");
+ is(
+ button.getAttribute("badge") || "",
+ details.badge,
+ "badge text is correct"
+ );
is(
button.getAttribute("disabled") == "true",
!details.enabled,
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
index 8e89457904..26d1536de1 100644
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
@@ -430,9 +430,6 @@ async function browseraction_contextmenu_remove_extension_helper() {
},
useAddonManager: "temporary",
});
- let brand = Services.strings
- .createBundle("chrome://branding/locale/brand.properties")
- .GetStringFromName("brandShorterName");
let { prompt } = Services;
let promptService = {
_response: 1,
@@ -466,9 +463,6 @@ async function browseraction_contextmenu_remove_extension_helper() {
await closeChromeContextMenu(menuId, removeExtension);
let args = await confirmArgs;
is(args[1], `Remove ${name}?`);
- if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) {
- is(args[2], `Remove ${name} from ${brand}?`);
- }
is(args[4], "Remove");
return menu;
}
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js
index 98e66e6c7a..289cbf8a88 100644
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js
@@ -50,7 +50,7 @@ async function installTestAddon(addonId, unpacked = false) {
// This temporary directory is going to be removed from the
// cleanup function, but also make it unique as we do for the
// other temporary files (e.g. like getTemporaryFile as defined
- // in XPInstall.jsm).
+ // in XPIInstall.sys.mjs).
const random = Math.round(Math.random() * 36 ** 3).toString(36);
const tmpDirName = `mochitest_unpacked_addons_${random}`;
let tmpExtPath = FileUtils.getDir("TmpD", [tmpDirName]);
diff --git a/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
index b67952c03c..1c9ee9a6c1 100644
--- a/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
+++ b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
@@ -388,7 +388,7 @@ add_task(async function test_doorhanger_homepage_button() {
await ext2.startup();
let popupShown = promisePopupShown(panel);
- BrowserHome();
+ BrowserCommands.home();
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, () =>
gURLBar.value.endsWith("ext2.html")
);
@@ -410,7 +410,7 @@ add_task(async function test_doorhanger_homepage_button() {
popupShown = promisePopupShown(panel);
await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
let openHomepage = TestUtils.topicObserved("browser-open-homepage-start");
- BrowserHome();
+ BrowserCommands.home();
await openHomepage;
await popupShown;
await TestUtils.waitForCondition(
@@ -432,7 +432,7 @@ add_task(async function test_doorhanger_homepage_button() {
BrowserTestUtils.removeTab(gBrowser.selectedTab);
openHomepage = TestUtils.topicObserved("browser-open-homepage-start");
- BrowserHome();
+ BrowserCommands.home();
await openHomepage;
is(getHomePageURL(), defaultHomePage, "The homepage is set back to default");
@@ -507,7 +507,7 @@ add_task(async function test_doorhanger_new_window() {
let popupShown = promisePopupShown(panel);
await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank");
let openHomepage = TestUtils.topicObserved("browser-open-homepage-start");
- win.BrowserHome();
+ win.BrowserCommands.home();
await openHomepage;
await popupShown;
@@ -547,7 +547,7 @@ async function testHomePageWindow(options = {}) {
let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(doc);
let popupShown = options.expectPanel && promisePopupShown(panel);
- win.BrowserHome();
+ win.BrowserCommands.home();
await Promise.all([
BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser),
openHomepage,
diff --git a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
index db900f7ea4..abde8f90f7 100644
--- a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
+++ b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
@@ -226,7 +226,16 @@ add_task(async function test_user_defined_commands() {
}
function background() {
- browser.commands.onCommand.addListener(commandName => {
+ browser.commands.onCommand.addListener(async (commandName, tab) => {
+ let [expectedTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ browser.test.assertEq(
+ tab.id,
+ expectedTab.id,
+ "Expected onCommand listener to pass the current tab"
+ );
browser.test.sendMessage("oncommand", commandName);
});
browser.test.sendMessage("ready");
@@ -408,8 +417,9 @@ add_task(async function test_commands_event_page() {
},
},
background() {
- browser.commands.onCommand.addListener(name => {
+ browser.commands.onCommand.addListener((name, tab) => {
browser.test.assertEq(name, "toggle-feature", "command received");
+ browser.test.assertTrue(!!tab, "tab received");
browser.test.sendMessage("onCommand");
});
browser.test.sendMessage("ready");
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js b/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js
index 548c35399f..33ad85f268 100644
--- a/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js
@@ -8,11 +8,17 @@ function extensionScript() {
let FRAME_URL = browser.runtime.getManifest().content_scripts[0].matches[0];
// Cannot use :8888 in the manifest because of bug 1468162.
FRAME_URL = FRAME_URL.replace("mochi.test", "mochi.test:8888");
+ let FRAME_ORIGIN = new URL(FRAME_URL).origin;
browser.runtime.onConnect.addListener(port => {
browser.test.assertEq(port.sender.tab, undefined, "Sender is not a tab");
browser.test.assertEq(port.sender.frameId, undefined, "frameId unset");
browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL");
+ browser.test.assertEq(
+ port.sender.origin,
+ FRAME_ORIGIN,
+ "Expected sender origin"
+ );
port.onMessage.addListener(msg => {
browser.test.assertEq("pong", msg, "Reply from content script");
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js b/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js
index f751c3c202..08f19013c4 100644
--- a/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js
@@ -2,12 +2,16 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
+const FILE_URL = Services.io.newFileURI(
+ new FileUtils.File(getTestFilePath("file_dummy.html"))
+).spec;
+
add_task(async function test_sender_url() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
- matches: ["http://mochi.test/*"],
+ matches: ["http://mochi.test/*", "file:///*"],
run_at: "document_start",
js: ["script.js"],
},
@@ -18,6 +22,7 @@ add_task(async function test_sender_url() {
browser.runtime.onMessage.addListener((msg, sender) => {
browser.test.log("Message received.");
browser.test.sendMessage("sender.url", sender.url);
+ browser.test.sendMessage("sender.origin", sender.origin);
});
},
@@ -53,6 +58,9 @@ add_task(async function test_sender_url() {
let url = await extension.awaitMessage("sender.url");
is(url, image, `Correct sender.url: ${url}`);
+ let origin = await extension.awaitMessage("sender.origin");
+ is(origin, "http://mochi.test:8888", `Correct sender.origin: ${origin}`);
+
let wentBack = awaitNewTab();
await browser.goBack();
await wentBack;
@@ -60,6 +68,17 @@ add_task(async function test_sender_url() {
await browser.goForward();
url = await extension.awaitMessage("sender.url");
is(url, image, `Correct sender.url: ${url}`);
+
+ origin = await extension.awaitMessage("sender.origin");
+ is(origin, "http://mochi.test:8888", `Correct sender.origin: ${origin}`);
+ });
+
+ await BrowserTestUtils.withNewTab(FILE_URL, async () => {
+ let url = await extension.awaitMessage("sender.url");
+ ok(url.endsWith("/file_dummy.html"), `Correct sender.url: ${url}`);
+
+ let origin = await extension.awaitMessage("sender.origin");
+ is(origin, "null", `Correct sender.origin: ${origin}`);
});
await extension.unload();
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
index 30d4d528b2..cb81d9a93b 100644
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
@@ -269,7 +269,7 @@ add_task(async function test_manifest_without_icons() {
let items = menu.getElementsByAttribute("label", "first item");
is(items.length, 1, "Found first item");
// manifest.json does not declare icons, so the root menu item shouldn't have an icon either.
- is(items[0].getAttribute("image"), "", "Root menu must not have an icon");
+ is(items[0].getAttribute("image"), null, "Root menu must not have an icon");
await closeExtensionContextMenu(items[0]);
await extension.awaitMessage("added-second-item");
@@ -281,7 +281,7 @@ add_task(async function test_manifest_without_icons() {
is(items.length, 1, "Auto-generated root item exists");
is(
items[0].getAttribute("image"),
- "",
+ null,
"Auto-generated menu root must not have an icon"
);
@@ -464,7 +464,7 @@ add_task(async function test_child_icon_update() {
contextMenuChild2 = contextMenu.getElementsByAttribute("label", "child2")[0];
is(
contextMenuChild2.getAttribute("image"),
- "",
+ null,
"Second child should not have an icon"
);
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js b/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js
index 0004f60853..014b6dddf2 100644
--- a/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js
@@ -26,7 +26,7 @@ add_task(async function test_onCreated_active() {
await extension.startup();
await extension.awaitMessage("ready");
- BrowserOpenTab();
+ BrowserCommands.openTab();
let tab = await extension.awaitMessage("onCreated");
is(true, tab.active, "Tab should be active");
diff --git a/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
index 988f44bd5d..a3ccc5a521 100644
--- a/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
+++ b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
@@ -45,7 +45,7 @@ async function promiseNewTab(expectUrl = AboutNewTab.newTabURL, win = window) {
`Should open correct new tab url ${expectUrl}.`
);
- win.BrowserOpenTab();
+ win.BrowserCommands.openTab();
const newTabCreatedPromise = newTabStartPromise;
const browser = await newTabCreatedPromise;
await newtabShown;
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions.js b/browser/components/extensions/test/browser/browser_unified_extensions.js
index 7ab7753c0e..2c65f47c4e 100644
--- a/browser/components/extensions/test/browser/browser_unified_extensions.js
+++ b/browser/components/extensions/test/browser/browser_unified_extensions.js
@@ -1222,7 +1222,11 @@ add_task(async function test_hover_message_when_button_updates_itself() {
// Move cursor to the center of the entire browser UI to avoid issues with
// other focus/hover checks. We do this to avoid intermittent test failures.
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive content of the page.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
EventUtils.synthesizeMouseAtCenter(document.documentElement, {});
+ AccessibilityUtils.resetEnv();
await extension.unload();
});
diff --git a/browser/components/firefoxview/HistoryController.mjs b/browser/components/firefoxview/HistoryController.mjs
new file mode 100644
index 0000000000..d2bda5cec2
--- /dev/null
+++ b/browser/components/firefoxview/HistoryController.mjs
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FirefoxViewPlacesQuery:
+ "resource:///modules/firefox-view-places-query.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+let XPCOMUtils = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+).XPCOMUtils;
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "maxRowsPref",
+ "browser.firefox-view.max-history-rows",
+ -1
+);
+
+const HISTORY_MAP_L10N_IDS = {
+ sidebar: {
+ "history-date-today": "sidebar-history-date-today",
+ "history-date-yesterday": "sidebar-history-date-yesterday",
+ "history-date-this-month": "sidebar-history-date-this-month",
+ "history-date-prev-month": "sidebar-history-date-prev-month",
+ },
+ firefoxview: {
+ "history-date-today": "firefoxview-history-date-today",
+ "history-date-yesterday": "firefoxview-history-date-yesterday",
+ "history-date-this-month": "firefoxview-history-date-this-month",
+ "history-date-prev-month": "firefoxview-history-date-prev-month",
+ },
+};
+
+export class HistoryController {
+ host;
+ allHistoryItems;
+ historyMapByDate;
+ historyMapBySite;
+ searchQuery;
+ searchResults;
+ sortOption;
+
+ constructor(host, options) {
+ this.allHistoryItems = new Map();
+ this.historyMapByDate = [];
+ this.historyMapBySite = [];
+ this.placesQuery = new lazy.FirefoxViewPlacesQuery();
+ this.searchQuery = "";
+ this.searchResults = null;
+ this.sortOption = "date";
+ this.searchResultsLimit = options?.searchResultsLimit || 300;
+ this.component = HISTORY_MAP_L10N_IDS?.[options?.component]
+ ? options?.component
+ : "firefoxview";
+ this.host = host;
+
+ host.addController(this);
+ }
+
+ async hostConnected() {
+ this.placesQuery.observeHistory(data => this.updateAllHistoryItems(data));
+ await this.updateHistoryData();
+ this.createHistoryMaps();
+ }
+
+ hostDisconnected() {
+ this.placesQuery.close();
+ }
+
+ deleteFromHistory() {
+ lazy.PlacesUtils.history.remove(this.host.triggerNode.url);
+ }
+
+ async onSearchQuery(e) {
+ this.searchQuery = e.detail.query;
+ await this.updateSearchResults();
+ this.host.requestUpdate();
+ }
+
+ async onChangeSortOption(e) {
+ this.sortOption = e.target.value;
+ await this.updateHistoryData();
+ await this.updateSearchResults();
+ this.host.requestUpdate();
+ }
+
+ async updateHistoryData() {
+ this.allHistoryItems = await this.placesQuery.getHistory({
+ daysOld: 60,
+ limit: lazy.maxRowsPref,
+ sortBy: this.sortOption,
+ });
+ }
+
+ async updateAllHistoryItems(allHistoryItems) {
+ if (allHistoryItems) {
+ this.allHistoryItems = allHistoryItems;
+ } else {
+ await this.updateHistoryData();
+ }
+ this.resetHistoryMaps();
+ this.host.requestUpdate();
+ await this.updateSearchResults();
+ }
+
+ async updateSearchResults() {
+ if (this.searchQuery) {
+ try {
+ this.searchResults = await this.placesQuery.searchHistory(
+ this.searchQuery,
+ this.searchResultsLimit
+ );
+ } catch (e) {
+ // Connection interrupted, ignore.
+ }
+ } else {
+ this.searchResults = null;
+ }
+ }
+
+ resetHistoryMaps() {
+ this.historyMapByDate = [];
+ this.historyMapBySite = [];
+ }
+
+ createHistoryMaps() {
+ if (!this.historyMapByDate.length) {
+ const {
+ visitsFromToday,
+ visitsFromYesterday,
+ visitsByDay,
+ visitsByMonth,
+ } = this.placesQuery;
+
+ // Add visits from today and yesterday.
+ if (visitsFromToday.length) {
+ this.historyMapByDate.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"],
+ items: visitsFromToday,
+ });
+ }
+ if (visitsFromYesterday.length) {
+ this.historyMapByDate.push({
+ l10nId:
+ HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"],
+ items: visitsFromYesterday,
+ });
+ }
+
+ // Add visits from this month, grouped by day.
+ visitsByDay.forEach(visits => {
+ this.historyMapByDate.push({
+ l10nId:
+ HISTORY_MAP_L10N_IDS[this.component]["history-date-this-month"],
+ items: visits,
+ });
+ });
+
+ // Add visits from previous months, grouped by month.
+ visitsByMonth.forEach(visits => {
+ this.historyMapByDate.push({
+ l10nId:
+ HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"],
+ items: visits,
+ });
+ });
+ } else if (
+ this.sortOption === "site" &&
+ !this.historyMapBySite.length &&
+ this.component === "firefoxview"
+ ) {
+ this.historyMapBySite = Array.from(
+ this.allHistoryItems.entries(),
+ ([domain, items]) => ({
+ domain,
+ items,
+ l10nId: domain ? null : "firefoxview-history-site-localhost",
+ })
+ ).sort((a, b) => a.domain.localeCompare(b.domain));
+ }
+ this.host.requestUpdate();
+ }
+}
diff --git a/browser/components/firefoxview/OpenTabs.sys.mjs b/browser/components/firefoxview/OpenTabs.sys.mjs
index 0771bf9e65..6d67ca44cc 100644
--- a/browser/components/firefoxview/OpenTabs.sys.mjs
+++ b/browser/components/firefoxview/OpenTabs.sys.mjs
@@ -33,6 +33,7 @@ const TAB_CHANGE_EVENTS = Object.freeze([
]);
const TAB_RECENCY_CHANGE_EVENTS = Object.freeze([
"activate",
+ "sizemodechange",
"TabAttrModified",
"TabClose",
"TabOpen",
@@ -75,6 +76,10 @@ class OpenTabsTarget extends EventTarget {
TabChange: new Set(),
TabRecencyChange: new Set(),
};
+ #sourceEventsByType = {
+ TabChange: new Set(),
+ TabRecencyChange: new Set(),
+ };
#dispatchChangesTask;
#started = false;
#watchedWindows = new Set();
@@ -143,7 +148,7 @@ class OpenTabsTarget extends EventTarget {
windowList.map(win => win.delayedStartupPromise)
).then(() => {
// re-filter the list as properties might have changed in the interim
- return windowList.filter(win => this.includeWindowFilter);
+ return windowList.filter(() => this.includeWindowFilter);
});
}
@@ -223,6 +228,9 @@ class OpenTabsTarget extends EventTarget {
for (let changedWindows of Object.values(this.#changedWindowsByType)) {
changedWindows.clear();
}
+ for (let sourceEvents of Object.values(this.#sourceEventsByType)) {
+ sourceEvents.clear();
+ }
this.#watchedWindows.clear();
this.#dispatchChangesTask?.disarm();
}
@@ -245,9 +253,16 @@ class OpenTabsTarget extends EventTarget {
tabContainer.addEventListener("TabUnpinned", this);
tabContainer.addEventListener("TabSelect", this);
win.addEventListener("activate", this);
+ win.addEventListener("sizemodechange", this);
- this.#scheduleEventDispatch("TabChange", {});
- this.#scheduleEventDispatch("TabRecencyChange", {});
+ this.#scheduleEventDispatch("TabChange", {
+ sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: "watchWindow",
+ });
+ this.#scheduleEventDispatch("TabRecencyChange", {
+ sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: "watchWindow",
+ });
}
/**
@@ -270,9 +285,16 @@ class OpenTabsTarget extends EventTarget {
tabContainer.removeEventListener("TabSelect", this);
tabContainer.removeEventListener("TabUnpinned", this);
win.removeEventListener("activate", this);
+ win.removeEventListener("sizemodechange", this);
- this.#scheduleEventDispatch("TabChange", {});
- this.#scheduleEventDispatch("TabRecencyChange", {});
+ this.#scheduleEventDispatch("TabChange", {
+ sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: "unwatchWindow",
+ });
+ this.#scheduleEventDispatch("TabRecencyChange", {
+ sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: "unwatchWindow",
+ });
}
}
@@ -281,11 +303,12 @@ class OpenTabsTarget extends EventTarget {
* Repeated calls within approx 16ms will be consolidated
* into one event dispatch.
*/
- #scheduleEventDispatch(eventType, { sourceWindowId } = {}) {
+ #scheduleEventDispatch(eventType, { sourceWindowId, sourceEvent } = {}) {
if (!this.haveListenersForEvent(eventType)) {
return;
}
+ this.#sourceEventsByType[eventType].add(sourceEvent);
this.#changedWindowsByType[eventType].add(sourceWindowId);
// Queue up an event dispatch - we use a deferred task to make this less noisy by
// consolidating multiple change events into one.
@@ -302,16 +325,18 @@ class OpenTabsTarget extends EventTarget {
for (let [eventType, changedWindowIds] of Object.entries(
this.#changedWindowsByType
)) {
+ let sourceEvents = this.#sourceEventsByType[eventType];
if (this.haveListenersForEvent(eventType) && changedWindowIds.size) {
- this.dispatchEvent(
- new CustomEvent(eventType, {
- detail: {
- windowIds: [...changedWindowIds],
- },
- })
- );
+ let changeEvent = new CustomEvent(eventType, {
+ detail: {
+ windowIds: [...changedWindowIds],
+ sourceEvents: [...sourceEvents],
+ },
+ });
+ this.dispatchEvent(changeEvent);
changedWindowIds.clear();
}
+ sourceEvents?.clear();
}
}
@@ -362,11 +387,13 @@ class OpenTabsTarget extends EventTarget {
if (TAB_RECENCY_CHANGE_EVENTS.includes(type)) {
this.#scheduleEventDispatch("TabRecencyChange", {
sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: type,
});
}
if (TAB_CHANGE_EVENTS.includes(type)) {
this.#scheduleEventDispatch("TabChange", {
sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: type,
});
}
}
@@ -377,7 +404,7 @@ const gExclusiveWindows = new (class {
constructor() {
Services.obs.addObserver(this, "domwindowclosed");
}
- observe(subject, topic, data) {
+ observe(subject) {
let win = subject;
let winTarget = this.perWindowInstances.get(win);
if (winTarget) {
diff --git a/browser/components/firefoxview/SyncedTabsController.sys.mjs b/browser/components/firefoxview/SyncedTabsController.sys.mjs
new file mode 100644
index 0000000000..6ab8249bfe
--- /dev/null
+++ b/browser/components/firefoxview/SyncedTabsController.sys.mjs
@@ -0,0 +1,333 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
+ SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
+});
+
+import { SyncedTabsErrorHandler } from "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs";
+import { TabsSetupFlowManager } from "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs";
+import { searchTabList } from "chrome://browser/content/firefoxview/helpers.mjs";
+
+const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
+const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
+
+/**
+ * The controller for synced tabs components.
+ *
+ * @implements {ReactiveController}
+ */
+export class SyncedTabsController {
+ /**
+ * @type {boolean}
+ */
+ contextMenu;
+ currentSetupStateIndex = -1;
+ currentSyncedTabs = [];
+ devices = [];
+ /**
+ * The current error state as determined by `SyncedTabsErrorHandler`.
+ *
+ * @type {number}
+ */
+ errorState = null;
+ /**
+ * Component associated with this controller.
+ *
+ * @type {ReactiveControllerHost}
+ */
+ host;
+ /**
+ * @type {Function}
+ */
+ pairDeviceCallback;
+ searchQuery = "";
+ /**
+ * @type {Function}
+ */
+ signupCallback;
+
+ /**
+ * Construct a new SyncedTabsController.
+ *
+ * @param {ReactiveControllerHost} host
+ * @param {object} options
+ * @param {boolean} [options.contextMenu]
+ * Whether synced tab items have a secondary context menu.
+ * @param {Function} [options.pairDeviceCallback]
+ * The function to call when the pair device window is opened.
+ * @param {Function} [options.signupCallback]
+ * The function to call when the signup window is opened.
+ */
+ constructor(host, { contextMenu, pairDeviceCallback, signupCallback } = {}) {
+ this.contextMenu = contextMenu;
+ this.pairDeviceCallback = pairDeviceCallback;
+ this.signupCallback = signupCallback;
+ this.observe = this.observe.bind(this);
+ this.host = host;
+ this.host.addController(this);
+ }
+
+ hostConnected() {
+ this.host.addEventListener("click", this);
+ }
+
+ hostDisconnected() {
+ this.host.removeEventListener("click", this);
+ }
+
+ addSyncObservers() {
+ Services.obs.addObserver(this.observe, SYNCED_TABS_CHANGED);
+ Services.obs.addObserver(this.observe, TOPIC_SETUPSTATE_CHANGED);
+ }
+
+ removeSyncObservers() {
+ Services.obs.removeObserver(this.observe, SYNCED_TABS_CHANGED);
+ Services.obs.removeObserver(this.observe, TOPIC_SETUPSTATE_CHANGED);
+ }
+
+ handleEvent(event) {
+ if (event.type == "click" && event.target.dataset.action) {
+ const { ErrorType } = SyncedTabsErrorHandler;
+ switch (event.target.dataset.action) {
+ case `${ErrorType.SYNC_ERROR}`:
+ case `${ErrorType.NETWORK_OFFLINE}`:
+ case `${ErrorType.PASSWORD_LOCKED}`: {
+ TabsSetupFlowManager.tryToClearError();
+ break;
+ }
+ case `${ErrorType.SIGNED_OUT}`:
+ case "sign-in": {
+ TabsSetupFlowManager.openFxASignup(event.target.ownerGlobal);
+ this.signupCallback?.();
+ break;
+ }
+ case "add-device": {
+ TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal);
+ this.pairDeviceCallback?.();
+ break;
+ }
+ case "sync-tabs-disabled": {
+ TabsSetupFlowManager.syncOpenTabs(event.target);
+ break;
+ }
+ case `${ErrorType.SYNC_DISCONNECTED}`: {
+ const win = event.target.ownerGlobal;
+ const { switchToTabHavingURI } =
+ win.docShell.chromeEventHandler.ownerGlobal;
+ switchToTabHavingURI(
+ "about:preferences?action=choose-what-to-sync#sync",
+ true,
+ {}
+ );
+ break;
+ }
+ }
+ }
+ }
+
+ async observe(_, topic, errorState) {
+ if (topic == TOPIC_SETUPSTATE_CHANGED) {
+ await this.updateStates(errorState);
+ }
+ if (topic == SYNCED_TABS_CHANGED) {
+ await this.getSyncedTabData();
+ }
+ }
+
+ async updateStates(errorState) {
+ let stateIndex = TabsSetupFlowManager.uiStateIndex;
+ errorState = errorState || SyncedTabsErrorHandler.getErrorType();
+
+ if (stateIndex == 4 && this.currentSetupStateIndex !== stateIndex) {
+ // trigger an initial request for the synced tabs list
+ await this.getSyncedTabData();
+ }
+
+ this.currentSetupStateIndex = stateIndex;
+ this.errorState = errorState;
+ this.host.requestUpdate();
+ }
+
+ actionMappings = {
+ "sign-in": {
+ header: "firefoxview-syncedtabs-signin-header",
+ description: "firefoxview-syncedtabs-signin-description",
+ buttonLabel: "firefoxview-syncedtabs-signin-primarybutton",
+ },
+ "add-device": {
+ header: "firefoxview-syncedtabs-adddevice-header",
+ description: "firefoxview-syncedtabs-adddevice-description",
+ buttonLabel: "firefoxview-syncedtabs-adddevice-primarybutton",
+ descriptionLink: {
+ name: "url",
+ url: "https://support.mozilla.org/kb/how-do-i-set-sync-my-computer#w_connect-additional-devices-to-sync",
+ },
+ },
+ "sync-tabs-disabled": {
+ header: "firefoxview-syncedtabs-synctabs-header",
+ description: "firefoxview-syncedtabs-synctabs-description",
+ buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton",
+ },
+ loading: {
+ header: "firefoxview-syncedtabs-loading-header",
+ description: "firefoxview-syncedtabs-loading-description",
+ },
+ };
+
+ #getMessageCardForState({ error = false, action, errorState }) {
+ errorState = errorState || this.errorState;
+ let header,
+ description,
+ descriptionLink,
+ buttonLabel,
+ headerIconUrl,
+ mainImageUrl;
+ let descriptionArray;
+ if (error) {
+ let link;
+ ({ header, description, link, buttonLabel } =
+ SyncedTabsErrorHandler.getFluentStringsForErrorType(errorState));
+ action = `${errorState}`;
+ headerIconUrl = "chrome://global/skin/icons/info-filled.svg";
+ mainImageUrl =
+ "chrome://browser/content/firefoxview/synced-tabs-error.svg";
+ descriptionArray = [description];
+ if (errorState == "password-locked") {
+ descriptionLink = {};
+ // This is ugly, but we need to special case this link so we can
+ // coexist with the old view.
+ descriptionArray.push("firefoxview-syncedtab-password-locked-link");
+ descriptionLink.name = "syncedtab-password-locked-link";
+ descriptionLink.url = link.href;
+ }
+ } else {
+ header = this.actionMappings[action].header;
+ description = this.actionMappings[action].description;
+ buttonLabel = this.actionMappings[action].buttonLabel;
+ descriptionLink = this.actionMappings[action].descriptionLink;
+ mainImageUrl =
+ "chrome://browser/content/firefoxview/synced-tabs-error.svg";
+ descriptionArray = [description];
+ }
+ return {
+ action,
+ buttonLabel,
+ descriptionArray,
+ descriptionLink,
+ error,
+ header,
+ headerIconUrl,
+ mainImageUrl,
+ };
+ }
+
+ getRenderInfo() {
+ let renderInfo = {};
+ for (let tab of this.currentSyncedTabs) {
+ if (!(tab.client in renderInfo)) {
+ renderInfo[tab.client] = {
+ name: tab.device,
+ deviceType: tab.deviceType,
+ tabs: [],
+ };
+ }
+ renderInfo[tab.client].tabs.push(tab);
+ }
+
+ // Add devices without tabs
+ for (let device of this.devices) {
+ if (!(device.id in renderInfo)) {
+ renderInfo[device.id] = {
+ name: device.name,
+ deviceType: device.clientType,
+ tabs: [],
+ };
+ }
+ }
+
+ for (let id in renderInfo) {
+ renderInfo[id].tabItems = this.searchQuery
+ ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs))
+ : this.getTabItems(renderInfo[id].tabs);
+ }
+ return renderInfo;
+ }
+
+ getMessageCard() {
+ switch (this.currentSetupStateIndex) {
+ case 0 /* error-state */:
+ if (this.errorState) {
+ return this.#getMessageCardForState({ error: true });
+ }
+ return this.#getMessageCardForState({ action: "loading" });
+ case 1 /* not-signed-in */:
+ if (Services.prefs.prefHasUserValue("services.sync.lastversion")) {
+ // If this pref is set, the user has signed out of sync.
+ // This path is also taken if we are disconnected from sync. See bug 1784055
+ return this.#getMessageCardForState({
+ error: true,
+ errorState: "signed-out",
+ });
+ }
+ return this.#getMessageCardForState({ action: "sign-in" });
+ case 2 /* connect-secondary-device*/:
+ return this.#getMessageCardForState({ action: "add-device" });
+ case 3 /* disabled-tab-sync */:
+ return this.#getMessageCardForState({ action: "sync-tabs-disabled" });
+ case 4 /* synced-tabs-loaded*/:
+ // There seems to be an edge case where sync says everything worked
+ // fine but we have no devices.
+ if (!this.devices.length) {
+ return this.#getMessageCardForState({ action: "add-device" });
+ }
+ }
+ return null;
+ }
+
+ getTabItems(tabs) {
+ return tabs?.map(tab => ({
+ icon: tab.icon,
+ title: tab.title,
+ time: tab.lastUsed * 1000,
+ url: tab.url,
+ primaryL10nId: "firefoxview-tabs-list-tab-button",
+ primaryL10nArgs: JSON.stringify({ targetURI: tab.url }),
+ secondaryL10nId: this.contextMenu
+ ? "fxviewtabrow-options-menu-button"
+ : undefined,
+ secondaryL10nArgs: this.contextMenu
+ ? JSON.stringify({ tabTitle: tab.title })
+ : undefined,
+ }));
+ }
+
+ updateTabsList(syncedTabs) {
+ if (!syncedTabs.length) {
+ this.currentSyncedTabs = syncedTabs;
+ }
+
+ const tabsToRender = syncedTabs;
+
+ // Return early if new tabs are the same as previous ones
+ if (lazy.ObjectUtils.deepEqual(tabsToRender, this.currentSyncedTabs)) {
+ return;
+ }
+
+ this.currentSyncedTabs = tabsToRender;
+ this.host.requestUpdate();
+ }
+
+ async getSyncedTabData() {
+ this.devices = await lazy.SyncedTabs.getTabClients();
+ let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, {
+ removeAllDupes: false,
+ removeDeviceDupes: true,
+ });
+
+ this.updateTabsList(tabs);
+ }
+}
diff --git a/browser/components/firefoxview/card-container.css b/browser/components/firefoxview/card-container.css
index 953437bec1..0c6a81899b 100644
--- a/browser/components/firefoxview/card-container.css
+++ b/browser/components/firefoxview/card-container.css
@@ -14,9 +14,9 @@
}
}
-@media (prefers-contrast) {
+@media (forced-colors) or (prefers-contrast) {
.card-container {
- border: 1px solid CanvasText;
+ border: 1px solid var(--fxview-border);
}
}
@@ -83,7 +83,7 @@
background-color: var(--fxview-element-background-hover);
}
-@media (prefers-contrast) {
+@media (forced-colors) {
.chevron-icon {
border: 1px solid ButtonText;
color: ButtonText;
diff --git a/browser/components/firefoxview/card-container.mjs b/browser/components/firefoxview/card-container.mjs
index b58f42204a..1755d97555 100644
--- a/browser/components/firefoxview/card-container.mjs
+++ b/browser/components/firefoxview/card-container.mjs
@@ -118,7 +118,7 @@ class CardContainer extends MozLitElement {
}
updateTabLists() {
- let tabLists = this.querySelectorAll("fxview-tab-list");
+ let tabLists = this.querySelectorAll("fxview-tab-list, opentabs-tab-list");
if (tabLists) {
tabLists.forEach(tabList => {
tabList.updatesPaused = !this.visible || !this.isExpanded;
diff --git a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
index 4c43eea1b6..e1c999d89c 100644
--- a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
+++ b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
@@ -591,12 +591,6 @@ export const TabsSetupFlowManager = new (class {
);
this.didFxaTabOpen = true;
openTabInWindow(window, url, true);
- Services.telemetry.recordEvent(
- "firefoxview_next",
- "fxa_continue",
- "sync",
- null
- );
}
async openFxAPairDevice(window) {
@@ -605,18 +599,9 @@ export const TabsSetupFlowManager = new (class {
});
this.didFxaTabOpen = true;
openTabInWindow(window, url, true);
- Services.telemetry.recordEvent(
- "firefoxview_next",
- "fxa_mobile",
- "sync",
- null,
- {
- has_devices: this.secondaryDeviceConnected.toString(),
- }
- );
}
- syncOpenTabs(containerElem) {
+ syncOpenTabs() {
// Flip the pref on.
// The observer should trigger re-evaluating state and advance to next step
Services.prefs.setBoolPref(SYNC_TABS_PREF, true);
diff --git a/browser/components/firefoxview/firefoxview.css b/browser/components/firefoxview/firefoxview.css
index 6811ca54c4..a91c90c39e 100644
--- a/browser/components/firefoxview/firefoxview.css
+++ b/browser/components/firefoxview/firefoxview.css
@@ -31,6 +31,17 @@
--newtab-background-color: #F9F9FB;
--fxview-card-header-font-weight: 500;
+
+ /* Make the attention dot color match the browser UI on Linux, and on HCM
+ * with a lightweight theme. */
+ &[lwtheme] {
+ --attention-dot-color: light-dark(#2ac3a2, #54ffbd);
+ }
+ @media (-moz-platform: linux) {
+ &:not([lwtheme]) {
+ --attention-dot-color: AccentColor;
+ }
+ }
}
@media (prefers-color-scheme: dark) {
@@ -47,7 +58,7 @@
}
}
-@media (prefers-contrast) {
+@media (forced-colors) {
:root {
--fxview-element-background-hover: ButtonText;
--fxview-element-background-active: ButtonText;
@@ -59,6 +70,12 @@
}
}
+@media (prefers-contrast) {
+ :root {
+ --fxview-border: var(--border-color);
+ }
+}
+
@media (max-width: 52rem) {
:root {
--fxview-sidebar-width: 82px;
diff --git a/browser/components/firefoxview/firefoxview.html b/browser/components/firefoxview/firefoxview.html
index 6fa0f59a8f..5bffb5a1d8 100644
--- a/browser/components/firefoxview/firefoxview.html
+++ b/browser/components/firefoxview/firefoxview.html
@@ -72,6 +72,7 @@
>
</moz-page-nav-button>
<moz-page-nav-button
+ class="sync-ui-item"
view="syncedtabs"
data-l10n-id="firefoxview-synced-tabs-nav"
iconSrc="chrome://browser/content/firefoxview/view-syncedtabs.svg"
@@ -95,7 +96,10 @@
<view-recentlyclosed slot="recentlyclosed"></view-recentlyclosed>
</div>
<div>
- <view-syncedtabs slot="syncedtabs"></view-syncedtabs>
+ <view-syncedtabs
+ class="sync-ui-item"
+ slot="syncedtabs"
+ ></view-syncedtabs>
</div>
</view-recentbrowsing>
<view-history name="history" type="page"></view-history>
@@ -104,7 +108,11 @@
name="recentlyclosed"
type="page"
></view-recentlyclosed>
- <view-syncedtabs name="syncedtabs" type="page"></view-syncedtabs>
+ <view-syncedtabs
+ class="sync-ui-item"
+ name="syncedtabs"
+ type="page"
+ ></view-syncedtabs>
</named-deck>
</div>
</main>
diff --git a/browser/components/firefoxview/firefoxview.mjs b/browser/components/firefoxview/firefoxview.mjs
index 3e61482cc0..e31536bc8b 100644
--- a/browser/components/firefoxview/firefoxview.mjs
+++ b/browser/components/firefoxview/firefoxview.mjs
@@ -80,6 +80,16 @@ async function updateSearchKeyboardShortcut() {
searchKeyboardShortcut = key.toLocaleLowerCase();
}
+function updateSyncVisibility() {
+ const syncEnabled = Services.prefs.getBoolPref(
+ "identity.fxaccounts.enabled",
+ false
+ );
+ for (const el of document.querySelectorAll(".sync-ui-item")) {
+ el.hidden = !syncEnabled;
+ }
+}
+
window.addEventListener("DOMContentLoaded", async () => {
recordEnteredTelemetry();
@@ -106,6 +116,7 @@ window.addEventListener("DOMContentLoaded", async () => {
onViewsDeckViewChange();
await updateSearchTextboxSize();
await updateSearchKeyboardShortcut();
+ updateSyncVisibility();
if (Cu.isInAutomation) {
Services.obs.notifyObservers(null, "firefoxview-entered");
@@ -150,12 +161,17 @@ window.addEventListener(
document.body.textContent = "";
topChromeWindow.removeEventListener("command", onCommand);
Services.obs.removeObserver(onLocalesChanged, "intl:app-locales-changed");
+ Services.prefs.removeObserver(
+ "identity.fxaccounts.enabled",
+ updateSyncVisibility
+ );
},
{ once: true }
);
topChromeWindow.addEventListener("command", onCommand);
Services.obs.addObserver(onLocalesChanged, "intl:app-locales-changed");
+Services.prefs.addObserver("identity.fxaccounts.enabled", updateSyncVisibility);
function onCommand(e) {
if (document.hidden || !e.target.closest("#contentAreaContextMenu")) {
diff --git a/browser/components/firefoxview/fxview-empty-state.css b/browser/components/firefoxview/fxview-empty-state.css
index 80b4099e6a..8c0d08c1f8 100644
--- a/browser/components/firefoxview/fxview-empty-state.css
+++ b/browser/components/firefoxview/fxview-empty-state.css
@@ -93,7 +93,7 @@
img.greyscale {
filter: grayscale(100%);
- @media not (prefers-contrast) {
+ @media not (forced-colors) {
opacity: 0.5;
}
}
diff --git a/browser/components/firefoxview/fxview-tab-list.css b/browser/components/firefoxview/fxview-tab-list.css
index 5a4bff023a..f0881d8ce8 100644
--- a/browser/components/firefoxview/fxview-tab-list.css
+++ b/browser/components/firefoxview/fxview-tab-list.css
@@ -9,35 +9,21 @@
.fxview-tab-list {
display: grid;
- grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content;
+ grid-template-columns: min-content 3fr 2fr 1fr 1fr min-content;
gap: var(--space-xsmall);
- &.pinned {
- display: flex;
- flex-wrap: wrap;
-
- > virtual-list {
- display: block;
- }
-
- > fxview-tab-row {
- display: block;
- margin-block-end: var(--space-xsmall);
- }
- }
-
:host([compactRows]) & {
- grid-template-columns: min-content 1fr min-content min-content min-content;
+ grid-template-columns: min-content 1fr min-content min-content;
}
}
virtual-list {
display: grid;
- grid-column: span 9;
+ grid-column: span 7;
grid-template-columns: subgrid;
.top-padding,
.bottom-padding {
- grid-column: span 9;
+ grid-column: span 7;
}
}
diff --git a/browser/components/firefoxview/fxview-tab-list.mjs b/browser/components/firefoxview/fxview-tab-list.mjs
index 978ab79724..57181e3bea 100644
--- a/browser/components/firefoxview/fxview-tab-list.mjs
+++ b/browser/components/firefoxview/fxview-tab-list.mjs
@@ -12,6 +12,8 @@ import {
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
import { escapeRegExp } from "./helpers.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
const NOW_THRESHOLD_MS = 91000;
const FXVIEW_ROW_HEIGHT_PX = 32;
@@ -45,13 +47,13 @@ if (!window.IS_STORYBOOK) {
* @property {string} dateTimeFormat - Expected format for date and/or time
* @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required
* @property {number} maxTabsLength - The max number of tabs for the list
- * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view
* @property {Array} tabItems - Items to show in the tab list
* @property {string} searchQuery - The query string to highlight, if provided.
+ * @property {string} searchInProgress - Whether a search has been initiated.
* @property {string} secondaryActionClass - The class used to style the secondary action element
* @property {string} tertiaryActionClass - The class used to style the tertiary action element
*/
-export default class FxviewTabList extends MozLitElement {
+export class FxviewTabListBase extends MozLitElement {
constructor() {
super();
window.MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
@@ -62,10 +64,8 @@ export default class FxviewTabList extends MozLitElement {
this.dateTimeFormat = "relative";
this.maxTabsLength = 25;
this.tabItems = [];
- this.pinnedTabs = [];
- this.pinnedTabsGridView = false;
- this.unpinnedTabs = [];
this.compactRows = false;
+ this.searchInProgress = false;
this.updatesPaused = true;
this.#register();
}
@@ -77,16 +77,18 @@ export default class FxviewTabList extends MozLitElement {
dateTimeFormat: { type: String },
hasPopup: { type: String },
maxTabsLength: { type: Number },
- pinnedTabsGridView: { type: Boolean },
tabItems: { type: Array },
updatesPaused: { type: Boolean },
searchQuery: { type: String },
+ searchInProgress: { type: Boolean },
secondaryActionClass: { type: String },
tertiaryActionClass: { type: String },
};
static queries = {
- rowEls: { all: "fxview-tab-row" },
+ rowEls: {
+ all: "fxview-tab-row",
+ },
rootVirtualListEl: "virtual-list",
};
@@ -108,20 +110,7 @@ export default class FxviewTabList extends MozLitElement {
}
}
- // Move pinned tabs to the beginning of the list
- if (this.pinnedTabsGridView) {
- // Can set maxTabsLength to -1 to have no max
- this.unpinnedTabs = this.tabItems.filter(
- tab => !tab.indicators?.includes("pinned")
- );
- this.pinnedTabs = this.tabItems.filter(tab =>
- tab.indicators?.includes("pinned")
- );
- if (this.maxTabsLength > 0) {
- this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength);
- }
- this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs];
- } else if (this.maxTabsLength > 0) {
+ if (this.maxTabsLength > 0) {
this.tabItems = this.tabItems.slice(0, this.maxTabsLength);
}
}
@@ -148,7 +137,7 @@ export default class FxviewTabList extends MozLitElement {
"timeMsPref",
"browser.tabs.firefox-view.updateTimeMs",
NOW_THRESHOLD_MS,
- (prefName, oldVal, newVal) => {
+ () => {
this.clearIntervalTimer();
if (!this.isConnected) {
return;
@@ -197,93 +186,32 @@ export default class FxviewTabList extends MozLitElement {
if (e.code == "ArrowUp") {
// Focus either the link or button of the previous row based on this.currentActiveElementId
e.preventDefault();
- if (
- (this.pinnedTabsGridView &&
- this.activeIndex >= this.pinnedTabs.length) ||
- !this.pinnedTabsGridView
- ) {
- this.focusPrevRow();
- }
+ this.focusPrevRow();
} else if (e.code == "ArrowDown") {
// Focus either the link or button of the next row based on this.currentActiveElementId
e.preventDefault();
- if (
- this.pinnedTabsGridView &&
- this.activeIndex < this.pinnedTabs.length
- ) {
- this.focusIndex(this.pinnedTabs.length);
- } else {
- this.focusNextRow();
- }
+ this.focusNextRow();
} else if (e.code == "ArrowRight") {
// Focus either the link or the button in the current row and
// set this.currentActiveElementId to that element's ID
e.preventDefault();
if (document.dir == "rtl") {
- this.moveFocusLeft(fxviewTabRow);
+ fxviewTabRow.moveFocusLeft();
} else {
- this.moveFocusRight(fxviewTabRow);
+ fxviewTabRow.moveFocusRight();
}
} else if (e.code == "ArrowLeft") {
// Focus either the link or the button in the current row and
// set this.currentActiveElementId to that element's ID
e.preventDefault();
if (document.dir == "rtl") {
- this.moveFocusRight(fxviewTabRow);
+ fxviewTabRow.moveFocusRight();
} else {
- this.moveFocusLeft(fxviewTabRow);
+ fxviewTabRow.moveFocusLeft();
}
}
}
- moveFocusRight(fxviewTabRow) {
- if (
- this.pinnedTabsGridView &&
- fxviewTabRow.indicators?.includes("pinned")
- ) {
- this.focusNextRow();
- } else if (
- (fxviewTabRow.indicators?.includes("soundplaying") ||
- fxviewTabRow.indicators?.includes("muted")) &&
- this.currentActiveElementId === "fxview-tab-row-main"
- ) {
- this.currentActiveElementId = fxviewTabRow.focusMediaButton();
- } else if (
- this.currentActiveElementId === "fxview-tab-row-media-button" ||
- this.currentActiveElementId === "fxview-tab-row-main"
- ) {
- this.currentActiveElementId = fxviewTabRow.focusSecondaryButton();
- } else if (
- fxviewTabRow.tertiaryButtonEl &&
- this.currentActiveElementId === "fxview-tab-row-secondary-button"
- ) {
- this.currentActiveElementId = fxviewTabRow.focusTertiaryButton();
- }
- }
-
- moveFocusLeft(fxviewTabRow) {
- if (
- this.pinnedTabsGridView &&
- (fxviewTabRow.indicators?.includes("pinned") ||
- (this.currentActiveElementId === "fxview-tab-row-main" &&
- this.activeIndex === this.pinnedTabs.length))
- ) {
- this.focusPrevRow();
- } else if (
- this.currentActiveElementId === "fxview-tab-row-tertiary-button"
- ) {
- this.currentActiveElementId = fxviewTabRow.focusSecondaryButton();
- } else if (
- (fxviewTabRow.indicators?.includes("soundplaying") ||
- fxviewTabRow.indicators?.includes("muted")) &&
- this.currentActiveElementId === "fxview-tab-row-secondary-button"
- ) {
- this.currentActiveElementId = fxviewTabRow.focusMediaButton();
- } else {
- this.currentActiveElementId = fxviewTabRow.focusLink();
- }
- }
-
focusPrevRow() {
this.focusIndex(this.activeIndex - 1);
}
@@ -294,18 +222,12 @@ export default class FxviewTabList extends MozLitElement {
async focusIndex(index) {
// Focus link or button of item
- if (
- ((this.pinnedTabsGridView && index > this.pinnedTabs.length) ||
- !this.pinnedTabsGridView) &&
- lazy.virtualListEnabledPref
- ) {
- let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length);
+ if (lazy.virtualListEnabledPref) {
+ let row = this.rootVirtualListEl.getItem(index);
if (!row) {
return;
}
- let subList = this.rootVirtualListEl.getSubListForItem(
- index - this.pinnedTabs.length
- );
+ let subList = this.rootVirtualListEl.getSubListForItem(index);
if (!subList) {
return;
}
@@ -347,27 +269,15 @@ export default class FxviewTabList extends MozLitElement {
time = tabItem.time || tabItem.closedAt;
}
}
+
return html`
<fxview-tab-row
- exportparts="secondary-button"
- class=${classMap({
- pinned:
- this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"),
- })}
?active=${i == this.activeIndex}
?compact=${this.compactRows}
- .hasPopup=${this.hasPopup}
- .containerObj=${ifDefined(tabItem.containerObj)}
.currentActiveElementId=${this.currentActiveElementId}
- .dateTimeFormat=${this.dateTimeFormat}
.favicon=${tabItem.icon}
- .indicators=${ifDefined(tabItem.indicators)}
- .pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)}
.primaryL10nId=${tabItem.primaryL10nId}
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
- role=${this.pinnedTabsGridView && tabItem.indicators?.includes("pinned")
- ? "none"
- : "listitem"}
.secondaryL10nId=${tabItem.secondaryL10nId}
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
.tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)}
@@ -377,41 +287,36 @@ export default class FxviewTabList extends MozLitElement {
.sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
.sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
.closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
- .searchQuery=${ifDefined(this.searchQuery)}
+ role="listitem"
.tabElement=${ifDefined(tabItem.tabElement)}
.time=${ifDefined(time)}
- .timeMsPref=${ifDefined(this.timeMsPref)}
.title=${tabItem.title}
.url=${tabItem.url}
+ .searchQuery=${ifDefined(this.searchQuery)}
+ .timeMsPref=${ifDefined(this.timeMsPref)}
+ .hasPopup=${this.hasPopup}
+ .dateTimeFormat=${this.dateTimeFormat}
></fxview-tab-row>
`;
};
+ stylesheets() {
+ return html`<link
+ rel="stylesheet"
+ href="chrome://browser/content/firefoxview/fxview-tab-list.css"
+ />`;
+ }
+
render() {
- if (this.searchQuery && this.tabItems.length === 0) {
- return this.#emptySearchResultsTemplate();
+ if (
+ this.searchQuery &&
+ this.tabItems.length === 0 &&
+ !this.searchInProgress
+ ) {
+ return this.emptySearchResultsTemplate();
}
return html`
- <link
- rel="stylesheet"
- href="chrome://browser/content/firefoxview/fxview-tab-list.css"
- />
- ${when(
- this.pinnedTabsGridView && this.pinnedTabs.length,
- () => html`
- <div
- id="fxview-tab-list"
- class="fxview-tab-list pinned"
- data-l10n-id="firefoxview-pinned-tabs"
- role="tablist"
- @keydown=${this.handleFocusElementInRow}
- >
- ${this.pinnedTabs.map((tabItem, i) =>
- this.itemTemplate(tabItem, i)
- )}
- </div>
- `
- )}
+ ${this.stylesheets()}
<div
id="fxview-tab-list"
class="fxview-tab-list"
@@ -424,28 +329,21 @@ export default class FxviewTabList extends MozLitElement {
() => html`
<virtual-list
.activeIndex=${this.activeIndex}
- .pinnedTabsIndexOffset=${this.pinnedTabsGridView
- ? this.pinnedTabs.length
- : 0}
- .items=${this.pinnedTabsGridView
- ? this.unpinnedTabs
- : this.tabItems}
+ .items=${this.tabItems}
.template=${this.itemTemplate}
></virtual-list>
- `
- )}
- ${when(
- !lazy.virtualListEnabledPref,
- () => html`
- ${this.tabItems.map((tabItem, i) => this.itemTemplate(tabItem, i))}
- `
+ `,
+ () =>
+ html`${this.tabItems.map((tabItem, i) =>
+ this.itemTemplate(tabItem, i)
+ )}`
)}
</div>
<slot name="menu"></slot>
`;
}
- #emptySearchResultsTemplate() {
+ emptySearchResultsTemplate() {
return html` <fxview-empty-state
class="search-results"
headerLabel="firefoxview-search-results-empty"
@@ -455,23 +353,20 @@ export default class FxviewTabList extends MozLitElement {
</fxview-empty-state>`;
}
}
-customElements.define("fxview-tab-list", FxviewTabList);
+customElements.define("fxview-tab-list", FxviewTabListBase);
/**
* A tab item that displays favicon, title, url, and time of last access
*
* @property {boolean} active - Should current item have focus on keydown
* @property {boolean} compact - Whether to hide the URL and date/time for this tab.
- * @property {object} containerObj - Info about an open tab's container if within one
* @property {string} currentActiveElementId - ID of currently focused element within each tab item
* @property {string} dateTimeFormat - Expected format for date and/or time
* @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required
- * @property {string} indicators - An array of tab indicators if any are present
* @property {number} closedId - The tab ID for when the tab item was closed.
* @property {number} sourceClosedId - The closedId of the closed window its from if applicable
* @property {number} sourceWindowId - The sessionstore id of the window its from if applicable
* @property {string} favicon - The favicon for the tab item.
- * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view
* @property {string} primaryL10nId - The l10n id used for the primary action element
* @property {string} primaryL10nArgs - The l10n args used for the primary action element
* @property {string} secondaryL10nId - The l10n id used for the secondary action button
@@ -487,23 +382,14 @@ customElements.define("fxview-tab-list", FxviewTabList);
* @property {number} timeMsPref - The frequency in milliseconds of updates to relative time
* @property {string} searchQuery - The query string to highlight, if provided.
*/
-export class FxviewTabRow extends MozLitElement {
- constructor() {
- super();
- this.active = false;
- this.currentActiveElementId = "fxview-tab-row-main";
- }
-
+export class FxviewTabRowBase extends MozLitElement {
static properties = {
active: { type: Boolean },
compact: { type: Boolean },
- containerObj: { type: Object },
currentActiveElementId: { type: String },
dateTimeFormat: { type: String },
favicon: { type: String },
hasPopup: { type: String },
- indicators: { type: Array },
- pinnedTabsGridView: { type: Boolean },
primaryL10nId: { type: String },
primaryL10nArgs: { type: String },
secondaryL10nId: { type: String },
@@ -523,12 +409,16 @@ export class FxviewTabRow extends MozLitElement {
searchQuery: { type: String },
};
+ constructor() {
+ super();
+ this.active = false;
+ this.currentActiveElementId = "fxview-tab-row-main";
+ }
+
static queries = {
mainEl: "#fxview-tab-row-main",
secondaryButtonEl: "#fxview-tab-row-secondary-button:not([hidden])",
tertiaryButtonEl: "#fxview-tab-row-tertiary-button",
- mediaButtonEl: "#fxview-tab-row-media-button",
- pinnedTabButtonEl: "button#fxview-tab-row-main",
};
get currentFocusable() {
@@ -539,50 +429,45 @@ export class FxviewTabRow extends MozLitElement {
return focusItem;
}
- connectedCallback() {
- super.connectedCallback();
- this.addEventListener("keydown", this.handleKeydown);
- }
-
- disconnectedCallback() {
- super.disconnectedCallback();
- this.removeEventListener("keydown", this.handleKeydown);
- }
-
- handleKeydown(e) {
- if (
- this.active &&
- this.pinnedTabsGridView &&
- this.indicators?.includes("pinned") &&
- e.key === "m" &&
- e.ctrlKey
- ) {
- this.muteOrUnmuteTab();
- }
- }
-
focus() {
this.currentFocusable.focus();
}
focusSecondaryButton() {
+ let tabList = this.getRootNode().host;
this.secondaryButtonEl.focus();
- return this.secondaryButtonEl.id;
+ tabList.currentActiveElementId = this.secondaryButtonEl.id;
}
focusTertiaryButton() {
+ let tabList = this.getRootNode().host;
this.tertiaryButtonEl.focus();
- return this.tertiaryButtonEl.id;
- }
-
- focusMediaButton() {
- this.mediaButtonEl.focus();
- return this.mediaButtonEl.id;
+ tabList.currentActiveElementId = this.tertiaryButtonEl.id;
}
focusLink() {
+ let tabList = this.getRootNode().host;
this.mainEl.focus();
- return this.mainEl.id;
+ tabList.currentActiveElementId = this.mainEl.id;
+ }
+
+ moveFocusRight() {
+ if (this.currentActiveElementId === "fxview-tab-row-main") {
+ this.focusSecondaryButton();
+ } else if (
+ this.tertiaryButtonEl &&
+ this.currentActiveElementId === "fxview-tab-row-secondary-button"
+ ) {
+ this.focusTertiaryButton();
+ }
+ }
+
+ moveFocusLeft() {
+ if (this.currentActiveElementId === "fxview-tab-row-tertiary-button") {
+ this.focusSecondaryButton();
+ } else {
+ this.focusLink();
+ }
}
dateFluentArgs(timestamp, dateTimeFormat) {
@@ -652,16 +537,6 @@ export class FxviewTabRow extends MozLitElement {
return icon;
}
- getContainerClasses() {
- let containerClasses = ["fxview-tab-row-container-indicator", "icon"];
- if (this.containerObj) {
- let { icon, color } = this.containerObj;
- containerClasses.push(`identity-icon-${icon}`);
- containerClasses.push(`identity-color-${color}`);
- }
- return containerClasses;
- }
-
primaryActionHandler(event) {
if (
(event.type == "click" && !event.altKey) ||
@@ -683,9 +558,6 @@ export class FxviewTabRow extends MozLitElement {
secondaryActionHandler(event) {
if (
- (this.pinnedTabsGridView &&
- this.indicators?.includes("pinned") &&
- event.type == "contextmenu") ||
(event.type == "click" && event.detail && !event.altKey) ||
// detail=0 is from keyboard
(event.type == "click" && !event.detail)
@@ -718,92 +590,80 @@ export class FxviewTabRow extends MozLitElement {
}
}
- muteOrUnmuteTab(e) {
- e?.preventDefault();
- // If the tab has no sound playing, the mute/unmute button will be removed when toggled.
- // We should move the focus to the right in that case. This does not apply to pinned tabs
- // on the Open Tabs page.
- let shouldMoveFocus =
- (!this.pinnedTabsGridView ||
- (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) &&
- this.mediaButtonEl &&
- !this.indicators.includes("soundplaying") &&
- this.currentActiveElementId === "fxview-tab-row-media-button";
-
- // detail=0 is from keyboard
- if (e?.type == "click" && !e?.detail && shouldMoveFocus) {
- let tabList = this.getRootNode().host;
- if (document.dir == "rtl") {
- tabList.moveFocusLeft(this);
- } else {
- tabList.moveFocusRight(this);
- }
+ /**
+ * Find all matches of query within the given string, and compute the result
+ * to be rendered.
+ *
+ * @param {string} query
+ * @param {string} string
+ */
+ highlightSearchMatches(query, string) {
+ const fragments = [];
+ const regex = RegExp(escapeRegExp(query), "dgi");
+ let prevIndexEnd = 0;
+ let result;
+ while ((result = regex.exec(string)) !== null) {
+ const [indexStart, indexEnd] = result.indices[0];
+ fragments.push(string.substring(prevIndexEnd, indexStart));
+ fragments.push(
+ html`<strong>${string.substring(indexStart, indexEnd)}</strong>`
+ );
+ prevIndexEnd = regex.lastIndex;
}
- this.tabElement.toggleMuteAudio();
+ fragments.push(string.substring(prevIndexEnd));
+ return fragments;
+ }
+
+ stylesheets() {
+ return html`<link
+ rel="stylesheet"
+ href="chrome://browser/content/firefoxview/fxview-tab-row.css"
+ />`;
}
- #faviconTemplate() {
+ faviconTemplate() {
return html`<span
- class="${classMap({
- "fxview-tab-row-favicon-wrapper": true,
- pinned: this.indicators?.includes("pinned"),
- pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"),
- attention: this.indicators?.includes("attention"),
- bookmark: this.indicators?.includes("bookmark"),
- })}"
+ class="fxview-tab-row-favicon icon"
+ id="fxview-tab-row-favicon"
+ style=${styleMap({
+ backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
+ })}
+ ></span>`;
+ }
+
+ titleTemplate() {
+ const title = this.title;
+ return html`<span
+ class="fxview-tab-row-title text-truncated-ellipsis"
+ id="fxview-tab-row-title"
+ dir="auto"
>
- <span
- class="fxview-tab-row-favicon icon"
- id="fxview-tab-row-favicon"
- style=${styleMap({
- backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
- })}
- ></span>
${when(
- this.pinnedTabsGridView &&
- this.indicators?.includes("pinned") &&
- (this.indicators?.includes("muted") ||
- this.indicators?.includes("soundplaying")),
- () => html`
- <button
- class="fxview-tab-row-pinned-media-button ghost-button icon-button"
- id="fxview-tab-row-media-button"
- tabindex="-1"
- data-l10n-id=${this.indicators?.includes("muted")
- ? "fxviewtabrow-unmute-tab-button-no-context"
- : "fxviewtabrow-mute-tab-button-no-context"}
- muted=${this.indicators?.includes("muted")}
- soundplaying=${this.indicators?.includes("soundplaying") &&
- !this.indicators?.includes("muted")}
- @click=${this.muteOrUnmuteTab}
- ></button>
- `
+ this.searchQuery,
+ () => this.highlightSearchMatches(this.searchQuery, title),
+ () => title
)}
</span>`;
}
- #pinnedTabItemTemplate() {
- return html` <button
- class="fxview-tab-row-main ghost-button semi-transparent"
- id="fxview-tab-row-main"
- aria-haspopup=${ifDefined(this.hasPopup)}
- data-l10n-id=${ifDefined(this.primaryL10nId)}
- data-l10n-args=${ifDefined(this.primaryL10nArgs)}
- tabindex=${this.active &&
- this.currentActiveElementId === "fxview-tab-row-main"
- ? "0"
- : "-1"}
- role="tab"
- @click=${this.primaryActionHandler}
- @keydown=${this.primaryActionHandler}
- @contextmenu=${this.secondaryActionHandler}
+ urlTemplate() {
+ return html`<span
+ class="fxview-tab-row-url text-truncated-ellipsis"
+ id="fxview-tab-row-url"
>
- ${this.#faviconTemplate()}
- </button>`;
+ ${when(
+ this.searchQuery,
+ () =>
+ this.highlightSearchMatches(
+ this.searchQuery,
+ this.formatURIForDisplay(this.url)
+ ),
+ () => this.formatURIForDisplay(this.url)
+ )}
+ </span>`;
}
- #unpinnedTabItemTemplate() {
- const title = this.title;
+ dateTemplate() {
const relativeString = this.relativeTime(
this.time,
this.dateTimeFormat,
@@ -815,11 +675,81 @@ export class FxviewTabRow extends MozLitElement {
!window.IS_STORYBOOK ? this.timeMsPref : NOW_THRESHOLD_MS
);
const dateArgs = this.dateFluentArgs(this.time, this.dateTimeFormat);
+ return html`<span class="fxview-tab-row-date" id="fxview-tab-row-date">
+ <span
+ ?hidden=${relativeString || !dateString}
+ data-l10n-id=${ifDefined(dateString)}
+ data-l10n-args=${ifDefined(dateArgs)}
+ ></span>
+ <span ?hidden=${!relativeString}>${relativeString}</span>
+ </span>`;
+ }
+
+ timeTemplate() {
const timeString = this.timeFluentId(this.dateTimeFormat);
const time = this.time;
const timeArgs = JSON.stringify({ time });
+ return html`<span
+ class="fxview-tab-row-time"
+ id="fxview-tab-row-time"
+ ?hidden=${!timeString}
+ data-timestamp=${ifDefined(this.time)}
+ data-l10n-id=${ifDefined(timeString)}
+ data-l10n-args=${ifDefined(timeArgs)}
+ >
+ </span>`;
+ }
- return html`<a
+ secondaryButtonTemplate() {
+ return html`${when(
+ this.secondaryL10nId && this.secondaryActionHandler,
+ () => html`<moz-button
+ type="icon ghost"
+ class=${classMap({
+ "fxview-tab-row-button": true,
+ [this.secondaryActionClass]: this.secondaryActionClass,
+ })}
+ id="fxview-tab-row-secondary-button"
+ data-l10n-id=${this.secondaryL10nId}
+ data-l10n-args=${ifDefined(this.secondaryL10nArgs)}
+ aria-haspopup=${ifDefined(this.hasPopup)}
+ @click=${this.secondaryActionHandler}
+ tabindex="${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-secondary-button"
+ ? "0"
+ : "-1"}"
+ ></moz-button>`
+ )}`;
+ }
+
+ tertiaryButtonTemplate() {
+ return html`${when(
+ this.tertiaryL10nId && this.tertiaryActionHandler,
+ () => html`<moz-button
+ type="icon ghost"
+ class=${classMap({
+ "fxview-tab-row-button": true,
+ [this.tertiaryActionClass]: this.tertiaryActionClass,
+ })}
+ id="fxview-tab-row-tertiary-button"
+ data-l10n-id=${this.tertiaryL10nId}
+ data-l10n-args=${ifDefined(this.tertiaryL10nArgs)}
+ aria-haspopup=${ifDefined(this.hasPopup)}
+ @click=${this.tertiaryActionHandler}
+ tabindex="${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-tertiary-button"
+ ? "0"
+ : "-1"}"
+ ></moz-button>`
+ )}`;
+ }
+}
+
+export class FxviewTabRow extends FxviewTabRowBase {
+ render() {
+ return html`
+ ${this.stylesheets()}
+ <a
href=${ifDefined(this.url)}
class="fxview-tab-row-main"
id="fxview-tab-row-main"
@@ -833,176 +763,16 @@ export class FxviewTabRow extends MozLitElement {
@keydown=${this.primaryActionHandler}
title=${!this.primaryL10nId ? this.url : null}
>
- ${this.#faviconTemplate()}
- <span
- class="fxview-tab-row-title text-truncated-ellipsis"
- id="fxview-tab-row-title"
- dir="auto"
- >
- ${when(
- this.searchQuery,
- () => this.#highlightSearchMatches(this.searchQuery, title),
- () => title
- )}
- </span>
- <span class=${this.getContainerClasses().join(" ")}></span>
- <span
- class="fxview-tab-row-url text-truncated-ellipsis"
- id="fxview-tab-row-url"
- ?hidden=${this.compact}
- >
- ${when(
- this.searchQuery,
- () =>
- this.#highlightSearchMatches(
- this.searchQuery,
- this.formatURIForDisplay(this.url)
- ),
- () => this.formatURIForDisplay(this.url)
- )}
- </span>
- <span
- class="fxview-tab-row-date"
- id="fxview-tab-row-date"
- ?hidden=${this.compact}
- >
- <span
- ?hidden=${relativeString || !dateString}
- data-l10n-id=${ifDefined(dateString)}
- data-l10n-args=${ifDefined(dateArgs)}
- ></span>
- <span ?hidden=${!relativeString}>${relativeString}</span>
- </span>
- <span
- class="fxview-tab-row-time"
- id="fxview-tab-row-time"
- ?hidden=${this.compact || !timeString}
- data-timestamp=${ifDefined(this.time)}
- data-l10n-id=${ifDefined(timeString)}
- data-l10n-args=${ifDefined(timeArgs)}
- >
- </span>
+ ${this.faviconTemplate()} ${this.titleTemplate()}
+ ${when(
+ !this.compact,
+ () => html`${this.urlTemplate()} ${this.dateTemplate()}
+ ${this.timeTemplate()}`
+ )}
</a>
- ${when(
- this.indicators?.includes("soundplaying") ||
- this.indicators?.includes("muted"),
- () => html`<button
- class=fxview-tab-row-button ghost-button icon-button semi-transparent"
- id="fxview-tab-row-media-button"
- data-l10n-id=${
- this.indicators?.includes("muted")
- ? "fxviewtabrow-unmute-tab-button-no-context"
- : "fxviewtabrow-mute-tab-button-no-context"
- }
- muted=${this.indicators?.includes("muted")}
- soundplaying=${
- this.indicators?.includes("soundplaying") &&
- !this.indicators?.includes("muted")
- }
- @click=${this.muteOrUnmuteTab}
- tabindex="${
- this.active &&
- this.currentActiveElementId === "fxview-tab-row-media-button"
- ? "0"
- : "-1"
- }"
- ></button>`,
- () => html`<span></span>`
- )}
- ${when(
- this.secondaryL10nId && this.secondaryActionHandler,
- () => html`<button
- class=${classMap({
- "fxview-tab-row-button": true,
- "ghost-button": true,
- "icon-button": true,
- "semi-transparent": true,
- [this.secondaryActionClass]: this.secondaryActionClass,
- })}
- id="fxview-tab-row-secondary-button"
- data-l10n-id=${this.secondaryL10nId}
- data-l10n-args=${ifDefined(this.secondaryL10nArgs)}
- aria-haspopup=${ifDefined(this.hasPopup)}
- @click=${this.secondaryActionHandler}
- tabindex="${this.active &&
- this.currentActiveElementId === "fxview-tab-row-secondary-button"
- ? "0"
- : "-1"}"
- ></button>`
- )}
- ${when(
- this.tertiaryL10nId && this.tertiaryActionHandler,
- () => html`<button
- class=${classMap({
- "fxview-tab-row-button": true,
- "ghost-button": true,
- "icon-button": true,
- "semi-transparent": true,
- [this.tertiaryActionClass]: this.tertiaryActionClass,
- })}
- id="fxview-tab-row-tertiary-button"
- data-l10n-id=${this.tertiaryL10nId}
- data-l10n-args=${ifDefined(this.tertiaryL10nArgs)}
- aria-haspopup=${ifDefined(this.hasPopup)}
- @click=${this.tertiaryActionHandler}
- tabindex="${this.active &&
- this.currentActiveElementId === "fxview-tab-row-tertiary-button"
- ? "0"
- : "-1"}"
- ></button>`
- )}`;
- }
-
- render() {
- return html`
- ${when(
- this.containerObj,
- () => html`
- <link
- rel="stylesheet"
- href="chrome://browser/content/usercontext/usercontext.css"
- />
- `
- )}
- <link
- rel="stylesheet"
- href="chrome://global/skin/in-content/common.css"
- />
- <link
- rel="stylesheet"
- href="chrome://browser/content/firefoxview/fxview-tab-row.css"
- />
- ${when(
- this.pinnedTabsGridView && this.indicators?.includes("pinned"),
- this.#pinnedTabItemTemplate.bind(this),
- this.#unpinnedTabItemTemplate.bind(this)
- )}
+ ${this.secondaryButtonTemplate()} ${this.tertiaryButtonTemplate()}
`;
}
-
- /**
- * Find all matches of query within the given string, and compute the result
- * to be rendered.
- *
- * @param {string} query
- * @param {string} string
- */
- #highlightSearchMatches(query, string) {
- const fragments = [];
- const regex = RegExp(escapeRegExp(query), "dgi");
- let prevIndexEnd = 0;
- let result;
- while ((result = regex.exec(string)) !== null) {
- const [indexStart, indexEnd] = result.indices[0];
- fragments.push(string.substring(prevIndexEnd, indexStart));
- fragments.push(
- html`<strong>${string.substring(indexStart, indexEnd)}</strong>`
- );
- prevIndexEnd = regex.lastIndex;
- }
- fragments.push(string.substring(prevIndexEnd));
- return fragments;
- }
}
customElements.define("fxview-tab-row", FxviewTabRow);
@@ -1040,10 +810,16 @@ export class VirtualList extends MozLitElement {
this.isSubList = false;
this.isVisible = false;
this.intersectionObserver = new IntersectionObserver(
- ([entry]) => (this.isVisible = entry.isIntersecting),
+ ([entry]) => {
+ this.isVisible = entry.isIntersecting;
+ },
{ root: this.ownerDocument }
);
- this.resizeObserver = new ResizeObserver(([entry]) => {
+ this.selfResizeObserver = new ResizeObserver(() => {
+ // Trigger the intersection observer once the tab rows have rendered
+ this.triggerIntersectionObserver();
+ });
+ this.childResizeObserver = new ResizeObserver(([entry]) => {
if (entry.contentRect?.height > 0) {
// Update properties on top-level virtual-list
this.parentElement.itemHeightEstimate = entry.contentRect.height;
@@ -1058,7 +834,8 @@ export class VirtualList extends MozLitElement {
disconnectedCallback() {
super.disconnectedCallback();
this.intersectionObserver.disconnect();
- this.resizeObserver.disconnect();
+ this.childResizeObserver.disconnect();
+ this.selfResizeObserver.disconnect();
}
triggerIntersectionObserver() {
@@ -1090,7 +867,6 @@ export class VirtualList extends MozLitElement {
this.items.slice(i, i + this.maxRenderCountEstimate)
);
}
- this.triggerIntersectionObserver();
}
}
@@ -1103,13 +879,17 @@ export class VirtualList extends MozLitElement {
firstUpdated() {
this.intersectionObserver.observe(this);
+ this.selfResizeObserver.observe(this);
if (this.isSubList && this.children[0]) {
- this.resizeObserver.observe(this.children[0]);
+ this.childResizeObserver.observe(this.children[0]);
}
}
updated(changedProperties) {
this.updateListHeight(changedProperties);
+ if (changedProperties.has("items") && !this.isSubList) {
+ this.triggerIntersectionObserver();
+ }
}
updateListHeight(changedProperties) {
@@ -1157,5 +937,4 @@ export class VirtualList extends MozLitElement {
return "";
}
}
-
customElements.define("virtual-list", VirtualList);
diff --git a/browser/components/firefoxview/fxview-tab-row.css b/browser/components/firefoxview/fxview-tab-row.css
index 219d7e8aa2..c1c8f967a7 100644
--- a/browser/components/firefoxview/fxview-tab-row.css
+++ b/browser/components/firefoxview/fxview-tab-row.css
@@ -2,9 +2,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+@import url("chrome://global/skin/design-system/text-and-typography.css");
+
:host {
- --fxviewtabrow-element-background-hover: color-mix(in srgb, currentColor 14%, transparent);
- --fxviewtabrow-element-background-active: color-mix(in srgb, currentColor 21%, transparent);
+ --fxviewtabrow-element-background-hover: var(--button-background-color-ghost-hover);
+ --fxviewtabrow-element-background-active: var(--button-background-color-ghost-active);
display: grid;
grid-template-columns: subgrid;
grid-column: span 9;
@@ -12,7 +14,7 @@
border-radius: 4px;
}
-@media (prefers-contrast) {
+@media (forced-colors) {
:host {
--fxviewtabrow-element-background-hover: ButtonText;
--fxviewtabrow-element-background-active: ButtonText;
@@ -32,115 +34,42 @@
cursor: pointer;
text-decoration: none;
- :host(.pinned) & {
- padding: var(--space-small);
- min-width: unset;
- margin: 0;
+ :host([compact]) & {
+ grid-template-columns: min-content auto;
}
}
.fxview-tab-row-main,
.fxview-tab-row-main:visited,
-.fxview-tab-row-main:hover:active,
-.fxview-tab-row-button {
+.fxview-tab-row-main:hover:active {
color: inherit;
}
-.fxview-tab-row-main:hover,
-.fxview-tab-row-button.ghost-button.icon-button:enabled:hover {
+.fxview-tab-row-main:hover {
background-color: var(--fxviewtabrow-element-background-hover);
color: var(--fxviewtabrow-text-color-hover);
-
- & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after {
- stroke: var(--fxview-indicator-stroke-color-hover);
- }
}
-.fxview-tab-row-main:hover:active,
-.fxview-tab-row-button.ghost-button.icon-button:enabled:hover:active {
+.fxview-tab-row-main:hover:active {
background-color: var(--fxviewtabrow-element-background-active);
}
-@media (prefers-contrast) {
- a.fxview-tab-row-main,
- a.fxview-tab-row-main:hover,
- a.fxview-tab-row-main:active {
+@media (forced-colors) {
+ .fxview-tab-row-main,
+ .fxview-tab-row-main:hover,
+ .fxview-tab-row-main:active {
background-color: transparent;
border: 1px solid LinkText;
color: LinkText;
}
- a.fxview-tab-row-main:visited,
- a.fxview-tab-row-main:visited:hover {
+ .fxview-tab-row-main:visited,
+ .fxview-tab-row-main:visited:hover {
border: 1px solid VisitedText;
color: VisitedText;
}
}
-.fxview-tab-row-favicon-wrapper {
- height: 16px;
- position: relative;
-
- .fxview-tab-row-favicon::after,
- .fxview-tab-row-button::after,
- &.pinned .fxview-tab-row-pinned-media-button {
- display: block;
- content: "";
- background-size: 12px;
- background-position: center;
- background-repeat: no-repeat;
- position: relative;
- height: 12px;
- width: 12px;
- -moz-context-properties: fill, stroke;
- fill: currentColor;
- stroke: var(--fxview-background-color-secondary);
- }
-
- &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after {
- inset-block-start: 9px;
- inset-inline-end: -6px;
- }
-
- &.pinnedOnNewTab .fxview-tab-row-favicon::after,
- &.pinnedOnNewTab .fxview-tab-row-button::after {
- background-image: url("chrome://browser/skin/pin-12.svg");
- }
-
- &.bookmark .fxview-tab-row-favicon::after,
- &.bookmark .fxview-tab-row-button::after {
- background-image: url("chrome://browser/skin/bookmark-12.svg");
- fill: var(--fxview-primary-action-background);
- }
-
- &.attention .fxview-tab-row-favicon::after,
- &.attention .fxview-tab-row-button::after {
- background-image: radial-gradient(circle, light-dark(rgb(42, 195, 162), rgb(84, 255, 189)), light-dark(rgb(42, 195, 162), rgb(84, 255, 189)) 2px, transparent 2px);
- height: 4px;
- width: 100%;
- inset-block-start: 20px;
- }
-
- &.pinned .fxview-tab-row-pinned-media-button {
- inset-block-start: -10px;
- inset-inline-end: -10px;
- border-radius: 100%;
- background-color: var(--fxview-background-color-secondary);
- padding: 6px;
- min-width: 0;
- min-height: 0;
- position: absolute;
-
- &[muted="true"] {
- background-image: url("chrome://global/skin/media/audio-muted.svg");
- }
-
- &[soundplaying="true"] {
- background-image: url("chrome://global/skin/media/audio.svg");
- }
- }
-}
-
.fxview-tab-row-favicon {
background-size: cover;
-moz-context-properties: fill;
@@ -155,15 +84,6 @@
text-align: match-parent;
}
-.fxview-tab-row-container-indicator {
- height: 16px;
- width: 16px;
- background-image: var(--identity-icon);
- background-size: cover;
- -moz-context-properties: fill;
- fill: var(--identity-icon-color);
-}
-
.fxview-tab-row-url {
color: var(--text-color-deemphasized);
text-decoration-line: underline;
@@ -182,62 +102,22 @@
font-weight: 400;
}
-.fxview-tab-row-button {
- margin: 0;
- cursor: pointer;
- min-width: 0;
- background-color: transparent;
-
- &[muted="true"],
- &[soundplaying="true"] {
- background-size: 16px;
- background-repeat: no-repeat;
- background-position: center;
- -moz-context-properties: fill;
- fill: currentColor;
- }
-
- &[muted="true"] {
- background-image: url("chrome://global/skin/media/audio-muted.svg");
- }
-
- &[soundplaying="true"] {
- background-image: url("chrome://global/skin/media/audio.svg");
- }
-
- &.dismiss-button {
- background-image: url("chrome://global/skin/icons/close.svg");
- }
-
- &.options-button {
- background-image: url("chrome://global/skin/icons/more.svg");
- }
+.fxview-tab-row-button::part(button) {
+ color: var(--fxview-text-primary-color)
}
-@media (prefers-contrast) {
- .fxview-tab-row-button,
- button.fxview-tab-row-main {
- border: 1px solid ButtonText;
- color: ButtonText;
- }
+.fxview-tab-row-button[muted="true"]::part(button) {
+ background-image: url("chrome://global/skin/media/audio-muted.svg");
+}
- .fxview-tab-row-button.ghost-button.icon-button:enabled:hover,
- button.fxview-tab-row-main:enabled:hover {
- border: 1px solid SelectedItem;
- color: SelectedItem;
- }
+.fxview-tab-row-button[soundplaying="true"]::part(button) {
+ background-image: url("chrome://global/skin/media/audio.svg");
+}
- .fxview-tab-row-button.ghost-button.icon-button:enabled:active,
- button.fxview-tab-row-main:enabled:active {
- color: SelectedItem;
- }
+.fxview-tab-row-button.dismiss-button::part(button) {
+ background-image: url("chrome://global/skin/icons/close.svg");
+}
- .fxview-tab-row-button.ghost-button.icon-button:enabled,
- .fxview-tab-row-button.ghost-button.icon-button:enabled:hover,
- .fxview-tab-row-button.ghost-button.icon-button:enabled:active
- button.fxview-tab-row-main:enabled,
- button.fxview-tab-row-main:enabled:hover,
- button.fxview-tab-row-main:enabled:active {
- background-color: ButtonFace;
- }
+.fxview-tab-row-button.options-button::part(button) {
+ background-image: url("chrome://global/skin/icons/more.svg");
}
diff --git a/browser/components/firefoxview/helpers.mjs b/browser/components/firefoxview/helpers.mjs
index 3cb308a587..b206deef18 100644
--- a/browser/components/firefoxview/helpers.mjs
+++ b/browser/components/firefoxview/helpers.mjs
@@ -173,3 +173,20 @@ export function escapeHtmlEntities(text) {
.replace(/"/g, "&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..478422d49b 100644
--- a/browser/components/firefoxview/history.mjs
+++ b/browser/components/firefoxview/history.mjs
@@ -7,18 +7,21 @@ import {
ifDefined,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
-import { escapeHtmlEntities, isSearchEnabled } from "./helpers.mjs";
+import {
+ escapeHtmlEntities,
+ isSearchEnabled,
+ navigateToLink,
+} from "./helpers.mjs";
import { ViewPage } from "./viewpage.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/migration/migration-wizard.mjs";
+import { HistoryController } from "./HistoryController.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
- FirefoxViewPlacesQuery:
- "resource:///modules/firefox-view-places-query.sys.mjs",
- PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
});
@@ -26,13 +29,6 @@ let XPCOMUtils = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
).XPCOMUtils;
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- "maxRowsPref",
- "browser.firefox-view.max-history-rows",
- -1
-);
-
const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history";
const IMPORT_HISTORY_DISMISSED_PREF =
@@ -44,35 +40,30 @@ class HistoryInView extends ViewPage {
constructor() {
super();
this._started = false;
- this.allHistoryItems = new Map();
- this.historyMapByDate = [];
- this.historyMapBySite = [];
// Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1;
- this.placesQuery = new lazy.FirefoxViewPlacesQuery();
- this.searchQuery = "";
- this.searchResults = null;
- this.sortOption = "date";
this.profileAge = 8;
this.fullyUpdated = false;
this.cumulativeSearches = 0;
}
+ controller = new HistoryController(this, {
+ searchResultsLimit: SEARCH_RESULTS_LIMIT,
+ });
+
start() {
if (this._started) {
return;
}
this._started = true;
- this.#updateAllHistoryItems();
- this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data));
+ this.controller.updateAllHistoryItems();
this.toggleVisibilityInCardContainer();
}
async connectedCallback() {
super.connectedCallback();
- await this.updateHistoryData();
XPCOMUtils.defineLazyPreferenceGetter(
this,
"importHistoryDismissedPref",
@@ -91,6 +82,7 @@ class HistoryInView extends ViewPage {
this.requestUpdate();
}
);
+
if (!this.importHistoryDismissedPref && !this.hasImportedHistoryPrefs) {
let profileAccessor = await lazy.ProfileAge();
let profileCreateTime = await profileAccessor.created;
@@ -106,7 +98,6 @@ class HistoryInView extends ViewPage {
return;
}
this._started = false;
- this.placesQuery.close();
this.toggleVisibilityInCardContainer();
}
@@ -120,32 +111,6 @@ class HistoryInView extends ViewPage {
);
}
- async #updateAllHistoryItems(allHistoryItems) {
- if (allHistoryItems) {
- this.allHistoryItems = allHistoryItems;
- } else {
- await this.updateHistoryData();
- }
- this.resetHistoryMaps();
- this.lists.forEach(list => list.requestUpdate());
- await this.#updateSearchResults();
- }
-
- async #updateSearchResults() {
- if (this.searchQuery) {
- try {
- this.searchResults = await this.placesQuery.searchHistory(
- this.searchQuery,
- SEARCH_RESULTS_LIMIT
- );
- } catch (e) {
- // Connection interrupted, ignore.
- }
- } else {
- this.searchResults = null;
- }
- }
-
viewVisibleCallback() {
this.start();
}
@@ -166,14 +131,8 @@ class HistoryInView extends ViewPage {
};
static properties = {
- ...ViewPage.properties,
- allHistoryItems: { type: Map },
- historyMapByDate: { type: Array },
- historyMapBySite: { type: Array },
// Making profileAge a reactive property for testing
profileAge: { type: Number },
- searchResults: { type: Array },
- sortOption: { type: String },
};
async getUpdateComplete() {
@@ -181,70 +140,8 @@ class HistoryInView extends ViewPage {
await Promise.all(Array.from(this.cards).map(card => card.updateComplete));
}
- async updateHistoryData() {
- this.allHistoryItems = await this.placesQuery.getHistory({
- daysOld: 60,
- limit: lazy.maxRowsPref,
- sortBy: this.sortOption,
- });
- }
-
- resetHistoryMaps() {
- this.historyMapByDate = [];
- this.historyMapBySite = [];
- }
-
- createHistoryMaps() {
- if (this.sortOption === "date" && !this.historyMapByDate.length) {
- const {
- visitsFromToday,
- visitsFromYesterday,
- visitsByDay,
- visitsByMonth,
- } = this.placesQuery;
-
- // Add visits from today and yesterday.
- if (visitsFromToday.length) {
- this.historyMapByDate.push({
- l10nId: "firefoxview-history-date-today",
- items: visitsFromToday,
- });
- }
- if (visitsFromYesterday.length) {
- this.historyMapByDate.push({
- l10nId: "firefoxview-history-date-yesterday",
- items: visitsFromYesterday,
- });
- }
-
- // Add visits from this month, grouped by day.
- visitsByDay.forEach(visits => {
- this.historyMapByDate.push({
- l10nId: "firefoxview-history-date-this-month",
- items: visits,
- });
- });
-
- // Add visits from previous months, grouped by month.
- visitsByMonth.forEach(visits => {
- this.historyMapByDate.push({
- l10nId: "firefoxview-history-date-prev-month",
- items: visits,
- });
- });
- } else if (this.sortOption === "site" && !this.historyMapBySite.length) {
- this.historyMapBySite = Array.from(
- this.allHistoryItems.entries(),
- ([domain, items]) => ({
- domain,
- items,
- l10nId: domain ? null : "firefoxview-history-site-localhost",
- })
- ).sort((a, b) => a.domain.localeCompare(b.domain));
- }
- }
-
onPrimaryAction(e) {
+ navigateToLink(e);
// Record telemetry
Services.telemetry.recordEvent(
"firefoxview_next",
@@ -254,26 +151,13 @@ class HistoryInView extends ViewPage {
{}
);
- if (this.searchQuery) {
+ if (this.controller.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
searchesHistogram.add("history", this.cumulativeSearches);
this.cumulativeSearches = 0;
}
-
- let currentWindow = this.getWindow();
- if (currentWindow.openTrustedLinkIn) {
- let where = lazy.BrowserUtils.whereToOpenLink(
- e.detail.originalEvent,
- false,
- true
- );
- if (where == "current") {
- where = "tab";
- }
- currentWindow.openTrustedLinkIn(e.originalTarget.url, where);
- }
}
onSecondaryAction(e) {
@@ -282,24 +166,29 @@ class HistoryInView extends ViewPage {
}
deleteFromHistory(e) {
- lazy.PlacesUtils.history.remove(this.triggerNode.url);
+ this.controller.deleteFromHistory();
this.recordContextMenuTelemetry("delete-from-history", e);
}
async onChangeSortOption(e) {
- this.sortOption = e.target.value;
+ await this.controller.onChangeSortOption(e);
Services.telemetry.recordEvent(
"firefoxview_next",
"sort_history",
"tabs",
null,
{
- sort_type: this.sortOption,
- search_start: this.searchQuery ? "true" : "false",
+ sort_type: this.controller.sortOption,
+ search_start: this.controller.searchQuery ? "true" : "false",
}
);
- await this.updateHistoryData();
- await this.#updateSearchResults();
+ }
+
+ async onSearchQuery(e) {
+ await this.controller.onSearchQuery(e);
+ this.cumulativeSearches = this.controller.searchQuery
+ ? this.cumulativeSearches + 1
+ : 0;
}
showAllHistory() {
@@ -396,9 +285,9 @@ class HistoryInView extends ViewPage {
* The template to use for cards-container.
*/
get cardsTemplate() {
- if (this.searchResults) {
+ if (this.controller.searchResults) {
return this.#searchResultsTemplate();
- } else if (this.allHistoryItems.size) {
+ } else if (this.controller.allHistoryItems.size) {
return this.#historyCardsTemplate();
}
return this.#emptyMessageTemplate();
@@ -406,8 +295,11 @@ class HistoryInView extends ViewPage {
#historyCardsTemplate() {
let cardsTemplate = [];
- if (this.sortOption === "date" && this.historyMapByDate.length) {
- this.historyMapByDate.forEach(historyItem => {
+ if (
+ this.controller.sortOption === "date" &&
+ this.controller.historyMapByDate.length
+ ) {
+ this.controller.historyMapByDate.forEach(historyItem => {
if (historyItem.items.length) {
let dateArg = JSON.stringify({ date: historyItem.items[0].time });
cardsTemplate.push(html`<card-container>
@@ -424,7 +316,7 @@ class HistoryInView extends ViewPage {
: "time"}
hasPopup="menu"
maxTabsLength=${this.maxTabsLength}
- .tabItems=${historyItem.items}
+ .tabItems=${[...historyItem.items]}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
@@ -433,8 +325,8 @@ class HistoryInView extends ViewPage {
</card-container>`);
}
});
- } else if (this.historyMapBySite.length) {
- this.historyMapBySite.forEach(historyItem => {
+ } else if (this.controller.historyMapBySite.length) {
+ this.controller.historyMapBySite.forEach(historyItem => {
if (historyItem.items.length) {
cardsTemplate.push(html`<card-container>
<h3 slot="header" data-l10n-id="${ifDefined(historyItem.l10nId)}">
@@ -446,7 +338,7 @@ class HistoryInView extends ViewPage {
dateTimeFormat="dateTime"
hasPopup="menu"
maxTabsLength=${this.maxTabsLength}
- .tabItems=${historyItem.items}
+ .tabItems=${[...historyItem.items]}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
@@ -504,17 +396,17 @@ class HistoryInView extends ViewPage {
slot="header"
data-l10n-id="firefoxview-search-results-header"
data-l10n-args=${JSON.stringify({
- query: escapeHtmlEntities(this.searchQuery),
+ query: escapeHtmlEntities(this.controller.searchQuery),
})}
></h3>
${when(
- this.searchResults.length,
+ this.controller.searchResults.length,
() =>
html`<h3
slot="secondary-header"
data-l10n-id="firefoxview-search-results-count"
data-l10n-args="${JSON.stringify({
- count: this.searchResults.length,
+ count: this.controller.searchResults.length,
})}"
></h3>`
)}
@@ -524,10 +416,11 @@ class HistoryInView extends ViewPage {
dateTimeFormat="dateTime"
hasPopup="menu"
maxTabsLength="-1"
- .searchQuery=${this.searchQuery}
- .tabItems=${this.searchResults}
+ .searchQuery=${this.controller.searchQuery}
+ .tabItems=${this.controller.searchResults}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
+ .searchInProgress=${this.controller.placesQuery.searchInProgress}
>
${this.panelListTemplate()}
</fxview-tab-list>
@@ -569,7 +462,7 @@ class HistoryInView extends ViewPage {
id="sort-by-date"
name="history-sort-option"
value="date"
- ?checked=${this.sortOption === "date"}
+ ?checked=${this.controller.sortOption === "date"}
@click=${this.onChangeSortOption}
/>
<label
@@ -583,7 +476,7 @@ class HistoryInView extends ViewPage {
id="sort-by-site"
name="history-sort-option"
value="site"
- ?checked=${this.sortOption === "site"}
+ ?checked=${this.controller.sortOption === "site"}
@click=${this.onChangeSortOption}
/>
<label
@@ -612,11 +505,12 @@ class HistoryInView extends ViewPage {
data-l10n-id="firefoxview-choose-browser-button"
@click=${this.openMigrationWizard}
></button>
- <button
- class="close ghost-button"
+ <moz-button
+ class="close"
+ type="icon ghost"
data-l10n-id="firefoxview-import-history-close-button"
@click=${this.dismissImportHistory}
- ></button>
+ ></moz-button>
</div>
</div>
</card-container>
@@ -624,32 +518,24 @@ class HistoryInView extends ViewPage {
</div>
<div
class="show-all-history-footer"
- ?hidden=${!this.allHistoryItems.size}
+ ?hidden=${!this.controller.allHistoryItems.size}
>
<button
class="show-all-history-button"
data-l10n-id="firefoxview-show-all-history"
@click=${this.showAllHistory}
- ?hidden=${this.searchResults}
+ ?hidden=${this.controller.searchResults}
></button>
</div>
`;
}
- async onSearchQuery(e) {
- this.searchQuery = e.detail.query;
- this.cumulativeSearches = this.searchQuery
- ? this.cumulativeSearches + 1
- : 0;
- this.#updateSearchResults();
- }
-
- willUpdate(changedProperties) {
+ willUpdate() {
this.fullyUpdated = false;
- if (this.allHistoryItems.size && !changedProperties.has("sortOption")) {
+ if (this.controller.allHistoryItems.size) {
// onChangeSortOption() will update history data once it has been fetched
// from the API.
- this.createHistoryMaps();
+ this.controller.createHistoryMaps();
}
}
}
diff --git a/browser/components/firefoxview/jar.mn b/browser/components/firefoxview/jar.mn
index 1e5cc3e690..8bf3597aa5 100644
--- a/browser/components/firefoxview/jar.mn
+++ b/browser/components/firefoxview/jar.mn
@@ -9,6 +9,7 @@ browser.jar:
content/browser/firefoxview/firefoxview.mjs
content/browser/firefoxview/history.css
content/browser/firefoxview/history.mjs
+ content/browser/firefoxview/HistoryController.mjs
content/browser/firefoxview/opentabs.mjs
content/browser/firefoxview/view-opentabs.css
content/browser/firefoxview/syncedtabs.mjs
@@ -23,6 +24,9 @@ browser.jar:
content/browser/firefoxview/fxview-tab-list.css
content/browser/firefoxview/fxview-tab-list.mjs
content/browser/firefoxview/fxview-tab-row.css
+ content/browser/firefoxview/opentabs-tab-list.css
+ content/browser/firefoxview/opentabs-tab-list.mjs
+ content/browser/firefoxview/opentabs-tab-row.css
content/browser/firefoxview/recentlyclosed.mjs
content/browser/firefoxview/viewpage.mjs
content/browser/firefoxview/history-empty.svg (content/history-empty.svg)
diff --git a/browser/components/firefoxview/opentabs-tab-list.css b/browser/components/firefoxview/opentabs-tab-list.css
new file mode 100644
index 0000000000..9245a0fada
--- /dev/null
+++ b/browser/components/firefoxview/opentabs-tab-list.css
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.fxview-tab-list {
+ &.pinned {
+ display: flex;
+ flex-wrap: wrap;
+
+ > virtual-list {
+ display: block;
+ }
+
+ > opentabs-tab-row {
+ display: block;
+ margin-block-end: var(--space-xsmall);
+ }
+ }
+
+ &.hasContainerTab {
+ grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content;
+ }
+}
+
+virtual-list {
+ grid-column: span 9;
+
+ .top-padding,
+ .bottom-padding {
+ grid-column: span 9;
+ }
+}
diff --git a/browser/components/firefoxview/opentabs-tab-list.mjs b/browser/components/firefoxview/opentabs-tab-list.mjs
new file mode 100644
index 0000000000..4b6d6b3c86
--- /dev/null
+++ b/browser/components/firefoxview/opentabs-tab-list.mjs
@@ -0,0 +1,593 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import {
+ classMap,
+ html,
+ ifDefined,
+ styleMap,
+ when,
+} from "chrome://global/content/vendor/lit.all.mjs";
+import {
+ FxviewTabListBase,
+ FxviewTabRowBase,
+} from "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
+
+const lazy = {};
+let XPCOMUtils;
+
+XPCOMUtils = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+).XPCOMUtils;
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "virtualListEnabledPref",
+ "browser.firefox-view.virtual-list.enabled"
+);
+
+/**
+ * A list of clickable tab items
+ *
+ * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view
+ */
+
+export class OpenTabsTabList extends FxviewTabListBase {
+ constructor() {
+ super();
+ this.pinnedTabsGridView = false;
+ this.pinnedTabs = [];
+ this.unpinnedTabs = [];
+ }
+
+ static properties = {
+ pinnedTabsGridView: { type: Boolean },
+ };
+
+ static queries = {
+ ...FxviewTabListBase.queries,
+ rowEls: {
+ all: "opentabs-tab-row",
+ },
+ };
+
+ willUpdate(changes) {
+ this.activeIndex = Math.min(
+ Math.max(this.activeIndex, 0),
+ this.tabItems.length - 1
+ );
+
+ if (changes.has("dateTimeFormat") || changes.has("updatesPaused")) {
+ this.clearIntervalTimer();
+ if (!this.updatesPaused && this.dateTimeFormat == "relative") {
+ this.startIntervalTimer();
+ this.onIntervalUpdate();
+ }
+ }
+
+ // Move pinned tabs to the beginning of the list
+ if (this.pinnedTabsGridView) {
+ // Can set maxTabsLength to -1 to have no max
+ this.unpinnedTabs = this.tabItems.filter(
+ tab => !tab.indicators.includes("pinned")
+ );
+ this.pinnedTabs = this.tabItems.filter(tab =>
+ tab.indicators.includes("pinned")
+ );
+ if (this.maxTabsLength > 0) {
+ this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength);
+ }
+ this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs];
+ } else if (this.maxTabsLength > 0) {
+ this.tabItems = this.tabItems.slice(0, this.maxTabsLength);
+ }
+ }
+
+ /**
+ * Focuses the expected element (either the link or button) within fxview-tab-row
+ * The currently focused/active element ID within a row is stored in this.currentActiveElementId
+ */
+ handleFocusElementInRow(e) {
+ let fxviewTabRow = e.target;
+ if (e.code == "ArrowUp") {
+ // Focus either the link or button of the previous row based on this.currentActiveElementId
+ e.preventDefault();
+ if (
+ (this.pinnedTabsGridView &&
+ this.activeIndex >= this.pinnedTabs.length) ||
+ !this.pinnedTabsGridView
+ ) {
+ this.focusPrevRow();
+ }
+ } else if (e.code == "ArrowDown") {
+ // Focus either the link or button of the next row based on this.currentActiveElementId
+ e.preventDefault();
+ if (
+ this.pinnedTabsGridView &&
+ this.activeIndex < this.pinnedTabs.length
+ ) {
+ this.focusIndex(this.pinnedTabs.length);
+ } else {
+ this.focusNextRow();
+ }
+ } else if (e.code == "ArrowRight") {
+ // Focus either the link or the button in the current row and
+ // set this.currentActiveElementId to that element's ID
+ e.preventDefault();
+ if (document.dir == "rtl") {
+ fxviewTabRow.moveFocusLeft();
+ } else {
+ fxviewTabRow.moveFocusRight();
+ }
+ } else if (e.code == "ArrowLeft") {
+ // Focus either the link or the button in the current row and
+ // set this.currentActiveElementId to that element's ID
+ e.preventDefault();
+ if (document.dir == "rtl") {
+ fxviewTabRow.moveFocusRight();
+ } else {
+ fxviewTabRow.moveFocusLeft();
+ }
+ }
+ }
+
+ async focusIndex(index) {
+ // Focus link or button of item
+ if (
+ ((this.pinnedTabsGridView && index > this.pinnedTabs.length) ||
+ !this.pinnedTabsGridView) &&
+ lazy.virtualListEnabledPref
+ ) {
+ let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length);
+ if (!row) {
+ return;
+ }
+ let subList = this.rootVirtualListEl.getSubListForItem(
+ index - this.pinnedTabs.length
+ );
+ if (!subList) {
+ return;
+ }
+ this.activeIndex = index;
+
+ // In Bug 1866845, these manual updates to the sublists should be removed
+ // and scrollIntoView() should also be iterated on so that we aren't constantly
+ // moving the focused item to the center of the viewport
+ for (const sublist of Array.from(this.rootVirtualListEl.children)) {
+ await sublist.requestUpdate();
+ await sublist.updateComplete;
+ }
+ row.scrollIntoView({ block: "center" });
+ row.focus();
+ } else if (index >= 0 && index < this.rowEls?.length) {
+ this.rowEls[index].focus();
+ this.activeIndex = index;
+ }
+ }
+
+ #getTabListWrapperClasses() {
+ let wrapperClasses = ["fxview-tab-list"];
+ let tabsToCheck = this.pinnedTabsGridView
+ ? this.unpinnedTabs
+ : this.tabItems;
+ if (tabsToCheck.some(tab => tab.containerObj)) {
+ wrapperClasses.push(`hasContainerTab`);
+ }
+ return wrapperClasses;
+ }
+
+ itemTemplate = (tabItem, i) => {
+ let time;
+ if (tabItem.time || tabItem.closedAt) {
+ let stringTime = (tabItem.time || tabItem.closedAt).toString();
+ // Different APIs return time in different units, so we use
+ // the length to decide if it's milliseconds or nanoseconds.
+ if (stringTime.length === 16) {
+ time = (tabItem.time || tabItem.closedAt) / 1000;
+ } else {
+ time = tabItem.time || tabItem.closedAt;
+ }
+ }
+
+ return html`<opentabs-tab-row
+ ?active=${i == this.activeIndex}
+ class=${classMap({
+ pinned:
+ this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"),
+ })}
+ .currentActiveElementId=${this.currentActiveElementId}
+ .favicon=${tabItem.icon}
+ .compact=${this.compactRows}
+ .containerObj=${ifDefined(tabItem.containerObj)}
+ .indicators=${tabItem.indicators}
+ .pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)}
+ .primaryL10nId=${tabItem.primaryL10nId}
+ .primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
+ .secondaryL10nId=${tabItem.secondaryL10nId}
+ .secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
+ .tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)}
+ .tertiaryL10nArgs=${ifDefined(tabItem.tertiaryL10nArgs)}
+ .secondaryActionClass=${this.secondaryActionClass}
+ .tertiaryActionClass=${ifDefined(this.tertiaryActionClass)}
+ .sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
+ .sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
+ .closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
+ role=${tabItem.pinned && this.pinnedTabsGridView ? "tab" : "listitem"}
+ .tabElement=${ifDefined(tabItem.tabElement)}
+ .time=${ifDefined(time)}
+ .title=${tabItem.title}
+ .url=${tabItem.url}
+ .searchQuery=${ifDefined(this.searchQuery)}
+ .timeMsPref=${ifDefined(this.timeMsPref)}
+ .hasPopup=${this.hasPopup}
+ .dateTimeFormat=${this.dateTimeFormat}
+ ></opentabs-tab-row>`;
+ };
+
+ render() {
+ if (this.searchQuery && this.tabItems.length === 0) {
+ return this.emptySearchResultsTemplate();
+ }
+ return html`
+ ${this.stylesheets()}
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/firefoxview/opentabs-tab-list.css"
+ />
+ ${when(
+ this.pinnedTabsGridView && this.pinnedTabs.length,
+ () => html`
+ <div
+ id="fxview-tab-list"
+ class="fxview-tab-list pinned"
+ data-l10n-id="firefoxview-pinned-tabs"
+ role="tablist"
+ @keydown=${this.handleFocusElementInRow}
+ >
+ ${this.pinnedTabs.map((tabItem, i) =>
+ this.customItemTemplate
+ ? this.customItemTemplate(tabItem, i)
+ : this.itemTemplate(tabItem, i)
+ )}
+ </div>
+ `
+ )}
+ <div
+ id="fxview-tab-list"
+ class=${this.#getTabListWrapperClasses().join(" ")}
+ data-l10n-id="firefoxview-tabs"
+ role="list"
+ @keydown=${this.handleFocusElementInRow}
+ >
+ ${when(
+ lazy.virtualListEnabledPref,
+ () => html`
+ <virtual-list
+ .activeIndex=${this.activeIndex}
+ .pinnedTabsIndexOffset=${this.pinnedTabsGridView
+ ? this.pinnedTabs.length
+ : 0}
+ .items=${this.pinnedTabsGridView
+ ? this.unpinnedTabs
+ : this.tabItems}
+ .template=${this.itemTemplate}
+ ></virtual-list>
+ `,
+ () =>
+ html`${this.tabItems.map((tabItem, i) =>
+ this.itemTemplate(tabItem, i)
+ )}`
+ )}
+ </div>
+ <slot name="menu"></slot>
+ `;
+ }
+}
+customElements.define("opentabs-tab-list", OpenTabsTabList);
+
+/**
+ * A tab item that displays favicon, title, url, and time of last access
+ *
+ * @property {object} containerObj - Info about an open tab's container if within one
+ * @property {string} indicators - An array of tab indicators if any are present
+ * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view
+ */
+
+export class OpenTabsTabRow extends FxviewTabRowBase {
+ constructor() {
+ super();
+ this.indicators = [];
+ this.pinnedTabsGridView = false;
+ }
+
+ static properties = {
+ ...FxviewTabRowBase.properties,
+ containerObj: { type: Object },
+ indicators: { type: Array },
+ pinnedTabsGridView: { type: Boolean },
+ };
+
+ static queries = {
+ ...FxviewTabRowBase.queries,
+ mediaButtonEl: "#fxview-tab-row-media-button",
+ pinnedTabButtonEl: "moz-button#fxview-tab-row-main",
+ };
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.addEventListener("keydown", this.handleKeydown);
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListener("keydown", this.handleKeydown);
+ }
+
+ handleKeydown(e) {
+ if (
+ this.active &&
+ this.pinnedTabsGridView &&
+ this.indicators?.includes("pinned") &&
+ e.key === "m" &&
+ e.ctrlKey
+ ) {
+ this.muteOrUnmuteTab();
+ }
+ }
+
+ moveFocusRight() {
+ let tabList = this.getRootNode().host;
+ if (this.pinnedTabsGridView && this.indicators?.includes("pinned")) {
+ tabList.focusNextRow();
+ } else if (
+ (this.indicators?.includes("soundplaying") ||
+ this.indicators?.includes("muted")) &&
+ this.currentActiveElementId === "fxview-tab-row-main"
+ ) {
+ this.focusMediaButton();
+ } else if (
+ this.currentActiveElementId === "fxview-tab-row-media-button" ||
+ this.currentActiveElementId === "fxview-tab-row-main"
+ ) {
+ this.focusSecondaryButton();
+ } else if (
+ this.tertiaryButtonEl &&
+ this.currentActiveElementId === "fxview-tab-row-secondary-button"
+ ) {
+ this.focusTertiaryButton();
+ }
+ }
+
+ moveFocusLeft() {
+ let tabList = this.getRootNode().host;
+ if (
+ this.pinnedTabsGridView &&
+ (this.indicators?.includes("pinned") ||
+ (tabList.currentActiveElementId === "fxview-tab-row-main" &&
+ tabList.activeIndex === tabList.pinnedTabs.length))
+ ) {
+ tabList.focusPrevRow();
+ } else if (
+ tabList.currentActiveElementId === "fxview-tab-row-tertiary-button"
+ ) {
+ this.focusSecondaryButton();
+ } else if (
+ (this.indicators?.includes("soundplaying") ||
+ this.indicators?.includes("muted")) &&
+ tabList.currentActiveElementId === "fxview-tab-row-secondary-button"
+ ) {
+ this.focusMediaButton();
+ } else {
+ this.focusLink();
+ }
+ }
+
+ focusMediaButton() {
+ let tabList = this.getRootNode().host;
+ this.mediaButtonEl.focus();
+ tabList.currentActiveElementId = this.mediaButtonEl.id;
+ }
+
+ #secondaryActionHandler(event) {
+ if (
+ (this.pinnedTabsGridView &&
+ this.indicators?.includes("pinned") &&
+ event.type == "contextmenu") ||
+ (event.type == "click" && event.detail && !event.altKey) ||
+ // detail=0 is from keyboard
+ (event.type == "click" && !event.detail)
+ ) {
+ event.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent("fxview-tab-list-secondary-action", {
+ bubbles: true,
+ composed: true,
+ detail: { originalEvent: event, item: this },
+ })
+ );
+ }
+ }
+
+ #faviconTemplate() {
+ return html`<span
+ class="${classMap({
+ "fxview-tab-row-favicon-wrapper": true,
+ pinned: this.indicators?.includes("pinned"),
+ pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"),
+ attention: this.indicators?.includes("attention"),
+ bookmark: this.indicators?.includes("bookmark"),
+ })}"
+ >
+ <span
+ class="fxview-tab-row-favicon icon"
+ id="fxview-tab-row-favicon"
+ style=${styleMap({
+ backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
+ })}
+ ></span>
+ ${when(
+ this.pinnedTabsGridView &&
+ this.indicators?.includes("pinned") &&
+ (this.indicators?.includes("muted") ||
+ this.indicators?.includes("soundplaying")),
+ () => html`
+ <button
+ class="fxview-tab-row-pinned-media-button"
+ id="fxview-tab-row-media-button"
+ tabindex="-1"
+ data-l10n-id=${this.indicators?.includes("muted")
+ ? "fxviewtabrow-unmute-tab-button-no-context"
+ : "fxviewtabrow-mute-tab-button-no-context"}
+ muted=${this.indicators?.includes("muted")}
+ soundplaying=${this.indicators?.includes("soundplaying") &&
+ !this.indicators?.includes("muted")}
+ @click=${this.muteOrUnmuteTab}
+ ></button>
+ `
+ )}
+ </span>`;
+ }
+
+ #getContainerClasses() {
+ let containerClasses = ["fxview-tab-row-container-indicator", "icon"];
+ if (this.containerObj) {
+ let { icon, color } = this.containerObj;
+ containerClasses.push(`identity-icon-${icon}`);
+ containerClasses.push(`identity-color-${color}`);
+ }
+ return containerClasses;
+ }
+
+ muteOrUnmuteTab(e) {
+ e?.preventDefault();
+ // If the tab has no sound playing, the mute/unmute button will be removed when toggled.
+ // We should move the focus to the right in that case. This does not apply to pinned tabs
+ // on the Open Tabs page.
+ let shouldMoveFocus =
+ (!this.pinnedTabsGridView ||
+ (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) &&
+ this.mediaButtonEl &&
+ !this.indicators.includes("soundplaying") &&
+ this.currentActiveElementId === "fxview-tab-row-media-button";
+
+ // detail=0 is from keyboard
+ if (e?.type == "click" && !e?.detail && shouldMoveFocus) {
+ if (document.dir == "rtl") {
+ this.moveFocusLeft();
+ } else {
+ this.moveFocusRight();
+ }
+ }
+ this.tabElement.toggleMuteAudio();
+ }
+
+ #mediaButtonTemplate() {
+ return html`${when(
+ this.indicators?.includes("soundplaying") ||
+ this.indicators?.includes("muted"),
+ () => html`<moz-button
+ type="icon ghost"
+ class="fxview-tab-row-button"
+ id="fxview-tab-row-media-button"
+ data-l10n-id=${this.indicators?.includes("muted")
+ ? "fxviewtabrow-unmute-tab-button-no-context"
+ : "fxviewtabrow-mute-tab-button-no-context"}
+ muted=${this.indicators?.includes("muted")}
+ soundplaying=${this.indicators?.includes("soundplaying") &&
+ !this.indicators?.includes("muted")}
+ @click=${this.muteOrUnmuteTab}
+ tabindex="${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-media-button"
+ ? "0"
+ : "-1"}"
+ ></moz-button>`,
+ () => html`<span></span>`
+ )}`;
+ }
+
+ #containerIndicatorTemplate() {
+ let tabList = this.getRootNode().host;
+ let tabsToCheck = tabList.pinnedTabsGridView
+ ? tabList.unpinnedTabs
+ : tabList.tabItems;
+ return html`${when(
+ tabsToCheck.some(tab => tab.containerObj),
+ () => html`<span class=${this.#getContainerClasses().join(" ")}></span>`
+ )}`;
+ }
+
+ #pinnedTabItemTemplate() {
+ return html`
+ <moz-button
+ type="icon ghost"
+ id="fxview-tab-row-main"
+ aria-haspopup=${ifDefined(this.hasPopup)}
+ data-l10n-id=${ifDefined(this.primaryL10nId)}
+ data-l10n-args=${ifDefined(this.primaryL10nArgs)}
+ tabindex=${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-main"
+ ? "0"
+ : "-1"}
+ role="tab"
+ @click=${this.primaryActionHandler}
+ @keydown=${this.primaryActionHandler}
+ @contextmenu=${this.#secondaryActionHandler}
+ >
+ ${this.#faviconTemplate()}
+ </moz-button>
+ `;
+ }
+
+ #unpinnedTabItemTemplate() {
+ return html`<a
+ href=${ifDefined(this.url)}
+ class="fxview-tab-row-main"
+ id="fxview-tab-row-main"
+ tabindex=${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-main"
+ ? "0"
+ : "-1"}
+ data-l10n-id=${ifDefined(this.primaryL10nId)}
+ data-l10n-args=${ifDefined(this.primaryL10nArgs)}
+ @click=${this.primaryActionHandler}
+ @keydown=${this.primaryActionHandler}
+ title=${!this.primaryL10nId ? this.url : null}
+ >
+ ${this.#faviconTemplate()} ${this.titleTemplate()}
+ ${when(
+ !this.compact,
+ () => html`${this.#containerIndicatorTemplate()} ${this.urlTemplate()}
+ ${this.dateTemplate()} ${this.timeTemplate()}`
+ )}
+ </a>
+ ${this.#mediaButtonTemplate()} ${this.secondaryButtonTemplate()}
+ ${this.tertiaryButtonTemplate()}`;
+ }
+
+ render() {
+ return html`
+ ${this.stylesheets()}
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/firefoxview/opentabs-tab-row.css"
+ />
+ ${when(
+ this.containerObj,
+ () => html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/usercontext/usercontext.css"
+ />
+ `
+ )}
+ ${when(
+ this.pinnedTabsGridView && this.indicators?.includes("pinned"),
+ this.#pinnedTabItemTemplate.bind(this),
+ this.#unpinnedTabItemTemplate.bind(this)
+ )}
+ `;
+ }
+}
+customElements.define("opentabs-tab-row", OpenTabsTabRow);
diff --git a/browser/components/firefoxview/opentabs-tab-row.css b/browser/components/firefoxview/opentabs-tab-row.css
new file mode 100644
index 0000000000..e5c00884b3
--- /dev/null
+++ b/browser/components/firefoxview/opentabs-tab-row.css
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.fxview-tab-row-favicon-wrapper {
+ height: 16px;
+ position: relative;
+ display: block;
+
+ .fxview-tab-row-favicon::after,
+ .fxview-tab-row-button::after,
+ &.pinned .fxview-tab-row-pinned-media-button {
+ display: block;
+ content: "";
+ background-size: 12px;
+ background-position: center;
+ background-repeat: no-repeat;
+ position: relative;
+ height: 12px;
+ width: 12px;
+ -moz-context-properties: fill, stroke;
+ fill: currentColor;
+ stroke: var(--fxview-background-color-secondary);
+ }
+
+ &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after {
+ inset-block-start: 9px;
+ inset-inline-end: -6px;
+ }
+
+ &.pinnedOnNewTab .fxview-tab-row-favicon::after,
+ &.pinnedOnNewTab .fxview-tab-row-button::after {
+ background-image: url("chrome://browser/skin/pin-12.svg");
+ }
+
+ &.bookmark .fxview-tab-row-favicon::after,
+ &.bookmark .fxview-tab-row-button::after {
+ background-image: url("chrome://browser/skin/bookmark-12.svg");
+ fill: var(--fxview-primary-action-background);
+ }
+
+ &.attention .fxview-tab-row-favicon::after,
+ &.attention .fxview-tab-row-button::after {
+ background-image: radial-gradient(circle, var(--attention-dot-color), var(--attention-dot-color) 2px, transparent 2px);
+ height: 4px;
+ width: 100%;
+ inset-block-start: 20px;
+ }
+
+ &.pinned .fxview-tab-row-pinned-media-button {
+ inset-block-start: -5px;
+ inset-inline-end: 1px;
+ border: var(--button-border);
+ border-radius: 100%;
+ background-color: var(--fxview-background-color-secondary);
+ padding: 6px;
+ min-width: 0;
+ min-height: 0;
+ position: absolute;
+
+ &[muted="true"] {
+ background-image: url("chrome://global/skin/media/audio-muted.svg");
+ }
+
+ &[soundplaying="true"] {
+ background-image: url("chrome://global/skin/media/audio.svg");
+ }
+
+ &:active,
+ &:hover:active {
+ background-color: var(--button-background-color-active);
+ }
+ }
+}
+
+.fxview-tab-row-container-indicator {
+ height: 16px;
+ width: 16px;
+ background-image: var(--identity-icon);
+ background-size: cover;
+ -moz-context-properties: fill;
+ fill: var(--identity-icon-color);
+}
+
+.fxview-tab-row-main {
+ :host(.pinned) & {
+ padding: var(--space-small);
+ min-width: unset;
+ margin: 0;
+ }
+}
+
+button.fxview-tab-row-main:hover {
+ & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after {
+ stroke: var(--fxview-indicator-stroke-color-hover);
+ }
+}
+
+@media (prefers-contrast) {
+ button.fxview-tab-row-main {
+ border: 1px solid ButtonText;
+ color: ButtonText;
+ }
+
+ button.fxview-tab-row-main:enabled:hover {
+ border: 1px solid SelectedItem;
+ color: SelectedItem;
+ }
+
+ button.fxview-tab-row-main:enabled:active {
+ color: SelectedItem;
+ }
+
+ button.fxview-tab-row-main:enabled,
+ button.fxview-tab-row-main:enabled:hover,
+ button.fxview-tab-row-main:enabled:active {
+ background-color: ButtonFace;
+ }
+}
diff --git a/browser/components/firefoxview/opentabs.mjs b/browser/components/firefoxview/opentabs.mjs
index 8d7723e931..fb84553e26 100644
--- a/browser/components/firefoxview/opentabs.mjs
+++ b/browser/components/firefoxview/opentabs.mjs
@@ -17,6 +17,8 @@ import {
MAX_TABS_FOR_RECENT_BROWSING,
} from "./helpers.mjs";
import { ViewPage, ViewPageContent } from "./viewpage.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/firefoxview/opentabs-tab-list.mjs";
const lazy = {};
@@ -36,6 +38,9 @@ ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
).getFxAccountsSingleton();
});
+const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed";
+const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
+
/**
* A collection of open tabs grouped by window.
*
@@ -339,7 +344,7 @@ class OpenTabsInView extends ViewPage {
></view-opentabs-card>`;
}
- handleEvent({ detail, target, type }) {
+ handleEvent({ detail, type }) {
if (this.recentBrowsing && type === "fxview-search-textbox-query") {
this.onSearchQuery({ detail });
return;
@@ -424,7 +429,7 @@ class OpenTabsInViewCard extends ViewPageContent {
static queries = {
cardEl: "card-container",
tabContextMenu: "view-opentabs-contextmenu",
- tabList: "fxview-tab-list",
+ tabList: "opentabs-tab-list",
};
openContextMenu(e) {
@@ -565,7 +570,7 @@ class OpenTabsInViewCard extends ViewPageContent {
() => html`<h3 slot="header">${this.title}</h3>`
)}
<div class="fxview-tab-list-container" slot="main">
- <fxview-tab-list
+ <opentabs-tab-list
.hasPopup=${"menu"}
?compactRows=${this.classList.contains("width-limited")}
@fxview-tab-list-primary-action=${this.onTabListRowClick}
@@ -579,7 +584,7 @@ class OpenTabsInViewCard extends ViewPageContent {
.searchQuery=${this.searchQuery}
.pinnedTabsGridView=${!this.recentBrowsing}
><view-opentabs-contextmenu slot="menu"></view-opentabs-contextmenu>
- </fxview-tab-list>
+ </opentabs-tab-list>
</div>
${when(
this.recentBrowsing,
@@ -659,7 +664,7 @@ customElements.define("view-opentabs-card", OpenTabsInViewCard);
class OpenTabsContextMenu extends MozLitElement {
static properties = {
devices: { type: Array },
- triggerNode: { type: Object },
+ triggerNode: { hasChanged: () => true, type: Object },
};
static queries = {
@@ -669,6 +674,7 @@ class OpenTabsContextMenu extends MozLitElement {
constructor() {
super();
this.triggerNode = null;
+ this.boundObserve = (...args) => this.observe(...args);
this.devices = [];
}
@@ -680,6 +686,28 @@ class OpenTabsContextMenu extends MozLitElement {
return this.ownerDocument.querySelector("view-opentabs");
}
+ connectedCallback() {
+ super.connectedCallback();
+ this.fetchDevicesPromise = this.fetchDevices();
+ Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
+ Services.obs.addObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED);
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
+ Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED);
+ }
+
+ observe(_subject, topic, _data) {
+ if (
+ topic == TOPIC_DEVICELIST_UPDATED ||
+ topic == TOPIC_DEVICESTATE_CHANGED
+ ) {
+ this.fetchDevicesPromise = this.fetchDevices();
+ }
+ }
+
async fetchDevices() {
const currentWindow = this.ownerViewPage.getWindow();
if (currentWindow?.gSync) {
@@ -699,7 +727,7 @@ class OpenTabsContextMenu extends MozLitElement {
return;
}
this.triggerNode = triggerNode;
- await this.fetchDevices();
+ await this.fetchDevicesPromise;
await this.getUpdateComplete();
this.panelList.toggle(originalEvent);
}
@@ -1022,7 +1050,7 @@ function getTabListItems(tabs, isRecentBrowsing) {
? JSON.stringify({ tabTitle: tab.label })
: null,
tabElement: tab,
- time: tab.lastAccessed,
+ time: tab.lastSeenActive,
title: tab.label,
url,
};
diff --git a/browser/components/firefoxview/recentlyclosed.mjs b/browser/components/firefoxview/recentlyclosed.mjs
index 83c323256c..7efd8d09f2 100644
--- a/browser/components/firefoxview/recentlyclosed.mjs
+++ b/browser/components/firefoxview/recentlyclosed.mjs
@@ -65,7 +65,7 @@ class RecentlyClosedTabsInView extends ViewPage {
tabList: "fxview-tab-list",
};
- observe(subject, topic, data) {
+ observe(subject, topic) {
if (
topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED ||
(topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH &&
@@ -249,13 +249,22 @@ class RecentlyClosedTabsInView extends ViewPage {
onDismissTab(e) {
const closedId = parseInt(e.originalTarget.closedId, 10);
const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10);
- const sourceWindowId = e.originalTarget.souceWindowId;
- if (sourceWindowId || !isNaN(sourceClosedId)) {
+ const sourceWindowId = e.originalTarget.sourceWindowId;
+ if (!isNaN(sourceClosedId)) {
+ // the sourceClosedId is an identifier for a now-closed window the tab
+ // was closed in.
lazy.SessionStore.forgetClosedTabById(closedId, {
sourceClosedId,
+ });
+ } else if (sourceWindowId) {
+ // the sourceWindowId is an identifier for a currently-open window the tab
+ // was closed in.
+ lazy.SessionStore.forgetClosedTabById(closedId, {
sourceWindowId,
});
} else {
+ // without either identifier, SessionStore will need to walk its window collections
+ // to find the close tab with matching closedId
lazy.SessionStore.forgetClosedTabById(closedId);
}
@@ -387,7 +396,6 @@ class RecentlyClosedTabsInView extends ViewPage {
() =>
html`
<fxview-tab-list
- class="with-dismiss-button"
slot="main"
.maxTabsLength=${!this.recentBrowsing || this.showAll
? -1
diff --git a/browser/components/firefoxview/syncedtabs.mjs b/browser/components/firefoxview/syncedtabs.mjs
index d64da45a30..1c65650c10 100644
--- a/browser/components/firefoxview/syncedtabs.mjs
+++ b/browser/components/firefoxview/syncedtabs.mjs
@@ -4,13 +4,9 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
- SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
+ SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs",
});
-const { SyncedTabsErrorHandler } = ChromeUtils.importESModule(
- "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs"
-);
const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
);
@@ -24,43 +20,52 @@ import { ViewPage } from "./viewpage.mjs";
import {
escapeHtmlEntities,
isSearchEnabled,
- searchTabList,
MAX_TABS_FOR_RECENT_BROWSING,
+ navigateToLink,
} from "./helpers.mjs";
-const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
-const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open";
class SyncedTabsInView extends ViewPage {
+ controller = new lazy.SyncedTabsController(this, {
+ contextMenu: true,
+ pairDeviceCallback: () =>
+ Services.telemetry.recordEvent(
+ "firefoxview_next",
+ "fxa_mobile",
+ "sync",
+ null,
+ {
+ has_devices: TabsSetupFlowManager.secondaryDeviceConnected.toString(),
+ }
+ ),
+ signupCallback: () =>
+ Services.telemetry.recordEvent(
+ "firefoxview_next",
+ "fxa_continue",
+ "sync",
+ null
+ ),
+ });
+
constructor() {
super();
this._started = false;
- this.boundObserve = (...args) => this.observe(...args);
- this._currentSetupStateIndex = -1;
- this.errorState = null;
this._id = Math.floor(Math.random() * 10e6);
- this.currentSyncedTabs = [];
if (this.recentBrowsing) {
this.maxTabsLength = MAX_TABS_FOR_RECENT_BROWSING;
} else {
// Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1;
}
- this.devices = [];
this.fullyUpdated = false;
- this.searchQuery = "";
this.showAll = false;
this.cumulativeSearches = 0;
+ this.onSearchQuery = this.onSearchQuery.bind(this);
}
static properties = {
...ViewPage.properties,
- errorState: { type: Number },
- currentSyncedTabs: { type: Array },
- _currentSetupStateIndex: { type: Number },
- devices: { type: Array },
- searchQuery: { type: String },
showAll: { type: Boolean },
cumulativeSearches: { type: Number },
};
@@ -72,26 +77,19 @@ class SyncedTabsInView extends ViewPage {
tabLists: { all: "fxview-tab-list" },
};
- connectedCallback() {
- super.connectedCallback();
- this.addEventListener("click", this);
- }
-
start() {
if (this._started) {
return;
}
this._started = true;
- Services.obs.addObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
- Services.obs.addObserver(this.boundObserve, SYNCED_TABS_CHANGED);
-
- this.updateStates();
+ this.controller.addSyncObservers();
+ this.controller.updateStates();
this.onVisibilityChange();
if (this.recentBrowsing) {
this.recentBrowsingElement.addEventListener(
"fxview-search-textbox-query",
- this
+ this.onSearchQuery
);
}
}
@@ -103,75 +101,21 @@ class SyncedTabsInView extends ViewPage {
this._started = false;
TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded");
this.onVisibilityChange();
-
- Services.obs.removeObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
- Services.obs.removeObserver(this.boundObserve, SYNCED_TABS_CHANGED);
+ this.controller.removeSyncObservers();
if (this.recentBrowsing) {
this.recentBrowsingElement.removeEventListener(
"fxview-search-textbox-query",
- this
+ this.onSearchQuery
);
}
}
- willUpdate(changedProperties) {
- if (changedProperties.has("searchQuery")) {
- this.cumulativeSearches = this.searchQuery
- ? this.cumulativeSearches + 1
- : 0;
- }
- }
-
disconnectedCallback() {
super.disconnectedCallback();
this.stop();
}
- handleEvent(event) {
- if (event.type == "click" && event.target.dataset.action) {
- const { ErrorType } = SyncedTabsErrorHandler;
- switch (event.target.dataset.action) {
- case `${ErrorType.SYNC_ERROR}`:
- case `${ErrorType.NETWORK_OFFLINE}`:
- case `${ErrorType.PASSWORD_LOCKED}`: {
- TabsSetupFlowManager.tryToClearError();
- break;
- }
- case `${ErrorType.SIGNED_OUT}`:
- case "sign-in": {
- TabsSetupFlowManager.openFxASignup(event.target.ownerGlobal);
- break;
- }
- case "add-device": {
- TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal);
- break;
- }
- case "sync-tabs-disabled": {
- TabsSetupFlowManager.syncOpenTabs(event.target);
- break;
- }
- case `${ErrorType.SYNC_DISCONNECTED}`: {
- const win = event.target.ownerGlobal;
- const { switchToTabHavingURI } =
- win.docShell.chromeEventHandler.ownerGlobal;
- switchToTabHavingURI(
- "about:preferences?action=choose-what-to-sync#sync",
- true,
- {}
- );
- break;
- }
- }
- }
- if (event.type == "change") {
- TabsSetupFlowManager.syncOpenTabs(event.target);
- }
- if (this.recentBrowsing && event.type === "fxview-search-textbox-query") {
- this.onSearchQuery(event);
- }
- }
-
viewVisibleCallback() {
this.start();
}
@@ -196,90 +140,16 @@ class SyncedTabsInView extends ViewPage {
this.toggleVisibilityInCardContainer();
}
- async observe(subject, topic, errorState) {
- if (topic == TOPIC_SETUPSTATE_CHANGED) {
- this.updateStates(errorState);
- }
- if (topic == SYNCED_TABS_CHANGED) {
- this.getSyncedTabData();
- }
- }
-
- updateStates(errorState) {
- let stateIndex = TabsSetupFlowManager.uiStateIndex;
- errorState = errorState || SyncedTabsErrorHandler.getErrorType();
-
- if (stateIndex == 4 && this._currentSetupStateIndex !== stateIndex) {
- // trigger an initial request for the synced tabs list
- this.getSyncedTabData();
- }
-
- this._currentSetupStateIndex = stateIndex;
- this.errorState = errorState;
- }
-
- actionMappings = {
- "sign-in": {
- header: "firefoxview-syncedtabs-signin-header",
- description: "firefoxview-syncedtabs-signin-description",
- buttonLabel: "firefoxview-syncedtabs-signin-primarybutton",
- },
- "add-device": {
- header: "firefoxview-syncedtabs-adddevice-header",
- description: "firefoxview-syncedtabs-adddevice-description",
- buttonLabel: "firefoxview-syncedtabs-adddevice-primarybutton",
- descriptionLink: {
- name: "url",
- url: "https://support.mozilla.org/kb/how-do-i-set-sync-my-computer#w_connect-additional-devices-to-sync",
- },
- },
- "sync-tabs-disabled": {
- header: "firefoxview-syncedtabs-synctabs-header",
- description: "firefoxview-syncedtabs-synctabs-description",
- buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton",
- },
- loading: {
- header: "firefoxview-syncedtabs-loading-header",
- description: "firefoxview-syncedtabs-loading-description",
- },
- };
-
- generateMessageCard({ error = false, action, errorState }) {
- errorState = errorState || this.errorState;
- let header,
- description,
- descriptionLink,
- buttonLabel,
- headerIconUrl,
- mainImageUrl;
- let descriptionArray;
- if (error) {
- let link;
- ({ header, description, link, buttonLabel } =
- SyncedTabsErrorHandler.getFluentStringsForErrorType(errorState));
- action = `${errorState}`;
- headerIconUrl = "chrome://global/skin/icons/info-filled.svg";
- mainImageUrl =
- "chrome://browser/content/firefoxview/synced-tabs-error.svg";
- descriptionArray = [description];
- if (errorState == "password-locked") {
- descriptionLink = {};
- // This is ugly, but we need to special case this link so we can
- // coexist with the old view.
- descriptionArray.push("firefoxview-syncedtab-password-locked-link");
- descriptionLink.name = "syncedtab-password-locked-link";
- descriptionLink.url = link.href;
- }
- } else {
- header = this.actionMappings[action].header;
- description = this.actionMappings[action].description;
- buttonLabel = this.actionMappings[action].buttonLabel;
- descriptionLink = this.actionMappings[action].descriptionLink;
- mainImageUrl =
- "chrome://browser/content/firefoxview/synced-tabs-error.svg";
- descriptionArray = [description];
- }
-
+ generateMessageCard({
+ action,
+ buttonLabel,
+ descriptionArray,
+ descriptionLink,
+ error,
+ header,
+ headerIconUrl,
+ mainImageUrl,
+ }) {
return html`
<fxview-empty-state
headerLabel=${header}
@@ -299,7 +169,7 @@ class SyncedTabsInView extends ViewPage {
?hidden=${!buttonLabel}
data-l10n-id="${ifDefined(buttonLabel)}"
data-action="${action}"
- @click=${this.handleEvent}
+ @click=${e => this.controller.handleEvent(e)}
aria-details="empty-container"
></button>
</fxview-empty-state>
@@ -307,28 +177,19 @@ class SyncedTabsInView extends ViewPage {
}
onOpenLink(event) {
- let currentWindow = this.getWindow();
- if (currentWindow.openTrustedLinkIn) {
- let where = lazy.BrowserUtils.whereToOpenLink(
- event.detail.originalEvent,
- false,
- true
- );
- if (where == "current") {
- where = "tab";
+ navigateToLink(event);
+
+ Services.telemetry.recordEvent(
+ "firefoxview_next",
+ "synced_tabs",
+ "tabs",
+ null,
+ {
+ page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
}
- currentWindow.openTrustedLinkIn(event.originalTarget.url, where);
- Services.telemetry.recordEvent(
- "firefoxview_next",
- "synced_tabs",
- "tabs",
- null,
- {
- page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
- }
- );
- }
- if (this.searchQuery) {
+ );
+
+ if (this.controller.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
@@ -384,7 +245,7 @@ class SyncedTabsInView extends ViewPage {
class="blackbox notabs search-results-empty"
data-l10n-id="firefoxview-search-results-empty"
data-l10n-args=${JSON.stringify({
- query: escapeHtmlEntities(this.searchQuery),
+ query: escapeHtmlEntities(this.controller.searchQuery),
})}
></div>
`,
@@ -405,7 +266,8 @@ class SyncedTabsInView extends ViewPage {
}
onSearchQuery(e) {
- this.searchQuery = e.detail.query;
+ this.controller.searchQuery = e.detail.query;
+ this.cumulativeSearches = e.detail.query ? this.cumulativeSearches + 1 : 0;
this.showAll = false;
}
@@ -422,7 +284,7 @@ class SyncedTabsInView extends ViewPage {
secondaryActionClass="options-button"
hasPopup="menu"
.tabItems=${ifDefined(tabItems)}
- .searchQuery=${this.searchQuery}
+ .searchQuery=${this.controller.searchQuery}
maxTabsLength=${this.showAll ? -1 : this.maxTabsLength}
@fxview-tab-list-primary-action=${this.onOpenLink}
@fxview-tab-list-secondary-action=${this.onContextMenu}
@@ -434,33 +296,9 @@ class SyncedTabsInView extends ViewPage {
generateTabList() {
let renderArray = [];
- let renderInfo = {};
- for (let tab of this.currentSyncedTabs) {
- if (!(tab.client in renderInfo)) {
- renderInfo[tab.client] = {
- name: tab.device,
- deviceType: tab.deviceType,
- tabs: [],
- };
- }
- renderInfo[tab.client].tabs.push(tab);
- }
-
- // Add devices without tabs
- for (let device of this.devices) {
- if (!(device.id in renderInfo)) {
- renderInfo[device.id] = {
- name: device.name,
- deviceType: device.clientType,
- tabs: [],
- };
- }
- }
-
+ let renderInfo = this.controller.getRenderInfo();
for (let id in renderInfo) {
- let tabItems = this.searchQuery
- ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs))
- : this.getTabItems(renderInfo[id].tabs);
+ let tabItems = renderInfo[id].tabItems;
if (tabItems.length) {
const template = this.recentBrowsing
? this.deviceTemplate(
@@ -509,7 +347,7 @@ class SyncedTabsInView extends ViewPage {
isShowAllLinkVisible(tabItems) {
return (
this.recentBrowsing &&
- this.searchQuery &&
+ this.controller.searchQuery &&
tabItems.length > this.maxTabsLength &&
!this.showAll
);
@@ -536,35 +374,10 @@ class SyncedTabsInView extends ViewPage {
}
generateCardContent() {
- switch (this._currentSetupStateIndex) {
- case 0 /* error-state */:
- if (this.errorState) {
- return this.generateMessageCard({ error: true });
- }
- return this.generateMessageCard({ action: "loading" });
- case 1 /* not-signed-in */:
- if (Services.prefs.prefHasUserValue("services.sync.lastversion")) {
- // If this pref is set, the user has signed out of sync.
- // This path is also taken if we are disconnected from sync. See bug 1784055
- return this.generateMessageCard({
- error: true,
- errorState: "signed-out",
- });
- }
- return this.generateMessageCard({ action: "sign-in" });
- case 2 /* connect-secondary-device*/:
- return this.generateMessageCard({ action: "add-device" });
- case 3 /* disabled-tab-sync */:
- return this.generateMessageCard({ action: "sync-tabs-disabled" });
- case 4 /* synced-tabs-loaded*/:
- // There seems to be an edge case where sync says everything worked
- // fine but we have no devices.
- if (!this.devices.length) {
- return this.generateMessageCard({ action: "add-device" });
- }
- return this.generateTabList();
- }
- return html``;
+ const cardProperties = this.controller.getMessageCard();
+ return cardProperties
+ ? this.generateMessageCard(cardProperties)
+ : this.generateTabList();
}
render() {
@@ -589,7 +402,7 @@ class SyncedTabsInView extends ViewPage {
data-l10n-id="firefoxview-synced-tabs-header"
></h2>
${when(
- isSearchEnabled() || this._currentSetupStateIndex === 4,
+ isSearchEnabled() || this.controller.currentSetupStateIndex === 4,
() => html`<div class="syncedtabs-header">
${when(
isSearchEnabled(),
@@ -606,12 +419,12 @@ class SyncedTabsInView extends ViewPage {
</div>`
)}
${when(
- this._currentSetupStateIndex === 4,
+ this.controller.currentSetupStateIndex === 4,
() => html`
<button
class="small-button"
data-action="add-device"
- @click=${this.handleEvent}
+ @click=${e => this.controller.handleEvent(e)}
>
<img
class="icon"
@@ -635,9 +448,9 @@ class SyncedTabsInView extends ViewPage {
html`<card-container
preserveCollapseState
shortPageName="syncedtabs"
- ?showViewAll=${this._currentSetupStateIndex == 4 &&
- this.currentSyncedTabs.length}
- ?isEmptyState=${!this.currentSyncedTabs.length}
+ ?showViewAll=${this.controller.currentSetupStateIndex == 4 &&
+ this.controller.currentSyncedTabs.length}
+ ?isEmptyState=${!this.controller.currentSyncedTabs.length}
>
>
<h3
@@ -656,71 +469,9 @@ class SyncedTabsInView extends ViewPage {
return renderArray;
}
- async onReload() {
- await TabsSetupFlowManager.syncOnPageReload();
- }
-
- getTabItems(tabs) {
- tabs = tabs || this.tabs;
- return tabs?.map(tab => ({
- icon: tab.icon,
- title: tab.title,
- time: tab.lastUsed * 1000,
- url: tab.url,
- primaryL10nId: "firefoxview-tabs-list-tab-button",
- primaryL10nArgs: JSON.stringify({ targetURI: tab.url }),
- secondaryL10nId: "fxviewtabrow-options-menu-button",
- secondaryL10nArgs: JSON.stringify({ tabTitle: tab.title }),
- }));
- }
-
- updateTabsList(syncedTabs) {
- if (!syncedTabs.length) {
- this.currentSyncedTabs = syncedTabs;
- this.sendTabTelemetry(0);
- }
-
- const tabsToRender = syncedTabs;
-
- // Return early if new tabs are the same as previous ones
- if (
- JSON.stringify(tabsToRender) == JSON.stringify(this.currentSyncedTabs)
- ) {
- return;
- }
-
- this.currentSyncedTabs = tabsToRender;
- // Record the full tab count
- this.sendTabTelemetry(syncedTabs.length);
- }
-
- async getSyncedTabData() {
- this.devices = await lazy.SyncedTabs.getTabClients();
- let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, {
- removeAllDupes: false,
- removeDeviceDupes: true,
- });
-
- this.updateTabsList(tabs);
- }
-
updated() {
this.fullyUpdated = true;
this.toggleVisibilityInCardContainer();
}
-
- sendTabTelemetry(numTabs) {
- /*
- Services.telemetry.recordEvent(
- "firefoxview_next",
- "synced_tabs",
- "tabs",
- null,
- {
- count: numTabs.toString(),
- }
- );
-*/
- }
}
customElements.define("view-syncedtabs", SyncedTabsInView);
diff --git a/browser/components/firefoxview/tests/browser/browser.toml b/browser/components/firefoxview/tests/browser/browser.toml
index 9f9c1c0176..db8b2ea25c 100644
--- a/browser/components/firefoxview/tests/browser/browser.toml
+++ b/browser/components/firefoxview/tests/browser/browser.toml
@@ -27,6 +27,8 @@ skip-if = ["true"] # Bug 1869605 and # Bug 1870296
["browser_firefoxview.js"]
+["browser_firefoxview_dragDrop_pinned_tab.js"]
+
["browser_firefoxview_general_telemetry.js"]
["browser_firefoxview_navigation.js"]
@@ -51,17 +53,15 @@ skip-if = ["true"] # Bug 1851453
["browser_opentabs_firefoxview.js"]
["browser_opentabs_more.js"]
-fail-if = ["a11y_checks"] # Bugs 1858041, 1854625, and 1872174 clicked Show all link is not accessible because it is "hidden" when clicked
skip-if = ["verify"] # Bug 1886017
["browser_opentabs_pinned_tabs.js"]
["browser_opentabs_recency.js"]
skip-if = [
- "os == 'win'",
- "os == 'mac' && verify",
+ "os == 'mac'",
"os == 'linux'"
-] # macos times out, see bug 1857293, skipped for windows, see bug 1858460, Bug 1875877 - frequent fails on linux.
+] # macos times out, see bug 1857293, Bug 1875877 - frequent fails on linux.
["browser_opentabs_search.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js
new file mode 100644
index 0000000000..dd30d53030
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function dragAndDrop(
+ tab1,
+ tab2,
+ initialWindow = window,
+ destWindow = window,
+ afterTab = true,
+ context
+) {
+ let rect = tab2.getBoundingClientRect();
+ let event = {
+ ctrlKey: false,
+ altKey: false,
+ clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1),
+ clientY: rect.top + rect.height / 2,
+ };
+
+ if (destWindow != initialWindow) {
+ // Make sure that both tab1 and tab2 are visible
+ initialWindow.focus();
+ initialWindow.moveTo(rect.left, rect.top + rect.height * 3);
+ }
+
+ EventUtils.synthesizeDrop(
+ tab1,
+ tab2,
+ null,
+ "move",
+ initialWindow,
+ destWindow,
+ event
+ );
+
+ // Ensure dnd suppression is cleared.
+ EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, context);
+}
+
+add_task(async function () {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, URLs[0]);
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, URLs[1]);
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ let win1 = browser.ownerGlobal;
+ await navigateToViewAndWait(document, "opentabs");
+
+ let openTabs = document.querySelector("view-opentabs[name=opentabs]");
+ await openTabs.updateComplete;
+ await TestUtils.waitForCondition(
+ () => openTabs.viewCards[0].tabList.rowEls.length
+ );
+ await openTabs.openTabsTarget.readyWindowsPromise;
+ let card = openTabs.viewCards[0];
+ let tabRows = card.tabList.rowEls;
+ let tabChangeRaised;
+
+ // Pin first two tabs
+ for (var i = 0; i < 2; i++) {
+ tabChangeRaised = BrowserTestUtils.waitForEvent(
+ NonPrivateTabs,
+ "TabChange"
+ );
+ let currentTabEl = tabRows[i];
+ let currentTab = currentTabEl.tabElement;
+ info(`Pinning tab ${i + 1} with label: ${currentTab.label}`);
+ win1.gBrowser.pinTab(currentTab);
+ await tabChangeRaised;
+ await openTabs.updateComplete;
+ tabRows = card.tabList.rowEls;
+ currentTabEl = tabRows[i];
+
+ await TestUtils.waitForCondition(
+ () => currentTabEl.indicators.includes("pinned"),
+ `Tab ${i + 1} is pinned.`
+ );
+ }
+
+ info(`First two tabs are pinned.`);
+
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await openTabs.updateComplete;
+ await TestUtils.waitForCondition(
+ () => openTabs.viewCards.length === 2,
+ "Two windows are shown for Open Tabs in in Fx View."
+ );
+
+ let pinnedTab = win1.gBrowser.visibleTabs[0];
+ let newWindowTab = win2.gBrowser.visibleTabs[0];
+
+ dragAndDrop(newWindowTab, pinnedTab, win2, win1, true, content);
+
+ await switchToFxViewTab();
+ await openTabs.updateComplete;
+ await TestUtils.waitForCondition(
+ () => openTabs.viewCards.length === 1,
+ "One window is shown for Open Tabs in in Fx View."
+ );
+ });
+ cleanupTabs();
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js
index e61b48b472..52dfce962d 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js
@@ -191,42 +191,6 @@ async function checkFxRenderCalls(browser, elements, selectedView) {
sandbox.restore();
}
-function dragAndDrop(
- tab1,
- tab2,
- initialWindow = window,
- destWindow = window,
- afterTab = true,
- context
-) {
- let rect = tab2.getBoundingClientRect();
- let event = {
- ctrlKey: false,
- altKey: false,
- clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1),
- clientY: rect.top + rect.height / 2,
- };
-
- if (destWindow != initialWindow) {
- // Make sure that both tab1 and tab2 are visible
- initialWindow.focus();
- initialWindow.moveTo(rect.left, rect.top + rect.height * 3);
- }
-
- EventUtils.synthesizeDrop(
- tab1,
- tab2,
- null,
- "move",
- initialWindow,
- destWindow,
- event
- );
-
- // Ensure dnd suppression is cleared.
- EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, context);
-}
-
add_task(async function test_recentbrowsing() {
await setupOpenAndClosedTabs();
@@ -438,66 +402,3 @@ add_task(async function test_recentlyclosed() {
});
await BrowserTestUtils.removeTab(TestTabs.tab2);
});
-
-add_task(async function test_drag_drop_pinned_tab() {
- await setupOpenAndClosedTabs();
- await withFirefoxView({}, async browser => {
- const { document } = browser.contentWindow;
- let win1 = browser.ownerGlobal;
- await navigateToViewAndWait(document, "opentabs");
-
- let openTabs = document.querySelector("view-opentabs[name=opentabs]");
- await openTabs.updateComplete;
- await TestUtils.waitForCondition(
- () => openTabs.viewCards[0].tabList.rowEls.length
- );
- await openTabs.openTabsTarget.readyWindowsPromise;
- let card = openTabs.viewCards[0];
- let tabRows = card.tabList.rowEls;
- let tabChangeRaised;
-
- // Pin first two tabs
- for (var i = 0; i < 2; i++) {
- tabChangeRaised = BrowserTestUtils.waitForEvent(
- NonPrivateTabs,
- "TabChange"
- );
- let currentTabEl = tabRows[i];
- let currentTab = currentTabEl.tabElement;
- info(`Pinning tab ${i + 1} with label: ${currentTab.label}`);
- win1.gBrowser.pinTab(currentTab);
- await tabChangeRaised;
- await openTabs.updateComplete;
- tabRows = card.tabList.rowEls;
- currentTabEl = tabRows[i];
-
- await TestUtils.waitForCondition(
- () => currentTabEl.indicators.includes("pinned"),
- `Tab ${i + 1} is pinned.`
- );
- }
-
- info(`First two tabs are pinned.`);
-
- let win2 = await BrowserTestUtils.openNewBrowserWindow();
-
- await openTabs.updateComplete;
- await TestUtils.waitForCondition(
- () => openTabs.viewCards.length === 2,
- "Two windows are shown for Open Tabs in in Fx View."
- );
-
- let pinnedTab = win1.gBrowser.visibleTabs[0];
- let newWindowTab = win2.gBrowser.visibleTabs[0];
-
- dragAndDrop(newWindowTab, pinnedTab, win2, win1, true, content);
-
- await switchToFxViewTab();
- await openTabs.updateComplete;
- await TestUtils.waitForCondition(
- () => openTabs.viewCards.length === 1,
- "One window is shown for Open Tabs in in Fx View."
- );
- });
- cleanupTabs();
-});
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js
index c76a11d3ad..e1aa58ae49 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js
@@ -537,7 +537,7 @@ add_task(async function test_cumulative_searches_history_telemetry() {
() =>
history.fullyUpdated &&
history?.lists[0].rowEls?.length === 1 &&
- history?.searchQuery,
+ history?.controller?.searchQuery,
"Expected search results are not shown yet."
);
@@ -605,7 +605,8 @@ add_task(async function test_cumulative_searches_syncedtabs_telemetry() {
);
await TestUtils.waitForCondition(
() =>
- syncedTabs.tabLists[0].rowEls.length === 1 && syncedTabs?.searchQuery,
+ syncedTabs.tabLists[0].rowEls.length === 1 &&
+ syncedTabs.controller.searchQuery,
"Expected search results are not shown yet."
);
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js
index 037729ea7d..b556649d52 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js
@@ -78,7 +78,7 @@ add_task(async function aria_attributes() {
"true",
'Firefox View button should have `aria-pressed="true"` upon selecting it'
);
- win.BrowserOpenTab();
+ win.BrowserCommands.openTab();
is(
win.FirefoxViewHandler.button.getAttribute("aria-pressed"),
"false",
@@ -118,8 +118,8 @@ add_task(async function homepage_new_tab() {
win.gBrowser.tabContainer,
"TabOpen"
);
- win.BrowserHome();
- info("Waiting for BrowserHome() to open a new tab");
+ win.BrowserCommands.home();
+ info("Waiting for BrowserCommands.home() to open a new tab");
await newTabOpened;
assertFirefoxViewTab(win);
ok(
diff --git a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
index c4c096acff..847ce4d9fd 100644
--- a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
@@ -58,14 +58,14 @@ function isElInViewport(element) {
async function historyComponentReady(historyComponent, expectedHistoryItems) {
await TestUtils.waitForCondition(
() =>
- [...historyComponent.allHistoryItems.values()].reduce(
+ [...historyComponent.controller.allHistoryItems.values()].reduce(
(acc, { length }) => acc + length,
0
) === expectedHistoryItems,
"History component ready"
);
- let expected = historyComponent.historyMapByDate.length;
+ let expected = historyComponent.controller.historyMapByDate.length;
let actual = historyComponent.cards.length;
is(expected, actual, `Total number of cards should be ${expected}`);
@@ -242,7 +242,8 @@ add_task(async function test_list_ordering() {
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
await sortHistoryTelemetry(sortHistoryEvent);
- let expectedNumOfCards = historyComponent.historyMapBySite.length;
+ let expectedNumOfCards =
+ historyComponent.controller.historyMapBySite.length;
info(`Total number of cards should be ${expectedNumOfCards}`);
await BrowserTestUtils.waitForMutationCondition(
@@ -345,7 +346,7 @@ add_task(async function test_empty_states() {
"Import history banner is shown"
);
let importHistoryCloseButton =
- historyComponent.cards[0].querySelector("button.close");
+ historyComponent.cards[0].querySelector("moz-button.close");
importHistoryCloseButton.click();
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
ok(
@@ -484,7 +485,7 @@ add_task(async function test_search_history() {
{ childList: true, subtree: true },
() =>
historyComponent.cards.length ===
- historyComponent.historyMapByDate.length
+ historyComponent.controller.historyMapByDate.length
);
searchTextbox.blur();
@@ -513,7 +514,7 @@ add_task(async function test_search_history() {
{ childList: true, subtree: true },
() =>
historyComponent.cards.length ===
- historyComponent.historyMapByDate.length
+ historyComponent.controller.historyMapByDate.length
);
});
});
@@ -528,7 +529,7 @@ add_task(async function test_persist_collapse_card_after_view_change() {
historyComponent.profileAge = 8;
await TestUtils.waitForCondition(
() =>
- [...historyComponent.allHistoryItems.values()].reduce(
+ [...historyComponent.controller.allHistoryItems.values()].reduce(
(acc, { length }) => acc + length,
0
) === 4
diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js b/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js
index d4de3ae5a9..5fdcf89d70 100644
--- a/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js
+++ b/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js
@@ -203,13 +203,15 @@ add_task(async function open_tab_new_window() {
const cards = getOpenTabsCards(openTabs);
const originalWinRows = await getTabRowsForCard(cards[1]);
const [row] = originalWinRows;
+
+ // We hide date/time and URL columns in tab rows when there are multiple window cards for spacial reasons
ok(
- row.shadowRoot.getElementById("fxview-tab-row-url").hidden,
- "The URL is hidden, since we have two windows."
+ !row.shadowRoot.getElementById("fxview-tab-row-url"),
+ "The URL span element isn't found within the tab row as expected, since we have two open windows."
);
ok(
- row.shadowRoot.getElementById("fxview-tab-row-date").hidden,
- "The date is hidden, since we have two windows."
+ !row.shadowRoot.getElementById("fxview-tab-row-date"),
+ "The date span element isn't found within the tab row as expected, since we have two open windows."
);
info("Select a tab from the original window.");
tabChangeRaised = BrowserTestUtils.waitForEvent(
diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js
index 955c2363d7..2c415e7aa2 100644
--- a/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js
@@ -131,7 +131,7 @@ async function moreMenuSetup() {
}
add_task(async function test_close_open_tab() {
- await withFirefoxView({}, async browser => {
+ await withFirefoxView({}, async () => {
const [cards, rows] = await moreMenuSetup();
const firstTab = rows[0];
const tertiaryButtonEl = firstTab.tertiaryButtonEl;
@@ -321,7 +321,7 @@ add_task(async function test_send_device_submenu() {
.stub(gSync, "getSendTabTargets")
.callsFake(() => fxaDevicesWithCommands);
- await withFirefoxView({}, async browser => {
+ await withFirefoxView({}, async () => {
// TEST_URL1 is our only tab, left over from previous test
Assert.deepEqual(
getVisibleTabURLs(),
diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js b/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js
index ee3f9981e1..fc10ef2eb0 100644
--- a/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js
+++ b/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js
@@ -2,23 +2,30 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
- This test checks the recent-browsing view of open tabs in about:firefoxview next
+ This test checks that the recent-browsing view of open tabs in about:firefoxview
presents the correct tab data in the correct order.
*/
+SimpleTest.requestCompleteLog();
+
+const { ObjectUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ObjectUtils.sys.mjs"
+);
+let origBrowserState;
const tabURL1 = "data:,Tab1";
const tabURL2 = "data:,Tab2";
const tabURL3 = "data:,Tab3";
const tabURL4 = "data:,Tab4";
-let gInitialTab;
-let gInitialTabURL;
-
add_setup(function () {
- gInitialTab = gBrowser.selectedTab;
- gInitialTabURL = tabUrl(gInitialTab);
+ origBrowserState = SessionStore.getBrowserState();
});
+async function cleanup() {
+ await switchToWindow(window);
+ await SessionStoreTestUtils.promiseBrowserState(origBrowserState);
+}
+
function tabUrl(tab) {
return tab.linkedBrowser.currentURI?.spec;
}
@@ -37,6 +44,12 @@ async function minimizeWindow(win) {
ok(win.document.hidden, "Top level window should be hidden");
}
+function getAllSelectedTabURLs() {
+ return BrowserWindowTracker.orderedWindows.map(win =>
+ tabUrl(win.gBrowser.selectedTab)
+ );
+}
+
async function restoreWindow(win) {
ok(win.document.hidden, "Top level window should be hidden");
let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
@@ -93,86 +106,91 @@ async function restoreWindow(win) {
ok(!win.document.hidden, "Top level window should be visible");
}
-async function prepareOpenTabs(urls, win = window) {
- const reusableTabURLs = ["about:newtab", "about:blank"];
- const gBrowser = win.gBrowser;
-
- for (let url of urls) {
- if (
- gBrowser.visibleTabs.length == 1 &&
- reusableTabURLs.includes(gBrowser.selectedBrowser.currentURI.spec)
- ) {
- // we'll load into this tab rather than opening a new one
- info(
- `Loading ${url} into blank tab: ${gBrowser.selectedBrowser.currentURI.spec}`
- );
- BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
- await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, null, url);
- } else {
- info(`Loading ${url} into new tab`);
- await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
- }
- await new Promise(res => win.requestAnimationFrame(res));
+async function prepareOpenWindowsAndTabs(windowsData) {
+ // windowsData selected tab URL should be unique so we can map tab URL to window
+ const browserState = {
+ windows: windowsData.map((winData, index) => {
+ const tabs = winData.tabs.map(url => ({
+ entries: [{ url, triggeringPrincipal_base64 }],
+ }));
+ return {
+ tabs,
+ selected: winData.selectedIndex + 1,
+ zIndex: index + 1,
+ };
+ }),
+ };
+ await SessionStoreTestUtils.promiseBrowserState(browserState);
+ await NonPrivateTabs.readyWindowsPromise;
+ const selectedTabURLOrder = browserState.windows.map(winData => {
+ return winData.tabs[winData.selected - 1].entries[0].url;
+ });
+ const windowByTabURL = new Map();
+ for (let win of BrowserWindowTracker.orderedWindows) {
+ windowByTabURL.set(tabUrl(win.gBrowser.selectedTab), win);
}
- Assert.equal(
- gBrowser.visibleTabs.length,
- urls.length,
- `Prepared ${urls.length} tabs as expected`
- );
- Assert.equal(
- tabUrl(gBrowser.selectedTab),
- urls[urls.length - 1],
- "The selectedTab is the last of the URLs given as expected"
+ is(
+ windowByTabURL.size,
+ windowsData.length,
+ "The tab URL to window mapping includes an entry for each window"
);
-}
-
-async function cleanup(...windowsToClose) {
- await Promise.all(
- windowsToClose.map(win => BrowserTestUtils.closeWindow(win))
+ info(
+ `After promiseBrowserState, selected tab order is: ${Array.from(
+ windowByTabURL.keys()
+ )}`
);
- while (gBrowser.visibleTabs.length > 1) {
- await SessionStoreTestUtils.closeTab(gBrowser.tabs.at(-1));
- }
- if (gBrowser.selectedBrowser.currentURI.spec !== gInitialTabURL) {
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- gInitialTabURL
- );
- await BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- null,
- gInitialTabURL
- );
+ // Make any corrections to the window order by selecting each in reverse order
+ for (let url of selectedTabURLOrder.toReversed()) {
+ await switchToWindow(windowByTabURL.get(url));
}
+ // Verify windows are in the expected order
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ selectedTabURLOrder,
+ "The windows and their selected tabs are in the expected order"
+ );
+ Assert.deepEqual(
+ BrowserWindowTracker.orderedWindows.map(win =>
+ win.gBrowser.visibleTabs.map(tab => tabUrl(tab))
+ ),
+ windowsData.map(winData => winData.tabs),
+ "We opened all the tabs in each window"
+ );
}
-function getOpenTabsComponent(browser) {
+function getRecentOpenTabsComponent(browser) {
return browser.contentDocument.querySelector(
"view-recentbrowsing view-opentabs"
);
}
-async function checkTabList(browser, expected) {
- const tabsView = getOpenTabsComponent(browser);
+async function checkRecentTabList(browser, expected) {
+ const tabsView = getRecentOpenTabsComponent(browser);
const [openTabsCard] = getOpenTabsCards(tabsView);
await openTabsCard.updateComplete;
const tabListRows = await getTabRowsForCard(openTabsCard);
Assert.ok(tabListRows, "Found the tab list element");
let actual = Array.from(tabListRows).map(row => row.url);
- Assert.deepEqual(
- actual,
- expected,
- "Tab list has items with URLs in the expected order"
+ await BrowserTestUtils.waitForCondition(
+ () => ObjectUtils.deepEqual(actual, expected),
+ "Waiting for tab list to hvae items with URLs in the expected order"
);
}
add_task(async function test_single_window_tabs() {
- await prepareOpenTabs([tabURL1, tabURL2]);
+ const testData = [
+ {
+ tabs: [tabURL1, tabURL2],
+ selectedIndex: 1, // the 2nd tab should be selected
+ },
+ ];
+ await prepareOpenWindowsAndTabs(testData);
+
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL2, tabURL1]);
+ await checkRecentTabList(browser, [tabURL2, tabURL1]);
// switch to the first tab
let promiseHidden = BrowserTestUtils.waitForEvent(
@@ -192,25 +210,62 @@ add_task(async function test_single_window_tabs() {
// and check the results in the open tabs section of Recent Browsing
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL1, tabURL2]);
+ await checkRecentTabList(browser, [tabURL1, tabURL2]);
});
await cleanup();
});
add_task(async function test_multiple_window_tabs() {
const fxViewURL = getFirefoxViewURL();
- const win1 = window;
+ const testData = [
+ {
+ // this window should be active after restore
+ tabs: [tabURL1, tabURL2],
+ selectedIndex: 0, // tabURL1 should be selected
+ },
+ {
+ tabs: [tabURL3, tabURL4],
+ selectedIndex: 0, // tabURL3 should be selected
+ },
+ ];
+ await prepareOpenWindowsAndTabs(testData);
+
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL1, tabURL3],
+ "The windows and their selected tabs are in the expected order"
+ );
let tabChangeRaised;
- await prepareOpenTabs([tabURL1, tabURL2]);
- const win2 = await BrowserTestUtils.openNewBrowserWindow();
- await prepareOpenTabs([tabURL3, tabURL4], win2);
+ const [win1, win2] = BrowserWindowTracker.orderedWindows;
+
+ info(`Switch to window 1's 2nd tab: ${tabUrl(win1.gBrowser.visibleTabs[1])}`);
+ await BrowserTestUtils.switchTab(gBrowser, win1.gBrowser.visibleTabs[1]);
+ await switchToWindow(win2);
+
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL3, tabURL2],
+ `Window 2 has selected the ${tabURL3} tab, window 1 has ${tabURL2}`
+ );
+ info(`Switch to window 2's 2nd tab: ${tabUrl(win2.gBrowser.visibleTabs[1])}`);
+ tabChangeRaised = BrowserTestUtils.waitForEvent(
+ NonPrivateTabs,
+ "TabRecencyChange"
+ );
+ await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[1]);
+ await tabChangeRaised;
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL4, tabURL2],
+ `window 2 has selected the ${tabURL4} tab, ${tabURL2} remains selected in window 1`
+ );
// to avoid confusing the results by activating different windows,
// check fxview in the current window - which is win2
info("Switching to fxview tab in win2");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]);
+ await checkRecentTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]);
Assert.equal(
tabUrl(win2.gBrowser.selectedTab),
@@ -218,7 +273,7 @@ add_task(async function test_multiple_window_tabs() {
`The selected tab in window 2 is ${fxViewURL}`
);
- info("Switching to first tab (tab3) in win2");
+ info("Switching to first tab in win2");
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
@@ -231,20 +286,20 @@ add_task(async function test_multiple_window_tabs() {
win2.gBrowser,
win2.gBrowser.visibleTabs[0]
);
- Assert.equal(
- tabUrl(win2.gBrowser.selectedTab),
- tabURL3,
- `The selected tab in window 2 is ${tabURL3}`
- );
await tabChangeRaised;
await promiseHidden;
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL3, tabURL2],
+ `window 2 has switched to ${tabURL3}, ${tabURL2} remains selected in window 1`
+ );
});
info("Opening fxview in win2 to confirm tab3 is most recent");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
info("Check result of selecting 1ist tab in window 2");
- await checkTabList(browser, [tabURL3, tabURL4, tabURL2, tabURL1]);
+ await checkRecentTabList(browser, [tabURL3, tabURL4, tabURL2, tabURL1]);
});
info("Focusing win1, where tab2 should be selected");
@@ -254,10 +309,10 @@ add_task(async function test_multiple_window_tabs() {
);
await switchToWindow(win1);
await tabChangeRaised;
- Assert.equal(
- tabUrl(win1.gBrowser.selectedTab),
- tabURL2,
- `The selected tab in window 1 is ${tabURL2}`
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL2, fxViewURL],
+ `The selected tab in window 1 is ${tabURL2}, ${fxViewURL} remains selected in window 2`
);
info("Opening fxview in win1 to confirm tab2 is most recent");
@@ -266,7 +321,7 @@ add_task(async function test_multiple_window_tabs() {
info(
"In fxview, check result of activating window 1, where tab 2 is selected"
);
- await checkTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]);
+ await checkRecentTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]);
let promiseHidden = BrowserTestUtils.waitForEvent(
browser.contentDocument,
@@ -284,45 +339,50 @@ add_task(async function test_multiple_window_tabs() {
await promiseHidden;
await tabChangeRaised;
});
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL1, fxViewURL],
+ `The selected tab in window 1 is ${tabURL1}, ${fxViewURL} remains selected in window 2`
+ );
// check result in the fxview in the 1st window
info("Opening fxview in win1 to confirm tab1 is most recent");
await openFirefoxViewTab(win1).then(async viewTab => {
const browser = viewTab.linkedBrowser;
info("Check result of selecting 1st tab in win1");
- await checkTabList(browser, [tabURL1, tabURL2, tabURL3, tabURL4]);
+ await checkRecentTabList(browser, [tabURL1, tabURL2, tabURL3, tabURL4]);
});
- await cleanup(win2);
+ await cleanup();
});
add_task(async function test_windows_activation() {
- const win1 = window;
- await prepareOpenTabs([tabURL1], win1);
- let fxViewTab;
- let tabChangeRaised;
- info("switch to firefox-view and leave it selected");
- await openFirefoxViewTab(win1).then(tab => (fxViewTab = tab));
+ // use Session restore to batch-open windows and tabs
+ const testData = [
+ {
+ // this window should be active after restore
+ tabs: [tabURL1],
+ selectedIndex: 0, // tabURL1 should be selected
+ },
+ {
+ tabs: [tabURL2],
+ selectedIndex: 0, // tabURL2 should be selected
+ },
+ {
+ tabs: [tabURL3],
+ selectedIndex: 0, // tabURL3 should be selected
+ },
+ ];
+ await prepareOpenWindowsAndTabs(testData);
- const win2 = await BrowserTestUtils.openNewBrowserWindow();
- await switchToWindow(win2);
- await prepareOpenTabs([tabURL2], win2);
-
- const win3 = await BrowserTestUtils.openNewBrowserWindow();
- await switchToWindow(win3);
- await prepareOpenTabs([tabURL3], win3);
-
- tabChangeRaised = BrowserTestUtils.waitForEvent(
- NonPrivateTabs,
- "TabRecencyChange"
- );
- info("Switching back to win 1");
- await switchToWindow(win1);
- info("Waiting for tabChangeRaised to resolve");
- await tabChangeRaised;
+ let tabChangeRaised;
+ const [win1, win2] = BrowserWindowTracker.orderedWindows;
- const browser = fxViewTab.linkedBrowser;
- await checkTabList(browser, [tabURL3, tabURL2, tabURL1]);
+ info("switch to firefox-view and leave it selected");
+ await openFirefoxViewTab(win1).then(async viewTab => {
+ const browser = viewTab.linkedBrowser;
+ await checkRecentTabList(browser, [tabURL1, tabURL2, tabURL3]);
+ });
info("switch to win2 and confirm its selected tab becomes most recent");
tabChangeRaised = BrowserTestUtils.waitForEvent(
@@ -331,24 +391,52 @@ add_task(async function test_windows_activation() {
);
await switchToWindow(win2);
await tabChangeRaised;
- await checkTabList(browser, [tabURL2, tabURL3, tabURL1]);
- await cleanup(win2, win3);
+ await openFirefoxViewTab(win1).then(async viewTab => {
+ await checkRecentTabList(viewTab.linkedBrowser, [
+ tabURL2,
+ tabURL1,
+ tabURL3,
+ ]);
+ });
+ await cleanup();
});
add_task(async function test_minimize_restore_windows() {
- const win1 = window;
- let tabChangeRaised;
- await prepareOpenTabs([tabURL1, tabURL2]);
- const win2 = await BrowserTestUtils.openNewBrowserWindow();
- await prepareOpenTabs([tabURL3, tabURL4], win2);
- await NonPrivateTabs.readyWindowsPromise;
+ const fxViewURL = getFirefoxViewURL();
+ const testData = [
+ {
+ // this window should be active after restore
+ tabs: [tabURL1, tabURL2],
+ selectedIndex: 1, // tabURL2 should be selected
+ },
+ {
+ tabs: [tabURL3, tabURL4],
+ selectedIndex: 0, // tabURL3 should be selected
+ },
+ ];
+ await prepareOpenWindowsAndTabs(testData);
+ const [win1, win2] = BrowserWindowTracker.orderedWindows;
+
+ // switch to the last (tabURL4) tab in window 2
+ await switchToWindow(win2);
+ let tabChangeRaised = BrowserTestUtils.waitForEvent(
+ NonPrivateTabs,
+ "TabRecencyChange"
+ );
+ await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[1]);
+ await tabChangeRaised;
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL4, tabURL2],
+ "The windows and their selected tabs are in the expected order"
+ );
// to avoid confusing the results by activating different windows,
// check fxview in the current window - which is win2
info("Opening fxview in win2 to confirm tab4 is most recent");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]);
+ await checkRecentTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]);
let promiseHidden = BrowserTestUtils.waitForEvent(
browser.contentDocument,
@@ -366,6 +454,11 @@ add_task(async function test_minimize_restore_windows() {
await promiseHidden;
await tabChangeRaised;
});
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL3, tabURL2],
+ `Window 2 has ${tabURL3} selected, window 1 remains at ${tabURL2}`
+ );
// then minimize the window, focusing the 1st window
info("Minimizing win2, leaving tab 3 selected");
@@ -378,32 +471,41 @@ add_task(async function test_minimize_restore_windows() {
await switchToWindow(win1);
await tabChangeRaised;
- Assert.equal(
- tabUrl(win1.gBrowser.selectedTab),
- tabURL2,
- `The selected tab in window 1 is ${tabURL2}`
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL2, tabURL3],
+ `Window 1 has ${tabURL2} selected, window 2 remains at ${tabURL3}`
);
info("Opening fxview in win1 to confirm tab2 is most recent");
await openFirefoxViewTab(win1).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]);
+ await checkRecentTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]);
info(
"Restoring win2 and focusing it - which should make its selected tab most recent"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
- "TabRecencyChange"
+ "TabRecencyChange",
+ false,
+ event => event.detail.sourceEvents?.includes("activate")
);
await restoreWindow(win2);
await switchToWindow(win2);
+ // make sure we wait for the activate event from OpenTabs.
await tabChangeRaised;
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL3, fxViewURL],
+ `Window 2 was restored and has ${tabURL3} selected, window 1 remains at ${fxViewURL}`
+ );
+
info(
"Checking tab order in fxview in win1, to confirm tab3 is most recent"
);
- await checkTabList(browser, [tabURL3, tabURL2, tabURL4, tabURL1]);
+ await checkRecentTabList(browser, [tabURL3, tabURL2, tabURL4, tabURL1]);
});
-
- await cleanup(win2);
+ info("test done, waiting for cleanup");
+ await cleanup();
});
diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js b/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js
index 78fab976ed..4403a8e36a 100644
--- a/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js
+++ b/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js
@@ -94,12 +94,16 @@ add_task(async function test_container_indicator() {
await TestUtils.waitForCondition(
() =>
Array.from(openTabs.viewCards[0].tabList.rowEls).some(rowEl => {
- containerTabElem = rowEl;
- return rowEl.containerObj;
+ let hasContainerObj;
+ if (rowEl.containerObj?.icon) {
+ containerTabElem = rowEl;
+ hasContainerObj = rowEl.containerObj;
+ }
+
+ return hasContainerObj;
}),
"The container tab element isn't marked in Fx View."
);
-
ok(
containerTabElem.shadowRoot
.querySelector(".fxview-tab-row-container-indicator")
diff --git a/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js
index fcfcf20562..85879667bb 100644
--- a/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js
@@ -372,6 +372,12 @@ add_task(async function test_dismiss_tab() {
info("calling dismiss_tab on the top, most-recently closed tab");
let closedTabItem = listItems[0];
+ // the most recently closed tab was in window 3 which got closed
+ // so we expect a sourceClosedId on the item element
+ ok(
+ !isNaN(closedTabItem.sourceClosedId),
+ "Item has a sourceClosedId property"
+ );
// dismiss the first tab and verify the list is correctly updated
await dismiss_tab(closedTabItem);
@@ -390,6 +396,12 @@ add_task(async function test_dismiss_tab() {
// dismiss the last tab and verify the list is correctly updated
closedTabItem = listItems[listItems.length - 1];
+ ok(
+ isNaN(closedTabItem.sourceClosedId),
+ "Item does not have a sourceClosedId property"
+ );
+ ok(closedTabItem.sourceWindowId, "Item has a sourceWindowId property");
+
await dismiss_tab(closedTabItem);
await listElem.getUpdateComplete;
diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js
index 86e4d9cdee..a644b39fc6 100644
--- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js
@@ -69,19 +69,23 @@ add_task(async function test_network_offline() {
"view-syncedtabs:not([slot=syncedtabs])"
);
await TestUtils.waitForCondition(() => syncedTabsComponent.fullyUpdated);
- await BrowserTestUtils.waitForMutationCondition(
- syncedTabsComponent.shadowRoot.querySelector(".cards-container"),
- { childList: true },
- () => syncedTabsComponent.shadowRoot.innerHTML.includes("network-offline")
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "Check your internet connection"
+ ),
+ "The expected network offline error message is displayed."
);
- let emptyState =
- syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
- emptyState.getAttribute("headerlabel").includes("network-offline"),
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("network-offline"),
"Network offline message is shown"
);
- emptyState.querySelector("button[data-action='network-offline']").click();
+ syncedTabsComponent.emptyState
+ .querySelector("button[data-action='network-offline']")
+ .click();
await BrowserTestUtils.waitForCondition(
() => TabsSetupFlowManager.tryToClearError.calledOnce
@@ -92,10 +96,10 @@ add_task(async function test_network_offline() {
"TabsSetupFlowManager.tryToClearError() was called once"
);
- emptyState =
- syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
- emptyState.getAttribute("headerlabel").includes("network-offline"),
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("network-offline"),
"Network offline message is still shown"
);
@@ -121,16 +125,18 @@ add_task(async function test_sync_error() {
"view-syncedtabs:not([slot=syncedtabs])"
);
await TestUtils.waitForCondition(() => syncedTabsComponent.fullyUpdated);
- await BrowserTestUtils.waitForMutationCondition(
- syncedTabsComponent.shadowRoot.querySelector(".cards-container"),
- { childList: true },
- () => syncedTabsComponent.shadowRoot.innerHTML.includes("sync-error")
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "having trouble syncing"
+ ),
+ "Sync error message is shown."
);
- let emptyState =
- syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
- emptyState.getAttribute("headerlabel").includes("sync-error"),
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("sync-error"),
"Correct message should show when there's a sync service error"
);
@@ -139,3 +145,233 @@ add_task(async function test_sync_error() {
});
await tearDown(sandbox);
});
+
+add_task(async function test_sync_disabled_by_policy() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", false]],
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ const recentBrowsingSyncedTabs = document.querySelector(
+ "view-syncedtabs[slot=syncedtabs]"
+ );
+ const syncedtabsPageNavButton = document.querySelector(
+ "moz-page-nav-button[view='syncedtabs']"
+ );
+
+ ok(
+ BrowserTestUtils.isHidden(recentBrowsingSyncedTabs),
+ "Synced tabs should not be visible from recent browsing."
+ );
+ ok(
+ BrowserTestUtils.isHidden(syncedtabsPageNavButton),
+ "Synced tabs nav button should not be visible."
+ );
+
+ document.location.assign(`${getFirefoxViewURL()}#syncedtabs`);
+ await TestUtils.waitForTick();
+ is(
+ document.querySelector("moz-page-nav").currentView,
+ "recentbrowsing",
+ "Should not be able to navigate to synced tabs."
+ );
+ });
+ await tearDown();
+});
+
+add_task(async function test_sync_error_signed_out() {
+ // sync error should not show if user is not signed in
+ let sandbox = await setupWithDesktopDevices(UIState.STATUS_NOT_CONFIGURED);
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "syncedtabs");
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "sign in to your account"
+ ),
+ "Sign in header is shown."
+ );
+
+ ok(
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("signin-header"),
+ "Sign in message is shown"
+ );
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_sync_disconnected_error() {
+ // it's possible for fxa to be enabled but sync not enabled.
+ const sandbox = setupSyncFxAMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ syncEnabled: false,
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "syncedtabs");
+
+ // triggered when user disconnects sync in about:preferences
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+ info("Waiting for the synced tabs error step to be visible");
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "allow syncing"
+ ),
+ "The expected synced tabs empty state header is shown."
+ );
+
+ info(
+ "Waiting for a mutation condition to ensure the right syncing error message"
+ );
+ ok(
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("sync-disconnected-header"),
+ "Correct message should show when sync's been disconnected error"
+ );
+
+ let preferencesTabPromise = BrowserTestUtils.waitForNewTab(
+ browser.getTabBrowser(),
+ "about:preferences?action=choose-what-to-sync#sync",
+ true
+ );
+ let emptyStateButton = syncedTabsComponent.emptyState.querySelector(
+ "button[data-action='sync-disconnected']"
+ );
+ EventUtils.synthesizeMouseAtCenter(emptyStateButton, {}, content);
+ let preferencesTab = await preferencesTabPromise;
+ await BrowserTestUtils.removeTab(preferencesTab);
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_password_change_disconnect_error() {
+ // When the user changes their password on another device, we get into a state
+ // where the user is signed out but sync is still enabled.
+ const sandbox = setupSyncFxAMocks({
+ state: UIState.STATUS_LOGIN_FAILED,
+ syncEnabled: true,
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "syncedtabs");
+
+ // triggered by the user changing fxa password on another device
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "sign in to your account"
+ ),
+ "The expected synced tabs empty state header is shown."
+ );
+
+ ok(
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("signin-header"),
+ "Sign in message is shown"
+ );
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_multiple_errors() {
+ let sandbox = await setupWithDesktopDevices();
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "syncedtabs");
+ // Simulate conditions in which both the locked password and sync error
+ // messages could be shown
+ LoginTestUtils.primaryPassword.enable();
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ info("Waiting for the primary password error message to be shown");
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "enter the Primary Password"
+ ),
+ "The expected synced tabs empty state header is shown."
+ );
+
+ ok(
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("password-locked-header"),
+ "Password locked message is shown"
+ );
+
+ const errorLink = syncedTabsComponent.emptyState.shadowRoot.querySelector(
+ "a[data-l10n-name=syncedtab-password-locked-link]"
+ );
+ ok(
+ errorLink && BrowserTestUtils.isVisible(errorLink),
+ "Error link is visible"
+ );
+
+ // Clear the primary password error message
+ LoginTestUtils.primaryPassword.disable();
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ info("Waiting for the sync error message to be shown");
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "having trouble syncing"
+ ),
+ "The expected synced tabs empty state header is shown."
+ );
+
+ ok(
+ errorLink && BrowserTestUtils.isHidden(errorLink),
+ "Error link is now hidden"
+ );
+
+ // Clear the sync error
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+ });
+ await tearDown(sandbox);
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
index 11f135cd52..1bf387f578 100644
--- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
@@ -276,9 +276,12 @@ add_task(async function test_tabs() {
});
await withFirefoxView({ openNewWindow: true }, async browser => {
+ // Notify observers while in recent browsing. Once synced tabs is selected,
+ // it should have the updated data.
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
const { document } = browser.contentWindow;
await navigateToViewAndWait(document, "syncedtabs");
- Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
@@ -309,7 +312,7 @@ add_task(async function test_tabs() {
);
ok(tabRow1[1].shadowRoot.textContent.includes, "Sandboxes - Sinon.JS");
is(tabRow1.length, 2, "Correct number of rows are displayed.");
- let tabRow2 = tabLists[1].shadowRoot.querySelectorAll("fxview-tab-row");
+ let tabRow2 = tabLists[1].rowEls;
is(tabRow2.length, 2, "Correct number of rows are dispayed.");
ok(tabRow1[0].shadowRoot.textContent.includes, "The Guardian");
ok(tabRow1[1].shadowRoot.textContent.includes, "The Times");
diff --git a/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js b/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js
index d83c1056e0..270c3b6809 100644
--- a/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js
+++ b/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js
@@ -93,7 +93,7 @@ add_task(async function test_focus_moves_after_unmute() {
);
// Unmute using keyboard
- card.tabList.currentActiveElementId = mutedTab.focusMediaButton();
+ mutedTab.focusMediaButton();
isActiveElement(mutedTab.mediaButtonEl);
info("The media button has focus.");
@@ -124,7 +124,7 @@ add_task(async function test_focus_moves_after_unmute() {
);
mutedTab = card.tabList.rowEls[0];
- card.tabList.currentActiveElementId = mutedTab.focusLink();
+ mutedTab.focusLink();
isActiveElement(mutedTab.mainEl);
info("The 'main' element has focus.");
diff --git a/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js b/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js
index 9980980c29..a63a55163a 100644
--- a/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js
+++ b/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js
@@ -31,7 +31,7 @@ add_task(
info("Opening Firefox View tab...");
await openFirefoxViewTab(win);
info("Trigger warnAboutClosingWindow()");
- win.BrowserTryToCloseWindow();
+ win.BrowserCommands.tryToCloseWindow();
await BrowserTestUtils.closeWindow(win);
ok(!dialogObserver.wasOpened, "Dialog was not opened");
dialogObserver.cleanup();
diff --git a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
index e48f776592..52ddc277c7 100644
--- a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
+++ b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
@@ -11,11 +11,6 @@
<script type="module" src="chrome://browser/content/firefoxview/fxview-tab-list.mjs"></script>
</head>
<body>
- <style>
- fxview-tab-list.history::part(secondary-button) {
- background-image: url("chrome://global/skin/icons/more.svg");
- }
- </style>
<p id="display"></p>
<div id="content" style="max-width: 750px">
<fxview-tab-list class="history" .dateTimeFormat="relative" .hasPopup="menu">
diff --git a/browser/components/ion/content/ion.js b/browser/components/ion/content/ion.js
index 3c34328d58..ef3217d239 100644
--- a/browser/components/ion/content/ion.js
+++ b/browser/components/ion/content/ion.js
@@ -444,7 +444,7 @@ async function setup(cachedAddons) {
document
.getElementById("join-ion-accept-dialog-button")
- .addEventListener("click", async event => {
+ .addEventListener("click", async () => {
const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
if (!ionId) {
@@ -501,7 +501,7 @@ async function setup(cachedAddons) {
document
.getElementById("leave-ion-accept-dialog-button")
- .addEventListener("click", async event => {
+ .addEventListener("click", async () => {
const completedStudies = Services.prefs.getStringPref(
PREF_ION_COMPLETED_STUDIES,
"{}"
@@ -567,7 +567,7 @@ async function setup(cachedAddons) {
document
.getElementById("join-study-accept-dialog-button")
- .addEventListener("click", async event => {
+ .addEventListener("click", async () => {
const dialog = document.getElementById("join-study-consent-dialog");
const studyAddonId = dialog.getAttribute("addon-id");
toggleEnrolled(studyAddonId, cachedAddons).then(dialog.close());
@@ -575,7 +575,7 @@ async function setup(cachedAddons) {
document
.getElementById("leave-study-accept-dialog-button")
- .addEventListener("click", async event => {
+ .addEventListener("click", async () => {
const dialog = document.getElementById("leave-study-consent-dialog");
const studyAddonId = dialog.getAttribute("addon-id");
await toggleEnrolled(studyAddonId, cachedAddons).then(dialog.close());
@@ -597,7 +597,7 @@ async function setup(cachedAddons) {
};
AddonManager.addAddonListener(addonsListener);
- window.addEventListener("unload", event => {
+ window.addEventListener("unload", () => {
AddonManager.removeAddonListener(addonsListener);
});
}
@@ -639,7 +639,7 @@ function updateContents(contents) {
}
}
-document.addEventListener("DOMContentLoaded", async domEvent => {
+document.addEventListener("DOMContentLoaded", async () => {
toggleContentBasedOnLocale();
showEnrollmentStatus();
diff --git a/browser/components/ion/test/browser/browser_ion_ui.js b/browser/components/ion/test/browser/browser_ion_ui.js
index e956cefa25..3cf3c47f96 100644
--- a/browser/components/ion/test/browser/browser_ion_ui.js
+++ b/browser/components/ion/test/browser/browser_ion_ui.js
@@ -321,7 +321,7 @@ add_task(async function testBadDefaultAddon() {
url: "about:ion",
gBrowser,
},
- async function taskFn(browser) {
+ async function taskFn() {
const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null);
Assert.strictEqual(
beforePref,
@@ -402,7 +402,7 @@ add_task(async function testAboutPage() {
url: "about:ion",
gBrowser,
},
- async function taskFn(browser) {
+ async function taskFn() {
const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null);
Assert.strictEqual(
beforePref,
@@ -694,19 +694,16 @@ add_task(async function testAboutPage() {
// Wait for deletion ping, uninstalls, and UI updates...
const ionUnenrolled = await new Promise((resolve, reject) => {
- Services.prefs.addObserver(
- PREF_ION_ID,
- function observer(subject, topic, data) {
- try {
- const prefValue = Services.prefs.getStringPref(PREF_ION_ID, null);
- Services.prefs.removeObserver(PREF_ION_ID, observer);
- resolve(prefValue);
- } catch (ex) {
- Services.prefs.removeObserver(PREF_ION_ID, observer);
- reject(ex);
- }
+ Services.prefs.addObserver(PREF_ION_ID, function observer() {
+ try {
+ const prefValue = Services.prefs.getStringPref(PREF_ION_ID, null);
+ Services.prefs.removeObserver(PREF_ION_ID, observer);
+ resolve(prefValue);
+ } catch (ex) {
+ Services.prefs.removeObserver(PREF_ION_ID, observer);
+ reject(ex);
}
- );
+ });
});
ok(!ionUnenrolled, "after accepting unenrollment, Ion pref is null.");
@@ -795,7 +792,7 @@ add_task(async function testEnrollmentPings() {
url: "about:ion",
gBrowser,
},
- async function taskFn(browser) {
+ async function taskFn() {
const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null);
Assert.strictEqual(
beforePref,
@@ -984,7 +981,7 @@ add_task(async function testContentReplacement() {
url: "about:ion",
gBrowser,
},
- async function taskFn(browser) {
+ async function taskFn() {
// Check that text was updated from Remote Settings.
console.log("debug:", content.document.getElementById("title").innerHTML);
Assert.equal(
@@ -1042,7 +1039,7 @@ add_task(async function testBadContentReplacement() {
url: "about:ion",
gBrowser,
},
- async function taskFn(browser) {
+ async function taskFn() {
// Check that text was updated from Remote Settings.
Assert.equal(
content.document.getElementById("join-ion-consent").innerHTML,
@@ -1081,7 +1078,7 @@ add_task(async function testLocaleGating() {
url: "about:ion",
gBrowser,
},
- async function taskFn(browser) {
+ async function taskFn() {
const localeNotificationBar = content.document.getElementById(
"locale-notification"
);
@@ -1107,7 +1104,7 @@ add_task(async function testLocaleGating() {
url: "about:ion",
gBrowser,
},
- async function taskFn(browser) {
+ async function taskFn() {
const localeNotificationBar = content.document.getElementById(
"locale-notification"
);
diff --git a/browser/components/messagepreview/messagepreview.js b/browser/components/messagepreview/messagepreview.js
index 48e5fb1ff5..bec0a2d8eb 100644
--- a/browser/components/messagepreview/messagepreview.js
+++ b/browser/components/messagepreview/messagepreview.js
@@ -6,13 +6,25 @@
"use strict";
+// decode a 16-bit string in which only one byte of each
+// 16-bit unit is occupied, to UTF-8. This is necessary to
+// comply with `btoa` API constraints.
+function fromBinary(encoded) {
+ const binary = atob(decodeURIComponent(encoded));
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < bytes.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ return String.fromCharCode(...new Uint16Array(bytes.buffer));
+}
+
function decodeMessageFromUrl() {
const url = new URL(document.location.href);
if (url.searchParams.has("json")) {
const encodedMessage = url.searchParams.get("json");
- return atob(encodedMessage);
+ return fromBinary(encodedMessage);
}
return null;
}
diff --git a/browser/components/migration/.eslintrc.js b/browser/components/migration/.eslintrc.js
index 34d8ceec2d..41a71782f1 100644
--- a/browser/components/migration/.eslintrc.js
+++ b/browser/components/migration/.eslintrc.js
@@ -5,7 +5,6 @@
"use strict";
module.exports = {
- extends: ["plugin:mozilla/require-jsdoc"],
rules: {
"block-scoped-var": "error",
complexity: ["error", { max: 22 }],
@@ -14,7 +13,7 @@ module.exports = {
"no-multi-str": "error",
"no-return-assign": "error",
"no-shadow": "error",
- "no-unused-vars": ["error", { args: "after-used", vars: "all" }],
+ "no-unused-vars": ["error", { argsIgnorePattern: "^_", vars: "all" }],
strict: ["error", "global"],
yoda: "error",
},
@@ -26,7 +25,7 @@ module.exports = {
"no-unused-vars": [
"error",
{
- args: "none",
+ argsIgnorePattern: "^_",
vars: "local",
},
],
diff --git a/browser/components/migration/FileMigrators.sys.mjs b/browser/components/migration/FileMigrators.sys.mjs
index 3384011c13..487d77aa6c 100644
--- a/browser/components/migration/FileMigrators.sys.mjs
+++ b/browser/components/migration/FileMigrators.sys.mjs
@@ -138,11 +138,10 @@ export class FileMigratorBase {
* from the native file picker. This will not be called if the user
* chooses to cancel the native file picker.
*
- * @param {string} filePath
+ * @param {string} _filePath
* The path that the user selected from the native file picker.
*/
- // eslint-disable-next-line no-unused-vars
- async migrate(filePath) {
+ async migrate(_filePath) {
throw new Error("FileMigrator.migrate must be overridden.");
}
}
diff --git a/browser/components/migration/MigratorBase.sys.mjs b/browser/components/migration/MigratorBase.sys.mjs
index 52bfc87b3e..32bed4e6ec 100644
--- a/browser/components/migration/MigratorBase.sys.mjs
+++ b/browser/components/migration/MigratorBase.sys.mjs
@@ -141,7 +141,7 @@ export class MigratorBase {
* bookmarks file exists.
*
* @abstract
- * @param {object|string} aProfile
+ * @param {object|string} _aProfile
* The profile from which data may be imported, or an empty string
* in the case of a single-profile migrator.
* In the case of multiple-profiles migrator, it is guaranteed that
@@ -149,8 +149,7 @@ export class MigratorBase {
* above).
* @returns {Promise<MigratorResource[]>|MigratorResource[]}
*/
- // eslint-disable-next-line no-unused-vars
- getResources(aProfile) {
+ getResources(_aProfile) {
throw new Error("getResources must be overridden");
}
@@ -223,14 +222,13 @@ export class MigratorBase {
* to getPermissions resolves to true, that the MigratorBase will be able to
* get read access to all of the resources it needs to do a migration.
*
- * @param {DOMWindow} win
+ * @param {DOMWindow} _win
* The top-level DOM window hosting the UI that is requesting the permission.
* This can be used to, for example, anchor a file picker window to the
* same window that is hosting the migration UI.
* @returns {Promise<boolean>}
*/
- // eslint-disable-next-line no-unused-vars
- async getPermissions(win) {
+ async getPermissions(_win) {
return Promise.resolve(true);
}
diff --git a/browser/components/migration/content/migration-wizard.mjs b/browser/components/migration/content/migration-wizard.mjs
index 6fc7a715d7..89872a1558 100644
--- a/browser/components/migration/content/migration-wizard.mjs
+++ b/browser/components/migration/content/migration-wizard.mjs
@@ -583,9 +583,14 @@ export class MigrationWizard extends HTMLElement {
"div[name='page-selection']"
);
+ let header = selectionPage.querySelector(".migration-wizard-header");
+ let selectionHeaderString = this.getAttribute("selection-header-string");
+
if (this.hasAttribute("selection-header-string")) {
- selectionPage.querySelector(".migration-wizard-header").textContent =
- this.getAttribute("selection-header-string");
+ header.textContent = selectionHeaderString;
+ header.toggleAttribute("hidden", !selectionHeaderString);
+ } else {
+ header.removeAttribute("hidden");
}
let selectionSubheaderString = this.getAttribute(
diff --git a/browser/components/newtab/.eslintrc.js b/browser/components/newtab/.eslintrc.js
index f541cdd988..29114a055a 100644
--- a/browser/components/newtab/.eslintrc.js
+++ b/browser/components/newtab/.eslintrc.js
@@ -15,11 +15,7 @@ module.exports = {
{
// TODO: Bug 1773467 - Move these to .mjs or figure out a generic way
// to identify these as modules.
- files: [
- "content-src/**/*.js",
- "test/schemas/**/*.js",
- "test/unit/**/*.js",
- ],
+ files: ["test/schemas/**/*.js", "test/unit/**/*.js"],
parserOptions: {
sourceType: "module",
},
@@ -92,8 +88,6 @@ module.exports = {
},
],
rules: {
- "fetch-options/no-fetch-credentials": "error",
-
"react/jsx-boolean-value": ["error", "always"],
"react/jsx-key": "error",
"react/jsx-no-bind": [
diff --git a/browser/components/newtab/common/Actions.sys.mjs b/browser/components/newtab/common/Actions.mjs
index df5c9f0c91..7273d80220 100644
--- a/browser/components/newtab/common/Actions.sys.mjs
+++ b/browser/components/newtab/common/Actions.mjs
@@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// This file is accessed from both content and system scopes.
+
export const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
export const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
export const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser";
@@ -158,6 +160,7 @@ for (const type of [
"UPDATE_PINNED_SEARCH_SHORTCUTS",
"UPDATE_SEARCH_SHORTCUTS",
"UPDATE_SECTION_PREFS",
+ "WALLPAPERS_SET",
"WEBEXT_CLICK",
"WEBEXT_DISMISS",
]) {
@@ -371,8 +374,11 @@ function DiscoveryStreamLoadedContent(
return importContext === UI_CODE ? AlsoToMain(action) : action;
}
-function SetPref(name, value, importContext = globalImportContext) {
- const action = { type: actionTypes.SET_PREF, data: { name, value } };
+function SetPref(prefName, value, importContext = globalImportContext) {
+ const action = {
+ type: actionTypes.SET_PREF,
+ data: { name: prefName, value },
+ };
return importContext === UI_CODE ? AlsoToMain(action) : action;
}
diff --git a/browser/components/newtab/common/Reducers.sys.mjs b/browser/components/newtab/common/Reducers.sys.mjs
index d4f879b834..326217538d 100644
--- a/browser/components/newtab/common/Reducers.sys.mjs
+++ b/browser/components/newtab/common/Reducers.sys.mjs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs";
+import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs";
import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs";
export const TOP_SITES_DEFAULT_ROWS = 1;
@@ -101,6 +101,9 @@ export const INITIAL_STATE = {
// Hide the search box after handing off to AwesomeBar and user starts typing.
hide: false,
},
+ Wallpapers: {
+ wallpaperList: [],
+ },
};
function App(prevState = INITIAL_STATE.App, action) {
@@ -841,6 +844,15 @@ function Search(prevState = INITIAL_STATE.Search, action) {
}
}
+function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) {
+ switch (action.type) {
+ case at.WALLPAPERS_SET:
+ return { wallpaperList: action.data };
+ default:
+ return prevState;
+ }
+}
+
export const reducers = {
TopSites,
App,
@@ -852,4 +864,5 @@ export const reducers = {
Personalization,
DiscoveryStream,
Search,
+ Wallpapers,
};
diff --git a/browser/components/newtab/content-src/activity-stream.jsx b/browser/components/newtab/content-src/activity-stream.jsx
index c588e8e850..57ba9f9c92 100644
--- a/browser/components/newtab/content-src/activity-stream.jsx
+++ b/browser/components/newtab/content-src/activity-stream.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { Base } from "content-src/components/Base/Base";
import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start";
import { initStore } from "content-src/lib/init-store";
diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx
index 20402b09f5..1738f8f51a 100644
--- a/browser/components/newtab/content-src/components/Base/Base.jsx
+++ b/browser/components/newtab/content-src/components/Base/Base.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { DiscoveryStreamAdmin } from "content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin";
import { ConfirmDialog } from "content-src/components/ConfirmDialog/ConfirmDialog";
import { connect } from "react-redux";
@@ -16,6 +13,9 @@ import React from "react";
import { Search } from "content-src/components/Search/Search";
import { Sections } from "content-src/components/Sections/Sections";
+const VISIBLE = "visible";
+const VISIBILITY_CHANGE_EVENT = "visibilitychange";
+
export const PrefsButton = ({ onClick, icon }) => (
<div className="prefs-button">
<button
@@ -76,7 +76,7 @@ export class _Base extends React.PureComponent {
]
.filter(v => v)
.join(" ");
- global.document.body.className = bodyClassName;
+ globalThis.document.body.className = bodyClassName;
}
render() {
@@ -110,17 +110,75 @@ export class BaseContent extends React.PureComponent {
this.handleOnKeyDown = this.handleOnKeyDown.bind(this);
this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5);
this.setPref = this.setPref.bind(this);
- this.state = { fixedSearch: false };
+ this.updateWallpaper = this.updateWallpaper.bind(this);
+ this.prefersDarkQuery = null;
+ this.handleColorModeChange = this.handleColorModeChange.bind(this);
+ this.state = {
+ fixedSearch: false,
+ firstVisibleTimestamp: null,
+ colorMode: "",
+ };
+ }
+
+ setFirstVisibleTimestamp() {
+ if (!this.state.firstVisibleTimestamp) {
+ this.setState({
+ firstVisibleTimestamp: Date.now(),
+ });
+ }
}
componentDidMount() {
global.addEventListener("scroll", this.onWindowScroll);
global.addEventListener("keydown", this.handleOnKeyDown);
+ if (this.props.document.visibilityState === VISIBLE) {
+ this.setFirstVisibleTimestamp();
+ } else {
+ this._onVisibilityChange = () => {
+ if (this.props.document.visibilityState === VISIBLE) {
+ this.setFirstVisibleTimestamp();
+ this.props.document.removeEventListener(
+ VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
+ this._onVisibilityChange = null;
+ }
+ };
+ this.props.document.addEventListener(
+ VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
+ }
+ // track change event to dark/light mode
+ this.prefersDarkQuery = globalThis.matchMedia(
+ "(prefers-color-scheme: dark)"
+ );
+
+ this.prefersDarkQuery.addEventListener(
+ "change",
+ this.handleColorModeChange
+ );
+ this.handleColorModeChange();
+ }
+
+ handleColorModeChange() {
+ const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light";
+ this.setState({ colorMode });
}
componentWillUnmount() {
+ this.prefersDarkQuery?.removeEventListener(
+ "change",
+ this.handleColorModeChange
+ );
global.removeEventListener("scroll", this.onWindowScroll);
global.removeEventListener("keydown", this.handleOnKeyDown);
+ if (this._onVisibilityChange) {
+ this.props.document.removeEventListener(
+ VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
+ }
}
onWindowScroll() {
@@ -160,11 +218,79 @@ export class BaseContent extends React.PureComponent {
this.props.dispatch(ac.SetPref(pref, value));
}
+ renderWallpaperAttribution() {
+ const { wallpaperList } = this.props.Wallpapers;
+ const activeWallpaper =
+ this.props.Prefs.values[
+ `newtabWallpapers.wallpaper-${this.state.colorMode}`
+ ];
+ const selected = wallpaperList.find(wp => wp.title === activeWallpaper);
+ // make sure a wallpaper is selected and that the attribution also exists
+ if (!selected?.attribution) {
+ return null;
+ }
+
+ const { name, webpage } = selected.attribution;
+ if (activeWallpaper && wallpaperList && name.url) {
+ return (
+ <p
+ className={`wallpaper-attribution`}
+ key={name}
+ data-l10n-id="newtab-wallpaper-attribution"
+ data-l10n-args={JSON.stringify({
+ author_string: name.string,
+ author_url: name.url,
+ webpage_string: webpage.string,
+ webpage_url: webpage.url,
+ })}
+ >
+ <a data-l10n-name="name-link" href={name.url}>
+ {name.string}
+ </a>
+ <a data-l10n-name="webpage-link" href={webpage.url}>
+ {webpage.string}
+ </a>
+ </p>
+ );
+ }
+ return null;
+ }
+
+ async updateWallpaper() {
+ const prefs = this.props.Prefs.values;
+ const { wallpaperList } = this.props.Wallpapers;
+
+ if (wallpaperList) {
+ const lightWallpaper =
+ wallpaperList.find(
+ wp => wp.title === prefs["newtabWallpapers.wallpaper-light"]
+ ) || "";
+ const darkWallpaper =
+ wallpaperList.find(
+ wp => wp.title === prefs["newtabWallpapers.wallpaper-dark"]
+ ) || "";
+ global.document?.body.style.setProperty(
+ `--newtab-wallpaper-light`,
+ `url(${lightWallpaper?.wallpaperUrl || ""})`
+ );
+
+ global.document?.body.style.setProperty(
+ `--newtab-wallpaper-dark`,
+ `url(${darkWallpaper?.wallpaperUrl || ""})`
+ );
+ }
+ }
+
render() {
const { props } = this;
const { App } = props;
const { initialized, customizeMenuVisible } = App;
const prefs = props.Prefs.values;
+
+ const activeWallpaper =
+ prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`];
+ const wallpapersEnabled = prefs["newtabWallpapers.enabled"];
+
const { pocketConfig } = prefs;
const isDiscoveryStream =
@@ -215,6 +341,9 @@ export class BaseContent extends React.PureComponent {
]
.filter(v => v)
.join(" ");
+ if (wallpapersEnabled) {
+ this.updateWallpaper();
+ }
return (
<div>
@@ -224,6 +353,8 @@ export class BaseContent extends React.PureComponent {
openPreferences={this.openPreferences}
setPref={this.setPref}
enabledSections={enabledSections}
+ wallpapersEnabled={wallpapersEnabled}
+ activeWallpaper={activeWallpaper}
pocketRegion={pocketRegion}
mayHaveSponsoredTopSites={mayHaveSponsoredTopSites}
mayHaveSponsoredStories={mayHaveSponsoredStories}
@@ -252,6 +383,7 @@ export class BaseContent extends React.PureComponent {
<DiscoveryStreamBase
locale={props.App.locale}
mayHaveSponsoredStories={mayHaveSponsoredStories}
+ firstVisibleTimestamp={this.state.firstVisibleTimestamp}
/>
</ErrorBoundary>
) : (
@@ -259,6 +391,7 @@ export class BaseContent extends React.PureComponent {
)}
</div>
<ConfirmDialog />
+ {wallpapersEnabled && this.renderWallpaperAttribution()}
</main>
</div>
</div>
@@ -266,10 +399,15 @@ export class BaseContent extends React.PureComponent {
}
}
+BaseContent.defaultProps = {
+ document: global.document,
+};
+
export const Base = connect(state => ({
App: state.App,
Prefs: state.Prefs,
Sections: state.Sections,
DiscoveryStream: state.DiscoveryStream,
Search: state.Search,
+ Wallpapers: state.Wallpapers,
}))(_Base);
diff --git a/browser/components/newtab/content-src/components/Base/_Base.scss b/browser/components/newtab/content-src/components/Base/_Base.scss
index 1282173df5..a9141e0923 100644
--- a/browser/components/newtab/content-src/components/Base/_Base.scss
+++ b/browser/components/newtab/content-src/components/Base/_Base.scss
@@ -24,10 +24,17 @@
}
main {
- margin: auto;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
width: $wrapper-default-width;
padding: 0;
+ .vertical-center-wrapper {
+ margin: auto 0;
+ }
+
section {
margin-bottom: $section-spacing;
position: relative;
@@ -124,3 +131,32 @@ main {
}
}
}
+
+.wallpaper-attribution {
+ padding: 0 $section-horizontal-padding;
+ font-size: 14px;
+
+ &.theme-light {
+ display: inline-block;
+
+ @include dark-theme-only {
+ display: none;
+ }
+ }
+
+ &.theme-dark {
+ display: none;
+
+ @include dark-theme-only {
+ display: inline-block;
+ }
+ }
+
+ a {
+ color: var(--newtab-element-color);
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+}
diff --git a/browser/components/newtab/content-src/components/Card/Card.jsx b/browser/components/newtab/content-src/components/Card/Card.jsx
index 9d03377f1b..da5e0346d7 100644
--- a/browser/components/newtab/content-src/components/Card/Card.jsx
+++ b/browser/components/newtab/content-src/components/Card/Card.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { cardContextTypes } from "./types";
import { connect } from "react-redux";
import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
diff --git a/browser/components/newtab/content-src/components/Card/types.js b/browser/components/newtab/content-src/components/Card/types.mjs
index 0b17eea408..0b17eea408 100644
--- a/browser/components/newtab/content-src/components/Card/types.js
+++ b/browser/components/newtab/content-src/components/Card/types.mjs
diff --git a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx
index 98bf88fbea..2046617ad6 100644
--- a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx
+++ b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx
@@ -119,7 +119,7 @@ export class _CollapsibleSection extends React.PureComponent {
}
_CollapsibleSection.defaultProps = {
- document: global.document || {
+ document: globalThis.document || {
addEventListener: () => {},
removeEventListener: () => {},
visibilityState: "hidden",
diff --git a/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx b/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx
index 4efd8c712e..ffcc6b62f4 100644
--- a/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx
+++ b/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { perfService as perfSvc } from "content-src/lib/perf-service";
import React from "react";
diff --git a/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx b/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx
index f69e540079..734f261b27 100644
--- a/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx
+++ b/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionCreators as ac, actionTypes } from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes } from "common/Actions.mjs";
import { connect } from "react-redux";
import React from "react";
diff --git a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx
index 5ea6a57f71..458f65e644 100644
--- a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx
+++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx
@@ -26,12 +26,12 @@ export class ContextMenu extends React.PureComponent {
componentDidMount() {
this.onShow();
setTimeout(() => {
- global.addEventListener("click", this.hideContext);
+ globalThis.addEventListener("click", this.hideContext);
}, 0);
}
componentWillUnmount() {
- global.removeEventListener("click", this.hideContext);
+ globalThis.removeEventListener("click", this.hideContext);
}
onClick(event) {
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx
index 298dedcee5..1dd13fc965 100644
--- a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx
+++ b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx
@@ -3,8 +3,9 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import { SafeAnchor } from "../../DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
+import { WallpapersSection } from "../../WallpapersSection/WallpapersSection";
export class ContentSection extends React.PureComponent {
constructor(props) {
@@ -98,6 +99,9 @@ export class ContentSection extends React.PureComponent {
mayHaveRecentSaves,
openPreferences,
spocMessageVariant,
+ wallpapersEnabled,
+ activeWallpaper,
+ setPref,
} = this.props;
const {
topSitesEnabled,
@@ -111,6 +115,15 @@ export class ContentSection extends React.PureComponent {
return (
<div className="home-section">
+ {wallpapersEnabled && (
+ <div className="wallpapers-section">
+ <h2 data-l10n-id="newtab-wallpaper-title"></h2>
+ <WallpapersSection
+ setPref={setPref}
+ activeWallpaper={activeWallpaper}
+ />
+ </div>
+ )}
<div id="shortcuts-section" className="section">
<moz-toggle
id="shortcuts-toggle"
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx
index 54dcd550c4..f1c723fed2 100644
--- a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx
+++ b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx
@@ -2,7 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { BackgroundsSection } from "content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection";
import { ContentSection } from "content-src/components/CustomizeMenu/ContentSection/ContentSection";
import { connect } from "react-redux";
import React from "react";
@@ -62,11 +61,12 @@ export class _CustomizeMenu extends React.PureComponent {
data-l10n-id="newtab-custom-close-button"
ref={c => (this.closeButton = c)}
/>
- <BackgroundsSection />
<ContentSection
openPreferences={this.props.openPreferences}
setPref={this.props.setPref}
enabledSections={this.props.enabledSections}
+ wallpapersEnabled={this.props.wallpapersEnabled}
+ activeWallpaper={this.props.activeWallpaper}
pocketRegion={this.props.pocketRegion}
mayHaveSponsoredTopSites={this.props.mayHaveSponsoredTopSites}
mayHaveSponsoredStories={this.props.mayHaveSponsoredStories}
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss
index 579e455a3f..c20da5ce50 100644
--- a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss
+++ b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss
@@ -119,6 +119,10 @@
grid-row-gap: 32px;
padding: 0 16px;
+ .wallpapers-section h2 {
+ font-size: inherit;
+ }
+
.section {
moz-toggle {
margin-bottom: 10px;
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx
index 3c31a5a29f..8b9d64dfc1 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { connect } from "react-redux";
import React from "react";
import { SimpleHashRouter } from "./SimpleHashRouter";
@@ -445,9 +442,9 @@ export class CollapseToggle extends React.PureComponent {
setBodyClass() {
if (this.renderAdmin && !this.state.collapsed) {
- global.document.body.classList.add("no-scroll");
+ globalThis.document.body.classList.add("no-scroll");
} else {
- global.document.body.classList.remove("no-scroll");
+ globalThis.document.body.classList.remove("no-scroll");
}
}
@@ -460,7 +457,7 @@ export class CollapseToggle extends React.PureComponent {
}
componentWillUnmount() {
- global.document.body.classList.remove("no-scroll");
+ globalThis.document.body.classList.remove("no-scroll");
}
render() {
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx
index 9c3fd8579c..bc7b0c42c5 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx
@@ -8,19 +8,19 @@ export class SimpleHashRouter extends React.PureComponent {
constructor(props) {
super(props);
this.onHashChange = this.onHashChange.bind(this);
- this.state = { hash: global.location.hash };
+ this.state = { hash: globalThis.location.hash };
}
onHashChange() {
- this.setState({ hash: global.location.hash });
+ this.setState({ hash: globalThis.location.hash });
}
componentWillMount() {
- global.addEventListener("hashchange", this.onHashChange);
+ globalThis.addEventListener("hashchange", this.onHashChange);
}
componentWillUnmount() {
- global.removeEventListener("hashchange", this.onHashChange);
+ globalThis.removeEventListener("hashchange", this.onHashChange);
}
render() {
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
index 0f0ee51ab9..8b5826dd82 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
@@ -164,7 +164,7 @@ export class _DiscoveryStreamBase extends React.PureComponent {
privacyNoticeURL={component.properties.privacyNoticeURL}
/>
);
- case "CollectionCardGrid":
+ case "CollectionCardGrid": {
const { DiscoveryStream } = this.props;
return (
<CollectionCardGrid
@@ -178,6 +178,7 @@ export class _DiscoveryStreamBase extends React.PureComponent {
dispatch={this.props.dispatch}
/>
);
+ }
case "CardGrid":
return (
<CardGrid
@@ -200,6 +201,7 @@ export class _DiscoveryStreamBase extends React.PureComponent {
editorsPicksHeader={component.properties.editorsPicksHeader}
recentSavesEnabled={this.props.DiscoveryStream.recentSavesEnabled}
hideDescriptions={this.props.DiscoveryStream.hideDescriptions}
+ firstVisibleTimestamp={this.props.firstVisibleTimestamp}
/>
);
case "HorizontalRule":
@@ -384,6 +386,6 @@ export const DiscoveryStreamBase = connect(state => ({
DiscoveryStream: state.DiscoveryStream,
Prefs: state.Prefs,
Sections: state.Sections,
- document: global.document,
+ document: globalThis.document,
App: state.App,
}))(_DiscoveryStreamBase);
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
index cf00361df2..2a9497d1b4 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
@@ -8,10 +8,7 @@ import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDi
import { TopicsWidget } from "../TopicsWidget/TopicsWidget.jsx";
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx";
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import React, { useEffect, useState, useRef, useCallback } from "react";
import { connect, useSelector } from "react-redux";
const PREF_ONBOARDING_EXPERIENCE_DISMISSED =
@@ -31,7 +28,7 @@ export function DSSubHeader({ children }) {
);
}
-export function OnboardingExperience({ dispatch, windowObj = global }) {
+export function OnboardingExperience({ dispatch, windowObj = globalThis }) {
const [dismissed, setDismissed] = useState(false);
const [maxHeight, setMaxHeight] = useState(null);
const heightElement = useRef(null);
@@ -361,6 +358,7 @@ export class _CardGrid extends React.PureComponent {
url={rec.url}
id={rec.id}
shim={rec.shim}
+ fetchTimestamp={rec.fetchTimestamp}
type={this.props.type}
context={rec.context}
sponsor={rec.sponsor}
@@ -377,6 +375,7 @@ export class _CardGrid extends React.PureComponent {
ctaButtonVariant={ctaButtonVariant}
spocMessageVariant={spocMessageVariant}
recommendation_id={rec.recommendation_id}
+ firstVisibleTimestamp={this.props.firstVisibleTimestamp}
/>
)
);
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx
index d089a5c8ab..4f3f150a9b 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import { CardGrid } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid";
import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss";
import { LinkMenuOptions } from "content-src/lib/link-menu-options";
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
index f3e1eab503..b3d965530d 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { DSImage } from "../DSImage/DSImage.jsx";
import { DSLinkMenu } from "../DSLinkMenu/DSLinkMenu";
import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats";
@@ -198,6 +195,8 @@ export class _DSCard extends React.PureComponent {
...(this.props.shim && this.props.shim.click
? { shim: this.props.shim.click }
: {}),
+ fetchTimestamp: this.props.fetchTimestamp,
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp,
},
})
);
@@ -245,6 +244,8 @@ export class _DSCard extends React.PureComponent {
...(this.props.shim && this.props.shim.save
? { shim: this.props.shim.save }
: {}),
+ fetchTimestamp: this.props.fetchTimestamp,
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp,
},
})
);
@@ -441,10 +442,12 @@ export class _DSCard extends React.PureComponent {
? { shim: this.props.shim.impression }
: {}),
recommendation_id: this.props.recommendation_id,
+ fetchTimestamp: this.props.fetchTimestamp,
},
]}
dispatch={this.props.dispatch}
source={this.props.type}
+ firstVisibleTimestamp={this.props.firstVisibleTimestamp}
/>
</SafeAnchor>
{ctaButtonVariant === "variant-b" && (
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
index 6c0641cfc1..80af05c585 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { cardContextTypes } from "../../Card/types.js";
+import { cardContextTypes } from "../../Card/types.mjs";
import { SponsoredContentHighlight } from "../FeatureHighlight/SponsoredContentHighlight";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx";
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx
index ff3886b407..ed90f68606 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import React from "react";
export class DSEmptyState extends React.PureComponent {
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
index b75063940c..107adca4da 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
@@ -4,7 +4,7 @@
import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import React from "react";
export class DSLinkMenu extends React.PureComponent {
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx
index b251fb0401..2275f8b22b 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx
@@ -3,10 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { ModalOverlayWrapper } from "content-src/components/ModalOverlay/ModalOverlay";
export class DSPrivacyModal extends React.PureComponent {
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx
index b7e3205646..0a4d687c65 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats";
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
index 02a3326eb7..fc52decdf8 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss";
import { DSImage } from "../DSImage/DSImage.jsx";
import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats";
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx
index 792be40ba3..c650453393 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx
@@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React, { useState, useCallback, useRef, useEffect } from "react";
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
export function FeatureHighlight({
message,
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx
index 1062c3cade..43865c177c 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import React from "react";
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText";
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx
index 72ec94e1fe..b586730713 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import React from "react";
export class SafeAnchor extends React.PureComponent {
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx
index 1fe2343b94..59b44198a2 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx
@@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats";
import { connect } from "react-redux";
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
index 1eb4863271..9342fcd27a 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { TOP_SITES_SOURCE } from "../TopSites/TopSitesConstants";
import React from "react";
@@ -100,7 +97,9 @@ export class ImpressionStats extends React.PureComponent {
type: this.props.flightId ? "spoc" : "organic",
...(link.shim ? { shim: link.shim } : {}),
recommendation_id: link.recommendation_id,
+ fetchTimestamp: link.fetchTimestamp,
})),
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp,
})
);
this.impressionCardGuids = cards.map(link => link.id);
@@ -244,8 +243,8 @@ export class ImpressionStats extends React.PureComponent {
}
ImpressionStats.defaultProps = {
- IntersectionObserver: global.IntersectionObserver,
- document: global.document,
+ IntersectionObserver: globalThis.IntersectionObserver,
+ document: globalThis.document,
rows: [],
source: "",
};
diff --git a/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx b/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx
index 650a03eb95..65b1f38623 100644
--- a/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx
+++ b/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import { connect } from "react-redux";
import { ContextMenu } from "content-src/components/ContextMenu/ContextMenu";
import { LinkMenuOptions } from "content-src/lib/link-menu-options";
diff --git a/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx b/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx
index fdfdf22db2..5d902b43ba 100644
--- a/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx
+++ b/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx
@@ -53,4 +53,4 @@ export class ModalOverlayWrapper extends React.PureComponent {
}
}
-ModalOverlayWrapper.defaultProps = { document: global.document };
+ModalOverlayWrapper.defaultProps = { document: globalThis.document };
diff --git a/browser/components/newtab/content-src/components/Search/Search.jsx b/browser/components/newtab/content-src/components/Search/Search.jsx
index 64308963c9..ef7a3757d3 100644
--- a/browser/components/newtab/content-src/components/Search/Search.jsx
+++ b/browser/components/newtab/content-src/components/Search/Search.jsx
@@ -4,10 +4,7 @@
/* globals ContentSearchUIController, ContentSearchHandoffUIController */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { connect } from "react-redux";
import { IS_NEWTAB } from "content-src/lib/constants";
import React from "react";
diff --git a/browser/components/newtab/content-src/components/Sections/Sections.jsx b/browser/components/newtab/content-src/components/Sections/Sections.jsx
index e72e9145ad..01b50f6918 100644
--- a/browser/components/newtab/content-src/components/Sections/Sections.jsx
+++ b/browser/components/newtab/content-src/components/Sections/Sections.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { Card, PlaceholderCard } from "content-src/components/Card/Card";
import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection";
import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer";
@@ -33,7 +30,7 @@ export class Section extends React.PureComponent {
let cardsPerRow = CARDS_PER_ROW_DEFAULT;
if (
props.compactCards &&
- global.matchMedia(`(min-width: 1072px)`).matches
+ globalThis.matchMedia(`(min-width: 1072px)`).matches
) {
// If the section has compact cards and the viewport is wide enough, we show
// 4 columns instead of 3.
@@ -326,7 +323,7 @@ export class Section extends React.PureComponent {
}
Section.defaultProps = {
- document: global.document,
+ document: globalThis.document,
rows: [],
emptyState: {},
pref: {},
diff --git a/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx b/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx
index 4324c019f6..2d504c52ab 100644
--- a/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx
+++ b/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import React from "react";
import { TOP_SITES_SOURCE } from "./TopSitesConstants";
diff --git a/browser/components/newtab/content-src/components/TopSites/TopSite.jsx b/browser/components/newtab/content-src/components/TopSites/TopSite.jsx
index c0932104af..3d63398e0e 100644
--- a/browser/components/newtab/content-src/components/TopSites/TopSite.jsx
+++ b/browser/components/newtab/content-src/components/TopSites/TopSite.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import {
MIN_RICH_FAVICON_SIZE,
MIN_SMALL_FAVICON_SIZE,
diff --git a/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx b/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx
index 7dd61bdc93..9ca8991735 100644
--- a/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx
+++ b/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { A11yLinkButton } from "content-src/components/A11yLinkButton/A11yLinkButton";
import React from "react";
import { TOP_SITES_SOURCE } from "./TopSitesConstants";
diff --git a/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx b/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx
index 580809dd57..b654a803c7 100644
--- a/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx
+++ b/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import React from "react";
const VISIBLE = "visible";
@@ -142,8 +142,8 @@ export class TopSiteImpressionWrapper extends React.PureComponent {
}
TopSiteImpressionWrapper.defaultProps = {
- IntersectionObserver: global.IntersectionObserver,
- document: global.document,
+ IntersectionObserver: globalThis.IntersectionObserver,
+ document: globalThis.document,
actionType: null,
tile: null,
};
diff --git a/browser/components/newtab/content-src/components/TopSites/TopSites.jsx b/browser/components/newtab/content-src/components/TopSites/TopSites.jsx
index ba7676fd10..d9a12aa97d 100644
--- a/browser/components/newtab/content-src/components/TopSites/TopSites.jsx
+++ b/browser/components/newtab/content-src/components/TopSites/TopSites.jsx
@@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { MIN_RICH_FAVICON_SIZE, TOP_SITES_SOURCE } from "./TopSitesConstants";
import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection";
import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer";
@@ -93,7 +90,7 @@ export class _TopSites extends React.PureComponent {
// We hide 2 sites per row when not in the wide layout.
let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW;
// $break-point-widest = 1072px (from _variables.scss)
- if (!global.matchMedia(`(min-width: 1072px)`).matches) {
+ if (!globalThis.matchMedia(`(min-width: 1072px)`).matches) {
sitesPerRow -= 2;
}
return this.props.TopSites.rows.slice(
diff --git a/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js b/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs
index f488896238..f488896238 100644
--- a/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js
+++ b/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs
diff --git a/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx
new file mode 100644
index 0000000000..0b51a146f5
--- /dev/null
+++ b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+import { connect } from "react-redux";
+
+export class _WallpapersSection extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.handleChange = this.handleChange.bind(this);
+ this.handleReset = this.handleReset.bind(this);
+ this.prefersHighContrastQuery = null;
+ this.prefersDarkQuery = null;
+ }
+
+ componentDidMount() {
+ this.prefersDarkQuery = globalThis.matchMedia(
+ "(prefers-color-scheme: dark)"
+ );
+ }
+
+ handleChange(event) {
+ const { id } = event.target;
+ const prefs = this.props.Prefs.values;
+ const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light";
+ this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id);
+ // bug 1892095
+ if (
+ prefs["newtabWallpapers.wallpaper-dark"] === "" &&
+ colorMode === "light"
+ ) {
+ this.props.setPref(
+ "newtabWallpapers.wallpaper-dark",
+ id.replace("light", "dark")
+ );
+ }
+
+ if (
+ prefs["newtabWallpapers.wallpaper-light"] === "" &&
+ colorMode === "dark"
+ ) {
+ this.props.setPref(
+ `newtabWallpapers.wallpaper-light`,
+ id.replace("dark", "light")
+ );
+ }
+ }
+
+ handleReset() {
+ const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light";
+ this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, "");
+ }
+
+ render() {
+ const { wallpaperList } = this.props.Wallpapers;
+ const { activeWallpaper } = this.props;
+ return (
+ <div>
+ <fieldset className="wallpaper-list">
+ {wallpaperList.map(({ title, theme, fluent_id }) => {
+ return (
+ <>
+ <input
+ onChange={this.handleChange}
+ type="radio"
+ name={`wallpaper-${title}`}
+ id={title}
+ value={title}
+ checked={title === activeWallpaper}
+ aria-checked={title === activeWallpaper}
+ className={`wallpaper-input theme-${theme} ${title}`}
+ />
+ <label
+ htmlFor={title}
+ className="sr-only"
+ data-l10n-id={fluent_id}
+ >
+ {fluent_id}
+ </label>
+ </>
+ );
+ })}
+ </fieldset>
+ <button
+ className="wallpapers-reset"
+ onClick={this.handleReset}
+ data-l10n-id="newtab-wallpaper-reset"
+ />
+ </div>
+ );
+ }
+}
+
+export const WallpapersSection = connect(state => {
+ return {
+ Wallpapers: state.Wallpapers,
+ Prefs: state.Prefs,
+ };
+})(_WallpapersSection);
diff --git a/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss b/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss
new file mode 100644
index 0000000000..689661750b
--- /dev/null
+++ b/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss
@@ -0,0 +1,87 @@
+.wallpaper-list {
+ display: grid;
+ gap: 16px;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-auto-rows: 86px;
+ margin: 16px 0;
+ padding: 0;
+ border: none;
+
+ .wallpaper-input,
+ .sr-only {
+ &.theme-light {
+ display: inline-block;
+
+ @include dark-theme-only {
+ display: none;
+ }
+ }
+
+ &.theme-dark {
+ display: none;
+
+ @include dark-theme-only {
+ display: inline-block;
+ }
+ }
+ }
+
+ .wallpaper-input {
+ appearance: none;
+ margin: 0;
+ padding: 0;
+ height: 86px;
+ width: 100%;
+ box-shadow: $shadow-secondary;
+ border-radius: 8px;
+ background-clip: content-box;
+ background-repeat: no-repeat;
+ background-size: cover;
+ cursor: pointer;
+ outline: 2px solid transparent;
+
+ $wallpapers: dark-landscape, dark-color, dark-mountain, dark-panda, dark-sky, dark-beach, light-beach, light-color, light-landscape, light-mountain, light-panda, light-sky;
+
+ @each $wallpaper in $wallpapers {
+ &.#{$wallpaper} {
+ background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/#{$wallpaper}.avif')
+ }
+ }
+
+ &:checked {
+ outline-color: var(--color-accent-primary-active);
+ }
+
+ &:focus-visible {
+ outline-color: var(--newtab-primary-action-background);
+ }
+
+ &:hover {
+ filter: brightness(55%);
+ outline-color: transparent;
+ }
+ }
+
+ // visually hide label, but still read by screen readers
+ .sr-only {
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ pointer-events: none;
+ }
+}
+
+.wallpapers-reset {
+ background: none;
+ border: none;
+ text-decoration: underline;
+ margin-inline: auto;
+ display: block;
+ font-size: var(--font-size-small);
+ color: var(--newtab-text-primary-color);
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: none;
+ }
+}
diff --git a/browser/components/newtab/content-src/lib/constants.js b/browser/components/newtab/content-src/lib/constants.mjs
index 2c96160b4b..4f07a77e29 100644
--- a/browser/components/newtab/content-src/lib/constants.js
+++ b/browser/components/newtab/content-src/lib/constants.mjs
@@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
export const IS_NEWTAB =
- global.document && global.document.documentURI === "about:newtab";
+ globalThis.document && globalThis.document.documentURI === "about:newtab";
export const NEWTAB_DARK_THEME = {
ntp_background: {
r: 42,
diff --git a/browser/components/newtab/content-src/lib/detect-user-session-start.js b/browser/components/newtab/content-src/lib/detect-user-session-start.mjs
index 43aa388967..d4c36efd4a 100644
--- a/browser/components/newtab/content-src/lib/detect-user-session-start.js
+++ b/browser/components/newtab/content-src/lib/detect-user-session-start.mjs
@@ -5,8 +5,8 @@
import {
actionCreators as ac,
actionTypes as at,
-} from "common/Actions.sys.mjs";
-import { perfService as perfSvc } from "content-src/lib/perf-service";
+} from "../../common/Actions.mjs";
+import { perfService as perfSvc } from "./perf-service.mjs";
const VISIBLE = "visible";
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
@@ -15,7 +15,7 @@ export class DetectUserSessionStart {
constructor(store, options = {}) {
this._store = store;
// Overrides for testing
- this.document = options.document || global.document;
+ this.document = options.document || globalThis.document;
this._perfService = options.perfService || perfSvc;
this._onVisibilityChange = this._onVisibilityChange.bind(this);
}
diff --git a/browser/components/newtab/content-src/lib/init-store.js b/browser/components/newtab/content-src/lib/init-store.mjs
index f0ab2db86a..85b3b0b470 100644
--- a/browser/components/newtab/content-src/lib/init-store.js
+++ b/browser/components/newtab/content-src/lib/init-store.mjs
@@ -8,7 +8,10 @@ import {
actionCreators as ac,
actionTypes as at,
actionUtils as au,
-} from "common/Actions.sys.mjs";
+} from "../../common/Actions.mjs";
+// We disable import checking here as redux is installed via the npm packages
+// at the newtab level, rather than in the top-level package.json.
+// eslint-disable-next-line import/no-unresolved
import { applyMiddleware, combineReducers, createStore } from "redux";
export const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
@@ -117,12 +120,12 @@ export function initStore(reducers, initialState) {
const store = createStore(
mergeStateReducer(combineReducers(reducers)),
initialState,
- global.RPMAddMessageListener &&
+ globalThis.RPMAddMessageListener &&
applyMiddleware(rehydrationMiddleware, messageMiddleware)
);
- if (global.RPMAddMessageListener) {
- global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => {
+ if (globalThis.RPMAddMessageListener) {
+ globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => {
try {
store.dispatch(msg.data);
} catch (ex) {
diff --git a/browser/components/newtab/content-src/lib/link-menu-options.js b/browser/components/newtab/content-src/lib/link-menu-options.mjs
index 12e47259c1..f10a5e34c6 100644
--- a/browser/components/newtab/content-src/lib/link-menu-options.js
+++ b/browser/components/newtab/content-src/lib/link-menu-options.mjs
@@ -5,7 +5,7 @@
import {
actionCreators as ac,
actionTypes as at,
-} from "common/Actions.sys.mjs";
+} from "../../common/Actions.mjs";
const _OpenInPrivateWindow = site => ({
id: "newtab-menu-open-new-private-window",
diff --git a/browser/components/newtab/content-src/lib/perf-service.js b/browser/components/newtab/content-src/lib/perf-service.mjs
index 6ea99ce877..25fc430726 100644
--- a/browser/components/newtab/content-src/lib/perf-service.js
+++ b/browser/components/newtab/content-src/lib/perf-service.mjs
@@ -2,8 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-
let usablePerfObj = window.performance;
export function _PerfService(options) {
@@ -37,8 +35,8 @@ _PerfService.prototype = {
* @param {String} type eg "mark"
* @return {Array} Performance* objects
*/
- getEntriesByName: function getEntriesByName(name, type) {
- return this._perf.getEntriesByName(name, type);
+ getEntriesByName: function getEntriesByName(entryName, type) {
+ return this._perf.getEntriesByName(entryName, type);
},
/**
@@ -89,11 +87,11 @@ _PerfService.prototype = {
* See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303)
* for more info.
*/
- getMostRecentAbsMarkStartByName(name) {
- let entries = this.getEntriesByName(name, "mark");
+ getMostRecentAbsMarkStartByName(entryName) {
+ let entries = this.getEntriesByName(entryName, "mark");
if (!entries.length) {
- throw new Error(`No marks with the name ${name}`);
+ throw new Error(`No marks with the name ${entryName}`);
}
let mostRecentEntry = entries[entries.length - 1];
diff --git a/browser/components/newtab/content-src/lib/screenshot-utils.js b/browser/components/newtab/content-src/lib/screenshot-utils.mjs
index 7ea93f12ae..2d1342be4f 100644
--- a/browser/components/newtab/content-src/lib/screenshot-utils.js
+++ b/browser/components/newtab/content-src/lib/screenshot-utils.mjs
@@ -30,7 +30,7 @@ export const ScreenshotUtils = {
}
if (this.isBlob(false, remoteImage)) {
return {
- url: global.URL.createObjectURL(remoteImage.data),
+ url: globalThis.URL.createObjectURL(remoteImage.data),
path: remoteImage.path,
};
}
@@ -41,7 +41,7 @@ export const ScreenshotUtils = {
// This should always be called with a local image and not a remote image.
maybeRevokeBlobObjectURL(localImage) {
if (this.isBlob(true, localImage)) {
- global.URL.revokeObjectURL(localImage.url);
+ globalThis.URL.revokeObjectURL(localImage.url);
}
},
diff --git a/browser/components/newtab/content-src/lib/selectLayoutRender.js b/browser/components/newtab/content-src/lib/selectLayoutRender.mjs
index 8ef4dd428f..8ef4dd428f 100644
--- a/browser/components/newtab/content-src/lib/selectLayoutRender.js
+++ b/browser/components/newtab/content-src/lib/selectLayoutRender.mjs
diff --git a/browser/components/newtab/content-src/styles/_activity-stream.scss b/browser/components/newtab/content-src/styles/_activity-stream.scss
index 88ed530b6a..d2e66667b2 100644
--- a/browser/components/newtab/content-src/styles/_activity-stream.scss
+++ b/browser/components/newtab/content-src/styles/_activity-stream.scss
@@ -21,6 +21,17 @@ body {
background-color: var(--newtab-background-color);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif;
font-size: 16px;
+
+ // rules for HNT wallpapers
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+ background-image: var(--newtab-wallpaper-light, '');
+
+ @media (prefers-color-scheme: dark) {
+ background-image: var(--newtab-wallpaper-dark, '');
+ }
}
.no-scroll {
@@ -137,6 +148,7 @@ input {
@import '../components/ContextMenu/ContextMenu';
@import '../components/ConfirmDialog/ConfirmDialog';
@import '../components/CustomizeMenu/CustomizeMenu';
+@import '../components/WallpapersSection/WallpapersSection';
@import '../components/Card/Card';
@import '../components/CollapsibleSection/CollapsibleSection';
@import '../components/DiscoveryStreamAdmin/DiscoveryStreamAdmin';
diff --git a/browser/components/newtab/css/activity-stream-linux.css b/browser/components/newtab/css/activity-stream-linux.css
index 8773159737..131ffac535 100644
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -276,6 +276,16 @@ body {
background-color: var(--newtab-background-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif;
font-size: 16px;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+ background-image: var(--newtab-wallpaper-light, "");
+}
+@media (prefers-color-scheme: dark) {
+ body {
+ background-image: var(--newtab-wallpaper-dark, "");
+ }
}
.no-scroll {
@@ -405,10 +415,16 @@ input[type=text], input[type=search] {
}
main {
- margin: auto;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
width: 274px;
padding: 0;
}
+main .vertical-center-wrapper {
+ margin: auto 0;
+}
main section {
margin-bottom: 20px;
position: relative;
@@ -489,6 +505,29 @@ main section {
background-color: var(--newtab-element-active-color);
}
+.wallpaper-attribution {
+ padding: 0 25px;
+ font-size: 14px;
+}
+.wallpaper-attribution.theme-light {
+ display: inline-block;
+}
+[lwt-newtab-brighttext] .wallpaper-attribution.theme-light {
+ display: none;
+}
+.wallpaper-attribution.theme-dark {
+ display: none;
+}
+[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark {
+ display: inline-block;
+}
+.wallpaper-attribution a {
+ color: var(--newtab-element-color);
+}
+.wallpaper-attribution a:hover {
+ text-decoration: none;
+}
+
.as-error-fallback {
align-items: center;
border-radius: 3px;
@@ -1694,6 +1733,9 @@ main section {
grid-row-gap: 32px;
padding: 0 16px;
}
+.home-section .wallpapers-section h2 {
+ font-size: inherit;
+}
.home-section .section moz-toggle {
margin-bottom: 10px;
}
@@ -1830,6 +1872,112 @@ main section {
box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed);
}
+.wallpaper-list {
+ display: grid;
+ gap: 16px;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-auto-rows: 86px;
+ margin: 16px 0;
+ padding: 0;
+ border: none;
+}
+.wallpaper-list .wallpaper-input.theme-light,
+.wallpaper-list .sr-only.theme-light {
+ display: inline-block;
+}
+[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light,
+[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light {
+ display: none;
+}
+.wallpaper-list .wallpaper-input.theme-dark,
+.wallpaper-list .sr-only.theme-dark {
+ display: none;
+}
+[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark,
+[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark {
+ display: inline-block;
+}
+.wallpaper-list .wallpaper-input {
+ appearance: none;
+ margin: 0;
+ padding: 0;
+ height: 86px;
+ width: 100%;
+ box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2);
+ border-radius: 8px;
+ background-clip: content-box;
+ background-repeat: no-repeat;
+ background-size: cover;
+ cursor: pointer;
+ outline: 2px solid transparent;
+}
+.wallpaper-list .wallpaper-input.dark-landscape {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif");
+}
+.wallpaper-list .wallpaper-input.dark-color {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif");
+}
+.wallpaper-list .wallpaper-input.dark-mountain {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif");
+}
+.wallpaper-list .wallpaper-input.dark-panda {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif");
+}
+.wallpaper-list .wallpaper-input.dark-sky {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif");
+}
+.wallpaper-list .wallpaper-input.dark-beach {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif");
+}
+.wallpaper-list .wallpaper-input.light-beach {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif");
+}
+.wallpaper-list .wallpaper-input.light-color {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif");
+}
+.wallpaper-list .wallpaper-input.light-landscape {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif");
+}
+.wallpaper-list .wallpaper-input.light-mountain {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif");
+}
+.wallpaper-list .wallpaper-input.light-panda {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif");
+}
+.wallpaper-list .wallpaper-input.light-sky {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif");
+}
+.wallpaper-list .wallpaper-input:checked {
+ outline-color: var(--color-accent-primary-active);
+}
+.wallpaper-list .wallpaper-input:focus-visible {
+ outline-color: var(--newtab-primary-action-background);
+}
+.wallpaper-list .wallpaper-input:hover {
+ filter: brightness(55%);
+ outline-color: transparent;
+}
+.wallpaper-list .sr-only {
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ pointer-events: none;
+}
+
+.wallpapers-reset {
+ background: none;
+ border: none;
+ text-decoration: underline;
+ margin-inline: auto;
+ display: block;
+ font-size: var(--font-size-small);
+ color: var(--newtab-text-primary-color);
+ cursor: pointer;
+}
+.wallpapers-reset:hover {
+ text-decoration: none;
+}
+
/* stylelint-disable max-nesting-depth */
.card-outer {
background: var(--newtab-background-color-secondary);
diff --git a/browser/components/newtab/css/activity-stream-mac.css b/browser/components/newtab/css/activity-stream-mac.css
index 87b942818a..416209d511 100644
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -280,6 +280,16 @@ body {
background-color: var(--newtab-background-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif;
font-size: 16px;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+ background-image: var(--newtab-wallpaper-light, "");
+}
+@media (prefers-color-scheme: dark) {
+ body {
+ background-image: var(--newtab-wallpaper-dark, "");
+ }
}
.no-scroll {
@@ -409,10 +419,16 @@ input[type=text], input[type=search] {
}
main {
- margin: auto;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
width: 274px;
padding: 0;
}
+main .vertical-center-wrapper {
+ margin: auto 0;
+}
main section {
margin-bottom: 20px;
position: relative;
@@ -493,6 +509,29 @@ main section {
background-color: var(--newtab-element-active-color);
}
+.wallpaper-attribution {
+ padding: 0 25px;
+ font-size: 14px;
+}
+.wallpaper-attribution.theme-light {
+ display: inline-block;
+}
+[lwt-newtab-brighttext] .wallpaper-attribution.theme-light {
+ display: none;
+}
+.wallpaper-attribution.theme-dark {
+ display: none;
+}
+[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark {
+ display: inline-block;
+}
+.wallpaper-attribution a {
+ color: var(--newtab-element-color);
+}
+.wallpaper-attribution a:hover {
+ text-decoration: none;
+}
+
.as-error-fallback {
align-items: center;
border-radius: 3px;
@@ -1698,6 +1737,9 @@ main section {
grid-row-gap: 32px;
padding: 0 16px;
}
+.home-section .wallpapers-section h2 {
+ font-size: inherit;
+}
.home-section .section moz-toggle {
margin-bottom: 10px;
}
@@ -1834,6 +1876,112 @@ main section {
box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed);
}
+.wallpaper-list {
+ display: grid;
+ gap: 16px;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-auto-rows: 86px;
+ margin: 16px 0;
+ padding: 0;
+ border: none;
+}
+.wallpaper-list .wallpaper-input.theme-light,
+.wallpaper-list .sr-only.theme-light {
+ display: inline-block;
+}
+[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light,
+[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light {
+ display: none;
+}
+.wallpaper-list .wallpaper-input.theme-dark,
+.wallpaper-list .sr-only.theme-dark {
+ display: none;
+}
+[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark,
+[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark {
+ display: inline-block;
+}
+.wallpaper-list .wallpaper-input {
+ appearance: none;
+ margin: 0;
+ padding: 0;
+ height: 86px;
+ width: 100%;
+ box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2);
+ border-radius: 8px;
+ background-clip: content-box;
+ background-repeat: no-repeat;
+ background-size: cover;
+ cursor: pointer;
+ outline: 2px solid transparent;
+}
+.wallpaper-list .wallpaper-input.dark-landscape {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif");
+}
+.wallpaper-list .wallpaper-input.dark-color {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif");
+}
+.wallpaper-list .wallpaper-input.dark-mountain {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif");
+}
+.wallpaper-list .wallpaper-input.dark-panda {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif");
+}
+.wallpaper-list .wallpaper-input.dark-sky {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif");
+}
+.wallpaper-list .wallpaper-input.dark-beach {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif");
+}
+.wallpaper-list .wallpaper-input.light-beach {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif");
+}
+.wallpaper-list .wallpaper-input.light-color {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif");
+}
+.wallpaper-list .wallpaper-input.light-landscape {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif");
+}
+.wallpaper-list .wallpaper-input.light-mountain {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif");
+}
+.wallpaper-list .wallpaper-input.light-panda {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif");
+}
+.wallpaper-list .wallpaper-input.light-sky {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif");
+}
+.wallpaper-list .wallpaper-input:checked {
+ outline-color: var(--color-accent-primary-active);
+}
+.wallpaper-list .wallpaper-input:focus-visible {
+ outline-color: var(--newtab-primary-action-background);
+}
+.wallpaper-list .wallpaper-input:hover {
+ filter: brightness(55%);
+ outline-color: transparent;
+}
+.wallpaper-list .sr-only {
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ pointer-events: none;
+}
+
+.wallpapers-reset {
+ background: none;
+ border: none;
+ text-decoration: underline;
+ margin-inline: auto;
+ display: block;
+ font-size: var(--font-size-small);
+ color: var(--newtab-text-primary-color);
+ cursor: pointer;
+}
+.wallpapers-reset:hover {
+ text-decoration: none;
+}
+
/* stylelint-disable max-nesting-depth */
.card-outer {
background: var(--newtab-background-color-secondary);
diff --git a/browser/components/newtab/css/activity-stream-windows.css b/browser/components/newtab/css/activity-stream-windows.css
index 25370fdf19..f6118e3c18 100644
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -276,6 +276,16 @@ body {
background-color: var(--newtab-background-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif;
font-size: 16px;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+ background-image: var(--newtab-wallpaper-light, "");
+}
+@media (prefers-color-scheme: dark) {
+ body {
+ background-image: var(--newtab-wallpaper-dark, "");
+ }
}
.no-scroll {
@@ -405,10 +415,16 @@ input[type=text], input[type=search] {
}
main {
- margin: auto;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
width: 274px;
padding: 0;
}
+main .vertical-center-wrapper {
+ margin: auto 0;
+}
main section {
margin-bottom: 20px;
position: relative;
@@ -489,6 +505,29 @@ main section {
background-color: var(--newtab-element-active-color);
}
+.wallpaper-attribution {
+ padding: 0 25px;
+ font-size: 14px;
+}
+.wallpaper-attribution.theme-light {
+ display: inline-block;
+}
+[lwt-newtab-brighttext] .wallpaper-attribution.theme-light {
+ display: none;
+}
+.wallpaper-attribution.theme-dark {
+ display: none;
+}
+[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark {
+ display: inline-block;
+}
+.wallpaper-attribution a {
+ color: var(--newtab-element-color);
+}
+.wallpaper-attribution a:hover {
+ text-decoration: none;
+}
+
.as-error-fallback {
align-items: center;
border-radius: 3px;
@@ -1694,6 +1733,9 @@ main section {
grid-row-gap: 32px;
padding: 0 16px;
}
+.home-section .wallpapers-section h2 {
+ font-size: inherit;
+}
.home-section .section moz-toggle {
margin-bottom: 10px;
}
@@ -1830,6 +1872,112 @@ main section {
box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed);
}
+.wallpaper-list {
+ display: grid;
+ gap: 16px;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-auto-rows: 86px;
+ margin: 16px 0;
+ padding: 0;
+ border: none;
+}
+.wallpaper-list .wallpaper-input.theme-light,
+.wallpaper-list .sr-only.theme-light {
+ display: inline-block;
+}
+[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light,
+[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light {
+ display: none;
+}
+.wallpaper-list .wallpaper-input.theme-dark,
+.wallpaper-list .sr-only.theme-dark {
+ display: none;
+}
+[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark,
+[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark {
+ display: inline-block;
+}
+.wallpaper-list .wallpaper-input {
+ appearance: none;
+ margin: 0;
+ padding: 0;
+ height: 86px;
+ width: 100%;
+ box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2);
+ border-radius: 8px;
+ background-clip: content-box;
+ background-repeat: no-repeat;
+ background-size: cover;
+ cursor: pointer;
+ outline: 2px solid transparent;
+}
+.wallpaper-list .wallpaper-input.dark-landscape {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif");
+}
+.wallpaper-list .wallpaper-input.dark-color {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif");
+}
+.wallpaper-list .wallpaper-input.dark-mountain {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif");
+}
+.wallpaper-list .wallpaper-input.dark-panda {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif");
+}
+.wallpaper-list .wallpaper-input.dark-sky {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif");
+}
+.wallpaper-list .wallpaper-input.dark-beach {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif");
+}
+.wallpaper-list .wallpaper-input.light-beach {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif");
+}
+.wallpaper-list .wallpaper-input.light-color {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif");
+}
+.wallpaper-list .wallpaper-input.light-landscape {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif");
+}
+.wallpaper-list .wallpaper-input.light-mountain {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif");
+}
+.wallpaper-list .wallpaper-input.light-panda {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif");
+}
+.wallpaper-list .wallpaper-input.light-sky {
+ background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif");
+}
+.wallpaper-list .wallpaper-input:checked {
+ outline-color: var(--color-accent-primary-active);
+}
+.wallpaper-list .wallpaper-input:focus-visible {
+ outline-color: var(--newtab-primary-action-background);
+}
+.wallpaper-list .wallpaper-input:hover {
+ filter: brightness(55%);
+ outline-color: transparent;
+}
+.wallpaper-list .sr-only {
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ pointer-events: none;
+}
+
+.wallpapers-reset {
+ background: none;
+ border: none;
+ text-decoration: underline;
+ margin-inline: auto;
+ display: block;
+ font-size: var(--font-size-small);
+ color: var(--newtab-text-primary-color);
+ cursor: pointer;
+}
+.wallpapers-reset:hover {
+ text-decoration: none;
+}
+
/* stylelint-disable max-nesting-depth */
.card-outer {
background: var(--newtab-background-color-secondary);
diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js
index 8904ba87d1..395e8c5bb3 100644
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -70,11 +70,13 @@ __webpack_require__.d(__webpack_exports__, {
renderWithoutState: () => (/* binding */ renderWithoutState)
});
-;// CONCATENATED MODULE: ./common/Actions.sys.mjs
+;// CONCATENATED MODULE: ./common/Actions.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// This file is accessed from both content and system scopes.
+
const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser";
@@ -231,6 +233,7 @@ for (const type of [
"UPDATE_PINNED_SEARCH_SHORTCUTS",
"UPDATE_SEARCH_SHORTCUTS",
"UPDATE_SECTION_PREFS",
+ "WALLPAPERS_SET",
"WEBEXT_CLICK",
"WEBEXT_DISMISS",
]) {
@@ -444,8 +447,11 @@ function DiscoveryStreamLoadedContent(
return importContext === UI_CODE ? AlsoToMain(action) : action;
}
-function SetPref(name, value, importContext = globalImportContext) {
- const action = { type: actionTypes.SET_PREF, data: { name, value } };
+function SetPref(prefName, value, importContext = globalImportContext) {
+ const action = {
+ type: actionTypes.SET_PREF,
+ data: { name: prefName, value },
+ };
return importContext === UI_CODE ? AlsoToMain(action) : action;
}
@@ -545,19 +551,19 @@ class SimpleHashRouter extends (external_React_default()).PureComponent {
super(props);
this.onHashChange = this.onHashChange.bind(this);
this.state = {
- hash: __webpack_require__.g.location.hash
+ hash: globalThis.location.hash
};
}
onHashChange() {
this.setState({
- hash: __webpack_require__.g.location.hash
+ hash: globalThis.location.hash
});
}
componentWillMount() {
- __webpack_require__.g.addEventListener("hashchange", this.onHashChange);
+ globalThis.addEventListener("hashchange", this.onHashChange);
}
componentWillUnmount() {
- __webpack_require__.g.removeEventListener("hashchange", this.onHashChange);
+ globalThis.removeEventListener("hashchange", this.onHashChange);
}
render() {
const [, ...routes] = this.state.hash.split("-");
@@ -882,9 +888,9 @@ class CollapseToggle extends (external_React_default()).PureComponent {
}
setBodyClass() {
if (this.renderAdmin && !this.state.collapsed) {
- __webpack_require__.g.document.body.classList.add("no-scroll");
+ globalThis.document.body.classList.add("no-scroll");
} else {
- __webpack_require__.g.document.body.classList.remove("no-scroll");
+ globalThis.document.body.classList.remove("no-scroll");
}
}
componentDidMount() {
@@ -894,7 +900,7 @@ class CollapseToggle extends (external_React_default()).PureComponent {
this.setBodyClass();
}
componentWillUnmount() {
- __webpack_require__.g.document.body.classList.remove("no-scroll");
+ globalThis.document.body.classList.remove("no-scroll");
}
render() {
const {
@@ -1262,11 +1268,11 @@ class ContextMenu extends (external_React_default()).PureComponent {
componentDidMount() {
this.onShow();
setTimeout(() => {
- __webpack_require__.g.addEventListener("click", this.hideContext);
+ globalThis.addEventListener("click", this.hideContext);
}, 0);
}
componentWillUnmount() {
- __webpack_require__.g.removeEventListener("click", this.hideContext);
+ globalThis.removeEventListener("click", this.hideContext);
}
onClick(event) {
// Eat all clicks on the context menu so they don't bubble up to window.
@@ -1392,23 +1398,21 @@ class _ContextMenuItem extends (external_React_default()).PureComponent {
const ContextMenuItem = (0,external_ReactRedux_namespaceObject.connect)(state => ({
Prefs: state.Prefs
}))(_ContextMenuItem);
-;// CONCATENATED MODULE: ./content-src/lib/link-menu-options.js
+;// CONCATENATED MODULE: ./content-src/lib/link-menu-options.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
const _OpenInPrivateWindow = site => ({
id: "newtab-menu-open-new-private-window",
icon: "new-window-private",
action: actionCreators.OnlyToMain({
type: actionTypes.OPEN_PRIVATE_WINDOW,
- data: {
- url: site.url,
- referrer: site.referrer
- }
+ data: { url: site.url, referrer: site.referrer },
}),
- userEvent: "OPEN_PRIVATE_WINDOW"
+ userEvent: "OPEN_PRIVATE_WINDOW",
});
/**
@@ -1417,19 +1421,15 @@ const _OpenInPrivateWindow = site => ({
* the index of the site.
*/
const LinkMenuOptions = {
- Separator: () => ({
- type: "separator"
- }),
- EmptyItem: () => ({
- type: "empty"
- }),
+ Separator: () => ({ type: "separator" }),
+ EmptyItem: () => ({ type: "empty" }),
ShowPrivacyInfo: () => ({
id: "newtab-menu-show-privacy-info",
icon: "info",
action: {
- type: actionTypes.SHOW_PRIVACY_INFO
+ type: actionTypes.SHOW_PRIVACY_INFO,
},
- userEvent: "SHOW_PRIVACY_INFO"
+ userEvent: "SHOW_PRIVACY_INFO",
}),
AboutSponsored: site => ({
id: "newtab-menu-show-privacy-info",
@@ -1439,32 +1439,28 @@ const LinkMenuOptions = {
data: {
advertiser_name: (site.label || site.hostname).toLocaleLowerCase(),
position: site.sponsored_position,
- tile_id: site.sponsored_tile_id
- }
+ tile_id: site.sponsored_tile_id,
+ },
}),
- userEvent: "TOPSITE_SPONSOR_INFO"
+ userEvent: "TOPSITE_SPONSOR_INFO",
}),
RemoveBookmark: site => ({
id: "newtab-menu-remove-bookmark",
icon: "bookmark-added",
action: actionCreators.AlsoToMain({
type: actionTypes.DELETE_BOOKMARK_BY_ID,
- data: site.bookmarkGuid
+ data: site.bookmarkGuid,
}),
- userEvent: "BOOKMARK_DELETE"
+ userEvent: "BOOKMARK_DELETE",
}),
AddBookmark: site => ({
id: "newtab-menu-bookmark",
icon: "bookmark-hollow",
action: actionCreators.AlsoToMain({
type: actionTypes.BOOKMARK_URL,
- data: {
- url: site.url,
- title: site.title,
- type: site.type
- }
+ data: { url: site.url, title: site.title, type: site.type },
}),
- userEvent: "BOOKMARK_ADD"
+ userEvent: "BOOKMARK_ADD",
}),
OpenInNewWindow: site => ({
id: "newtab-menu-open-new-window",
@@ -1475,10 +1471,10 @@ const LinkMenuOptions = {
referrer: site.referrer,
typedBonus: site.typedBonus,
url: site.url,
- sponsored_tile_id: site.sponsored_tile_id
- }
+ sponsored_tile_id: site.sponsored_tile_id,
+ },
}),
- userEvent: "OPEN_NEW_WINDOW"
+ userEvent: "OPEN_NEW_WINDOW",
}),
// This blocks the url for regular stories,
// but also sends a message to DiscoveryStream with flight_id.
@@ -1499,20 +1495,20 @@ const LinkMenuOptions = {
pocket_id: site.pocket_id,
// used by PlacesFeed and TopSitesFeed for sponsored top sites blocking.
isSponsoredTopSite: site.sponsored_position,
- ...(site.flight_id ? {
- flight_id: site.flight_id
- } : {}),
+ ...(site.flight_id ? { flight_id: site.flight_id } : {}),
// If not sponsored, hostname could be anything (Cat3 Data!).
// So only put in advertiser_name for sponsored topsites.
- ...(site.sponsored_position ? {
- advertiser_name: (site.label || site.hostname)?.toLocaleLowerCase()
- } : {}),
+ ...(site.sponsored_position
+ ? {
+ advertiser_name: (
+ site.label || site.hostname
+ )?.toLocaleLowerCase(),
+ }
+ : {}),
position: pos,
- ...(site.sponsored_tile_id ? {
- tile_id: site.sponsored_tile_id
- } : {}),
- is_pocket_card: site.type === "CardGrid"
- }))
+ ...(site.sponsored_tile_id ? { tile_id: site.sponsored_tile_id } : {}),
+ is_pocket_card: site.type === "CardGrid",
+ })),
}),
impression: actionCreators.ImpressionStats({
source: eventSource,
@@ -1520,13 +1516,12 @@ const LinkMenuOptions = {
tiles: tiles.map((site, index) => ({
id: site.guid,
pos: pos + index,
- ...(site.shim && site.shim.delete ? {
- shim: site.shim.delete
- } : {})
- }))
+ ...(site.shim && site.shim.delete ? { shim: site.shim.delete } : {}),
+ })),
}),
- userEvent: "BLOCK"
+ userEvent: "BLOCK",
}),
+
// This is an option for web extentions which will result in remove items from
// memory and notify the web extenion, rather than using the built-in block list.
WebExtDismiss: (site, index, eventSource) => ({
@@ -1536,8 +1531,8 @@ const LinkMenuOptions = {
action: actionCreators.WebExtEvent(actionTypes.WEBEXT_DISMISS, {
source: eventSource,
url: site.url,
- action_position: index
- })
+ action_position: index,
+ }),
}),
DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({
id: "newtab-menu-delete-history",
@@ -1545,77 +1540,74 @@ const LinkMenuOptions = {
action: {
type: actionTypes.DIALOG_OPEN,
data: {
- onConfirm: [actionCreators.AlsoToMain({
- type: actionTypes.DELETE_HISTORY_URL,
- data: {
- url: site.url,
- pocket_id: site.pocket_id,
- forceBlock: site.bookmarkGuid
- }
- }), actionCreators.UserEvent(Object.assign({
- event: "DELETE",
- source: eventSource,
- action_position: index
- }, siteInfo))],
+ onConfirm: [
+ actionCreators.AlsoToMain({
+ type: actionTypes.DELETE_HISTORY_URL,
+ data: {
+ url: site.url,
+ pocket_id: site.pocket_id,
+ forceBlock: site.bookmarkGuid,
+ },
+ }),
+ actionCreators.UserEvent(
+ Object.assign(
+ { event: "DELETE", source: eventSource, action_position: index },
+ siteInfo
+ )
+ ),
+ ],
eventSource,
- body_string_id: ["newtab-confirm-delete-history-p1", "newtab-confirm-delete-history-p2"],
+ body_string_id: [
+ "newtab-confirm-delete-history-p1",
+ "newtab-confirm-delete-history-p2",
+ ],
confirm_button_string_id: "newtab-topsites-delete-history-button",
cancel_button_string_id: "newtab-topsites-cancel-button",
- icon: "modal-delete"
- }
+ icon: "modal-delete",
+ },
},
- userEvent: "DIALOG_OPEN"
+ userEvent: "DIALOG_OPEN",
}),
ShowFile: site => ({
id: "newtab-menu-show-file",
icon: "search",
action: actionCreators.OnlyToMain({
type: actionTypes.SHOW_DOWNLOAD_FILE,
- data: {
- url: site.url
- }
- })
+ data: { url: site.url },
+ }),
}),
OpenFile: site => ({
id: "newtab-menu-open-file",
icon: "open-file",
action: actionCreators.OnlyToMain({
type: actionTypes.OPEN_DOWNLOAD_FILE,
- data: {
- url: site.url
- }
- })
+ data: { url: site.url },
+ }),
}),
CopyDownloadLink: site => ({
id: "newtab-menu-copy-download-link",
icon: "copy",
action: actionCreators.OnlyToMain({
type: actionTypes.COPY_DOWNLOAD_LINK,
- data: {
- url: site.url
- }
- })
+ data: { url: site.url },
+ }),
}),
GoToDownloadPage: site => ({
id: "newtab-menu-go-to-download-page",
icon: "download",
action: actionCreators.OnlyToMain({
type: actionTypes.OPEN_LINK,
- data: {
- url: site.referrer
- }
+ data: { url: site.referrer },
}),
- disabled: !site.referrer
+ disabled: !site.referrer,
}),
RemoveDownload: site => ({
id: "newtab-menu-remove-download",
icon: "delete",
action: actionCreators.OnlyToMain({
type: actionTypes.REMOVE_DOWNLOAD_FILE,
- data: {
- url: site.url
- }
- })
+ data: { url: site.url },
+ }),
}),
PinTopSite: (site, index) => ({
id: "newtab-menu-pin",
@@ -1624,23 +1616,19 @@ const LinkMenuOptions = {
type: actionTypes.TOP_SITES_PIN,
data: {
site,
- index
- }
+ index,
+ },
}),
- userEvent: "PIN"
+ userEvent: "PIN",
}),
UnpinTopSite: site => ({
id: "newtab-menu-unpin",
icon: "unpin",
action: actionCreators.AlsoToMain({
type: actionTypes.TOP_SITES_UNPIN,
- data: {
- site: {
- url: site.url
- }
- }
+ data: { site: { url: site.url } },
}),
- userEvent: "UNPIN"
+ userEvent: "UNPIN",
}),
SaveToPocket: (site, index, eventSource = "CARDGRID") => ({
id: "newtab-menu-save-to-pocket",
@@ -1648,65 +1636,76 @@ const LinkMenuOptions = {
action: actionCreators.AlsoToMain({
type: actionTypes.SAVE_TO_POCKET,
data: {
- site: {
- url: site.url,
- title: site.title
- }
- }
+ site: { url: site.url, title: site.title },
+ },
}),
impression: actionCreators.ImpressionStats({
source: eventSource,
pocket: 0,
- tiles: [{
- id: site.guid,
- pos: index,
- ...(site.shim && site.shim.save ? {
- shim: site.shim.save
- } : {})
- }]
+ tiles: [
+ {
+ id: site.guid,
+ pos: index,
+ ...(site.shim && site.shim.save ? { shim: site.shim.save } : {}),
+ },
+ ],
}),
- userEvent: "SAVE_TO_POCKET"
+ userEvent: "SAVE_TO_POCKET",
}),
DeleteFromPocket: site => ({
id: "newtab-menu-delete-pocket",
icon: "pocket-delete",
action: actionCreators.AlsoToMain({
type: actionTypes.DELETE_FROM_POCKET,
- data: {
- pocket_id: site.pocket_id
- }
+ data: { pocket_id: site.pocket_id },
}),
- userEvent: "DELETE_FROM_POCKET"
+ userEvent: "DELETE_FROM_POCKET",
}),
ArchiveFromPocket: site => ({
id: "newtab-menu-archive-pocket",
icon: "pocket-archive",
action: actionCreators.AlsoToMain({
type: actionTypes.ARCHIVE_FROM_POCKET,
- data: {
- pocket_id: site.pocket_id
- }
+ data: { pocket_id: site.pocket_id },
}),
- userEvent: "ARCHIVE_FROM_POCKET"
+ userEvent: "ARCHIVE_FROM_POCKET",
}),
EditTopSite: (site, index) => ({
id: "newtab-menu-edit-topsites",
icon: "edit",
action: {
type: actionTypes.TOP_SITES_EDIT,
- data: {
- index
- }
- }
+ data: { index },
+ },
}),
- CheckBookmark: site => site.bookmarkGuid ? LinkMenuOptions.RemoveBookmark(site) : LinkMenuOptions.AddBookmark(site),
- CheckPinTopSite: (site, index) => site.isPinned ? LinkMenuOptions.UnpinTopSite(site) : LinkMenuOptions.PinTopSite(site, index),
- CheckSavedToPocket: (site, index, source) => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.SaveToPocket(site, index, source),
- CheckBookmarkOrArchive: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.CheckBookmark(site),
- CheckArchiveFromPocket: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.EmptyItem(),
- CheckDeleteFromPocket: site => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.EmptyItem(),
- OpenInPrivateWindow: (site, index, eventSource, isEnabled) => isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem()
+ CheckBookmark: site =>
+ site.bookmarkGuid
+ ? LinkMenuOptions.RemoveBookmark(site)
+ : LinkMenuOptions.AddBookmark(site),
+ CheckPinTopSite: (site, index) =>
+ site.isPinned
+ ? LinkMenuOptions.UnpinTopSite(site)
+ : LinkMenuOptions.PinTopSite(site, index),
+ CheckSavedToPocket: (site, index, source) =>
+ site.pocket_id
+ ? LinkMenuOptions.DeleteFromPocket(site)
+ : LinkMenuOptions.SaveToPocket(site, index, source),
+ CheckBookmarkOrArchive: site =>
+ site.pocket_id
+ ? LinkMenuOptions.ArchiveFromPocket(site)
+ : LinkMenuOptions.CheckBookmark(site),
+ CheckArchiveFromPocket: site =>
+ site.pocket_id
+ ? LinkMenuOptions.ArchiveFromPocket(site)
+ : LinkMenuOptions.EmptyItem(),
+ CheckDeleteFromPocket: site =>
+ site.pocket_id
+ ? LinkMenuOptions.DeleteFromPocket(site)
+ : LinkMenuOptions.EmptyItem(),
+ OpenInPrivateWindow: (site, index, eventSource, isEnabled) =>
+ isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(),
};
+
;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -1927,21 +1926,47 @@ class DSLinkMenu extends (external_React_default()).PureComponent {
})));
}
}
-;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSitesConstants.js
+;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSitesConstants.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const TOP_SITES_SOURCE = "TOP_SITES";
-const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"];
-const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "ShowPrivacyInfo"];
-const TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "AboutSponsored"];
+const TOP_SITES_CONTEXT_MENU_OPTIONS = [
+ "CheckPinTopSite",
+ "EditTopSite",
+ "Separator",
+ "OpenInNewWindow",
+ "OpenInPrivateWindow",
+ "Separator",
+ "BlockUrl",
+ "DeleteUrl",
+];
+const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = [
+ "OpenInNewWindow",
+ "OpenInPrivateWindow",
+ "Separator",
+ "BlockUrl",
+ "ShowPrivacyInfo",
+];
+const TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS = [
+ "OpenInNewWindow",
+ "OpenInPrivateWindow",
+ "Separator",
+ "BlockUrl",
+ "AboutSponsored",
+];
// the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite
-const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "BlockUrl"];
+const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = [
+ "CheckPinTopSite",
+ "Separator",
+ "BlockUrl",
+];
// minimum size necessary to show a rich icon instead of a screenshot
const MIN_RICH_FAVICON_SIZE = 96;
// minimum size necessary to show any icon
const MIN_SMALL_FAVICON_SIZE = 16;
+
;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -2033,8 +2058,10 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom
...(link.shim ? {
shim: link.shim
} : {}),
- recommendation_id: link.recommendation_id
- }))
+ recommendation_id: link.recommendation_id,
+ fetchTimestamp: link.fetchTimestamp
+ })),
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp
}));
this.impressionCardGuids = cards.map(link => link.id);
}
@@ -2146,8 +2173,8 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom
}
}
ImpressionStats_ImpressionStats.defaultProps = {
- IntersectionObserver: __webpack_require__.g.IntersectionObserver,
- document: __webpack_require__.g.document,
+ IntersectionObserver: globalThis.IntersectionObserver,
+ document: globalThis.document,
rows: [],
source: ""
};
@@ -2224,7 +2251,7 @@ class SafeAnchor extends (external_React_default()).PureComponent {
}, this.props.children);
}
}
-;// CONCATENATED MODULE: ./content-src/components/Card/types.js
+;// CONCATENATED MODULE: ./content-src/components/Card/types.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -2232,29 +2259,30 @@ class SafeAnchor extends (external_React_default()).PureComponent {
const cardContextTypes = {
history: {
fluentID: "newtab-label-visited",
- icon: "history-item"
+ icon: "history-item",
},
removedBookmark: {
fluentID: "newtab-label-removed-bookmark",
- icon: "bookmark-removed"
+ icon: "bookmark-removed",
},
bookmark: {
fluentID: "newtab-label-bookmarked",
- icon: "bookmark-added"
+ icon: "bookmark-added",
},
trending: {
fluentID: "newtab-label-recommended",
- icon: "trending"
+ icon: "trending",
},
pocket: {
fluentID: "newtab-label-saved",
- icon: "pocket"
+ icon: "pocket",
},
download: {
fluentID: "newtab-label-download",
- icon: "download"
- }
+ icon: "download",
+ },
};
+
;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -2710,7 +2738,9 @@ class _DSCard extends (external_React_default()).PureComponent {
tile_id: this.props.id,
...(this.props.shim && this.props.shim.click ? {
shim: this.props.shim.click
- } : {})
+ } : {}),
+ fetchTimestamp: this.props.fetchTimestamp,
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp
}
}));
this.props.dispatch(actionCreators.ImpressionStats({
@@ -2751,7 +2781,9 @@ class _DSCard extends (external_React_default()).PureComponent {
tile_id: this.props.id,
...(this.props.shim && this.props.shim.save ? {
shim: this.props.shim.save
- } : {})
+ } : {}),
+ fetchTimestamp: this.props.fetchTimestamp,
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp
}
}));
this.props.dispatch(actionCreators.ImpressionStats({
@@ -2913,10 +2945,12 @@ class _DSCard extends (external_React_default()).PureComponent {
...(this.props.shim && this.props.shim.impression ? {
shim: this.props.shim.impression
} : {}),
- recommendation_id: this.props.recommendation_id
+ recommendation_id: this.props.recommendation_id,
+ fetchTimestamp: this.props.fetchTimestamp
}],
dispatch: this.props.dispatch,
- source: this.props.type
+ source: this.props.type,
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp
})), ctaButtonVariant === "variant-b" && /*#__PURE__*/external_React_default().createElement("div", {
className: "cta-header"
}, "Shop Now"), /*#__PURE__*/external_React_default().createElement(DefaultMeta, {
@@ -3273,7 +3307,7 @@ function DSSubHeader({
}
function OnboardingExperience({
dispatch,
- windowObj = __webpack_require__.g
+ windowObj = globalThis
}) {
const [dismissed, setDismissed] = (0,external_React_namespaceObject.useState)(false);
const [maxHeight, setMaxHeight] = (0,external_React_namespaceObject.useState)(null);
@@ -3549,6 +3583,7 @@ class _CardGrid extends (external_React_default()).PureComponent {
url: rec.url,
id: rec.id,
shim: rec.shim,
+ fetchTimestamp: rec.fetchTimestamp,
type: this.props.type,
context: rec.context,
sponsor: rec.sponsor,
@@ -3564,7 +3599,8 @@ class _CardGrid extends (external_React_default()).PureComponent {
ctaButtonSponsors: ctaButtonSponsors,
ctaButtonVariant: ctaButtonVariant,
spocMessageVariant: spocMessageVariant,
- recommendation_id: rec.recommendation_id
+ recommendation_id: rec.recommendation_id,
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp
}));
}
if (widgets?.positions?.length && widgets?.data?.length) {
@@ -4023,7 +4059,7 @@ class _CollapsibleSection extends (external_React_default()).PureComponent {
}
}
_CollapsibleSection.defaultProps = {
- document: __webpack_require__.g.document || {
+ document: globalThis.document || {
addEventListener: () => {},
removeEventListener: () => {},
visibilityState: "hidden"
@@ -4111,7 +4147,7 @@ class ModalOverlayWrapper extends (external_React_default()).PureComponent {
}
}
ModalOverlayWrapper.defaultProps = {
- document: __webpack_require__.g.document
+ document: globalThis.document
};
;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
@@ -4443,7 +4479,7 @@ class DSTextPromo extends (external_React_default()).PureComponent {
})));
}
}
-;// CONCATENATED MODULE: ./content-src/lib/screenshot-utils.js
+;// CONCATENATED MODULE: ./content-src/lib/screenshot-utils.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -4462,8 +4498,13 @@ class DSTextPromo extends (external_React_default()).PureComponent {
*/
const ScreenshotUtils = {
isBlob(isLocal, image) {
- return !!(image && image.path && (!isLocal && image.data || isLocal && image.url));
+ return !!(
+ image &&
+ image.path &&
+ ((!isLocal && image.data) || (isLocal && image.url))
+ );
},
+
// This should always be called with a remote image and not a local image.
createLocalImageObject(remoteImage) {
if (!remoteImage) {
@@ -4471,33 +4512,36 @@ const ScreenshotUtils = {
}
if (this.isBlob(false, remoteImage)) {
return {
- url: __webpack_require__.g.URL.createObjectURL(remoteImage.data),
- path: remoteImage.path
+ url: globalThis.URL.createObjectURL(remoteImage.data),
+ path: remoteImage.path,
};
}
- return {
- url: remoteImage
- };
+ return { url: remoteImage };
},
+
// Revokes the object URL of the image if the local image is a blob.
// This should always be called with a local image and not a remote image.
maybeRevokeBlobObjectURL(localImage) {
if (this.isBlob(true, localImage)) {
- __webpack_require__.g.URL.revokeObjectURL(localImage.url);
+ globalThis.URL.revokeObjectURL(localImage.url);
}
},
+
// Checks if remoteImage and localImage are the same.
isRemoteImageLocal(localImage, remoteImage) {
// Both remoteImage and localImage are present.
if (remoteImage && localImage) {
- return this.isBlob(false, remoteImage) ? localImage.path === remoteImage.path : localImage.url === remoteImage;
+ return this.isBlob(false, remoteImage)
+ ? localImage.path === remoteImage.path
+ : localImage.url === remoteImage;
}
// This will only handle the remaining three possible outcomes.
// (i.e. everything except when both image and localImage are present)
return !remoteImage && !localImage;
- }
+ },
};
+
;// CONCATENATED MODULE: ./content-src/components/Card/Card.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -4822,14 +4866,13 @@ const PlaceholderCard = props => /*#__PURE__*/external_React_default().createEle
placeholder: true,
className: props.className
});
-;// CONCATENATED MODULE: ./content-src/lib/perf-service.js
+;// CONCATENATED MODULE: ./content-src/lib/perf-service.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-
let usablePerfObj = window.performance;
+
function _PerfService(options) {
// For testing, so that we can use a fake Window.performance object with
// known state.
@@ -4839,6 +4882,7 @@ function _PerfService(options) {
this._perf = usablePerfObj;
}
}
+
_PerfService.prototype = {
/**
* Calls the underlying mark() method on the appropriate Window.performance
@@ -4851,6 +4895,7 @@ _PerfService.prototype = {
mark: function mark(str) {
this._perf.mark(str);
},
+
/**
* Calls the underlying getEntriesByName on the appropriate Window.performance
* object.
@@ -4859,9 +4904,10 @@ _PerfService.prototype = {
* @param {String} type eg "mark"
* @return {Array} Performance* objects
*/
- getEntriesByName: function getEntriesByName(name, type) {
- return this._perf.getEntriesByName(name, type);
+ getEntriesByName: function getEntriesByName(entryName, type) {
+ return this._perf.getEntriesByName(entryName, type);
},
+
/**
* The timeOrigin property from the appropriate performance object.
* Used to ensure that timestamps from the add-on code and the content code
@@ -4880,6 +4926,7 @@ _PerfService.prototype = {
get timeOrigin() {
return this._perf.timeOrigin;
},
+
/**
* Returns the "absolute" version of performance.now(), i.e. one that
* should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)
@@ -4890,6 +4937,7 @@ _PerfService.prototype = {
absNow: function absNow() {
return this.timeOrigin + this._perf.now();
},
+
/**
* This returns the absolute startTime from the most recent performance.mark()
* with the given name.
@@ -4908,16 +4956,20 @@ _PerfService.prototype = {
* See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303)
* for more info.
*/
- getMostRecentAbsMarkStartByName(name) {
- let entries = this.getEntriesByName(name, "mark");
+ getMostRecentAbsMarkStartByName(entryName) {
+ let entries = this.getEntriesByName(entryName, "mark");
+
if (!entries.length) {
- throw new Error(`No marks with the name ${name}`);
+ throw new Error(`No marks with the name ${entryName}`);
}
+
let mostRecentEntry = entries[entries.length - 1];
return this._perf.timeOrigin + mostRecentEntry.startTime;
- }
+ },
};
+
const perfService = new _PerfService();
+
;// CONCATENATED MODULE: ./content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -5479,6 +5531,9 @@ const INITIAL_STATE = {
// Hide the search box after handing off to AwesomeBar and user starts typing.
hide: false,
},
+ Wallpapers: {
+ wallpaperList: [],
+ },
};
function App(prevState = INITIAL_STATE.App, action) {
@@ -6219,6 +6274,15 @@ function Search(prevState = INITIAL_STATE.Search, action) {
}
}
+function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) {
+ switch (action.type) {
+ case actionTypes.WALLPAPERS_SET:
+ return { wallpaperList: action.data };
+ default:
+ return prevState;
+ }
+}
+
const reducers = {
TopSites,
App,
@@ -6230,6 +6294,7 @@ const reducers = {
Personalization: Reducers_sys_Personalization,
DiscoveryStream,
Search,
+ Wallpapers,
};
;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx
@@ -6448,8 +6513,8 @@ class TopSiteImpressionWrapper extends (external_React_default()).PureComponent
}
}
TopSiteImpressionWrapper.defaultProps = {
- IntersectionObserver: __webpack_require__.g.IntersectionObserver,
- document: __webpack_require__.g.document,
+ IntersectionObserver: globalThis.IntersectionObserver,
+ document: globalThis.document,
actionType: null,
tile: null
};
@@ -7601,7 +7666,7 @@ class _TopSites extends (external_React_default()).PureComponent {
// We hide 2 sites per row when not in the wide layout.
let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW;
// $break-point-widest = 1072px (from _variables.scss)
- if (!__webpack_require__.g.matchMedia(`(min-width: 1072px)`).matches) {
+ if (!globalThis.matchMedia(`(min-width: 1072px)`).matches) {
sitesPerRow -= 2;
}
return this.props.TopSites.rows.slice(0, this.props.TopSitesRows * sitesPerRow);
@@ -7733,7 +7798,7 @@ class Section extends (external_React_default()).PureComponent {
props
} = this;
let cardsPerRow = CARDS_PER_ROW_DEFAULT;
- if (props.compactCards && __webpack_require__.g.matchMedia(`(min-width: 1072px)`).matches) {
+ if (props.compactCards && globalThis.matchMedia(`(min-width: 1072px)`).matches) {
// If the section has compact cards and the viewport is wide enough, we show
// 4 columns instead of 3.
// $break-point-widest = 1072px (from _variables.scss)
@@ -7969,7 +8034,7 @@ class Section extends (external_React_default()).PureComponent {
}
}
Section.defaultProps = {
- document: __webpack_require__.g.document,
+ document: globalThis.document,
rows: [],
emptyState: {},
pref: {},
@@ -8188,20 +8253,13 @@ class SectionTitle extends (external_React_default()).PureComponent {
}, subtitle) : null);
}
}
-;// CONCATENATED MODULE: ./content-src/lib/selectLayoutRender.js
+;// CONCATENATED MODULE: ./content-src/lib/selectLayoutRender.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-const selectLayoutRender = ({
- state = {},
- prefs = {}
-}) => {
- const {
- layout,
- feeds,
- spocs
- } = state;
+const selectLayoutRender = ({ state = {}, prefs = {} }) => {
+ const { layout, feeds, spocs } = state;
let spocIndexPlacementMap = {};
/* This function fills spoc positions on a per placement basis with available spocs.
@@ -8210,8 +8268,16 @@ const selectLayoutRender = ({
* If it sees the same placement again, it remembers the previous spoc index, and continues.
* If it sees a blocked spoc, it skips that position leaving in a regular story.
*/
- function fillSpocPositionsForPlacement(data, spocsConfig, spocsData, placementName) {
- if (!spocIndexPlacementMap[placementName] && spocIndexPlacementMap[placementName] !== 0) {
+ function fillSpocPositionsForPlacement(
+ data,
+ spocsConfig,
+ spocsData,
+ placementName
+ ) {
+ if (
+ !spocIndexPlacementMap[placementName] &&
+ spocIndexPlacementMap[placementName] !== 0
+ ) {
spocIndexPlacementMap[placementName] = 0;
}
const results = [...data];
@@ -8234,107 +8300,154 @@ const selectLayoutRender = ({
results.splice(position.index, 0, spoc);
}
}
+
return results;
}
+
const positions = {};
- const DS_COMPONENTS = ["Message", "TextPromo", "SectionTitle", "Signup", "Navigation", "CardGrid", "CollectionCardGrid", "HorizontalRule", "PrivacyLink"];
+ const DS_COMPONENTS = [
+ "Message",
+ "TextPromo",
+ "SectionTitle",
+ "Signup",
+ "Navigation",
+ "CardGrid",
+ "CollectionCardGrid",
+ "HorizontalRule",
+ "PrivacyLink",
+ ];
+
const filterArray = [];
+
if (!prefs["feeds.topsites"]) {
filterArray.push("TopSites");
}
- const pocketEnabled = prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"];
+
+ const pocketEnabled =
+ prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"];
if (!pocketEnabled) {
filterArray.push(...DS_COMPONENTS);
}
+
const placeholderComponent = component => {
if (!component.feed) {
// TODO we now need a placeholder for topsites and textPromo.
return {
...component,
data: {
- spocs: []
- }
+ spocs: [],
+ },
};
}
const data = {
- recommendations: []
+ recommendations: [],
};
+
let items = 0;
if (component.properties && component.properties.items) {
items = component.properties.items;
}
for (let i = 0; i < items; i++) {
- data.recommendations.push({
- placeholder: true
- });
+ data.recommendations.push({ placeholder: true });
}
- return {
- ...component,
- data
- };
+
+ return { ...component, data };
};
// TODO update devtools to show placements
const handleSpocs = (data, component) => {
let result = [...data];
// Do we ever expect to possibly have a spoc.
- if (component.spocs && component.spocs.positions && component.spocs.positions.length) {
+ if (
+ component.spocs &&
+ component.spocs.positions &&
+ component.spocs.positions.length
+ ) {
const placement = component.placement || {};
const placementName = placement.name || "spocs";
const spocsData = spocs.data[placementName];
// We expect a spoc, spocs are loaded, and the server returned spocs.
- if (spocs.loaded && spocsData && spocsData.items && spocsData.items.length) {
- result = fillSpocPositionsForPlacement(result, component.spocs, spocsData.items, placementName);
+ if (
+ spocs.loaded &&
+ spocsData &&
+ spocsData.items &&
+ spocsData.items.length
+ ) {
+ result = fillSpocPositionsForPlacement(
+ result,
+ component.spocs,
+ spocsData.items,
+ placementName
+ );
}
}
return result;
};
+
const handleComponent = component => {
- if (component.spocs && component.spocs.positions && component.spocs.positions.length) {
+ if (
+ component.spocs &&
+ component.spocs.positions &&
+ component.spocs.positions.length
+ ) {
const placement = component.placement || {};
const placementName = placement.name || "spocs";
const spocsData = spocs.data[placementName];
- if (spocs.loaded && spocsData && spocsData.items && spocsData.items.length) {
+ if (
+ spocs.loaded &&
+ spocsData &&
+ spocsData.items &&
+ spocsData.items.length
+ ) {
return {
...component,
data: {
- spocs: spocsData.items.filter(spoc => spoc && !spocs.blocked.includes(spoc.url)).map((spoc, index) => ({
- ...spoc,
- pos: index
- }))
- }
+ spocs: spocsData.items
+ .filter(spoc => spoc && !spocs.blocked.includes(spoc.url))
+ .map((spoc, index) => ({
+ ...spoc,
+ pos: index,
+ })),
+ },
};
}
}
return {
...component,
data: {
- spocs: []
- }
+ spocs: [],
+ },
};
};
+
const handleComponentWithFeed = component => {
positions[component.type] = positions[component.type] || 0;
let data = {
- recommendations: []
+ recommendations: [],
};
+
const feed = feeds.data[component.feed.url];
if (feed && feed.data) {
data = {
...feed.data,
- recommendations: [...(feed.data.recommendations || [])]
+ recommendations: [...(feed.data.recommendations || [])],
};
}
+
if (component && component.properties && component.properties.offset) {
data = {
...data,
- recommendations: data.recommendations.slice(component.properties.offset)
+ recommendations: data.recommendations.slice(
+ component.properties.offset
+ ),
};
}
+
data = {
...data,
- recommendations: handleSpocs(data.recommendations, component)
+ recommendations: handleSpocs(data.recommendations, component),
};
+
let items = 0;
if (component.properties && component.properties.items) {
items = Math.min(component.properties.items, data.recommendations.length);
@@ -8346,27 +8459,36 @@ const selectLayoutRender = ({
for (let i = 0; i < items; i++) {
data.recommendations[i] = {
...data.recommendations[i],
- pos: positions[component.type]++
+ pos: positions[component.type]++,
};
}
- return {
- ...component,
- data
- };
+
+ return { ...component, data };
};
+
const renderLayout = () => {
const renderedLayoutArray = [];
- for (const row of layout.filter(r => r.components.filter(c => !filterArray.includes(c.type)).length)) {
+ for (const row of layout.filter(
+ r => r.components.filter(c => !filterArray.includes(c.type)).length
+ )) {
let components = [];
renderedLayoutArray.push({
...row,
- components
+ components,
});
- for (const component of row.components.filter(c => !filterArray.includes(c.type))) {
+ for (const component of row.components.filter(
+ c => !filterArray.includes(c.type)
+ )) {
const spocsConfig = component.spocs;
if (spocsConfig || component.feed) {
// TODO make sure this still works for different loading cases.
- if (component.feed && !feeds.data[component.feed.url] || spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded) {
+ if (
+ (component.feed && !feeds.data[component.feed.url]) ||
+ (spocsConfig &&
+ spocsConfig.positions &&
+ spocsConfig.positions.length &&
+ !spocs.loaded)
+ ) {
components.push(placeholderComponent(component));
return renderedLayoutArray;
}
@@ -8382,11 +8504,12 @@ const selectLayoutRender = ({
}
return renderedLayoutArray;
};
+
const layoutRender = renderLayout();
- return {
- layoutRender
- };
+
+ return { layoutRender };
};
+
;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -8528,19 +8651,21 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent {
privacyNoticeURL: component.properties.privacyNoticeURL
});
case "CollectionCardGrid":
- const {
- DiscoveryStream
- } = this.props;
- return /*#__PURE__*/external_React_default().createElement(CollectionCardGrid, {
- data: component.data,
- feed: component.feed,
- spocs: DiscoveryStream.spocs,
- placement: component.placement,
- type: component.type,
- items: component.properties.items,
- dismissible: this.props.DiscoveryStream.isCollectionDismissible,
- dispatch: this.props.dispatch
- });
+ {
+ const {
+ DiscoveryStream
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement(CollectionCardGrid, {
+ data: component.data,
+ feed: component.feed,
+ spocs: DiscoveryStream.spocs,
+ placement: component.placement,
+ type: component.type,
+ items: component.properties.items,
+ dismissible: this.props.DiscoveryStream.isCollectionDismissible,
+ dispatch: this.props.dispatch
+ });
+ }
case "CardGrid":
return /*#__PURE__*/external_React_default().createElement(CardGrid, {
title: component.header && component.header.title,
@@ -8561,7 +8686,8 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent {
spocMessageVariant: component.properties.spocMessageVariant,
editorsPicksHeader: component.properties.editorsPicksHeader,
recentSavesEnabled: this.props.DiscoveryStream.recentSavesEnabled,
- hideDescriptions: this.props.DiscoveryStream.hideDescriptions
+ hideDescriptions: this.props.DiscoveryStream.hideDescriptions,
+ firstVisibleTimestamp: this.props.firstVisibleTimestamp
});
case "HorizontalRule":
return /*#__PURE__*/external_React_default().createElement(HorizontalRule, null);
@@ -8718,20 +8844,87 @@ const DiscoveryStreamBase = (0,external_ReactRedux_namespaceObject.connect)(stat
DiscoveryStream: state.DiscoveryStream,
Prefs: state.Prefs,
Sections: state.Sections,
- document: __webpack_require__.g.document,
+ document: globalThis.document,
App: state.App
}))(_DiscoveryStreamBase);
-;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx
+;// CONCATENATED MODULE: ./content-src/components/WallpapersSection/WallpapersSection.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-class BackgroundsSection extends (external_React_default()).PureComponent {
+
+class _WallpapersSection extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.handleChange = this.handleChange.bind(this);
+ this.handleReset = this.handleReset.bind(this);
+ this.prefersHighContrastQuery = null;
+ this.prefersDarkQuery = null;
+ }
+ componentDidMount() {
+ this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)");
+ }
+ handleChange(event) {
+ const {
+ id
+ } = event.target;
+ const prefs = this.props.Prefs.values;
+ const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light";
+ this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id);
+ // bug 1892095
+ if (prefs["newtabWallpapers.wallpaper-dark"] === "" && colorMode === "light") {
+ this.props.setPref("newtabWallpapers.wallpaper-dark", id.replace("light", "dark"));
+ }
+ if (prefs["newtabWallpapers.wallpaper-light"] === "" && colorMode === "dark") {
+ this.props.setPref(`newtabWallpapers.wallpaper-light`, id.replace("dark", "light"));
+ }
+ }
+ handleReset() {
+ const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light";
+ this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, "");
+ }
render() {
- return /*#__PURE__*/external_React_default().createElement("div", null);
+ const {
+ wallpaperList
+ } = this.props.Wallpapers;
+ const {
+ activeWallpaper
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("fieldset", {
+ className: "wallpaper-list"
+ }, wallpaperList.map(({
+ title,
+ theme,
+ fluent_id
+ }) => {
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("input", {
+ onChange: this.handleChange,
+ type: "radio",
+ name: `wallpaper-${title}`,
+ id: title,
+ value: title,
+ checked: title === activeWallpaper,
+ "aria-checked": title === activeWallpaper,
+ className: `wallpaper-input theme-${theme} ${title}`
+ }), /*#__PURE__*/external_React_default().createElement("label", {
+ htmlFor: title,
+ className: "sr-only",
+ "data-l10n-id": fluent_id
+ }, fluent_id));
+ })), /*#__PURE__*/external_React_default().createElement("button", {
+ className: "wallpapers-reset",
+ onClick: this.handleReset,
+ "data-l10n-id": "newtab-wallpaper-reset"
+ }));
}
}
+const WallpapersSection = (0,external_ReactRedux_namespaceObject.connect)(state => {
+ return {
+ Wallpapers: state.Wallpapers,
+ Prefs: state.Prefs
+ };
+})(_WallpapersSection);
;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -8740,6 +8933,7 @@ class BackgroundsSection extends (external_React_default()).PureComponent {
+
class ContentSection extends (external_React_default()).PureComponent {
constructor(props) {
super(props);
@@ -8818,7 +9012,10 @@ class ContentSection extends (external_React_default()).PureComponent {
mayHaveSponsoredStories,
mayHaveRecentSaves,
openPreferences,
- spocMessageVariant
+ spocMessageVariant,
+ wallpapersEnabled,
+ activeWallpaper,
+ setPref
} = this.props;
const {
topSitesEnabled,
@@ -8831,7 +9028,14 @@ class ContentSection extends (external_React_default()).PureComponent {
} = enabledSections;
return /*#__PURE__*/external_React_default().createElement("div", {
className: "home-section"
- }, /*#__PURE__*/external_React_default().createElement("div", {
+ }, wallpapersEnabled && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "wallpapers-section"
+ }, /*#__PURE__*/external_React_default().createElement("h2", {
+ "data-l10n-id": "newtab-wallpaper-title"
+ }), /*#__PURE__*/external_React_default().createElement(WallpapersSection, {
+ setPref: setPref,
+ activeWallpaper: activeWallpaper
+ })), /*#__PURE__*/external_React_default().createElement("div", {
id: "shortcuts-section",
className: "section"
}, /*#__PURE__*/external_React_default().createElement("moz-toggle", {
@@ -8979,7 +9183,6 @@ class ContentSection extends (external_React_default()).PureComponent {
-
class _CustomizeMenu extends (external_React_default()).PureComponent {
constructor(props) {
super(props);
@@ -9023,10 +9226,12 @@ class _CustomizeMenu extends (external_React_default()).PureComponent {
className: "close-button",
"data-l10n-id": "newtab-custom-close-button",
ref: c => this.closeButton = c
- }), /*#__PURE__*/external_React_default().createElement(BackgroundsSection, null), /*#__PURE__*/external_React_default().createElement(ContentSection, {
+ }), /*#__PURE__*/external_React_default().createElement(ContentSection, {
openPreferences: this.props.openPreferences,
setPref: this.props.setPref,
enabledSections: this.props.enabledSections,
+ wallpapersEnabled: this.props.wallpapersEnabled,
+ activeWallpaper: this.props.activeWallpaper,
pocketRegion: this.props.pocketRegion,
mayHaveSponsoredTopSites: this.props.mayHaveSponsoredTopSites,
mayHaveSponsoredStories: this.props.mayHaveSponsoredStories,
@@ -9039,44 +9244,46 @@ class _CustomizeMenu extends (external_React_default()).PureComponent {
const CustomizeMenu = (0,external_ReactRedux_namespaceObject.connect)(state => ({
DiscoveryStream: state.DiscoveryStream
}))(_CustomizeMenu);
-;// CONCATENATED MODULE: ./content-src/lib/constants.js
+;// CONCATENATED MODULE: ./content-src/lib/constants.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-const IS_NEWTAB = __webpack_require__.g.document && __webpack_require__.g.document.documentURI === "about:newtab";
+const IS_NEWTAB =
+ globalThis.document && globalThis.document.documentURI === "about:newtab";
const NEWTAB_DARK_THEME = {
ntp_background: {
r: 42,
g: 42,
b: 46,
- a: 1
+ a: 1,
},
ntp_card_background: {
r: 66,
g: 65,
b: 77,
- a: 1
+ a: 1,
},
ntp_text: {
r: 249,
g: 249,
b: 250,
- a: 1
+ a: 1,
},
sidebar: {
r: 56,
g: 56,
b: 61,
- a: 1
+ a: 1,
},
sidebar_text: {
r: 249,
g: 249,
b: 250,
- a: 1
- }
+ a: 1,
+ },
};
+
;// CONCATENATED MODULE: ./content-src/components/Search/Search.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -9258,6 +9465,8 @@ function Base_extends() { Base_extends = Object.assign ? Object.assign.bind() :
+const Base_VISIBLE = "visible";
+const Base_VISIBILITY_CHANGE_EVENT = "visibilitychange";
const PrefsButton = ({
onClick,
icon
@@ -9306,7 +9515,7 @@ class _Base extends (external_React_default()).PureComponent {
// If we skipped the about:welcome overlay and removed the CSS classes
// we don't want to add them back to the Activity Stream view
document.body.classList.contains("inline-onboarding") ? "inline-onboarding" : ""].filter(v => v).join(" ");
- __webpack_require__.g.document.body.className = bodyClassName;
+ globalThis.document.body.className = bodyClassName;
}
render() {
const {
@@ -9337,17 +9546,55 @@ class BaseContent extends (external_React_default()).PureComponent {
this.handleOnKeyDown = this.handleOnKeyDown.bind(this);
this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5);
this.setPref = this.setPref.bind(this);
+ this.updateWallpaper = this.updateWallpaper.bind(this);
+ this.prefersDarkQuery = null;
+ this.handleColorModeChange = this.handleColorModeChange.bind(this);
this.state = {
- fixedSearch: false
+ fixedSearch: false,
+ firstVisibleTimestamp: null,
+ colorMode: ""
};
}
+ setFirstVisibleTimestamp() {
+ if (!this.state.firstVisibleTimestamp) {
+ this.setState({
+ firstVisibleTimestamp: Date.now()
+ });
+ }
+ }
componentDidMount() {
__webpack_require__.g.addEventListener("scroll", this.onWindowScroll);
__webpack_require__.g.addEventListener("keydown", this.handleOnKeyDown);
+ if (this.props.document.visibilityState === Base_VISIBLE) {
+ this.setFirstVisibleTimestamp();
+ } else {
+ this._onVisibilityChange = () => {
+ if (this.props.document.visibilityState === Base_VISIBLE) {
+ this.setFirstVisibleTimestamp();
+ this.props.document.removeEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ this._onVisibilityChange = null;
+ }
+ };
+ this.props.document.addEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ // track change event to dark/light mode
+ this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)");
+ this.prefersDarkQuery.addEventListener("change", this.handleColorModeChange);
+ this.handleColorModeChange();
+ }
+ handleColorModeChange() {
+ const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light";
+ this.setState({
+ colorMode
+ });
}
componentWillUnmount() {
+ this.prefersDarkQuery?.removeEventListener("change", this.handleColorModeChange);
__webpack_require__.g.removeEventListener("scroll", this.onWindowScroll);
__webpack_require__.g.removeEventListener("keydown", this.handleOnKeyDown);
+ if (this._onVisibilityChange) {
+ this.props.document.removeEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
}
onWindowScroll() {
const prefs = this.props.Prefs.values;
@@ -9396,6 +9643,53 @@ class BaseContent extends (external_React_default()).PureComponent {
setPref(pref, value) {
this.props.dispatch(actionCreators.SetPref(pref, value));
}
+ renderWallpaperAttribution() {
+ const {
+ wallpaperList
+ } = this.props.Wallpapers;
+ const activeWallpaper = this.props.Prefs.values[`newtabWallpapers.wallpaper-${this.state.colorMode}`];
+ const selected = wallpaperList.find(wp => wp.title === activeWallpaper);
+ // make sure a wallpaper is selected and that the attribution also exists
+ if (!selected?.attribution) {
+ return null;
+ }
+ const {
+ name,
+ webpage
+ } = selected.attribution;
+ if (activeWallpaper && wallpaperList && name.url) {
+ return /*#__PURE__*/external_React_default().createElement("p", {
+ className: `wallpaper-attribution`,
+ key: name,
+ "data-l10n-id": "newtab-wallpaper-attribution",
+ "data-l10n-args": JSON.stringify({
+ author_string: name.string,
+ author_url: name.url,
+ webpage_string: webpage.string,
+ webpage_url: webpage.url
+ })
+ }, /*#__PURE__*/external_React_default().createElement("a", {
+ "data-l10n-name": "name-link",
+ href: name.url
+ }, name.string), /*#__PURE__*/external_React_default().createElement("a", {
+ "data-l10n-name": "webpage-link",
+ href: webpage.url
+ }, webpage.string));
+ }
+ return null;
+ }
+ async updateWallpaper() {
+ const prefs = this.props.Prefs.values;
+ const {
+ wallpaperList
+ } = this.props.Wallpapers;
+ if (wallpaperList) {
+ const lightWallpaper = wallpaperList.find(wp => wp.title === prefs["newtabWallpapers.wallpaper-light"]) || "";
+ const darkWallpaper = wallpaperList.find(wp => wp.title === prefs["newtabWallpapers.wallpaper-dark"]) || "";
+ __webpack_require__.g.document?.body.style.setProperty(`--newtab-wallpaper-light`, `url(${lightWallpaper?.wallpaperUrl || ""})`);
+ __webpack_require__.g.document?.body.style.setProperty(`--newtab-wallpaper-dark`, `url(${darkWallpaper?.wallpaperUrl || ""})`);
+ }
+ }
render() {
const {
props
@@ -9408,6 +9702,8 @@ class BaseContent extends (external_React_default()).PureComponent {
customizeMenuVisible
} = App;
const prefs = props.Prefs.values;
+ const activeWallpaper = prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`];
+ const wallpapersEnabled = prefs["newtabWallpapers.enabled"];
const {
pocketConfig
} = prefs;
@@ -9435,12 +9731,17 @@ class BaseContent extends (external_React_default()).PureComponent {
mayHaveSponsoredTopSites
} = prefs;
const outerClassName = ["outer-wrapper", isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment", isDiscoveryStream && "ds-outer-wrapper-breakpoint-override", prefs.showSearch && this.state.fixedSearch && !noSectionsEnabled && "fixed-search", prefs.showSearch && noSectionsEnabled && "only-search", prefs["logowordmark.alwaysVisible"] && "visible-logo"].filter(v => v).join(" ");
+ if (wallpapersEnabled) {
+ this.updateWallpaper();
+ }
return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement(CustomizeMenu, {
onClose: this.closeCustomizationMenu,
onOpen: this.openCustomizationMenu,
openPreferences: this.openPreferences,
setPref: this.setPref,
enabledSections: enabledSections,
+ wallpapersEnabled: wallpapersEnabled,
+ activeWallpaper: activeWallpaper,
pocketRegion: pocketRegion,
mayHaveSponsoredTopSites: mayHaveSponsoredTopSites,
mayHaveSponsoredStories: mayHaveSponsoredStories,
@@ -9460,31 +9761,38 @@ class BaseContent extends (external_React_default()).PureComponent {
className: "borderless-error"
}, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamBase, {
locale: props.App.locale,
- mayHaveSponsoredStories: mayHaveSponsoredStories
- })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null))));
+ mayHaveSponsoredStories: mayHaveSponsoredStories,
+ firstVisibleTimestamp: this.state.firstVisibleTimestamp
+ })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null), wallpapersEnabled && this.renderWallpaperAttribution())));
}
}
+BaseContent.defaultProps = {
+ document: __webpack_require__.g.document
+};
const Base = (0,external_ReactRedux_namespaceObject.connect)(state => ({
App: state.App,
Prefs: state.Prefs,
Sections: state.Sections,
DiscoveryStream: state.DiscoveryStream,
- Search: state.Search
+ Search: state.Search,
+ Wallpapers: state.Wallpapers
}))(_Base);
-;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.js
+;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
const detect_user_session_start_VISIBLE = "visible";
const detect_user_session_start_VISIBILITY_CHANGE_EVENT = "visibilitychange";
+
class DetectUserSessionStart {
constructor(store, options = {}) {
this._store = store;
// Overrides for testing
- this.document = options.document || __webpack_require__.g.document;
+ this.document = options.document || globalThis.document;
this._perfService = options.perfService || perfService;
this._onVisibilityChange = this._onVisibilityChange.bind(this);
}
@@ -9502,7 +9810,10 @@ class DetectUserSessionStart {
this._sendEvent();
} else {
// If the document is not visible, listen for when it does become visible.
- this.document.addEventListener(detect_user_session_start_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ this.document.addEventListener(
+ detect_user_session_start_VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
}
}
@@ -9513,14 +9824,19 @@ class DetectUserSessionStart {
*/
_sendEvent() {
this._perfService.mark("visibility_event_rcvd_ts");
+
try {
- let visibility_event_rcvd_ts = this._perfService.getMostRecentAbsMarkStartByName("visibility_event_rcvd_ts");
- this._store.dispatch(actionCreators.AlsoToMain({
- type: actionTypes.SAVE_SESSION_PERF_DATA,
- data: {
- visibility_event_rcvd_ts
- }
- }));
+ let visibility_event_rcvd_ts =
+ this._perfService.getMostRecentAbsMarkStartByName(
+ "visibility_event_rcvd_ts"
+ );
+
+ this._store.dispatch(
+ actionCreators.AlsoToMain({
+ type: actionTypes.SAVE_SESSION_PERF_DATA,
+ data: { visibility_event_rcvd_ts },
+ })
+ );
} catch (ex) {
// If this failed, it's likely because the `privacy.resistFingerprinting`
// pref is true. We should at least not blow up.
@@ -9534,13 +9850,17 @@ class DetectUserSessionStart {
_onVisibilityChange() {
if (this.document.visibilityState === detect_user_session_start_VISIBLE) {
this._sendEvent();
- this.document.removeEventListener(detect_user_session_start_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ this.document.removeEventListener(
+ detect_user_session_start_VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
}
}
}
+
;// CONCATENATED MODULE: external "Redux"
const external_Redux_namespaceObject = Redux;
-;// CONCATENATED MODULE: ./content-src/lib/init-store.js
+;// CONCATENATED MODULE: ./content-src/lib/init-store.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -9548,6 +9868,10 @@ const external_Redux_namespaceObject = Redux;
/* eslint-env mozilla/remote-page */
+// We disable import checking here as redux is installed via the npm packages
+// at the newtab level, rather than in the top-level package.json.
+// eslint-disable-next-line import/no-unresolved
+
const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
@@ -9572,11 +9896,9 @@ const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
function mergeStateReducer(mainReducer) {
return (prevState, action) => {
if (action.type === MERGE_STORE_ACTION) {
- return {
- ...prevState,
- ...action.data
- };
+ return { ...prevState, ...action.data };
}
+
return mainReducer(prevState, action);
};
}
@@ -9593,9 +9915,8 @@ const messageMiddleware = () => next => action => {
next(action);
}
};
-const rehydrationMiddleware = ({
- getState
-}) => {
+
+const rehydrationMiddleware = ({ getState }) => {
// NB: The parameter here is MiddlewareAPI which looks like a Store and shares
// the same getState, so attached properties are accessible from the store.
getState.didRehydrate = false;
@@ -9604,17 +9925,24 @@ const rehydrationMiddleware = ({
if (getState.didRehydrate || window.__FROM_STARTUP_CACHE__) {
// Startup messages can be safely ignored by the about:home document
// stored in the startup cache.
- if (window.__FROM_STARTUP_CACHE__ && action.meta && action.meta.isStartup) {
+ if (
+ window.__FROM_STARTUP_CACHE__ &&
+ action.meta &&
+ action.meta.isStartup
+ ) {
return null;
}
return next(action);
}
+
const isMergeStoreAction = action.type === MERGE_STORE_ACTION;
const isRehydrationRequest = action.type === actionTypes.NEW_TAB_STATE_REQUEST;
+
if (isRehydrationRequest) {
getState.didRequestInitialState = true;
return next(action);
}
+
if (isMergeStoreAction) {
getState.didRehydrate = true;
return next(action);
@@ -9622,16 +9950,20 @@ const rehydrationMiddleware = ({
// If init happened after our request was made, we need to re-request
if (getState.didRequestInitialState && action.type === actionTypes.INIT) {
- return next(actionCreators.AlsoToMain({
- type: actionTypes.NEW_TAB_STATE_REQUEST
- }));
+ return next(actionCreators.AlsoToMain({ type: actionTypes.NEW_TAB_STATE_REQUEST }));
}
- if (actionUtils.isBroadcastToContent(action) || actionUtils.isSendToOneContent(action) || actionUtils.isSendToPreloaded(action)) {
+
+ if (
+ actionUtils.isBroadcastToContent(action) ||
+ actionUtils.isSendToOneContent(action) ||
+ actionUtils.isSendToPreloaded(action)
+ ) {
// Note that actions received before didRehydrate will not be dispatched
// because this could negatively affect preloading and the the state
// will be replaced by rehydration anyway.
return null;
}
+
return next(action);
};
};
@@ -9644,19 +9976,31 @@ const rehydrationMiddleware = ({
* @return {object} A redux store
*/
function initStore(reducers, initialState) {
- const store = (0,external_Redux_namespaceObject.createStore)(mergeStateReducer((0,external_Redux_namespaceObject.combineReducers)(reducers)), initialState, __webpack_require__.g.RPMAddMessageListener && (0,external_Redux_namespaceObject.applyMiddleware)(rehydrationMiddleware, messageMiddleware));
- if (__webpack_require__.g.RPMAddMessageListener) {
- __webpack_require__.g.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => {
+ const store = (0,external_Redux_namespaceObject.createStore)(
+ mergeStateReducer((0,external_Redux_namespaceObject.combineReducers)(reducers)),
+ initialState,
+ globalThis.RPMAddMessageListener &&
+ (0,external_Redux_namespaceObject.applyMiddleware)(rehydrationMiddleware, messageMiddleware)
+ );
+
+ if (globalThis.RPMAddMessageListener) {
+ globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => {
try {
store.dispatch(msg.data);
} catch (ex) {
console.error("Content msg:", msg, "Dispatch error: ", ex);
- dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`);
+ dump(
+ `Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${
+ ex.stack
+ }`
+ );
}
});
}
+
return store;
}
+
;// CONCATENATED MODULE: external "ReactDOM"
const external_ReactDOM_namespaceObject = ReactDOM;
var external_ReactDOM_default = /*#__PURE__*/__webpack_require__.n(external_ReactDOM_namespaceObject);
diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif
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..886b19df7b 100644
--- a/browser/components/newtab/karma.mc.config.js
+++ b/browser/components/newtab/karma.mc.config.js
@@ -158,6 +158,15 @@ module.exports = function (config) {
functions: 0,
branches: 0,
},
+ /**
+ * WallpaperFeed.sys.mjs is tested via an xpcshell test
+ */
+ "lib/WallpaperFeed.sys.mjs": {
+ statements: 0,
+ lines: 0,
+ functions: 0,
+ branches: 0,
+ },
"content-src/components/DiscoveryStreamComponents/**/*.jsx": {
statements: 90.48,
lines: 90.48,
@@ -170,6 +179,15 @@ module.exports = function (config) {
functions: 60,
branches: 50,
},
+ /**
+ * WallpaperSection.jsx is tested via an xpcshell test
+ */
+ "content-src/components/WallpapersSection/*.jsx": {
+ statements: 0,
+ lines: 0,
+ functions: 0,
+ branches: 0,
+ },
"content-src/components/DiscoveryStreamAdmin/*.jsx": {
statements: 0,
lines: 0,
@@ -211,7 +229,7 @@ module.exports = function (config) {
devtool: "inline-source-map",
// This resolve config allows us to import with paths relative to the root directory, e.g. "lib/ActivityStream.sys.mjs"
resolve: {
- extensions: [".js", ".jsx"],
+ extensions: [".js", ".jsx", ".mjs"],
modules: [PATHS.moduleResolveDirectory, "node_modules"],
alias: {
asrouter: path.join(__dirname, "../asrouter"),
@@ -260,7 +278,7 @@ module.exports = function (config) {
},
{
enforce: "post",
- test: /\.js[mx]?$/,
+ test: /\.js[x]?$/,
loader: "@jsdevtools/coverage-istanbul-loader",
options: { esModules: true },
include: [
diff --git a/browser/components/newtab/lib/AboutPreferences.sys.mjs b/browser/components/newtab/lib/AboutPreferences.sys.mjs
index 33f7ecdaeb..08e0ca422a 100644
--- a/browser/components/newtab/lib/AboutPreferences.sys.mjs
+++ b/browser/components/newtab/lib/AboutPreferences.sys.mjs
@@ -5,7 +5,7 @@
import {
actionTypes as at,
actionCreators as ac,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
const HTML_NS = "http://www.w3.org/1999/xhtml";
export const PREFERENCES_LOADED_EVENT = "home-pane-loaded";
diff --git a/browser/components/newtab/lib/ActivityStream.sys.mjs b/browser/components/newtab/lib/ActivityStream.sys.mjs
index f46e8aadf0..fa2d011f11 100644
--- a/browser/components/newtab/lib/ActivityStream.sys.mjs
+++ b/browser/components/newtab/lib/ActivityStream.sys.mjs
@@ -36,6 +36,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TelemetryFeed: "resource://activity-stream/lib/TelemetryFeed.sys.mjs",
TopSitesFeed: "resource://activity-stream/lib/TopSitesFeed.sys.mjs",
TopStoriesFeed: "resource://activity-stream/lib/TopStoriesFeed.sys.mjs",
+ WallpaperFeed: "resource://activity-stream/lib/WallpaperFeed.sys.mjs",
});
// NB: Eagerly load modules that will be loaded/constructed/initialized in the
@@ -43,7 +44,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
import {
actionCreators as ac,
actionTypes as at,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
const REGION_BASIC_CONFIG =
"browser.newtabpage.activity-stream.discoverystream.region-basic-config";
@@ -233,6 +234,27 @@ export const PREFS_CONFIG = new Map([
},
],
[
+ "newtabWallpapers.enabled",
+ {
+ title: "Boolean flag to turn wallpaper functionality on and off",
+ value: true,
+ },
+ ],
+ [
+ "newtabWallpapers.wallpaper-light",
+ {
+ title: "Currently set light wallpaper",
+ value: "",
+ },
+ ],
+ [
+ "newtabWallpapers.wallpaper-dark",
+ {
+ title: "Currently set dark wallpaper",
+ value: "",
+ },
+ ],
+ [
"improvesearch.noDefaultSearchTile",
{
title: "Remove tiles that are the same as the default search",
@@ -524,6 +546,12 @@ const FEEDS_DATA = [
title: "Handles new pocket ui for the new tab page",
value: true,
},
+ {
+ name: "wallpaperfeed",
+ factory: () => new lazy.WallpaperFeed(),
+ title: "Handles fetching and managing wallpaper data from RemoteSettings",
+ value: true,
+ },
];
const FEEDS_CONFIG = new Map();
diff --git a/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs b/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs
index 5392a421ca..3cb81b4793 100644
--- a/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs
+++ b/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs
@@ -13,7 +13,7 @@ import {
actionCreators as ac,
actionTypes as at,
actionUtils as au,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
const ABOUT_NEW_TAB_URL = "about:newtab";
diff --git a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs
index ee08462503..bff9f1e04e 100644
--- a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs
+++ b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs
@@ -26,7 +26,7 @@ const { setTimeout, clearTimeout } = ChromeUtils.importESModule(
import {
actionTypes as at,
actionCreators as ac,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
const CACHE_KEY = "discovery_stream";
const STARTUP_CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week
@@ -565,8 +565,8 @@ export class DiscoveryStreamFeed {
generateFeedUrl(isBff) {
if (isBff) {
- return `https://${lazy.NimbusFeatures.saveToPocket.getVariable(
- "bffApi"
+ return `https://${Services.prefs.getStringPref(
+ "extensions.pocket.bffApi"
)}/desktop/v1/recommendations?locale=$locale&region=$region&count=30`;
}
return FEED_URL;
@@ -986,8 +986,9 @@ export class DiscoveryStreamFeed {
});
if (spocsResponse) {
+ const fetchTimestamp = Date.now();
spocsState = {
- lastUpdated: Date.now(),
+ lastUpdated: fetchTimestamp,
spocs: {
...spocsResponse,
},
@@ -1050,8 +1051,13 @@ export class DiscoveryStreamFeed {
const { data: blockedResults } = this.filterBlocked(capResult);
+ const { data: spocsWithFetchTimestamp } = this.addFetchTimestamp(
+ blockedResults,
+ fetchTimestamp
+ );
+
const { data: scoredResults, personalized } =
- await this.scoreItems(blockedResults, "spocs");
+ await this.scoreItems(spocsWithFetchTimestamp, "spocs");
spocsState.spocs = {
...spocsState.spocs,
@@ -1209,6 +1215,22 @@ export class DiscoveryStreamFeed {
return { data };
}
+ // Add the fetch timestamp property to each spoc returned to communicate how
+ // old the spoc is in telemetry when it is used by the client
+ addFetchTimestamp(spocs, fetchTimestamp) {
+ if (spocs && spocs.length) {
+ return {
+ data: spocs.map(s => {
+ return {
+ ...s,
+ fetchTimestamp,
+ };
+ }),
+ };
+ }
+ return { data: spocs };
+ }
+
// For backwards compatibility, older spoc endpoint don't have flight_id,
// but instead had campaign_id we can use
//
@@ -1334,8 +1356,8 @@ export class DiscoveryStreamFeed {
let options = {};
if (this.isBff) {
const headers = new Headers();
- const oAuthConsumerKey = lazy.NimbusFeatures.saveToPocket.getVariable(
- "oAuthConsumerKeyBff"
+ const oAuthConsumerKey = Services.prefs.getStringPref(
+ "extensions.pocket.oAuthConsumerKeyBff"
);
headers.append("consumer_key", oAuthConsumerKey);
options = {
@@ -1768,7 +1790,7 @@ export class DiscoveryStreamFeed {
break;
// Check if spocs was disabled. Remove them if they were.
case PREF_SHOW_SPONSORED:
- case PREF_SHOW_SPONSORED_TOPSITES:
+ case PREF_SHOW_SPONSORED_TOPSITES: {
const dispatch = update =>
this.store.dispatch(ac.BroadcastToContent(update));
// We refresh placements data because one of the spocs were turned off.
@@ -1794,6 +1816,7 @@ export class DiscoveryStreamFeed {
await this.cache.set("spocs", {});
await this.loadSpocs(dispatch);
break;
+ }
}
}
diff --git a/browser/components/newtab/lib/DownloadsManager.sys.mjs b/browser/components/newtab/lib/DownloadsManager.sys.mjs
index a9a57222ee..f6e99e462a 100644
--- a/browser/components/newtab/lib/DownloadsManager.sys.mjs
+++ b/browser/components/newtab/lib/DownloadsManager.sys.mjs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs";
+import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs";
const lazy = {};
diff --git a/browser/components/newtab/lib/FaviconFeed.sys.mjs b/browser/components/newtab/lib/FaviconFeed.sys.mjs
index a76566d3e8..18c2231f58 100644
--- a/browser/components/newtab/lib/FaviconFeed.sys.mjs
+++ b/browser/components/newtab/lib/FaviconFeed.sys.mjs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs";
+import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs";
import { getDomain } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs";
// We use importESModule here instead of static import so that
diff --git a/browser/components/newtab/lib/HighlightsFeed.sys.mjs b/browser/components/newtab/lib/HighlightsFeed.sys.mjs
index c603b886da..00eb109896 100644
--- a/browser/components/newtab/lib/HighlightsFeed.sys.mjs
+++ b/browser/components/newtab/lib/HighlightsFeed.sys.mjs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs";
+import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs";
import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs";
import {
diff --git a/browser/components/newtab/lib/NewTabInit.sys.mjs b/browser/components/newtab/lib/NewTabInit.sys.mjs
index db30e009ec..768cc29ea4 100644
--- a/browser/components/newtab/lib/NewTabInit.sys.mjs
+++ b/browser/components/newtab/lib/NewTabInit.sys.mjs
@@ -5,7 +5,7 @@
import {
actionCreators as ac,
actionTypes as at,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
/**
* NewTabInit - A placeholder for now. This will send a copy of the state to all
diff --git a/browser/components/newtab/lib/PlacesFeed.sys.mjs b/browser/components/newtab/lib/PlacesFeed.sys.mjs
index 70011412f8..85679153bd 100644
--- a/browser/components/newtab/lib/PlacesFeed.sys.mjs
+++ b/browser/components/newtab/lib/PlacesFeed.sys.mjs
@@ -6,7 +6,7 @@ import {
actionCreators as ac,
actionTypes as at,
actionUtils as au,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs";
diff --git a/browser/components/newtab/lib/PrefsFeed.sys.mjs b/browser/components/newtab/lib/PrefsFeed.sys.mjs
index bb2502ac55..4cb41c0421 100644
--- a/browser/components/newtab/lib/PrefsFeed.sys.mjs
+++ b/browser/components/newtab/lib/PrefsFeed.sys.mjs
@@ -5,7 +5,7 @@
import {
actionCreators as ac,
actionTypes as at,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs";
// We use importESModule here instead of static import so that
diff --git a/browser/components/newtab/lib/RecommendationProvider.sys.mjs b/browser/components/newtab/lib/RecommendationProvider.sys.mjs
index 875c90492b..9fd6b71656 100644
--- a/browser/components/newtab/lib/RecommendationProvider.sys.mjs
+++ b/browser/components/newtab/lib/RecommendationProvider.sys.mjs
@@ -12,7 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
import {
actionTypes as at,
actionCreators as ac,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
const CACHE_KEY = "personalization";
const PREF_PERSONALIZATION_MODEL_KEYS =
diff --git a/browser/components/newtab/lib/SectionsManager.sys.mjs b/browser/components/newtab/lib/SectionsManager.sys.mjs
index 069ddbb224..a1634e0d47 100644
--- a/browser/components/newtab/lib/SectionsManager.sys.mjs
+++ b/browser/components/newtab/lib/SectionsManager.sys.mjs
@@ -15,7 +15,7 @@ const { EventEmitter } = ChromeUtils.importESModule(
import {
actionCreators as ac,
actionTypes as at,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
import { getDefaultOptions } from "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs";
const lazy = {};
@@ -389,7 +389,7 @@ export const SectionsManager = {
/**
* Sets each card in highlights' context menu options based on the card's type.
- * (See types.js for a list of types)
+ * (See types.mjs for a list of types)
*
* @param rows section rows containing a type for each card
*/
diff --git a/browser/components/newtab/lib/SystemTickFeed.sys.mjs b/browser/components/newtab/lib/SystemTickFeed.sys.mjs
index d87860fab2..fdbbda3ddd 100644
--- a/browser/components/newtab/lib/SystemTickFeed.sys.mjs
+++ b/browser/components/newtab/lib/SystemTickFeed.sys.mjs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs";
+import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs";
const lazy = {};
diff --git a/browser/components/newtab/lib/TelemetryFeed.sys.mjs b/browser/components/newtab/lib/TelemetryFeed.sys.mjs
index 1a9e9e3d34..6cf4dba4ab 100644
--- a/browser/components/newtab/lib/TelemetryFeed.sys.mjs
+++ b/browser/components/newtab/lib/TelemetryFeed.sys.mjs
@@ -18,13 +18,13 @@ const { XPCOMUtils } = ChromeUtils.importESModule(
// eslint-disable-next-line mozilla/use-static-import
const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule(
- "resource:///modules/asrouter/ActorConstants.sys.mjs"
+ "resource:///modules/asrouter/ActorConstants.mjs"
);
import {
actionTypes as at,
actionUtils as au,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs";
import { classifySite } from "resource://activity-stream/lib/SiteClassifier.sys.mjs";
@@ -454,8 +454,7 @@ export class TelemetryFeed {
event = await this.applyCFRPolicy(event);
break;
case "badge_user_event":
- case "whats-new-panel_user_event":
- event = await this.applyWhatsNewPolicy(event);
+ event = await this.applyToolbarBadgePolicy(event);
break;
case "infobar_user_event":
event = await this.applyInfoBarPolicy(event);
@@ -509,12 +508,12 @@ export class TelemetryFeed {
* Per Bug 1482134, all the metrics for What's New panel use client_id in
* all the release channels
*/
- async applyWhatsNewPolicy(ping) {
+ async applyToolbarBadgePolicy(ping) {
ping.client_id = await this.telemetryClientId;
ping.browser_session_id = lazy.browserSessionId;
// Attach page info to `event_context` if there is a session associated with this ping
delete ping.action;
- return { ping, pingType: "whats-new-panel" };
+ return { ping, pingType: "toolbar-badge" };
}
async applyInfoBarPolicy(ping) {
@@ -715,8 +714,16 @@ export class TelemetryFeed {
const session = this.sessions.get(au.getPortIdOfSender(action));
switch (action.data?.event) {
case "CLICK": {
- const { card_type, topic, recommendation_id, tile_id, shim, feature } =
- action.data.value ?? {};
+ const {
+ card_type,
+ topic,
+ recommendation_id,
+ tile_id,
+ shim,
+ fetchTimestamp,
+ firstVisibleTimestamp,
+ feature,
+ } = action.data.value ?? {};
if (
action.data.source === "POPULAR_TOPICS" ||
card_type === "topics_widget"
@@ -740,6 +747,14 @@ export class TelemetryFeed {
});
if (shim) {
Glean.pocket.shim.set(shim);
+ if (fetchTimestamp) {
+ Glean.pocket.fetchTimestamp.set(fetchTimestamp * 1000);
+ }
+ if (firstVisibleTimestamp) {
+ Glean.pocket.newtabCreationTimestamp.set(
+ firstVisibleTimestamp * 1000
+ );
+ }
GleanPings.spoc.submit("click");
}
}
@@ -755,6 +770,16 @@ export class TelemetryFeed {
});
if (action.data.value?.shim) {
Glean.pocket.shim.set(action.data.value.shim);
+ if (action.data.value.fetchTimestamp) {
+ Glean.pocket.fetchTimestamp.set(
+ action.data.value.fetchTimestamp * 1000
+ );
+ }
+ if (action.data.value.newtabCreationTimestamp) {
+ Glean.pocket.newtabCreationTimestamp.set(
+ action.data.value.newtabCreationTimestamp * 1000
+ );
+ }
GleanPings.spoc.submit("save");
}
break;
@@ -976,6 +1001,14 @@ export class TelemetryFeed {
});
if (tile.shim) {
Glean.pocket.shim.set(tile.shim);
+ if (tile.fetchTimestamp) {
+ Glean.pocket.fetchTimestamp.set(tile.fetchTimestamp * 1000);
+ }
+ if (data.firstVisibleTimestamp) {
+ Glean.pocket.newtabCreationTimestamp.set(
+ data.firstVisibleTimestamp * 1000
+ );
+ }
GleanPings.spoc.submit("impression");
}
});
diff --git a/browser/components/newtab/lib/TopSitesFeed.sys.mjs b/browser/components/newtab/lib/TopSitesFeed.sys.mjs
index 796211085b..e259253402 100644
--- a/browser/components/newtab/lib/TopSitesFeed.sys.mjs
+++ b/browser/components/newtab/lib/TopSitesFeed.sys.mjs
@@ -5,7 +5,7 @@
import {
actionCreators as ac,
actionTypes as at,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
import { TippyTopProvider } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs";
import {
insertPinned,
diff --git a/browser/components/newtab/lib/TopStoriesFeed.sys.mjs b/browser/components/newtab/lib/TopStoriesFeed.sys.mjs
index be030649dd..5986209a1c 100644
--- a/browser/components/newtab/lib/TopStoriesFeed.sys.mjs
+++ b/browser/components/newtab/lib/TopStoriesFeed.sys.mjs
@@ -5,7 +5,7 @@
import {
actionTypes as at,
actionCreators as ac,
-} from "resource://activity-stream/common/Actions.sys.mjs";
+} from "resource://activity-stream/common/Actions.mjs";
import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs";
import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs";
import { SectionsManager } from "resource://activity-stream/lib/SectionsManager.sys.mjs";
diff --git a/browser/components/newtab/lib/WallpaperFeed.sys.mjs b/browser/components/newtab/lib/WallpaperFeed.sys.mjs
new file mode 100644
index 0000000000..cb21311ddc
--- /dev/null
+++ b/browser/components/newtab/lib/WallpaperFeed.sys.mjs
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ Utils: "resource://services-settings/Utils.sys.mjs",
+});
+
+import {
+ actionTypes as at,
+ actionCreators as ac,
+} from "resource://activity-stream/common/Actions.mjs";
+
+const PREF_WALLPAPERS_ENABLED =
+ "browser.newtabpage.activity-stream.newtabWallpapers.enabled";
+
+export class WallpaperFeed {
+ constructor() {
+ this.loaded = false;
+ this.wallpaperClient = "";
+ this.wallpaperDB = "";
+ this.baseAttachmentURL = "";
+ }
+
+ /**
+ * This thin wrapper around global.fetch makes it easier for us to write
+ * automated tests that simulate responses from this fetch.
+ */
+ fetch(...args) {
+ return fetch(...args);
+ }
+
+ /**
+ * This thin wrapper around lazy.RemoteSettings makes it easier for us to write
+ * automated tests that simulate responses from this fetch.
+ */
+ RemoteSettings(...args) {
+ return lazy.RemoteSettings(...args);
+ }
+
+ async wallpaperSetup(isStartup = false) {
+ const wallpapersEnabled = Services.prefs.getBoolPref(
+ PREF_WALLPAPERS_ENABLED
+ );
+
+ if (wallpapersEnabled) {
+ if (!this.wallpaperClient) {
+ this.wallpaperClient = this.RemoteSettings("newtab-wallpapers");
+ }
+
+ await this.getBaseAttachment();
+ this.wallpaperClient.on("sync", () => this.updateWallpapers());
+ this.updateWallpapers(isStartup);
+ }
+ }
+
+ async getBaseAttachment() {
+ if (!this.baseAttachmentURL) {
+ const SERVER = lazy.Utils.SERVER_URL;
+ const serverInfo = await (
+ await this.fetch(`${SERVER}/`, {
+ credentials: "omit",
+ })
+ ).json();
+ const { base_url } = serverInfo.capabilities.attachments;
+ this.baseAttachmentURL = base_url;
+ }
+ }
+
+ async updateWallpapers(isStartup = false) {
+ const records = await this.wallpaperClient.get();
+ if (!records?.length) {
+ return;
+ }
+
+ if (!this.baseAttachmentURL) {
+ await this.getBaseAttachment();
+ }
+ const wallpapers = records.map(record => {
+ return {
+ ...record,
+ wallpaperUrl: `${this.baseAttachmentURL}${record.attachment.location}`,
+ };
+ });
+
+ this.store.dispatch(
+ ac.BroadcastToContent({
+ type: at.WALLPAPERS_SET,
+ data: wallpapers,
+ meta: {
+ isStartup,
+ },
+ })
+ );
+ }
+
+ async onAction(action) {
+ switch (action.type) {
+ case at.INIT:
+ await this.wallpaperSetup(true /* isStartup */);
+ break;
+ case at.UNINIT:
+ break;
+ case at.SYSTEM_TICK:
+ break;
+ case at.PREF_CHANGED:
+ if (action.data.name === "newtabWallpapers.enabled") {
+ await this.wallpaperSetup(false /* isStartup */);
+ }
+ break;
+ case at.WALLPAPERS_SET:
+ break;
+ }
+ }
+}
diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml
index bd74e609ad..c59247ceef 100644
--- a/browser/components/newtab/metrics.yaml
+++ b/browser/components/newtab/metrics.yaml
@@ -817,6 +817,35 @@ pocket:
send_in_pings:
- spoc
+ fetch_timestamp:
+ type: datetime
+ lifetime: ping
+ description: |
+ Timestamp of when the spoc was fetched by the client
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655
+ notification_emails:
+ - dmueller@mozilla.com
+ expires: never
+ send_in_pings:
+ - spoc
+
+ newtab_creation_timestamp:
+ type: datetime
+ lifetime: ping
+ description: |
+ Timestamp of when this instance of the newtab was first visible to the user.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655
+ notification_emails:
+ - dmueller@mozilla.com
+ expires: never
+ send_in_pings:
+ - spoc
messaging_system:
event_context_parse_error:
@@ -1031,7 +1060,7 @@ messaging_system:
type: string
description: >
Type of event the ping is capturing.
- e.g. "cfr", "whats-new-panel", "onboarding"
+ e.g. "cfr", "onboarding"
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1825863
data_reviews:
diff --git a/browser/components/newtab/test/browser/browser_as_load_location.js b/browser/components/newtab/test/browser/browser_as_load_location.js
index f11b6cf503..ce67ede0c6 100644
--- a/browser/components/newtab/test/browser/browser_as_load_location.js
+++ b/browser/components/newtab/test/browser/browser_as_load_location.js
@@ -8,7 +8,7 @@
*/
async function checkNewtabLoads(selector, message) {
// simulate a newtab open as a user would
- BrowserOpenTab();
+ BrowserCommands.openTab();
// wait until the browser loads
let browser = gBrowser.selectedBrowser;
diff --git a/browser/components/newtab/test/browser/browser_newtab_overrides.js b/browser/components/newtab/test/browser/browser_newtab_overrides.js
index 1d4a0c36e3..c876a62c4e 100644
--- a/browser/components/newtab/test/browser/browser_newtab_overrides.js
+++ b/browser/components/newtab/test/browser/browser_newtab_overrides.js
@@ -82,7 +82,7 @@ add_task(async function override_loads_in_browser() {
Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden");
// simulate a newtab open as a user would
- BrowserOpenTab();
+ BrowserCommands.openTab();
let browser = gBrowser.selectedBrowser;
await BrowserTestUtils.browserLoaded(browser);
@@ -116,7 +116,7 @@ add_task(async function override_blank_loads_in_browser() {
Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden");
// simulate a newtab open as a user would
- BrowserOpenTab();
+ BrowserCommands.openTab();
let browser = gBrowser.selectedBrowser;
await BrowserTestUtils.browserLoaded(browser);
diff --git a/browser/components/newtab/test/schemas/pings.js b/browser/components/newtab/test/schemas/pings.js
index fb52602bd4..2a1dd35ec6 100644
--- a/browser/components/newtab/test/schemas/pings.js
+++ b/browser/components/newtab/test/schemas/pings.js
@@ -1,7 +1,4 @@
-import {
- CONTENT_MESSAGE_TYPE,
- MAIN_MESSAGE_TYPE,
-} from "common/Actions.sys.mjs";
+import { CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE } from "common/Actions.mjs";
import Joi from "joi-browser";
export const baseKeys = {
diff --git a/browser/components/newtab/test/unit/common/Actions.test.js b/browser/components/newtab/test/unit/common/Actions.test.js
index 32e417ea3f..af8d18cee8 100644
--- a/browser/components/newtab/test/unit/common/Actions.test.js
+++ b/browser/components/newtab/test/unit/common/Actions.test.js
@@ -8,7 +8,7 @@ import {
MAIN_MESSAGE_TYPE,
PRELOAD_MESSAGE_TYPE,
UI_CODE,
-} from "common/Actions.sys.mjs";
+} from "common/Actions.mjs";
describe("Actions", () => {
it("should set globalImportContext to UI_CODE", () => {
diff --git a/browser/components/newtab/test/unit/common/Reducers.test.js b/browser/components/newtab/test/unit/common/Reducers.test.js
index 7343fc6224..62f6f48353 100644
--- a/browser/components/newtab/test/unit/common/Reducers.test.js
+++ b/browser/components/newtab/test/unit/common/Reducers.test.js
@@ -11,7 +11,7 @@ const {
Search,
ASRouter,
} = reducers;
-import { actionTypes as at } from "common/Actions.sys.mjs";
+import { actionTypes as at } from "common/Actions.mjs";
describe("Reducers", () => {
describe("App", () => {
diff --git a/browser/components/newtab/test/unit/content-src/components/Base.test.jsx b/browser/components/newtab/test/unit/content-src/components/Base.test.jsx
index c764348006..d8d300a3c9 100644
--- a/browser/components/newtab/test/unit/content-src/components/Base.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/Base.test.jsx
@@ -8,7 +8,7 @@ import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundar
import React from "react";
import { Search } from "content-src/components/Search/Search";
import { shallow } from "enzyme";
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
describe("<Base>", () => {
let DEFAULT_PROPS = {
@@ -21,6 +21,11 @@ describe("<Base>", () => {
adminContent: {
message: {},
},
+ document: {
+ visibilityState: "visible",
+ addEventListener: sinon.stub(),
+ removeEventListener: sinon.stub(),
+ },
};
it("should render Base component", () => {
@@ -76,6 +81,11 @@ describe("<BaseContent>", () => {
Sections: [],
DiscoveryStream: { config: { enabled: false } },
dispatch: () => {},
+ document: {
+ visibilityState: "visible",
+ addEventListener: sinon.stub(),
+ removeEventListener: sinon.stub(),
+ },
};
it("should render an ErrorBoundary with a Search child", () => {
@@ -114,6 +124,73 @@ describe("<BaseContent>", () => {
const wrapper = shallow(<BaseContent {...onlySearchProps} />);
assert.lengthOf(wrapper.find(".only-search"), 1);
});
+
+ it("should update firstVisibleTimestamp if it is visible immediately with no event listener", () => {
+ const props = Object.assign({}, DEFAULT_PROPS, {
+ document: {
+ visibilityState: "visible",
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ },
+ });
+
+ const wrapper = shallow(<BaseContent {...props} />);
+ assert.notCalled(props.document.addEventListener);
+ assert.isDefined(wrapper.state("firstVisibleTimestamp"));
+ });
+ it("should attach an event listener for visibility change if it is not visible", () => {
+ const props = Object.assign({}, DEFAULT_PROPS, {
+ document: {
+ visibilityState: "hidden",
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ },
+ });
+
+ const wrapper = shallow(<BaseContent {...props} />);
+ assert.calledWith(props.document.addEventListener, "visibilitychange");
+ assert.notExists(wrapper.state("firstVisibleTimestamp"));
+ });
+ it("should remove the event listener for visibility change when unmounted", () => {
+ const props = Object.assign({}, DEFAULT_PROPS, {
+ document: {
+ visibilityState: "hidden",
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ },
+ });
+
+ const wrapper = shallow(<BaseContent {...props} />);
+ const [, listener] = props.document.addEventListener.firstCall.args;
+
+ wrapper.unmount();
+ assert.calledWith(
+ props.document.removeEventListener,
+ "visibilitychange",
+ listener
+ );
+ });
+ it("should remove the event listener for visibility change after becoming visible", () => {
+ const listeners = new Set();
+ const props = Object.assign({}, DEFAULT_PROPS, {
+ document: {
+ visibilityState: "hidden",
+ addEventListener: (ev, cb) => listeners.add(cb),
+ removeEventListener: (ev, cb) => listeners.delete(cb),
+ },
+ });
+
+ const wrapper = shallow(<BaseContent {...props} />);
+ assert.equal(listeners.size, 1);
+ assert.notExists(wrapper.state("firstVisibleTimestamp"));
+
+ // Simulate listeners getting called
+ props.document.visibilityState = "visible";
+ listeners.forEach(l => l());
+
+ assert.equal(listeners.size, 0);
+ assert.isDefined(wrapper.state("firstVisibleTimestamp"));
+ });
});
describe("<PrefsButton>", () => {
diff --git a/browser/components/newtab/test/unit/content-src/components/Card.test.jsx b/browser/components/newtab/test/unit/content-src/components/Card.test.jsx
index 5f07570b2e..f7f065efae 100644
--- a/browser/components/newtab/test/unit/content-src/components/Card.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/Card.test.jsx
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import {
_Card as Card,
PlaceholderCard,
diff --git a/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx b/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx
index baf203947e..fcc1dd0f45 100644
--- a/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer";
import createMockRaf from "mock-raf";
import React from "react";
diff --git a/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx b/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx
index a471c09e66..3befa4403f 100644
--- a/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { _ConfirmDialog as ConfirmDialog } from "content-src/components/ConfirmDialog/ConfirmDialog";
import React from "react";
import { shallow } from "enzyme";
diff --git a/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx b/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx
index e1f84f7d84..0407622cf9 100644
--- a/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx
@@ -1,4 +1,4 @@
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import { ContentSection } from "content-src/components/CustomizeMenu/ContentSection/ContentSection";
import { mount } from "enzyme";
import React from "react";
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx
index 41849fba3e..7f40b66200 100644
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import {
DiscoveryStreamAdminInner,
CollapseToggle,
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx
index 418a731ba1..ffa32bfc3e 100644
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx
@@ -13,10 +13,7 @@ import {
PlaceholderDSCard,
} from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
import { TopicsWidget } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget";
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import React from "react";
import { shallow, mount } from "enzyme";
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
index 1d572ee3ce..afb6d6dcd2 100644
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
@@ -10,10 +10,7 @@ import {
StatusMessage,
SponsorLabel,
} from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter";
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { DSLinkMenu } from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu";
import React from "react";
import { INITIAL_STATE } from "common/Reducers.sys.mjs";
@@ -28,6 +25,8 @@ const DEFAULT_PROPS = {
isForStartupCache: false,
},
DiscoveryStream: INITIAL_STATE.DiscoveryStream,
+ fetchTimestamp: new Date("March 20, 2024 10:30:44").getTime(),
+ firstVisibleTimestamp: new Date("March 21, 2024 10:11:12").getTime(),
};
describe("<DSCard>", () => {
@@ -174,6 +173,8 @@ describe("<DSCard>", () => {
card_type: "organic",
recommendation_id: undefined,
tile_id: "fooidx",
+ fetchTimestamp: DEFAULT_PROPS.fetchTimestamp,
+ firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp,
},
})
);
@@ -212,6 +213,8 @@ describe("<DSCard>", () => {
card_type: "spoc",
recommendation_id: undefined,
tile_id: "fooidx",
+ fetchTimestamp: DEFAULT_PROPS.fetchTimestamp,
+ firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp,
},
})
);
@@ -258,6 +261,8 @@ describe("<DSCard>", () => {
recommendation_id: undefined,
tile_id: "fooidx",
shim: "click shim",
+ fetchTimestamp: DEFAULT_PROPS.fetchTimestamp,
+ firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp,
},
})
);
@@ -370,7 +375,12 @@ describe("<DSCard>", () => {
describe("DSCard onSaveClick", () => {
it("should fire telemetry for onSaveClick", () => {
- wrapper.setProps({ id: "fooidx", pos: 1, type: "foo" });
+ wrapper.setProps({
+ id: "fooidx",
+ pos: 1,
+ type: "foo",
+ fetchTimestamp: undefined,
+ });
wrapper.instance().onSaveClick();
assert.calledThrice(dispatch);
@@ -391,6 +401,8 @@ describe("<DSCard>", () => {
card_type: "organic",
recommendation_id: undefined,
tile_id: "fooidx",
+ fetchTimestamp: undefined,
+ firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp,
},
})
);
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
index 08ac7868ce..a18e688758 100644
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
@@ -5,7 +5,7 @@ import {
} from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter";
import React from "react";
import { mount } from "enzyme";
-import { cardContextTypes } from "content-src/components/Card/types.js";
+import { cardContextTypes } from "content-src/components/Card/types.mjs";
import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText.jsx";
describe("<DSContextFooter>", () => {
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx
index b4b743c7ff..b5acbf3b56 100644
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx
@@ -1,6 +1,6 @@
import { DSPrivacyModal } from "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal";
import { shallow, mount } from "enzyme";
-import { actionCreators as ac } from "common/Actions.sys.mjs";
+import { actionCreators as ac } from "common/Actions.mjs";
import React from "react";
describe("Discovery Stream <DSPrivacyModal>", () => {
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
index 4926cc6c70..c935acde1a 100644
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
@@ -2,7 +2,7 @@ import {
ImpressionStats,
INTERSECTION_RATIO,
} from "content-src/components/DiscoveryStreamImpressionStats/ImpressionStats";
-import { actionTypes as at } from "common/Actions.sys.mjs";
+import { actionTypes as at } from "common/Actions.mjs";
import React from "react";
import { shallow } from "enzyme";
@@ -33,12 +33,15 @@ describe("<ImpressionStats>", () => {
};
}
+ const TEST_FETCH_TIMESTAMP = Date.now();
+ const TEST_FIRST_VISIBLE_TIMESTAMP = Date.now();
const DEFAULT_PROPS = {
rows: [
- { id: 1, pos: 0 },
- { id: 2, pos: 1 },
- { id: 3, pos: 2 },
+ { id: 1, pos: 0, fetchTimestamp: TEST_FETCH_TIMESTAMP },
+ { id: 2, pos: 1, fetchTimestamp: TEST_FETCH_TIMESTAMP },
+ { id: 3, pos: 2, fetchTimestamp: TEST_FETCH_TIMESTAMP },
],
+ firstVisibleTimestamp: TEST_FIRST_VISIBLE_TIMESTAMP,
source: SOURCE,
IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
document: {
@@ -76,7 +79,7 @@ describe("<ImpressionStats>", () => {
assert.notCalled(dispatch);
});
- it("should noly send loaded content but not impression when the wrapped item is not visbible", () => {
+ it("should only send loaded content but not impression when the wrapped item is not visbible", () => {
const dispatch = sinon.spy();
const props = {
dispatch,
@@ -128,11 +131,37 @@ describe("<ImpressionStats>", () => {
[action] = dispatch.secondCall.args;
assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);
assert.equal(action.data.source, SOURCE);
+ assert.equal(
+ action.data.firstVisibleTimestamp,
+ TEST_FIRST_VISIBLE_TIMESTAMP
+ );
assert.deepEqual(action.data.tiles, [
- { id: 1, pos: 0, type: "organic", recommendation_id: undefined },
- { id: 2, pos: 1, type: "organic", recommendation_id: undefined },
- { id: 3, pos: 2, type: "organic", recommendation_id: undefined },
+ {
+ id: 1,
+ pos: 0,
+ type: "organic",
+ recommendation_id: undefined,
+ fetchTimestamp: TEST_FETCH_TIMESTAMP,
+ },
+ {
+ id: 2,
+ pos: 1,
+ type: "organic",
+ recommendation_id: undefined,
+ fetchTimestamp: TEST_FETCH_TIMESTAMP,
+ },
+ {
+ id: 3,
+ pos: 2,
+ type: "organic",
+ recommendation_id: undefined,
+ fetchTimestamp: TEST_FETCH_TIMESTAMP,
+ },
]);
+ assert.equal(
+ action.data.firstVisibleTimestamp,
+ TEST_FIRST_VISIBLE_TIMESTAMP
+ );
});
it("should send a DISCOVERY_STREAM_SPOC_IMPRESSION when the wrapped item has a flightId", () => {
const dispatch = sinon.spy();
@@ -207,10 +236,32 @@ describe("<ImpressionStats>", () => {
[action] = dispatch.firstCall.args;
assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);
assert.deepEqual(action.data.tiles, [
- { id: 1, pos: 0, type: "organic", recommendation_id: undefined },
- { id: 2, pos: 1, type: "organic", recommendation_id: undefined },
- { id: 3, pos: 2, type: "organic", recommendation_id: undefined },
+ {
+ id: 1,
+ pos: 0,
+ type: "organic",
+ recommendation_id: undefined,
+ fetchTimestamp: TEST_FETCH_TIMESTAMP,
+ },
+ {
+ id: 2,
+ pos: 1,
+ type: "organic",
+ recommendation_id: undefined,
+ fetchTimestamp: TEST_FETCH_TIMESTAMP,
+ },
+ {
+ id: 3,
+ pos: 2,
+ type: "organic",
+ recommendation_id: undefined,
+ fetchTimestamp: TEST_FETCH_TIMESTAMP,
+ },
]);
+ assert.equal(
+ action.data.firstVisibleTimestamp,
+ TEST_FIRST_VISIBLE_TIMESTAMP
+ );
});
it("should remove visibility change listener when the wrapper is removed", () => {
const props = {
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx
index f879600a8f..5c9dcb4c14 100644
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx
@@ -6,10 +6,7 @@ import {
TopicsWidget,
} from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget";
import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { mount } from "enzyme";
import React from "react";
diff --git a/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx b/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx
index 9f4008369a..69d023c668 100644
--- a/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx
@@ -5,7 +5,7 @@ import {
SectionIntl,
_Sections as Sections,
} from "content-src/components/Sections/Sections";
-import { actionTypes as at } from "common/Actions.sys.mjs";
+import { actionTypes as at } from "common/Actions.mjs";
import { mount, shallow } from "enzyme";
import { PlaceholderCard } from "content-src/components/Card/Card";
import { PocketLoggedInCta } from "content-src/components/PocketLoggedInCta/PocketLoggedInCta";
diff --git a/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx b/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx
index 798bb9b8c7..9797a4863e 100644
--- a/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
import { MIN_RICH_FAVICON_SIZE } from "content-src/components/TopSites/TopSitesConstants";
import {
diff --git a/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx b/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx
index 3f7e725de0..b1b501ca44 100644
--- a/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx
@@ -2,7 +2,7 @@ import {
TopSiteImpressionWrapper,
INTERSECTION_RATIO,
} from "content-src/components/TopSites/TopSiteImpressionWrapper";
-import { actionTypes as at } from "common/Actions.sys.mjs";
+import { actionTypes as at } from "common/Actions.mjs";
import React from "react";
import { shallow } from "enzyme";
diff --git a/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js b/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js
index 5a7fad7cc0..3629bb7a68 100644
--- a/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js
+++ b/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start";
describe("detectUserSessionStart", () => {
diff --git a/browser/components/newtab/test/unit/content-src/lib/init-store.test.js b/browser/components/newtab/test/unit/content-src/lib/init-store.test.js
index 0dd510ef1a..8f998b64d0 100644
--- a/browser/components/newtab/test/unit/content-src/lib/init-store.test.js
+++ b/browser/components/newtab/test/unit/content-src/lib/init-store.test.js
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { addNumberReducer, GlobalOverrider } from "test/unit/utils";
import {
INCOMING_MESSAGE_NAME,
diff --git a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js
index 233f31b6ca..fb28c9490b 100644
--- a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js
+++ b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js
@@ -1,5 +1,5 @@
import { combineReducers, createStore } from "redux";
-import { actionTypes as at } from "common/Actions.sys.mjs";
+import { actionTypes as at } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
import { reducers } from "common/Reducers.sys.mjs";
import { selectLayoutRender } from "content-src/lib/selectLayoutRender";
diff --git a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js
index 20765608fa..a19bf698d9 100644
--- a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js
+++ b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js
@@ -3,10 +3,7 @@ import {
AboutPreferences,
PREFERENCES_LOADED_EVENT,
} from "lib/AboutPreferences.sys.mjs";
-import {
- actionTypes as at,
- actionCreators as ac,
-} from "common/Actions.sys.mjs";
+import { actionTypes as at, actionCreators as ac } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
describe("AboutPreferences Feed", () => {
diff --git a/browser/components/newtab/test/unit/lib/ActivityStream.test.js b/browser/components/newtab/test/unit/lib/ActivityStream.test.js
index b9deba1069..7921ae2c91 100644
--- a/browser/components/newtab/test/unit/lib/ActivityStream.test.js
+++ b/browser/components/newtab/test/unit/lib/ActivityStream.test.js
@@ -1,4 +1,4 @@
-import { CONTENT_MESSAGE_TYPE } from "common/Actions.sys.mjs";
+import { CONTENT_MESSAGE_TYPE } from "common/Actions.mjs";
import { ActivityStream, PREFS_CONFIG } from "lib/ActivityStream.sys.mjs";
import { GlobalOverrider } from "test/unit/utils";
diff --git a/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js b/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js
index 4bea86331d..8df62b2903 100644
--- a/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js
+++ b/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import {
ActivityStreamMessageChannel,
DEFAULT_OPTIONS,
@@ -16,7 +13,7 @@ const OPTIONS = [
];
// Create an object containing details about a tab as expected within
-// the loaded tabs map in ActivityStreamMessageChannel.jsm.
+// the loaded tabs map in ActivityStreamMessageChannel.sys.mjs.
function getTabDetails(portID, url = "about:newtab", extraArgs = {}) {
let actor = {
portID,
diff --git a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
index 92e10facb3..e10a4cbc04 100644
--- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
@@ -2,7 +2,7 @@ import {
actionCreators as ac,
actionTypes as at,
actionUtils as au,
-} from "common/Actions.sys.mjs";
+} from "common/Actions.mjs";
import { combineReducers, createStore } from "redux";
import { GlobalOverrider } from "test/unit/utils";
import { DiscoveryStreamFeed } from "lib/DiscoveryStreamFeed.sys.mjs";
@@ -849,6 +849,8 @@ describe("DiscoveryStreamFeed", () => {
spocs: { items: [{ id: "data" }] },
});
sandbox.stub(feed.cache, "set").returns(Promise.resolve());
+ const loadTimestamp = 100;
+ clock.tick(loadTimestamp);
await feed.loadSpocs(feed.store.dispatch);
@@ -860,15 +862,15 @@ describe("DiscoveryStreamFeed", () => {
title: "",
sponsor: "",
sponsored_by_override: undefined,
- items: [{ id: "data", score: 1 }],
+ items: [{ id: "data", score: 1, fetchTimestamp: loadTimestamp }],
},
},
- lastUpdated: 0,
+ lastUpdated: loadTimestamp,
});
assert.deepEqual(
feed.store.getState().DiscoveryStream.spocs.data.spocs.items[0],
- { id: "data", score: 1 }
+ { id: "data", score: 1, fetchTimestamp: loadTimestamp }
);
});
it("should normalizeSpocsItems for older spoc data", async () => {
@@ -882,7 +884,7 @@ describe("DiscoveryStreamFeed", () => {
assert.deepEqual(
feed.store.getState().DiscoveryStream.spocs.data.spocs.items[0],
- { id: "data", score: 1 }
+ { id: "data", score: 1, fetchTimestamp: 0 }
);
});
it("should dispatch DISCOVERY_STREAM_PERSONALIZATION_OVERRIDE with feature_flags", async () => {
@@ -936,7 +938,7 @@ describe("DiscoveryStreamFeed", () => {
context: "",
sponsor: "",
sponsored_by_override: undefined,
- items: [{ id: "data", score: 1 }],
+ items: [{ id: "data", score: 1, fetchTimestamp: 0 }],
},
placement2: {
title: "",
@@ -978,7 +980,7 @@ describe("DiscoveryStreamFeed", () => {
context: "context",
sponsor: "",
sponsored_by_override: undefined,
- items: [{ id: "data", score: 1 }],
+ items: [{ id: "data", score: 1, fetchTimestamp: 0 }],
},
});
});
@@ -3444,16 +3446,12 @@ describe("DiscoveryStreamFeed", () => {
},
});
sandbox.stub(global.Region, "home").get(() => "DE");
- globals.set("NimbusFeatures", {
- saveToPocket: {
- getVariable: sandbox.stub(),
- },
- });
- global.NimbusFeatures.saveToPocket.getVariable
- .withArgs("bffApi")
+ sandbox.stub(global.Services.prefs, "getStringPref");
+ global.Services.prefs.getStringPref
+ .withArgs("extensions.pocket.bffApi")
.returns("bffApi");
- global.NimbusFeatures.saveToPocket.getVariable
- .withArgs("oAuthConsumerKeyBff")
+ global.Services.prefs.getStringPref
+ .withArgs("extensions.pocket.oAuthConsumerKeyBff")
.returns("oAuthConsumerKeyBff");
});
it("should return true with isBff", async () => {
diff --git a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js
index ac262baf90..5e2979893d 100644
--- a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js
+++ b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js
@@ -1,4 +1,4 @@
-import { actionTypes as at } from "common/Actions.sys.mjs";
+import { actionTypes as at } from "common/Actions.mjs";
import { DownloadsManager } from "lib/DownloadsManager.sys.mjs";
import { GlobalOverrider } from "test/unit/utils";
diff --git a/browser/components/newtab/test/unit/lib/FaviconFeed.test.js b/browser/components/newtab/test/unit/lib/FaviconFeed.test.js
index e9be9b86ba..8b9cf24984 100644
--- a/browser/components/newtab/test/unit/lib/FaviconFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/FaviconFeed.test.js
@@ -1,6 +1,6 @@
"use strict";
import { FaviconFeed, fetchIconFromRedirects } from "lib/FaviconFeed.sys.mjs";
-import { actionTypes as at } from "common/Actions.sys.mjs";
+import { actionTypes as at } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
const FAKE_ENDPOINT = "https://foo.com/";
diff --git a/browser/components/newtab/test/unit/lib/NewTabInit.test.js b/browser/components/newtab/test/unit/lib/NewTabInit.test.js
index 68ab9d7821..0def9293f0 100644
--- a/browser/components/newtab/test/unit/lib/NewTabInit.test.js
+++ b/browser/components/newtab/test/unit/lib/NewTabInit.test.js
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { NewTabInit } from "lib/NewTabInit.sys.mjs";
describe("NewTabInit", () => {
diff --git a/browser/components/newtab/test/unit/lib/PrefsFeed.test.js b/browser/components/newtab/test/unit/lib/PrefsFeed.test.js
index 498c7198ab..8f33dce24f 100644
--- a/browser/components/newtab/test/unit/lib/PrefsFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/PrefsFeed.test.js
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
import { PrefsFeed } from "lib/PrefsFeed.sys.mjs";
diff --git a/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js b/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js
index 9e68f4869a..05999be08d 100644
--- a/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js
+++ b/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js
@@ -1,7 +1,4 @@
-import {
- actionCreators as ac,
- actionTypes as at,
-} from "common/Actions.sys.mjs";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { RecommendationProvider } from "lib/RecommendationProvider.sys.mjs";
import { combineReducers, createStore } from "redux";
import { reducers } from "common/Reducers.sys.mjs";
diff --git a/browser/components/newtab/test/unit/lib/SectionsManager.test.js b/browser/components/newtab/test/unit/lib/SectionsManager.test.js
index b3a9abd70c..45c5b7c689 100644
--- a/browser/components/newtab/test/unit/lib/SectionsManager.test.js
+++ b/browser/components/newtab/test/unit/lib/SectionsManager.test.js
@@ -5,7 +5,7 @@ import {
CONTENT_MESSAGE_TYPE,
MAIN_MESSAGE_TYPE,
PRELOAD_MESSAGE_TYPE,
-} from "common/Actions.sys.mjs";
+} from "common/Actions.mjs";
import { EventEmitter, GlobalOverrider } from "test/unit/utils";
import { SectionsFeed, SectionsManager } from "lib/SectionsManager.sys.mjs";
diff --git a/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js b/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js
index a0789b182e..f5ba73d2ea 100644
--- a/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js
@@ -2,7 +2,7 @@ import {
SYSTEM_TICK_INTERVAL,
SystemTickFeed,
} from "lib/SystemTickFeed.sys.mjs";
-import { actionTypes as at } from "common/Actions.sys.mjs";
+import { actionTypes as at } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
describe("System Tick Feed", () => {
diff --git a/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js
index 31a03947cd..1cb8a44631 100644
--- a/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js
+++ b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js
@@ -4,7 +4,7 @@
"use strict";
const { actionTypes: at } = ChromeUtils.importESModule(
- "resource://activity-stream/common/Actions.sys.mjs"
+ "resource://activity-stream/common/Actions.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
diff --git a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js
index 19f9e343f5..78dda7818e 100644
--- a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js
+++ b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js
@@ -4,7 +4,7 @@
"use strict";
const { actionTypes: at, actionCreators: ac } = ChromeUtils.importESModule(
- "resource://activity-stream/common/Actions.sys.mjs"
+ "resource://activity-stream/common/Actions.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
diff --git a/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js
index 59d82f5583..354eac8c2a 100644
--- a/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js
+++ b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js
@@ -4,11 +4,11 @@
"use strict";
const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule(
- "resource://activity-stream/common/Actions.sys.mjs"
+ "resource://activity-stream/common/Actions.mjs"
);
const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule(
- "resource:///modules/asrouter/ActorConstants.sys.mjs"
+ "resource:///modules/asrouter/ActorConstants.mjs"
);
const { updateAppInfo } = ChromeUtils.importESModule(
@@ -947,18 +947,18 @@ add_task(
}
);
-add_task(async function test_applyWhatsNewPolicy() {
+add_task(async function test_applyToolbarBadgePolicy() {
info(
- "TelemetryFeed.applyWhatsNewPolicy should set client_id and set pingType"
+ "TelemetryFeed.applyToolbarBadgePolicy should set client_id and set pingType"
);
let instance = new TelemetryFeed();
- let { ping, pingType } = await instance.applyWhatsNewPolicy({});
+ let { ping, pingType } = await instance.applyToolbarBadgePolicy({});
Assert.equal(
ping.client_id,
Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
);
- Assert.equal(pingType, "whats-new-panel");
+ Assert.equal(pingType, "toolbar-badge");
});
add_task(async function test_applyInfoBarPolicy() {
@@ -1288,10 +1288,10 @@ add_task(async function test_createASRouterEvent_call_correctPolicy() {
message_id: "onboarding_message_01",
});
- testCallCorrectPolicy("applyWhatsNewPolicy", {
- action: "whats-new-panel_user_event",
- event: "CLICK_BUTTON",
- message_id: "whats-new-panel_message_01",
+ testCallCorrectPolicy("applyToolbarBadgePolicy", {
+ action: "badge_user_event",
+ event: "IMPRESSION",
+ message_id: "badge_message_01",
});
testCallCorrectPolicy("applyMomentsPolicy", {
@@ -2230,6 +2230,8 @@ add_task(
const POS_1 = 1;
const POS_2 = 4;
const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ=";
+ const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20");
+ const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30");
sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
let pingSubmitted = new Promise(resolve => {
@@ -2252,6 +2254,14 @@ add_task(
tile_id: String(2),
});
Assert.equal(Glean.pocket.shim.testGetValue(), SHIM);
+ Assert.deepEqual(
+ Glean.pocket.fetchTimestamp.testGetValue(),
+ FETCH_TIMESTAMP
+ );
+ Assert.deepEqual(
+ Glean.pocket.newtabCreationTimestamp.testGetValue(),
+ NEWTAB_CREATION_TIMESTAMP
+ );
resolve();
});
@@ -2272,10 +2282,12 @@ add_task(
type: "spoc",
recommendation_id: undefined,
shim: SHIM,
+ fetchTimestamp: FETCH_TIMESTAMP.valueOf(),
},
],
window_inner_width: 1000,
window_inner_height: 900,
+ firstVisibleTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(),
});
await pingSubmitted;
@@ -2949,6 +2961,8 @@ add_task(
Services.fog.testResetFOG();
const ACTION_POSITION = 42;
const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ=";
+ const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20");
+ const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30");
let action = ac.DiscoveryStreamUserEvent({
event: "CLICK",
action_position: ACTION_POSITION,
@@ -2957,6 +2971,8 @@ add_task(
recommendation_id: undefined,
tile_id: 448685088,
shim: SHIM,
+ fetchTimestamp: FETCH_TIMESTAMP.valueOf(),
+ firstVisibleTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(),
},
});
@@ -2966,6 +2982,14 @@ add_task(
let pingSubmitted = new Promise(resolve => {
GleanPings.spoc.testBeforeNextSubmit(reason => {
Assert.equal(reason, "click");
+ Assert.deepEqual(
+ Glean.pocket.fetchTimestamp.testGetValue(),
+ FETCH_TIMESTAMP
+ );
+ Assert.deepEqual(
+ Glean.pocket.newtabCreationTimestamp.testGetValue(),
+ NEWTAB_CREATION_TIMESTAMP
+ );
resolve();
});
});
@@ -3043,6 +3067,8 @@ add_task(
Services.fog.testResetFOG();
const ACTION_POSITION = 42;
const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ=";
+ const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20");
+ const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30");
let action = ac.DiscoveryStreamUserEvent({
event: "SAVE_TO_POCKET",
action_position: ACTION_POSITION,
@@ -3051,6 +3077,8 @@ add_task(
recommendation_id: undefined,
tile_id: 448685088,
shim: SHIM,
+ fetchTimestamp: FETCH_TIMESTAMP.valueOf(),
+ newtabCreationTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(),
},
});
@@ -3064,6 +3092,14 @@ add_task(
SHIM,
"Pocket shim was recorded"
);
+ Assert.deepEqual(
+ Glean.pocket.fetchTimestamp.testGetValue(),
+ FETCH_TIMESTAMP
+ );
+ Assert.deepEqual(
+ Glean.pocket.newtabCreationTimestamp.testGetValue(),
+ NEWTAB_CREATION_TIMESTAMP
+ );
resolve();
});
diff --git a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js
index 860e8758a5..4be520fcca 100644
--- a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js
+++ b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js
@@ -8,7 +8,7 @@ const { TopSitesFeed, DEFAULT_TOP_SITES } = ChromeUtils.importESModule(
);
const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule(
- "resource://activity-stream/common/Actions.sys.mjs"
+ "resource://activity-stream/common/Actions.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
diff --git a/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js b/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js
new file mode 100644
index 0000000000..c6c12c17bf
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { WallpaperFeed } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/WallpaperFeed.sys.mjs"
+);
+
+const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule(
+ "resource://activity-stream/common/Actions.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Utils: "resource://services-settings/Utils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const PREF_WALLPAPERS_ENABLED =
+ "browser.newtabpage.activity-stream.newtabWallpapers.enabled";
+
+add_task(async function test_construction() {
+ let feed = new WallpaperFeed();
+
+ info("WallpaperFeed constructor should create initial values");
+
+ Assert.ok(feed, "Could construct a WallpaperFeed");
+ Assert.ok(feed.loaded === false, "WallpaperFeed is not loaded");
+ Assert.ok(
+ feed.wallpaperClient === "",
+ "wallpaperClient is initialized as an empty string"
+ );
+ Assert.ok(
+ feed.wallpaperDB === "",
+ "wallpaperDB is initialized as an empty string"
+ );
+ Assert.ok(
+ feed.baseAttachmentURL === "",
+ "baseAttachmentURL is initialized as an empty string"
+ );
+});
+
+add_task(async function test_onAction_INIT() {
+ let sandbox = sinon.createSandbox();
+ let feed = new WallpaperFeed();
+ Services.prefs.setBoolPref(PREF_WALLPAPERS_ENABLED, true);
+ const attachment = {
+ attachment: {
+ location: "attachment",
+ },
+ };
+ sandbox.stub(feed, "RemoteSettings").returns({
+ get: () => [attachment],
+ on: () => {},
+ });
+ sandbox.stub(Utils, "SERVER_URL").returns("http://localhost:8888/v1");
+ feed.store = {
+ dispatch: sinon.spy(),
+ };
+ sandbox.stub(feed, "fetch").resolves({
+ json: () => ({
+ capabilities: {
+ attachments: {
+ base_url: "http://localhost:8888/base_url/",
+ },
+ },
+ }),
+ });
+
+ info("WallpaperFeed.onAction INIT should initialize wallpapers");
+
+ await feed.onAction({
+ type: at.INIT,
+ });
+
+ Assert.ok(feed.store.dispatch.calledOnce);
+ Assert.ok(
+ feed.store.dispatch.calledWith(
+ ac.BroadcastToContent({
+ type: at.WALLPAPERS_SET,
+ data: [
+ {
+ ...attachment,
+ wallpaperUrl: "http://localhost:8888/base_url/attachment",
+ },
+ ],
+ meta: {
+ isStartup: true,
+ },
+ })
+ )
+ );
+ Services.prefs.clearUserPref(PREF_WALLPAPERS_ENABLED);
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_PREF_CHANGED() {
+ let sandbox = sinon.createSandbox();
+ let feed = new WallpaperFeed();
+ Services.prefs.setBoolPref(PREF_WALLPAPERS_ENABLED, true);
+ sandbox.stub(feed, "wallpaperSetup").returns();
+
+ info("WallpaperFeed.onAction PREF_CHANGED should call wallpaperSetup");
+
+ feed.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: "newtabWallpapers.enabled" },
+ });
+
+ Assert.ok(feed.wallpaperSetup.calledOnce);
+ Assert.ok(feed.wallpaperSetup.calledWith(false));
+
+ Services.prefs.clearUserPref(PREF_WALLPAPERS_ENABLED);
+ sandbox.restore();
+});
diff --git a/browser/components/newtab/test/xpcshell/xpcshell.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml
index 87d73669d3..13c11b0541 100644
--- a/browser/components/newtab/test/xpcshell/xpcshell.toml
+++ b/browser/components/newtab/test/xpcshell/xpcshell.toml
@@ -26,3 +26,5 @@ support-files = ["../schemas/*.schema.json"]
["test_TopSitesFeed.js"]
["test_TopSitesFeed_glean.js"]
+
+["test_WallpaperFeed.js"]
diff --git a/browser/components/newtab/webpack.system-addon.config.js b/browser/components/newtab/webpack.system-addon.config.js
index a0400ec39e..68a384ea71 100644
--- a/browser/components/newtab/webpack.system-addon.config.js
+++ b/browser/components/newtab/webpack.system-addon.config.js
@@ -48,7 +48,7 @@ module.exports = (env = {}) => ({
},
// This resolve config allows us to import with paths relative to the root directory, e.g. "lib/ActivityStream.sys.mjs"
resolve: {
- extensions: [".js", ".jsx"],
+ extensions: [".js", ".jsx", ".mjs"],
modules: ["node_modules", "."],
},
externals: {
diff --git a/browser/components/originattributes/test/browser/browser.toml b/browser/components/originattributes/test/browser/browser.toml
index 5585b2a914..59c9d1c3c6 100644
--- a/browser/components/originattributes/test/browser/browser.toml
+++ b/browser/components/originattributes/test/browser/browser.toml
@@ -32,7 +32,7 @@ support-files = [
"file_thirdPartyChild.script.js",
"file_thirdPartyChild.sharedworker.js",
"file_thirdPartyChild.track.vtt",
- "file_thirdPartyChild.video.ogv",
+ "file_thirdPartyChild.video.webm",
"file_thirdPartyChild.worker.fetch.html",
"file_thirdPartyChild.worker.js",
"file_thirdPartyChild.worker.request.html",
diff --git a/browser/components/originattributes/test/browser/browser_cache.js b/browser/components/originattributes/test/browser/browser_cache.js
index ea8f0fe803..4c2369fc00 100644
--- a/browser/components/originattributes/test/browser/browser_cache.js
+++ b/browser/components/originattributes/test/browser/browser_cache.js
@@ -28,7 +28,7 @@ let suffixes = [
"xhr.html",
"worker.xhr.html",
"audio.ogg",
- "video.ogv",
+ "video.webm",
"track.vtt",
"fetch.html",
"worker.fetch.html",
@@ -56,7 +56,7 @@ function cacheDataForContext(loadContextInfo) {
return new Promise(resolve => {
let cacheEntries = [];
let cacheVisitor = {
- onCacheStorageInfo(num, consumption) {},
+ onCacheStorageInfo() {},
onCacheEntryInfo(uri, idEnhance) {
cacheEntries.push({ uri, idEnhance });
},
@@ -176,7 +176,7 @@ async function doTest(aBrowser) {
await SpecialPowers.spawn(aBrowser, [argObj], async function (arg) {
content.windowUtils.clearSharedStyleSheetCache();
- let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.ogv";
+ let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.webm";
let audioURL = arg.urlPrefix + "file_thirdPartyChild.audio.ogg";
let trackURL = arg.urlPrefix + "file_thirdPartyChild.track.vtt";
let URLSuffix = "?r=" + arg.randomSuffix;
@@ -257,7 +257,7 @@ async function doTest(aBrowser) {
}
// The check function, which checks the number of cache entries.
-async function doCheck(aShouldIsolate, aInputA, aInputB) {
+async function doCheck(aShouldIsolate) {
let expectedEntryCount = 1;
let data = [];
data = data.concat(
diff --git a/browser/components/originattributes/test/browser/browser_favicon_firstParty.js b/browser/components/originattributes/test/browser/browser_favicon_firstParty.js
index 300c2f9f25..0a3bf39886 100644
--- a/browser/components/originattributes/test/browser/browser_favicon_firstParty.js
+++ b/browser/components/originattributes/test/browser/browser_favicon_firstParty.js
@@ -56,7 +56,7 @@ function clearAllPlacesFavicons() {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
if (aTopic === "places-favicons-expired") {
resolve();
Services.obs.removeObserver(observer, "places-favicons-expired");
@@ -77,7 +77,7 @@ function observeFavicon(aFirstPartyDomain, aExpectedCookie, aPageURI) {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
// Make sure that the topic is 'http-on-modify-request'.
if (aTopic === "http-on-modify-request") {
// We check the firstPartyDomain for the originAttributes of the loading
@@ -136,7 +136,7 @@ function observeFavicon(aFirstPartyDomain, aExpectedCookie, aPageURI) {
function waitOnFaviconResponse(aFaviconURL) {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
if (
aTopic === "http-on-examine-response" ||
aTopic === "http-on-examine-cached-response"
diff --git a/browser/components/originattributes/test/browser/browser_favicon_userContextId.js b/browser/components/originattributes/test/browser/browser_favicon_userContextId.js
index 2f0a5d06a9..e8e687d938 100644
--- a/browser/components/originattributes/test/browser/browser_favicon_userContextId.js
+++ b/browser/components/originattributes/test/browser/browser_favicon_userContextId.js
@@ -57,7 +57,7 @@ function clearAllPlacesFavicons() {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
if (aTopic === "places-favicons-expired") {
resolve();
Services.obs.removeObserver(observer, "places-favicons-expired");
@@ -80,7 +80,7 @@ function FaviconObserver(
}
FaviconObserver.prototype = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
// Make sure that the topic is 'http-on-modify-request'.
if (aTopic === "http-on-modify-request") {
// We check the userContextId for the originAttributes of the loading
diff --git a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js
index 0266765782..450676eba2 100644
--- a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js
+++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js
@@ -15,7 +15,7 @@ const TEST_ORIGIN = `http://${TEST_FIRST_PARTY}`;
const TEST_BASE_PATH =
"/browser/browser/components/originattributes/test/browser/";
const TEST_PATH = `${TEST_BASE_PATH}file_saveAs.sjs`;
-const TEST_PATH_VIDEO = `${TEST_BASE_PATH}file_thirdPartyChild.video.ogv`;
+const TEST_PATH_VIDEO = `${TEST_BASE_PATH}file_thirdPartyChild.video.webm`;
const TEST_PATH_IMAGE = `${TEST_BASE_PATH}file_favicon.png`;
// For the "Save Page As" test, we will check the channel of the sub-resource
@@ -284,7 +284,7 @@ add_task(async function testPageInfoMediaSaveAs() {
);
info("Open the media panel of the pageinfo.");
- let pageInfo = BrowserPageInfo(
+ let pageInfo = BrowserCommands.pageInfo(
gBrowser.selectedBrowser.currentURI.spec,
"mediaTab"
);
diff --git a/browser/components/originattributes/test/browser/browser_httpauth.js b/browser/components/originattributes/test/browser/browser_httpauth.js
index b2e95e13ac..8821f3a92b 100644
--- a/browser/components/originattributes/test/browser/browser_httpauth.js
+++ b/browser/components/originattributes/test/browser/browser_httpauth.js
@@ -2,10 +2,6 @@ let { HttpServer } = ChromeUtils.importESModule(
"resource://testing-common/httpd.sys.mjs"
);
-let authPromptModalType = Services.prefs.getIntPref(
- "prompts.modalType.httpAuth"
-);
-
let server = new HttpServer();
server.registerPathHandler("/file.html", fileHandler);
server.start(-1);
@@ -57,7 +53,7 @@ function getResult() {
return credentialQueue.shift();
}
-async function doInit(aMode) {
+async function doInit() {
await SpecialPowers.pushPrefEnv({
set: [["privacy.partition.network_state", false]],
});
diff --git a/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js b/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js
index 34c77f746d..c826ec07d4 100644
--- a/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js
+++ b/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js
@@ -72,12 +72,12 @@ async function doBefore() {
}
// the test function does nothing on purpose.
-function doTest(aBrowser) {
+function doTest() {
return 0;
}
// the check function
-function doCheck(shouldIsolate, a, b) {
+function doCheck(shouldIsolate) {
// if we're doing first party isolation and the image cache isolation is
// working, then gHits should be 2 because the image would have been loaded
// one per first party domain. if first party isolation is disabled, then
diff --git a/browser/components/originattributes/test/browser/browser_sanitize.js b/browser/components/originattributes/test/browser/browser_sanitize.js
index 61d236f249..0256a91eb7 100644
--- a/browser/components/originattributes/test/browser/browser_sanitize.js
+++ b/browser/components/originattributes/test/browser/browser_sanitize.js
@@ -20,8 +20,8 @@ function cacheDataForContext(loadContextInfo) {
return new Promise(resolve => {
let cachedURIs = [];
let cacheVisitor = {
- onCacheStorageInfo(num, consumption) {},
- onCacheEntryInfo(uri, idEnhance) {
+ onCacheStorageInfo() {},
+ onCacheEntryInfo(uri) {
cachedURIs.push(uri.asciiSpec);
},
onCacheEntryVisitCompleted() {
diff --git a/browser/components/originattributes/test/browser/file_saveAs.sjs b/browser/components/originattributes/test/browser/file_saveAs.sjs
index 9b16250c76..8e1cc60dae 100644
--- a/browser/components/originattributes/test/browser/file_saveAs.sjs
+++ b/browser/components/originattributes/test/browser/file_saveAs.sjs
@@ -2,8 +2,8 @@ const HTTP_ORIGIN = "http://example.com";
const SECOND_ORIGIN = "http://example.org";
const URI_PATH = "/browser/browser/components/originattributes/test/browser/";
const LINK_PATH = `${URI_PATH}file_saveAs.sjs`;
-// Reusing existing ogv file for testing.
-const VIDEO_PATH = `${URI_PATH}file_thirdPartyChild.video.ogv`;
+// Reusing existing webm file for testing.
+const VIDEO_PATH = `${URI_PATH}file_thirdPartyChild.video.webm`;
// Reusing existing png file for testing.
const IMAGE_PATH = `${URI_PATH}file_favicon.png`;
const FRAME_PATH = `${SECOND_ORIGIN}${URI_PATH}file_saveAs.sjs?image=1`;
diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv b/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv
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/content/places.js b/browser/components/places/content/places.js
index 685fa12b51..9e2abaafcc 100644
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -1168,7 +1168,7 @@ var ViewMenu = {
menuitem.setAttribute("type", "radio");
menuitem.setAttribute("name", "columns");
// This column is the sort key. Its item is checked.
- if (column.getAttribute("sortDirection") != "") {
+ if (column.hasAttribute("sortDirection")) {
menuitem.setAttribute("checked", "true");
}
} else if (type == "checkbox") {
diff --git a/browser/components/places/content/places.xhtml b/browser/components/places/content/places.xhtml
index e1ac09878b..d0e6a65eb5 100644
--- a/browser/components/places/content/places.xhtml
+++ b/browser/components/places/content/places.xhtml
@@ -15,6 +15,10 @@
onunload="PlacesOrganizer.destroy();"
width="800" height="500"
screenX="10" screenY="10"
+#ifdef XP_MACOSX
+ drawtitle="true"
+ chromemargin="0,0,0,0"
+#endif
toggletoolbar="true"
persist="width height screenX screenY sizemode">
diff --git a/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js b/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js
index 16aeb08ad8..228fea654e 100644
--- a/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js
+++ b/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js
@@ -131,14 +131,14 @@ let checkContextMenu = async (cbfunc, optionItems, doc = document) => {
if (expectedOptionItems.includes("placesContext_open")) {
Assert.equal(
doc.getElementById("placesContext_open").getAttribute("default"),
- loadBookmarksInNewTab ? "" : "true",
+ loadBookmarksInNewTab ? null : "true",
`placesContext_open has the correct "default" attribute when loadBookmarksInTabs = ${loadBookmarksInNewTab}`
);
}
if (expectedOptionItems.includes("placesContext_open:newtab")) {
Assert.equal(
doc.getElementById("placesContext_open:newtab").getAttribute("default"),
- loadBookmarksInNewTab ? "true" : "",
+ loadBookmarksInNewTab ? "true" : null,
`placesContext_open:newtab has the correct "default" attribute when loadBookmarksInTabs = ${loadBookmarksInNewTab}`
);
}
diff --git a/browser/components/places/tests/browser/browser_sidebarpanels_click.js b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
index 3e5b1c6ec6..9a1b039e78 100644
--- a/browser/components/places/tests/browser/browser_sidebarpanels_click.js
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -157,16 +157,10 @@ function promiseAlertDialogObserved() {
async function observer(subject) {
info("alert dialog observed as expected");
Services.obs.removeObserver(observer, "common-dialog-loaded");
- Services.obs.removeObserver(observer, "tabmodal-dialog-loaded");
- if (subject.Dialog) {
- subject.Dialog.ui.button0.click();
- } else {
- subject.querySelector(".tabmodalprompt-button0").click();
- }
+ subject.Dialog.ui.button0.click();
resolve();
}
Services.obs.addObserver(observer, "common-dialog-loaded");
- Services.obs.addObserver(observer, "tabmodal-dialog-loaded");
});
}
diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js
index bcd89bce15..5459e6f924 100644
--- a/browser/components/places/tests/browser/head.js
+++ b/browser/components/places/tests/browser/head.js
@@ -194,7 +194,7 @@ function promiseSetToolbarVisibility(aToolbar, aVisible) {
function isToolbarVisible(aToolbar) {
let hidingAttribute =
aToolbar.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
- let hidingValue = aToolbar.getAttribute(hidingAttribute).toLowerCase();
+ let hidingValue = aToolbar.getAttribute(hidingAttribute)?.toLowerCase();
// Check for both collapsed="true" and collapsed="collapsed"
return hidingValue !== "true" && hidingValue !== hidingAttribute;
}
diff --git a/browser/components/pocket/content/SaveToPocket.sys.mjs b/browser/components/pocket/content/SaveToPocket.sys.mjs
index 60674acc82..9b9a30b4a2 100644
--- a/browser/components/pocket/content/SaveToPocket.sys.mjs
+++ b/browser/components/pocket/content/SaveToPocket.sys.mjs
@@ -101,7 +101,7 @@ export var SaveToPocket = {
);
},
- observe(subject, topic, data) {
+ observe(subject, topic) {
if (topic == "browser-delayed-startup-finished") {
// We only get here if pocket is disabled; the observer is removed when
// we're enabled.
diff --git a/browser/components/pocket/content/panels/js/components/Home/Home.jsx b/browser/components/pocket/content/panels/js/components/Home/Home.jsx
index 1036876725..8e15df1869 100644
--- a/browser/components/pocket/content/panels/js/components/Home/Home.jsx
+++ b/browser/components/pocket/content/panels/js/components/Home/Home.jsx
@@ -32,7 +32,7 @@ function Home(props) {
: ``
}`;
- const loadingRecentSaves = useCallback(resp => {
+ const loadingRecentSaves = useCallback(() => {
setArticlesState(prevState => ({
...prevState,
status: "loading",
diff --git a/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx b/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx
index 502c73b0a5..b750946234 100644
--- a/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx
+++ b/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx
@@ -85,7 +85,7 @@ function Saved(props) {
panelMessaging.addMessageListener(
"PKT_getArticleInfoAttempted",
- function (resp) {
+ function () {
setArticleInfoAttempted(true);
}
);
diff --git a/browser/components/pocket/content/panels/js/home/overlay.jsx b/browser/components/pocket/content/panels/js/home/overlay.jsx
index 4d49a09470..f8ee1578ba 100644
--- a/browser/components/pocket/content/panels/js/home/overlay.jsx
+++ b/browser/components/pocket/content/panels/js/home/overlay.jsx
@@ -7,7 +7,7 @@ import React from "react";
import ReactDOM from "react-dom";
import Home from "../components/Home/Home.jsx";
-var HomeOverlay = function (options) {
+var HomeOverlay = function () {
this.inited = false;
this.active = false;
};
diff --git a/browser/components/pocket/content/panels/js/main.bundle.js b/browser/components/pocket/content/panels/js/main.bundle.js
index 36e2f82973..39a3dbccd7 100644
--- a/browser/components/pocket/content/panels/js/main.bundle.js
+++ b/browser/components/pocket/content/panels/js/main.bundle.js
@@ -275,7 +275,7 @@ function Home(props) {
status: ""
});
const utmParams = `utm_source=${utmSource}${utmCampaign && utmContent ? `&utm_campaign=${utmCampaign}&utm_content=${utmContent}` : ``}`;
- const loadingRecentSaves = (0,react.useCallback)(resp => {
+ const loadingRecentSaves = (0,react.useCallback)(() => {
setArticlesState(prevState => ({
...prevState,
status: "loading"
@@ -381,7 +381,7 @@ It does not contain any logic for saving or communication with the extension or
-var HomeOverlay = function (options) {
+var HomeOverlay = function () {
this.inited = false;
this.active = false;
};
@@ -487,7 +487,7 @@ It does not contain any logic for saving or communication with the extension or
-var SignupOverlay = function (options) {
+var SignupOverlay = function () {
this.inited = false;
this.active = false;
this.create = function ({
@@ -772,7 +772,7 @@ function Saved(props) {
messages.addMessageListener("PKT_articleInfoFetched", function (resp) {
setSavedStoryState(resp?.data?.item_preview);
});
- messages.addMessageListener("PKT_getArticleInfoAttempted", function (resp) {
+ messages.addMessageListener("PKT_getArticleInfoAttempted", function () {
setArticleInfoAttempted(true);
});
@@ -843,7 +843,7 @@ It does not contain any logic for saving or communication with the extension or
-var SavedOverlay = function (options) {
+var SavedOverlay = function () {
this.inited = false;
this.active = false;
};
@@ -883,7 +883,7 @@ SavedOverlay.prototype = {
-var StyleGuideOverlay = function (options) {};
+var StyleGuideOverlay = function () {};
StyleGuideOverlay.prototype = {
create() {
// TODO: Wrap popular topics component in JSX to work without needing an explicit container hierarchy for styling
@@ -1072,7 +1072,7 @@ PKT_PANEL.prototype = {
const config = { attributes: false, childList: true, subtree: true };
// Callback function to execute when mutations are observed
- const callback = (mutationList, observer) => {
+ const callback = mutationList => {
mutationList.forEach(mutation => {
switch (mutation.type) {
case "childList": {
diff --git a/browser/components/pocket/content/panels/js/main.mjs b/browser/components/pocket/content/panels/js/main.mjs
index b5ae0e9c3a..2c1da9528c 100644
--- a/browser/components/pocket/content/panels/js/main.mjs
+++ b/browser/components/pocket/content/panels/js/main.mjs
@@ -86,7 +86,7 @@ PKT_PANEL.prototype = {
const config = { attributes: false, childList: true, subtree: true };
// Callback function to execute when mutations are observed
- const callback = (mutationList, observer) => {
+ const callback = mutationList => {
mutationList.forEach(mutation => {
switch (mutation.type) {
case "childList": {
diff --git a/browser/components/pocket/content/panels/js/saved/overlay.jsx b/browser/components/pocket/content/panels/js/saved/overlay.jsx
index ab2617f112..091821c149 100644
--- a/browser/components/pocket/content/panels/js/saved/overlay.jsx
+++ b/browser/components/pocket/content/panels/js/saved/overlay.jsx
@@ -7,7 +7,7 @@ import React from "react";
import ReactDOM from "react-dom";
import Saved from "../components/Saved/Saved.jsx";
-var SavedOverlay = function (options) {
+var SavedOverlay = function () {
this.inited = false;
this.active = false;
};
diff --git a/browser/components/pocket/content/panels/js/signup/overlay.jsx b/browser/components/pocket/content/panels/js/signup/overlay.jsx
index 6143afbc83..ce3b681f15 100644
--- a/browser/components/pocket/content/panels/js/signup/overlay.jsx
+++ b/browser/components/pocket/content/panels/js/signup/overlay.jsx
@@ -8,7 +8,7 @@ import ReactDOM from "react-dom";
import pktPanelMessaging from "../messages.mjs";
import Signup from "../components/Signup/Signup.jsx";
-var SignupOverlay = function (options) {
+var SignupOverlay = function () {
this.inited = false;
this.active = false;
diff --git a/browser/components/pocket/content/panels/js/style-guide/overlay.jsx b/browser/components/pocket/content/panels/js/style-guide/overlay.jsx
index fbc0dac069..b802a9159b 100644
--- a/browser/components/pocket/content/panels/js/style-guide/overlay.jsx
+++ b/browser/components/pocket/content/panels/js/style-guide/overlay.jsx
@@ -6,7 +6,7 @@ import Button from "../components/Button/Button.jsx";
import PopularTopics from "../components/PopularTopics/PopularTopics.jsx";
import TagPicker from "../components/TagPicker/TagPicker.jsx";
-var StyleGuideOverlay = function (options) {};
+var StyleGuideOverlay = function () {};
StyleGuideOverlay.prototype = {
create() {
diff --git a/browser/components/pocket/content/pktApi.sys.mjs b/browser/components/pocket/content/pktApi.sys.mjs
index 16d0948b36..132f9369ce 100644
--- a/browser/components/pocket/content/pktApi.sys.mjs
+++ b/browser/components/pocket/content/pktApi.sys.mjs
@@ -47,7 +47,6 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
IndexedDB: "resource://gre/modules/IndexedDB.sys.mjs",
- NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
@@ -291,12 +290,12 @@ export var pktApi = (function () {
"extensions.pocket.oAuthConsumerKey"
);
} else {
- baseAPIUrl = `https://${lazy.NimbusFeatures.saveToPocket.getVariable(
- "bffApi"
+ baseAPIUrl = `https://${Services.prefs.getStringPref(
+ "extensions.pocket.bffApi"
)}/desktop/v1`;
- oAuthConsumerKey = lazy.NimbusFeatures.saveToPocket.getVariable(
- "oAuthConsumerKeyBff"
+ oAuthConsumerKey = Services.prefs.getStringPref(
+ "extensions.pocket.oAuthConsumerKeyBff"
);
}
@@ -309,7 +308,7 @@ export var pktApi = (function () {
data.locale_lang = Services.locale.appLocaleAsBCP47;
data.consumer_key = oAuthConsumerKey;
- var request = new XMLHttpRequest();
+ var request = new XMLHttpRequest({ mozAnon: false });
if (!useBFF) {
request.open("POST", url, true);
@@ -317,7 +316,7 @@ export var pktApi = (function () {
request.open("GET", url, true);
}
- request.onreadystatechange = function (e) {
+ request.onreadystatechange = function () {
if (request.readyState == 4) {
// "done" is a completed XHR regardless of success/error:
if (options.done) {
@@ -487,7 +486,7 @@ export var pktApi = (function () {
access_token: getAccessToken(),
url,
},
- success(data) {
+ success() {
if (options.success) {
options.success.apply(options, Array.apply(null, arguments));
}
@@ -508,7 +507,7 @@ export var pktApi = (function () {
data: {
access_token: getAccessToken(),
},
- success(data) {
+ success() {
if (options.success) {
options.success.apply(options, Array.apply(null, arguments));
}
@@ -761,8 +760,9 @@ export var pktApi = (function () {
access_token: getAccessToken(),
});
- const useBFF =
- lazy.NimbusFeatures.saveToPocket.getVariable("bffRecentSaves");
+ const useBFF = Services.prefs.getBoolPref(
+ "extensions.pocket.bffRecentSaves"
+ );
return apiRequest(
{
@@ -816,8 +816,9 @@ export var pktApi = (function () {
{ count: 4 },
{
success(data) {
- const useBFF =
- lazy.NimbusFeatures.saveToPocket.getVariable("bffRecentSaves");
+ const useBFF = Services.prefs.getBoolPref(
+ "extensions.pocket.bffRecentSaves"
+ );
// Don't try to parse bad or missing data
if (
diff --git a/browser/components/pocket/content/pktUI.js b/browser/components/pocket/content/pktUI.js
index 60b7e3bcc3..05e31b5c30 100644
--- a/browser/components/pocket/content/pktUI.js
+++ b/browser/components/pocket/content/pktUI.js
@@ -46,7 +46,6 @@
ChromeUtils.defineESModuleGetters(this, {
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
- NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
pktApi: "chrome://pocket/content/pktApi.sys.mjs",
pktTelemetry: "chrome://pocket/content/pktTelemetry.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
@@ -125,11 +124,13 @@ var pktUI = (function () {
* Show the sign-up panel
*/
function showSignUp() {
- getFirefoxAccountSignedInUser(function (userdata) {
+ getFirefoxAccountSignedInUser(function () {
showPanel(
"about:pocket-signup?" +
"emailButton=" +
- NimbusFeatures.saveToPocket.getVariable("emailButton"),
+ Services.prefs.getBoolPref(
+ "extensions.pocket.refresh.emailButton.enabled"
+ ),
`signup`
);
});
@@ -154,8 +155,9 @@ var pktUI = (function () {
* Show the Pocket home panel state
*/
function showPocketHome() {
- const hideRecentSaves =
- NimbusFeatures.saveToPocket.getVariable("hideRecentSaves");
+ const hideRecentSaves = Services.prefs.getBoolPref(
+ "extensions.pocket.refresh.hideRecentSaves.enabled"
+ );
const locale = getUILocale();
let panel = `home_no_topics`;
if (locale.startsWith("en-")) {
@@ -232,7 +234,11 @@ var pktUI = (function () {
async function onShowHome() {
pktTelemetry.submitPocketButtonPing("click", "home_button");
- if (!NimbusFeatures.saveToPocket.getVariable("hideRecentSaves")) {
+ if (
+ !Services.prefs.getBoolPref(
+ "extensions.pocket.refresh.hideRecentSaves.enabled"
+ )
+ ) {
let recentSaves = await pktApi.getRecentSavesCache();
if (recentSaves) {
// We have cache, so we can use those.
@@ -284,7 +290,7 @@ var pktUI = (function () {
// Add url
var options = {
- success(data, request) {
+ success(data) {
var item = data.item;
var ho2 = data.ho2;
var accountState = data.account_state;
@@ -299,7 +305,11 @@ var pktUI = (function () {
pktUIMessaging.sendMessageToPanel(saveLinkMessageId, successResponse);
SaveToPocket.itemSaved();
- if (!NimbusFeatures.saveToPocket.getVariable("hideRecentSaves")) {
+ if (
+ !Services.prefs.getBoolPref(
+ "extensions.pocket.refresh.hideRecentSaves.enabled"
+ )
+ ) {
// Articles saved for the first time (by anyone) won't have a resolved_id
if (item?.resolved_id && item?.resolved_id !== "0") {
pktApi.getArticleInfo(item.resolved_url, {
@@ -493,7 +503,7 @@ var pktUI = (function () {
.then(userData => {
callback(userData);
})
- .then(null, error => {
+ .then(null, () => {
callback();
});
}
diff --git a/browser/components/pocket/test/browser_pocket_button_icon_state.js b/browser/components/pocket/test/browser_pocket_button_icon_state.js
index c2cba8133b..65c1608b9d 100644
--- a/browser/components/pocket/test/browser_pocket_button_icon_state.js
+++ b/browser/components/pocket/test/browser_pocket_button_icon_state.js
@@ -72,10 +72,14 @@ function checkPanelClosed() {
let pocketButton = document.getElementById("save-to-pocket-button");
// Something should have closed the Pocket panel, icon should no longer be red.
is(pocketButton.open, false, "Pocket button is closed");
- is(pocketButton.getAttribute("pocketed"), "", "Pocket item is not pocketed");
+ is(
+ pocketButton.getAttribute("pocketed"),
+ null,
+ "Pocket item is not pocketed"
+ );
}
-test_runner(async function test_pocketButtonState_changeTabs({ sandbox }) {
+test_runner(async function test_pocketButtonState_changeTabs() {
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com/browser/browser/components/pocket/test/test.html"
@@ -101,7 +105,7 @@ test_runner(async function test_pocketButtonState_changeTabs({ sandbox }) {
BrowserTestUtils.removeTab(tab);
});
-test_runner(async function test_pocketButtonState_changeLocation({ sandbox }) {
+test_runner(async function test_pocketButtonState_changeLocation() {
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com/browser/browser/components/pocket/test/test.html"
diff --git a/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js b/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js
index 5abe3b3db1..4b6cc8f7ad 100644
--- a/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js
+++ b/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js
@@ -78,9 +78,7 @@ test_runner(async function test_AboutPocketParent_sendResponseMessageToPanel({
});
test_runner(
- async function test_AboutPocketParent_receiveMessage_PKT_show_signup({
- sandbox,
- }) {
+ async function test_AboutPocketParent_receiveMessage_PKT_show_signup() {
await aboutPocketParent.receiveMessage({
name: "PKT_show_signup",
});
@@ -96,9 +94,7 @@ test_runner(
);
test_runner(
- async function test_AboutPocketParent_receiveMessage_PKT_show_saved({
- sandbox,
- }) {
+ async function test_AboutPocketParent_receiveMessage_PKT_show_saved() {
await aboutPocketParent.receiveMessage({
name: "PKT_show_saved",
});
@@ -113,9 +109,7 @@ test_runner(
}
);
-test_runner(async function test_AboutPocketParent_receiveMessage_PKT_close({
- sandbox,
-}) {
+test_runner(async function test_AboutPocketParent_receiveMessage_PKT_close() {
await aboutPocketParent.receiveMessage({
name: "PKT_close",
});
@@ -130,9 +124,7 @@ test_runner(async function test_AboutPocketParent_receiveMessage_PKT_close({
});
test_runner(
- async function test_AboutPocketParent_receiveMessage_PKT_openTabWithUrl({
- sandbox,
- }) {
+ async function test_AboutPocketParent_receiveMessage_PKT_openTabWithUrl() {
await aboutPocketParent.receiveMessage({
name: "PKT_openTabWithUrl",
data: { foo: 1 },
@@ -155,9 +147,7 @@ test_runner(
);
test_runner(
- async function test_AboutPocketParent_receiveMessage_PKT_openTabWithPocketUrl({
- sandbox,
- }) {
+ async function test_AboutPocketParent_receiveMessage_PKT_openTabWithPocketUrl() {
await aboutPocketParent.receiveMessage({
name: "PKT_openTabWithPocketUrl",
data: { foo: 1 },
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
index c30a51c67c..0cd498fd96 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -276,22 +276,6 @@ function init_all() {
});
}
-function telemetryBucketForCategory(category) {
- category = category.toLowerCase();
- switch (category) {
- case "containers":
- case "general":
- case "home":
- case "privacy":
- case "search":
- case "sync":
- case "searchresults":
- return category;
- default:
- return "unknown";
- }
-}
-
function onHashChange() {
gotoPref(null, "hash");
}
@@ -454,16 +438,6 @@ function search(aQuery, aAttribute) {
}
element.classList.remove("visually-hidden");
}
-
- let keysets = mainPrefPane.getElementsByTagName("keyset");
- for (let element of keysets) {
- let attributeValue = element.getAttribute(aAttribute);
- if (attributeValue == aQuery) {
- element.removeAttribute("disabled");
- } else {
- element.setAttribute("disabled", true);
- }
- }
}
async function spotlight(subcategory, category) {
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
index eee227822a..64062f1f77 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
@@ -85,6 +85,8 @@
<script type="module" src="chrome://global/content/elements/moz-toggle.mjs"/>
<script type="module" src="chrome://global/content/elements/moz-message-bar.mjs" />
<script type="module" src="chrome://global/content/elements/moz-label.mjs"/>
+ <script type="module" src="chrome://global/content/elements/moz-card.mjs"></script>
+ <script type="module" src="chrome://global/content/elements/moz-button.mjs"></script>
</head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
@@ -224,7 +226,7 @@
<hbox class="sticky-inner-container" pack="end" align="start">
<hbox id="policies-container" class="info-box-container smaller-font-size" flex="1" hidden="true">
<hbox class="info-icon-container">
- <html:img class="info-icon"></html:img>
+ <html:img class="info-icon" data-l10n-attrs="alt" data-l10n-id="managed-notice-info-icon"></html:img>
</hbox>
<hbox align="center" flex="1">
<html:a href="about:policies" target="_blank" data-l10n-id="managed-notice"/>
diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml
index 224a5f5cbb..5c45771bc2 100644
--- a/browser/components/preferences/privacy.inc.xhtml
+++ b/browser/components/preferences/privacy.inc.xhtml
@@ -1182,21 +1182,18 @@
<label class="doh-status-label" id="dohResolver"/>
<label class="doh-status-label" id="dohSteeringStatus" data-l10n-id="preferences-doh-steering-status" hidden="true"/>
</vbox>
- <hbox id="dohExceptionBox">
- <label flex="1" data-l10n-id="preferences-doh-exceptions-description"/>
- <button id="dohExceptionsButton"
- is="highlightable-button"
- class="accessory-button"
- data-l10n-id="preferences-doh-manage-exceptions"
- search-l10n-ids="
- permissions-doh-entry-field,
- permissions-doh-add-exception.label,
- permissions-doh-remove.label,
- permissions-doh-remove-all.label,
- permissions-exceptions-doh-window.title,
- permissions-exceptions-manage-doh-desc,
- "/>
- </hbox>
+ <button id="dohExceptionsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="preferences-doh-manage-exceptions"
+ search-l10n-ids="
+ permissions-doh-entry-field,
+ permissions-doh-add-exception.label,
+ permissions-doh-remove.label,
+ permissions-doh-remove-all.label,
+ permissions-exceptions-doh-window.title,
+ permissions-exceptions-manage-doh-desc,
+ "/>
<vbox>
<label><html:h2 id="dohGroupMessage" data-l10n-id="preferences-doh-group-message2"/></label>
<vbox id="dohCategories">
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
index 3b07b9cabf..89fed04e21 100644
--- a/browser/components/preferences/privacy.js
+++ b/browser/components/preferences/privacy.js
@@ -60,6 +60,13 @@ ChromeUtils.defineLazyGetter(this, "AlertsServiceDND", function () {
}
});
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gParentalControlsService",
+ "@mozilla.org/parental-controls-service;1",
+ "nsIParentalControlsService"
+);
+
XPCOMUtils.defineLazyPreferenceGetter(
this,
"OS_AUTH_ENABLED",
@@ -682,11 +689,14 @@ var gPrivacyPane = {
function computeStatus() {
let mode = Services.dns.currentTrrMode;
- let confirmationState = Services.dns.currentTrrConfirmationState;
if (
mode == Ci.nsIDNSService.MODE_TRRFIRST ||
mode == Ci.nsIDNSService.MODE_TRRONLY
) {
+ if (lazy.gParentalControlsService.parentalControlsEnabled) {
+ return "preferences-doh-status-not-active";
+ }
+ let confirmationState = Services.dns.currentTrrConfirmationState;
switch (confirmationState) {
case Ci.nsIDNSService.CONFIRM_TRYING_OK:
case Ci.nsIDNSService.CONFIRM_OK:
@@ -702,7 +712,16 @@ var gPrivacyPane = {
let errReason = "";
let confirmationStatus = Services.dns.lastConfirmationStatus;
- if (confirmationStatus != Cr.NS_OK) {
+ let mode = Services.dns.currentTrrMode;
+ if (
+ (mode == Ci.nsIDNSService.MODE_TRRFIRST ||
+ mode == Ci.nsIDNSService.MODE_TRRONLY) &&
+ lazy.gParentalControlsService.parentalControlsEnabled
+ ) {
+ errReason = Services.dns.getTRRSkipReasonName(
+ Ci.nsITRRSkipReason.TRR_PARENTAL_CONTROL
+ );
+ } else if (confirmationStatus != Cr.NS_OK) {
errReason = ChromeUtils.getXPCOMErrorName(confirmationStatus);
} else {
errReason = Services.dns.getTRRSkipReasonName(
diff --git a/browser/components/preferences/sync.inc.xhtml b/browser/components/preferences/sync.inc.xhtml
index d3af690b93..492491a369 100644
--- a/browser/components/preferences/sync.inc.xhtml
+++ b/browser/components/preferences/sync.inc.xhtml
@@ -22,7 +22,7 @@
<description id="noFxaDescription" class="description-deemphasized" flex="1" data-l10n-id="sync-signedout-description2"/>
</vbox>
<vbox>
- <image class="fxaSyncIllustration"/>
+ <image class="fxaSyncIllustration" alt=""/>
</vbox>
</hbox>
<hbox id="fxaNoLoginStatus" align="center" flex="1">
@@ -37,6 +37,7 @@
</hbox>
<label class="fxaMobilePromo" data-l10n-id="sync-mobile-promo">
<html:img
+ role="none"
src="chrome://browser/skin/logo-android.svg"
data-l10n-name="android-icon"
class="androidIcon"/>
@@ -44,6 +45,7 @@
data-l10n-name="android-link"
class="fxaMobilePromo-android text-link" target="_blank"/>
<html:img
+ role="none"
src="chrome://browser/skin/logo-ios.svg"
data-l10n-name="ios-icon"
class="iOSIcon"/>
@@ -66,7 +68,8 @@
<image id="openChangeProfileImage"
class="fxaProfileImage actionable"
role="button"
- data-l10n-id="sync-profile-picture"/>
+ data-l10n-attrs="alt"
+ data-l10n-id="sync-profile-picture-with-alt"/>
<vbox flex="1" pack="center">
<hbox flex="1" align="baseline">
<label id="fxaDisplayName" hidden="true">
@@ -88,11 +91,15 @@
<!-- logged in to an unverified account -->
<hbox id="fxaLoginUnverified">
<vbox>
- <image class="fxaProfileImage"/>
+ <image class="fxaProfileImage"
+ data-l10n-attrs="alt"
+ data-l10n-id="sync-profile-picture-account-problem"/>
</vbox>
<vbox flex="1" pack="center">
<hbox align="center">
- <image class="fxaLoginRejectedWarning"/>
+ <image class="fxaLoginRejectedWarning"
+ data-l10n-attrs="alt"
+ data-l10n-id="fxa-login-rejected-warning"/>
<description flex="1"
class="l10nArgsEmailAddress"
data-l10n-id="sync-signedin-unverified"
@@ -112,11 +119,15 @@
<!-- logged in locally but server rejected credentials -->
<hbox id="fxaLoginRejected">
<vbox>
- <image class="fxaProfileImage"/>
+ <image class="fxaProfileImage"
+ data-l10n-attrs="alt"
+ data-l10n-id="sync-profile-picture-account-problem"/>
</vbox>
<vbox flex="1" pack="center">
<hbox align="center">
- <image class="fxaLoginRejectedWarning"/>
+ <image class="fxaLoginRejectedWarning"
+ data-l10n-attrs="alt"
+ data-l10n-id="fxa-login-rejected-warning"/>
<description flex="1"
class="l10nArgsEmailAddress"
data-l10n-id="sync-signedin-login-failure"
@@ -187,35 +198,35 @@
<label data-l10n-id="sync-syncing-across-devices-heading"/>
<html:div class="sync-engines-list">
<html:div engine_preference="services.sync.engine.bookmarks">
- <image class="sync-engine-image sync-engine-bookmarks"/>
+ <image class="sync-engine-image sync-engine-bookmarks" alt=""/>
<label data-l10n-id="sync-currently-syncing-bookmarks"/>
</html:div>
<html:div engine_preference="services.sync.engine.history">
- <image class="sync-engine-image sync-engine-history"/>
+ <image class="sync-engine-image sync-engine-history" alt=""/>
<label data-l10n-id="sync-currently-syncing-history"/>
</html:div>
<html:div engine_preference="services.sync.engine.tabs">
- <image class="sync-engine-image sync-engine-tabs"/>
+ <image class="sync-engine-image sync-engine-tabs" alt=""/>
<label data-l10n-id="sync-currently-syncing-tabs"/>
</html:div>
<html:div engine_preference="services.sync.engine.passwords">
- <image class="sync-engine-image sync-engine-passwords"/>
+ <image class="sync-engine-image sync-engine-passwords" alt=""/>
<label data-l10n-id="sync-currently-syncing-passwords"/>
</html:div>
<html:div engine_preference="services.sync.engine.addresses">
- <image class="sync-engine-image sync-engine-addresses"/>
+ <image class="sync-engine-image sync-engine-addresses" alt=""/>
<label data-l10n-id="sync-currently-syncing-addresses"/>
</html:div>
<html:div engine_preference="services.sync.engine.creditcards">
- <image class="sync-engine-image sync-engine-creditcards"/>
+ <image class="sync-engine-image sync-engine-creditcards" alt=""/>
<label data-l10n-id="sync-currently-syncing-payment-methods"/>
</html:div>
<html:div engine_preference="services.sync.engine.addons">
- <image class="sync-engine-image sync-engine-addons"/>
+ <image class="sync-engine-image sync-engine-addons" alt=""/>
<label data-l10n-id="sync-currently-syncing-addons"/>
</html:div>
<html:div engine_preference="services.sync.engine.prefs">
- <image class="sync-engine-image sync-engine-prefs"/>
+ <image class="sync-engine-image sync-engine-prefs" alt=""/>
<label data-l10n-id="sync-currently-syncing-settings"/>
</html:div>
</html:div>
diff --git a/browser/components/preferences/tests/browser.toml b/browser/components/preferences/tests/browser.toml
index 9e619ce4be..523110dbc9 100644
--- a/browser/components/preferences/tests/browser.toml
+++ b/browser/components/preferences/tests/browser.toml
@@ -10,6 +10,11 @@ support-files = [
"addons/set_homepage.xpi",
"addons/set_newtab.xpi",
]
+skip-if = [
+ "os == 'linux' && os_version == '18.04' && asan", # manifest runs too long
+ "os == 'linux' && os_version == '18.04' && tsan", # manifest runs too long
+ "win11_2009 && asan", # manifest runs too long
+]
["browser_about_settings.js"]
@@ -276,7 +281,6 @@ support-files = [
"subdialog.xhtml",
"subdialog2.xhtml",
]
-fail-if = ["a11y_checks"] # Bug 1854636 clicked label.dialogTitle, vbox#dialogTemplate.dialogOverlay may not be focusable
["browser_sync_chooseWhatToSync.js"]
diff --git a/browser/components/preferences/tests/browser_applications_selection.js b/browser/components/preferences/tests/browser_applications_selection.js
index 683ce76a89..23f0e00af8 100644
--- a/browser/components/preferences/tests/browser_applications_selection.js
+++ b/browser/components/preferences/tests/browser_applications_selection.js
@@ -335,10 +335,12 @@ add_task(async function sortingCheck() {
"Number of items should not change."
);
for (let i = 0; i < siteItems.length - 1; ++i) {
- let aType = siteItems[i].getAttribute("actionDescription").toLowerCase();
- let bType = siteItems[i + 1]
- .getAttribute("actionDescription")
- .toLowerCase();
+ let aType = (
+ siteItems[i].getAttribute("actionDescription") || ""
+ ).toLowerCase();
+ let bType = (
+ siteItems[i + 1].getAttribute("actionDescription") || ""
+ ).toLowerCase();
let result = 0;
if (aType > bType) {
result = 1;
@@ -375,10 +377,12 @@ add_task(async function sortingCheck() {
"Number of items should not change."
);
for (let i = 0; i < siteItems.length - 1; ++i) {
- let aType = siteItems[i].getAttribute("typeDescription").toLowerCase();
- let bType = siteItems[i + 1]
- .getAttribute("typeDescription")
- .toLowerCase();
+ let aType = (
+ siteItems[i].getAttribute("typeDescription") || ""
+ ).toLowerCase();
+ let bType = (
+ siteItems[i + 1].getAttribute("typeDescription") || ""
+ ).toLowerCase();
let result = 0;
if (aType > bType) {
result = 1;
diff --git a/browser/components/preferences/tests/browser_contentblocking.js b/browser/components/preferences/tests/browser_contentblocking.js
index 3d33f2ed7d..c178233a72 100644
--- a/browser/components/preferences/tests/browser_contentblocking.js
+++ b/browser/components/preferences/tests/browser_contentblocking.js
@@ -1021,7 +1021,7 @@ add_task(async function testDisableTPCheckBoxDisablesEmailTP() {
// Verify the checkbox is unchecked after clicking.
is(
tpCheckbox.getAttribute("checked"),
- "",
+ null,
"Tracking protection checkbox is unchecked"
);
diff --git a/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js
index 48469cfce4..ebe9c41127 100644
--- a/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js
+++ b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js
@@ -16,6 +16,10 @@ ChromeUtils.defineESModuleGetters(this, {
DoHTestUtils: "resource://testing-common/DoHTestUtils.sys.mjs",
});
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
const TRR_MODE_PREF = "network.trr.mode";
const TRR_URI_PREF = "network.trr.uri";
const TRR_CUSTOM_URI_PREF = "network.trr.custom_uri";
@@ -106,6 +110,164 @@ function waitForPrefObserver(name) {
});
}
+// Mock parental controls service in order to enable it
+let parentalControlsService = {
+ parentalControlsEnabled: true,
+ QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]),
+};
+let mockParentalControlsServiceCid = undefined;
+
+async function setMockParentalControlEnabled(aEnabled) {
+ if (mockParentalControlsServiceCid != undefined) {
+ MockRegistrar.unregister(mockParentalControlsServiceCid);
+ mockParentalControlsServiceCid = undefined;
+ }
+ if (aEnabled) {
+ mockParentalControlsServiceCid = MockRegistrar.register(
+ "@mozilla.org/parental-controls-service;1",
+ parentalControlsService
+ );
+ }
+ Services.dns.reloadParentalControlEnabled();
+}
+
+add_task(async function testParentalControls() {
+ async function withConfiguration(configuration, fn) {
+ info("testParentalControls");
+
+ await resetPrefs();
+ Services.prefs.setIntPref(TRR_MODE_PREF, configuration.trr_mode);
+ await setMockParentalControlEnabled(configuration.parentalControlsState);
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let statusElement = doc.getElementById("dohStatus");
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ document.l10n.getAttributes(statusElement).args.status ==
+ configuration.wait_for_doh_status
+ );
+ });
+
+ await fn({
+ statusElement,
+ });
+
+ gBrowser.removeCurrentTab();
+ await setMockParentalControlEnabled(false);
+ }
+
+ info("Check parental controls disabled, TRR off");
+ await withConfiguration(
+ {
+ parentalControlsState: false,
+ trr_mode: 0,
+ wait_for_doh_status: "Off",
+ },
+ async res => {
+ is(
+ document.l10n.getAttributes(res.statusElement).args.status,
+ "Off",
+ "expecting status off"
+ );
+ }
+ );
+
+ info("Check parental controls enabled, TRR off");
+ await withConfiguration(
+ {
+ parentalControlsState: true,
+ trr_mode: 0,
+ wait_for_doh_status: "Off",
+ },
+ async res => {
+ is(
+ document.l10n.getAttributes(res.statusElement).args.status,
+ "Off",
+ "expecting status off"
+ );
+ }
+ );
+
+ // Enable the rollout.
+ await DoHTestUtils.loadRemoteSettingsConfig({
+ providers: "example",
+ rolloutEnabled: true,
+ steeringEnabled: false,
+ steeringProviders: "",
+ autoDefaultEnabled: false,
+ autoDefaultProviders: "",
+ id: "global",
+ });
+
+ info("Check parental controls disabled, TRR first");
+ await withConfiguration(
+ {
+ parentalControlsState: false,
+ trr_mode: 2,
+ wait_for_doh_status: "Active",
+ },
+ async res => {
+ is(
+ document.l10n.getAttributes(res.statusElement).args.status,
+ "Active",
+ "expecting status active"
+ );
+ }
+ );
+
+ info("Check parental controls enabled, TRR first");
+ await withConfiguration(
+ {
+ parentalControlsState: true,
+ trr_mode: 2,
+ wait_for_doh_status: "Not active (TRR_PARENTAL_CONTROL)",
+ },
+ async res => {
+ is(
+ document.l10n.getAttributes(res.statusElement).args.status,
+ "Not active (TRR_PARENTAL_CONTROL)",
+ "expecting status not active"
+ );
+ }
+ );
+
+ info("Check parental controls disabled, TRR only");
+ await withConfiguration(
+ {
+ parentalControlsState: false,
+ trr_mode: 3,
+ wait_for_doh_status: "Active",
+ },
+ async res => {
+ is(
+ document.l10n.getAttributes(res.statusElement).args.status,
+ "Active",
+ "expecting status active"
+ );
+ }
+ );
+
+ info("Check parental controls enabled, TRR only");
+ await withConfiguration(
+ {
+ parentalControlsState: true,
+ trr_mode: 3,
+ wait_for_doh_status: "Not active (TRR_PARENTAL_CONTROL)",
+ },
+ async res => {
+ is(
+ document.l10n.getAttributes(res.statusElement).args.status,
+ "Not active (TRR_PARENTAL_CONTROL)",
+ "expecting status not active"
+ );
+ }
+ );
+
+ await resetPrefs();
+});
+
async function testWithProperties(props, startTime) {
info(
Date.now() -
diff --git a/browser/components/preferences/tests/browser_subdialogs.js b/browser/components/preferences/tests/browser_subdialogs.js
index 8763ae9146..b604ac0a7f 100644
--- a/browser/components/preferences/tests/browser_subdialogs.js
+++ b/browser/components/preferences/tests/browser_subdialogs.js
@@ -173,7 +173,7 @@ async function close_subdialog_and_test_generic_end_state(
);
Assert.equal(
frame.getAttribute("style"),
- "",
+ null,
"inline styles should be cleared"
);
Assert.equal(
@@ -407,17 +407,29 @@ add_task(async function background_click_should_close_dialog() {
// Clicking on an inactive part of dialog itself should not close the dialog.
// Click the dialog title bar here to make sure nothing happens.
info("clicking the dialog title bar");
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive element to confirm the opened
+ // dialog won't be dismissed. It is not meant to be interactive and is not
+ // expected to be accessible, therefore this rule check shall be ignored by
+ // a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
BrowserTestUtils.synthesizeMouseAtCenter(
".dialogTitle",
{},
tab.linkedBrowser
);
+ AccessibilityUtils.resetEnv();
// Close the dialog by clicking on the overlay background. Simulate a click
// at point (2,2) instead of (0,0) so we are sure we're clicking on the
// overlay background instead of some boundary condition that a real user
// would never click.
info("clicking the overlay background");
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive element to dismiss the opened
+ // dialog with a mouse which can be done by assistive technology and keyboard
+ // by pressing `Esc` key, this rule check shall be ignored by a11y_checks.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
await close_subdialog_and_test_generic_end_state(
tab.linkedBrowser,
function () {
@@ -432,6 +444,7 @@ add_task(async function background_click_should_close_dialog() {
0,
{ runClosingFnOutsideOfContentTask: true }
);
+ AccessibilityUtils.resetEnv();
});
add_task(async function escape_should_close_dialog() {
diff --git a/browser/components/preferences/tests/siteData/browser.toml b/browser/components/preferences/tests/siteData/browser.toml
index 9f4f8306e1..b7e6ba1b6d 100644
--- a/browser/components/preferences/tests/siteData/browser.toml
+++ b/browser/components/preferences/tests/siteData/browser.toml
@@ -10,6 +10,8 @@ support-files = [
["browser_clearSiteData.js"]
+["browser_clearSiteData_v2.js"]
+
["browser_siteData.js"]
["browser_siteData2.js"]
diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData.js b/browser/components/preferences/tests/siteData/browser_clearSiteData.js
index 7ae1fda453..4924dccfea 100644
--- a/browser/components/preferences/tests/siteData/browser_clearSiteData.js
+++ b/browser/components/preferences/tests/siteData/browser_clearSiteData.js
@@ -7,10 +7,6 @@ const { PermissionTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PermissionTestUtils.sys.mjs"
);
-let useOldClearHistoryDialog = Services.prefs.getBoolPref(
- "privacy.sanitize.useOldClearHistoryDialog"
-);
-
async function testClearData(clearSiteData, clearCache) {
PermissionTestUtils.add(
TEST_QUOTA_USAGE_ORIGIN,
@@ -64,9 +60,7 @@ async function testClearData(clearSiteData, clearCache) {
let doc = gBrowser.selectedBrowser.contentDocument;
let clearSiteDataButton = doc.getElementById("clearSiteDataButton");
- let url = useOldClearHistoryDialog
- ? "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml"
- : "chrome://browser/content/sanitize_v2.xhtml";
+ let url = "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml";
let dialogOpened = promiseLoadSubDialog(url);
clearSiteDataButton.doCommand();
let dialogWin = await dialogOpened;
@@ -78,10 +72,8 @@ async function testClearData(clearSiteData, clearCache) {
// since we've had cache intermittently changing under our feet.
let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage);
- let cookiesCheckboxId = useOldClearHistoryDialog
- ? "clearSiteData"
- : "cookiesAndStorage";
- let cacheCheckboxId = useOldClearHistoryDialog ? "clearCache" : "cache";
+ let cookiesCheckboxId = "clearSiteData";
+ let cacheCheckboxId = "clearCache";
let clearSiteDataCheckbox =
dialogWin.document.getElementById(cookiesCheckboxId);
let clearCacheCheckbox = dialogWin.document.getElementById(cacheCheckboxId);
@@ -106,28 +98,13 @@ async function testClearData(clearSiteData, clearCache) {
clearSiteDataCheckbox.checked = clearSiteData;
clearCacheCheckbox.checked = clearCache;
- if (!useOldClearHistoryDialog) {
- // The new clear history dialog has a seperate checkbox for site settings
- let siteSettingsCheckbox =
- dialogWin.document.getElementById("siteSettings");
- siteSettingsCheckbox.checked = clearSiteData;
- // select clear everything to match the old dialog boxes behaviour for this test
- let timespanSelection = dialogWin.document.getElementById(
- "sanitizeDurationChoice"
- );
- timespanSelection.value = 0;
- }
// Some additional promises/assertions to wait for
// when deleting site data.
let acceptPromise;
let updatePromise;
let cookiesClearedPromise;
if (clearSiteData) {
- // the new clear history dialog does not have a extra prompt
- // to clear site data after clicking clear
- if (useOldClearHistoryDialog) {
- acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
- }
+ acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
updatePromise = promiseSiteDataManagerSitesUpdated();
cookiesClearedPromise = promiseCookiesCleared();
}
@@ -137,7 +114,7 @@ async function testClearData(clearSiteData, clearCache) {
let clearButton = dialogWin.document
.querySelector("dialog")
.getButton("accept");
- if (!clearSiteData && !clearCache && useOldClearHistoryDialog) {
+ if (!clearSiteData && !clearCache) {
// Simulate user input on one of the checkboxes to trigger the event listener for
// disabling the clearButton.
clearCacheCheckbox.doCommand();
@@ -158,7 +135,7 @@ async function testClearData(clearSiteData, clearCache) {
// For site data we display an extra warning dialog, make sure
// to accept it.
- if (clearSiteData && useOldClearHistoryDialog) {
+ if (clearSiteData) {
await acceptPromise;
}
@@ -222,6 +199,12 @@ async function testClearData(clearSiteData, clearCache) {
await SiteDataManager.removeAll();
}
+add_setup(function () {
+ SpecialPowers.pushPrefEnv({
+ set: [["privacy.sanitize.useOldClearHistoryDialog", true]],
+ });
+});
+
// Test opening the "Clear All Data" dialog and cancelling.
add_task(async function () {
await testClearData(false, false);
diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js b/browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js
new file mode 100644
index 0000000000..8cb8be25b3
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js
@@ -0,0 +1,258 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+async function testClearData(clearSiteData, clearCache) {
+ PermissionTestUtils.add(
+ TEST_QUOTA_USAGE_ORIGIN,
+ "persistent-storage",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Open a test site which saves into appcache.
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Fill indexedDB with test data.
+ // Don't wait for the page to load, to register the content event handler as quickly as possible.
+ // If this test goes intermittent, we might have to tell the page to wait longer before
+ // firing the event.
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false);
+ await BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "test-indexedDB-done",
+ false,
+ null,
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Register some service workers.
+ await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+ await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ // Test the initial states.
+ let cacheUsage = await SiteDataManager.getCacheSize();
+ let quotaUsage = await SiteDataTestUtils.getQuotaUsage(
+ TEST_QUOTA_USAGE_ORIGIN
+ );
+ let totalUsage = await SiteDataManager.getTotalUsage();
+ Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+ Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+ Assert.greater(totalUsage, 0, "The total usage should not be 0");
+
+ let initialSizeLabelValue = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let sizeLabel = content.document.getElementById("totalSiteDataSize");
+ return sizeLabel.textContent;
+ }
+ );
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let clearSiteDataButton = doc.getElementById("clearSiteDataButton");
+
+ let url = "chrome://browser/content/sanitize_v2.xhtml";
+ let dialogOpened = promiseLoadSubDialog(url);
+ clearSiteDataButton.doCommand();
+ let dialogWin = await dialogOpened;
+
+ // Convert the usage numbers in the same way the UI does it to assert
+ // that they're displayed in the dialog.
+ let [convertedTotalUsage] = DownloadUtils.convertByteUnits(totalUsage);
+ // For cache we just assert that the right unit (KB, probably) is displayed,
+ // since we've had cache intermittently changing under our feet.
+ let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage);
+
+ let cookiesCheckboxId = "cookiesAndStorage";
+ let cacheCheckboxId = "cache";
+ let clearSiteDataCheckbox =
+ dialogWin.document.getElementById(cookiesCheckboxId);
+ let clearCacheCheckbox = dialogWin.document.getElementById(cacheCheckboxId);
+ // The usage details are filled asynchronously, so we assert that they're present by
+ // waiting for them to be filled in.
+ await Promise.all([
+ TestUtils.waitForCondition(
+ () =>
+ clearSiteDataCheckbox.label &&
+ clearSiteDataCheckbox.label.includes(convertedTotalUsage),
+ "Should show the quota usage"
+ ),
+ TestUtils.waitForCondition(
+ () =>
+ clearCacheCheckbox.label &&
+ clearCacheCheckbox.label.includes(convertedCacheUnit),
+ "Should show the cache usage"
+ ),
+ ]);
+
+ // Check the boxes according to our test input.
+ clearSiteDataCheckbox.checked = clearSiteData;
+ clearCacheCheckbox.checked = clearCache;
+
+ // select clear everything to match the old dialog boxes behaviour for this test
+ let timespanSelection = dialogWin.document.getElementById(
+ "sanitizeDurationChoice"
+ );
+ timespanSelection.value = 1;
+
+ // Some additional promises/assertions to wait for
+ // when deleting site data.
+ let updatePromise;
+ if (clearSiteData) {
+ // the new clear history dialog does not have a extra prompt
+ // to clear site data after clicking clear
+ updatePromise = promiseSiteDataManagerSitesUpdated();
+ }
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+
+ let clearButton = dialogWin.document
+ .querySelector("dialog")
+ .getButton("accept");
+ let cancelButton = dialogWin.document
+ .querySelector("dialog")
+ .getButton("cancel");
+
+ if (!clearSiteData && !clearCache) {
+ // Cancel, since we can't delete anything.
+ cancelButton.click();
+ } else {
+ // Delete stuff!
+ clearButton.click();
+ }
+
+ await dialogClosed;
+
+ if (clearCache) {
+ TestUtils.waitForCondition(async function () {
+ let usage = await SiteDataManager.getCacheSize();
+ return usage == 0;
+ }, "The cache usage should be removed");
+ } else {
+ Assert.greater(
+ await SiteDataManager.getCacheSize(),
+ 0,
+ "The cache usage should not be 0"
+ );
+ }
+
+ if (clearSiteData) {
+ await updatePromise;
+ await promiseServiceWorkersCleared();
+
+ TestUtils.waitForCondition(async function () {
+ let usage = await SiteDataManager.getTotalUsage();
+ return usage == 0;
+ }, "The total usage should be removed");
+ } else {
+ quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+ totalUsage = await SiteDataManager.getTotalUsage();
+ Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+ Assert.greater(totalUsage, 0, "The total usage should not be 0");
+ }
+
+ if (clearCache || clearSiteData) {
+ // Check that the size label in about:preferences updates after we cleared data.
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ initialSizeLabelValue }],
+ async function (opts) {
+ let sizeLabel = content.document.getElementById("totalSiteDataSize");
+ await ContentTaskUtils.waitForCondition(
+ () => sizeLabel.textContent != opts.initialSizeLabelValue,
+ "Site data size label should have updated."
+ );
+ }
+ );
+ }
+
+ let permission = PermissionTestUtils.getPermissionObject(
+ TEST_QUOTA_USAGE_ORIGIN,
+ "persistent-storage"
+ );
+ is(
+ clearSiteData ? permission : permission.capability,
+ clearSiteData ? null : Services.perms.ALLOW_ACTION,
+ "Should have the correct permission state."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SiteDataManager.removeAll();
+}
+
+add_setup(function () {
+ SpecialPowers.pushPrefEnv({
+ set: [["privacy.sanitize.useOldClearHistoryDialog", false]],
+ });
+
+ // The tests in this file all test specific interactions with the new clear
+ // history dialog and can't be split up.
+ requestLongerTimeout(2);
+});
+
+// Test opening the "Clear All Data" dialog and cancelling.
+add_task(async function testNoSiteDataNoCacheClearing() {
+ await testClearData(false, false);
+});
+
+// Test opening the "Clear All Data" dialog and removing all site data.
+add_task(async function testSiteDataClearing() {
+ await testClearData(true, false);
+});
+
+// Test opening the "Clear All Data" dialog and removing all cache.
+add_task(async function testCacheClearing() {
+ await testClearData(false, true);
+});
+
+// Test opening the "Clear All Data" dialog and removing everything.
+add_task(async function testSiteDataAndCacheClearing() {
+ await testClearData(true, true);
+});
+
+// Test clearing persistent storage
+add_task(async function testPersistentStorage() {
+ PermissionTestUtils.add(
+ TEST_QUOTA_USAGE_ORIGIN,
+ "persistent-storage",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let clearSiteDataButton = doc.getElementById("clearSiteDataButton");
+
+ let url = "chrome://browser/content/sanitize_v2.xhtml";
+ let dialogOpened = promiseLoadSubDialog(url);
+ clearSiteDataButton.doCommand();
+ let dialogWin = await dialogOpened;
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+
+ let timespanSelection = dialogWin.document.getElementById(
+ "sanitizeDurationChoice"
+ );
+ timespanSelection.value = 1;
+ let clearButton = dialogWin.document
+ .querySelector("dialog")
+ .getButton("accept");
+ clearButton.click();
+ await dialogClosed;
+
+ let permission = PermissionTestUtils.getPermissionObject(
+ TEST_QUOTA_USAGE_ORIGIN,
+ "persistent-storage"
+ );
+ is(permission, null, "Should have the correct permission state.");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/translations.inc.xhtml b/browser/components/preferences/translations.inc.xhtml
index 9463fde707..1143cbc6d0 100644
--- a/browser/components/preferences/translations.inc.xhtml
+++ b/browser/components/preferences/translations.inc.xhtml
@@ -19,45 +19,63 @@
<p id="translations-settings-description" data-l10n-id="translations-settings-description"/>
- <div class="translations-settings-manage-list"
- id="translations-settings-manage-always-translate-list">
+ <moz-card class="translations-settings-manage-section" data-l10n-attrs="heading"
+ id="translations-settings-always-translate-section">
<div class="translations-settings-manage-language">
<h2 id="translations-settings-always-translate" data-l10n-id="translations-settings-always-translate"/>
<xul:menulist id="translations-settings-always-translate-list"
- data-l10n-id="translations-settings-add-language-button">
- <xul:menupopup/>
+ data-l10n-id="translations-settings-add-language-button"
+ aria-labelledby="translations-settings-always-translate">
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ <xul:menupopup id="translations-settings-always-translate-popup"/>
</xul:menulist>
</div>
- </div>
+ </moz-card>
- <div id="translations-settings-manage-never-translate-list"
- class="translations-settings-manage-list">
+ <moz-card id="translations-settings-never-translate-section"
+ class="translations-settings-manage-section">
<div class="translations-settings-manage-language">
<h2 id="translations-settings-never-translate" data-l10n-id="translations-settings-never-translate"/>
<xul:menulist id="translations-settings-never-translate-list"
- data-l10n-id="translations-settings-add-language-button">
- <xul:menupopup/>
+ data-l10n-id="translations-settings-add-language-button"
+ aria-labelledby="translations-settings-never-translate">
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ <xul:menupopup id="translations-settings-never-translate-popup"/>
</xul:menulist>
</div>
- </div>
+ </moz-card>
- <div id="translations-settings-never-sites-list" class="translations-settings-manage-list" >
- <div class="translations-settings-manage-list-info" >
+ <moz-card id="translations-settings-never-sites-section"
+ class="translations-settings-manage-section">
+ <div class="translations-settings-manage-section-info" >
<h2 id="translations-settings-never-sites-header"
data-l10n-id="translations-settings-never-sites-header"/>
<p id="translations-settings-never-sites"
data-l10n-id="translations-settings-never-sites-description"/>
</div>
- </div>
+ </moz-card>
- <div id="translations-settings-manage-install-list" class="translations-settings-manage-list">
- <div class="translations-settings-manage-list-info">
- <h2 id="translations-settings-download-languages"
- data-l10n-id="translations-settings-download-languages"/>
+ <moz-card id="translations-settings-download-section"
+ class="translations-settings-manage-section">
+ <div class="translations-settings-manage-section-info">
+ <h2 data-l10n-id="translations-settings-download-languages"/>
<a is="moz-support-link" class="learnMore"
id="download-languages-learn-more"
data-l10n-id="translations-settings-download-languages-link"
support-page="website-translation"/>
</div>
- </div>
+ <div class="translations-settings-languages-card">
+ <h3 class="translations-settings-language-header" data-l10n-id="translations-settings-language-header"></h3>
+ <div class="translations-settings-language-list">
+ <div class="translations-settings-language">
+ <moz-button class="translations-settings-download-icon" type="ghost icon"
+ aria-label="translations-settings-download-all-languages"></moz-button>
+ <!-- The option to "All languages" is added here.
+ In translations.js the option to download individual languages is
+ added dynamically based on the supported language list -->
+ <label id="translations-settings-download-all-languages" data-l10n-id="translations-settings-download-all-languages"></label>
+ </div>
+ </div>
+ </div>
+ </moz-card>
</div>
diff --git a/browser/components/preferences/translations.js b/browser/components/preferences/translations.js
index c9cfe472ac..1bfa021d59 100644
--- a/browser/components/preferences/translations.js
+++ b/browser/components/preferences/translations.js
@@ -4,6 +4,10 @@
/* import-globals-from preferences.js */
+ChromeUtils.defineESModuleGetters(this, {
+ TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
+});
+
let gTranslationsPane = {
init() {
document
@@ -11,5 +15,271 @@ let gTranslationsPane = {
.addEventListener("click", function () {
gotoPref("general");
});
+
+ document
+ .getElementById("translations-settings-always-translate-list")
+ .addEventListener("command", this.addAlwaysLanguage);
+
+ document
+ .getElementById("translations-settings-never-translate-list")
+ .addEventListener("command", this.addNeverLanguage);
+
+ this.buildLanguageDropDowns();
+ this.buildDownloadLanguageList();
+ },
+
+ /**
+ * Populate the Drop down list with the list of supported languages
+ * for the user to choose languages to add to Always translate and
+ * Never translate settings list.
+ */
+ async buildLanguageDropDowns() {
+ const { fromLanguages } = await TranslationsParent.getSupportedLanguages();
+ let alwaysLangPopup = document.getElementById(
+ "translations-settings-always-translate-popup"
+ );
+ let neverLangPopup = document.getElementById(
+ "translations-settings-never-translate-popup"
+ );
+
+ for (const { langTag, displayName } of fromLanguages) {
+ const alwaysLang = document.createXULElement("menuitem");
+ alwaysLang.setAttribute("value", langTag);
+ alwaysLang.setAttribute("label", displayName);
+ alwaysLangPopup.appendChild(alwaysLang);
+ const neverLang = document.createXULElement("menuitem");
+ neverLang.setAttribute("value", langTag);
+ neverLang.setAttribute("label", displayName);
+ neverLangPopup.appendChild(neverLang);
+ }
+ },
+
+ /**
+ * Show a list of languages for the user to be able to install
+ * and uninstall language models for local translation.
+ */
+ async buildDownloadLanguageList() {
+ const supportedLanguages = await TranslationsParent.getSupportedLanguages();
+ const languageList = TranslationsParent.getLanguageList(supportedLanguages);
+
+ let installList = document.querySelector(
+ ".translations-settings-language-list"
+ );
+
+ // The option to download "All languages" is added in xhtml.
+ // Here the option to download individual languages is dynamically added
+ // based on the supported language list
+ installList
+ .querySelector("moz-button")
+ .addEventListener("click", installLanguage);
+
+ for (const language of languageList) {
+ const languageElement = document.createElement("div");
+ languageElement.classList.add("translations-settings-language");
+
+ const languageLabel = document.createElement("label");
+ languageLabel.textContent = language.displayName;
+ languageLabel.setAttribute("value", language.langTag);
+ // Using the language tag suffix to create unique id for each language
+ languageLabel.id = "translations-settings-download-" + language.langTag;
+
+ const installButton = document.createElement("moz-button");
+ installButton.classList.add("translations-settings-download-icon");
+ installButton.setAttribute("type", "ghost icon");
+ installButton.addEventListener("click", installLanguage);
+ installButton.setAttribute("aria-label", languageLabel.id);
+
+ languageElement.appendChild(installButton);
+ languageElement.appendChild(languageLabel);
+ installList.appendChild(languageElement);
+ }
+ },
+
+ /**
+ * Event handler when the user wants to add a language to
+ * Always translate settings list.
+ */
+ addAlwaysLanguage(event) {
+ /* TODO:
+ The function addLanguage adds the HTML element.
+ It will be moved to the observer in the next Bug - 1881259 .
+ It is here just to test the UI.
+ For now a language can be added multiple times.
+
+ In the next bug we will maintain a local state of preferences.
+ When a language is added or removed, the user event updates the preferences in the Services,
+ This triggers the observer which compares the preferences in the local state and the
+ preferences in the Services and adds or removes the language in the local state and updates the
+ UI to reflect the updated Preferences in the Services.
+ */
+ addLanguage(
+ event,
+ "translations-settings-always-translate-section",
+ deleteAlwaysLanguage
+ );
+ },
+
+ /**
+ * Event handler when the user wants to add a language to
+ * Never translate settings list.
+ */
+ addNeverLanguage(event) {
+ /* TODO:
+ The function addLanguage adds the HTML element.
+ It will be moved to the observer in the next Bug - 1881259 .
+ It is here just to test the UI.
+ For now a language can be added multiple times.
+
+ In the next bug we will maintain a local state of preferences.
+ When a language is added or removed, the user event updates the preferences in the Services,
+ This triggers the observer which compares the preferences in the local state and the
+ preferences in the Services and adds or removes the language in the local state and updates the
+ UI to reflect the updated Preferences in the Services.
+ */
+ addLanguage(
+ event,
+ "translations-settings-never-translate-section",
+ deleteNeverLanguage
+ );
},
};
+
+/**
+ * Function to add a language selected by the user to the list of
+ * Always/Never translate settings list.
+ */
+async function addLanguage(event, listClass, delHandler) {
+ const translatePrefix =
+ listClass === "translations-settings-never-translate-section"
+ ? "never"
+ : "always";
+ let translateSection = document.getElementById(listClass);
+ let languageList = translateSection.querySelector(
+ ".translations-settings-language-list"
+ );
+
+ // While adding the first language, add the Header and language List div
+ if (!languageList) {
+ let languageCard = document.createElement("div");
+ languageCard.classList.add("translations-settings-languages-card");
+ translateSection.appendChild(languageCard);
+
+ let languageHeader = document.createElement("h3");
+ languageCard.appendChild(languageHeader);
+ languageHeader.setAttribute(
+ "data-l10n-id",
+ "translations-settings-language-header"
+ );
+ languageHeader.classList.add("translations-settings-language-header");
+
+ languageList = document.createElement("div");
+ languageList.classList.add("translations-settings-language-list");
+ languageCard.appendChild(languageList);
+ }
+ const languageElement = document.createElement("div");
+ languageElement.classList.add("translations-settings-language");
+ // Add the language after the Language Header
+ languageList.insertBefore(languageElement, languageList.firstChild);
+
+ const languageLabel = document.createElement("label");
+ languageLabel.textContent = event.target.getAttribute("label");
+ languageLabel.setAttribute("value", event.target.getAttribute("value"));
+ // Using the language tag suffix to create unique id for each language
+ // add prefix for the always/never translate
+ languageLabel.id =
+ "translations-settings-language-" +
+ translatePrefix +
+ "-" +
+ event.target.getAttribute("value");
+
+ const delButton = document.createElement("moz-button");
+ delButton.classList.add("translations-settings-delete-icon");
+ delButton.setAttribute("type", "ghost icon");
+ delButton.addEventListener("click", delHandler);
+ delButton.setAttribute("aria-label", languageLabel.id);
+
+ languageElement.appendChild(delButton);
+ languageElement.appendChild(languageLabel);
+
+ /* After a language is selected the menulist button display will be set to the
+ selected langauge. After processing the button event the
+ data-l10n-id of the menulist button is restored to "Add Language" */
+ const menuList = translateSection.querySelector("menulist");
+ await document.l10n.translateElements([menuList]);
+}
+
+/**
+ * Event Handler to delete a language selected by the user from the list of
+ * Always translate settings list.
+ */
+function deleteAlwaysLanguage(event) {
+ /* TODO:
+ The function removeLanguage removes the HTML element.
+ It will be moved to the observer in the next Bug - 1881259 .
+ It is here just to test the UI.
+
+ In the next bug we will maintain a local state of preferences.
+ When a language is added or removed, the user event updates the preferences in the Services,
+ This triggers the observer which compares the preferences in the local state and the
+ preferences in the Services and adds or removes the language in the local state and updates the
+ UI to reflect the updated Preferences in the Services.
+ */
+ removeLanguage(event);
+}
+
+/**
+ * Event Handler to delete a language selected by the user from the list of
+ * Never translate settings list.
+ */
+function deleteNeverLanguage(event) {
+ /* TODO:
+ The function removeLanguage removes the HTML element.
+ It will be moved to the observer in the next Bug - 1881259 .
+ It is here just to test the UI.
+
+ In the next bug we will maintain a local state of preferences.
+ When a language is added or removed, the user event updates the preferences in the Services,
+ This triggers the observer which compares the preferences in the local state and the
+ preferences in the Services and adds or removes the language in the local state and updates the
+ UI to reflect the updated Preferences in the Services.
+ */
+ removeLanguage(event);
+}
+
+/**
+ * Function to delete a language selected by the user from the list of
+ * Always/Never translate settings list.
+ */
+function removeLanguage(event) {
+ /* Langauge section moz-card -parent of-> Language card -parent of->
+ Language heading and Language list -parent of->
+ Language Element -parent of-> language button and label
+ */
+ let languageCard = event.target.parentNode.parentNode.parentNode;
+ event.target.parentNode.remove();
+ if (languageCard.children[1].childElementCount === 0) {
+ // If there is no language in the list remove the
+ // Language Header and language list div
+ languageCard.remove();
+ }
+}
+
+/**
+ * Event Handler to install a language model selected by the user
+ */
+function installLanguage(event) {
+ event.target.classList.remove("translations-settings-download-icon");
+ event.target.classList.add("translations-settings-delete-icon");
+ event.target.removeEventListener("click", installLanguage);
+ event.target.addEventListener("click", unInstallLanguage);
+}
+
+/**
+ * Event Handler to install a language model selected by the user
+ */
+function unInstallLanguage(event) {
+ event.target.classList.remove("translations-settings-delete-icon");
+ event.target.classList.add("translations-settings-download-icon");
+ event.target.removeEventListener("click", unInstallLanguage);
+ event.target.addEventListener("click", installLanguage);
+}
diff --git a/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js b/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js
index a1b9420171..818c412ef6 100644
--- a/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js
+++ b/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js
@@ -9,7 +9,7 @@ const DUMMY_PAGE = PATH + "empty_file.html";
add_task(
async function test_principal_right_click_open_link_in_new_private_win() {
- await BrowserTestUtils.withNewTab(TEST_PAGE, async function (browser) {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async function () {
let promiseNewWindow = BrowserTestUtils.waitForNewWindow({
url: DUMMY_PAGE,
});
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js
index bfe5708a5b..ef207916a5 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js
@@ -41,7 +41,7 @@ add_task(async function test_experiment_messaging_system_dismiss() {
let { win: win1, tab: tab1 } = await openTabAndWaitForRender();
- await SpecialPowers.spawn(tab1, [LOCALE], async function (locale) {
+ await SpecialPowers.spawn(tab1, [LOCALE], async function () {
content.document.querySelector("#dismiss-btn").click();
info("button clicked");
});
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js
index ac42caa2dd..0d4b0a1dbb 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js
@@ -47,7 +47,7 @@ add_task(async function test_experiment_messaging_system_impressions() {
let { win: win1, tab: tab1 } = await openTabAndWaitForRender();
- await SpecialPowers.spawn(tab1, [LOCALE], async function (locale) {
+ await SpecialPowers.spawn(tab1, [LOCALE], async function () {
is(
content.document
.querySelector(".promo button")
@@ -72,7 +72,7 @@ add_task(async function test_experiment_messaging_system_impressions() {
let { win: win2, tab: tab2 } = await openTabAndWaitForRender();
- await SpecialPowers.spawn(tab2, [LOCALE], async function (locale) {
+ await SpecialPowers.spawn(tab2, [LOCALE], async function () {
is(
content.document
.querySelector(".promo button")
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js
index de6aa1f6ba..9d274a28de 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js
@@ -46,7 +46,7 @@ function getStorageEntryCount(device, goon) {
var visitor = {
entryCount: 0,
- onCacheStorageInfo(aEntryCount, aConsumption) {},
+ onCacheStorageInfo() {},
onCacheEntryInfo(uri) {
var urispec = uri.asciiSpec;
info(device + ":" + urispec + "\n");
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js
index 9b796613a9..acdb4bb30d 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js
@@ -31,7 +31,7 @@ function test() {
};
function testCheckbox() {
win.removeEventListener("load", testCheckbox);
- Services.obs.addObserver(function onCertUI(aSubject, aTopic, aData) {
+ Services.obs.addObserver(function onCertUI() {
Services.obs.removeObserver(onCertUI, "cert-exception-ui-ready");
ok(win.gCert, "The certificate information should be available now");
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js
index 39e41589b4..ce86cab69c 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js
@@ -19,7 +19,7 @@ add_task(async () => {
await BrowserTestUtils.browserLoaded(privateTab);
let observerExited = {
- observe(aSubject, aTopic, aData) {
+ observe() {
ok(false, "Notification received!");
},
};
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js
index eea0ab07ca..46ef974677 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js
@@ -32,7 +32,7 @@ function clearAllPlacesFavicons() {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
if (aTopic === "places-favicons-expired") {
resolve();
Services.obs.removeObserver(observer, "places-favicons-expired");
@@ -59,7 +59,7 @@ function observeFavicon(aIsPrivate, aExpectedCookie, aPageURI) {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
// Make sure that the topic is 'http-on-modify-request'.
if (aTopic === "http-on-modify-request") {
// We check the privateBrowsingId for the originAttributes of the loading
@@ -121,7 +121,7 @@ function observeFavicon(aIsPrivate, aExpectedCookie, aPageURI) {
function waitOnFaviconResponse(aFaviconURL) {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
if (
aTopic === "http-on-examine-response" ||
aTopic === "http-on-examine-cached-response"
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html
index 01ed3f3d2c..e7c1920215 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html
@@ -5,7 +5,7 @@
</head>
<body>
<script type="text/javascript">
- navigator.geolocation.getCurrentPosition(function(pos) {
+ navigator.geolocation.getCurrentPosition(function() {
// ignore
});
</script>
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js
index 1fd28d4ca6..4874e61bd4 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js
@@ -6,7 +6,7 @@
add_task(async function test_no_notification_when_pb_autostart() {
let observedLastPBContext = false;
let observerExited = {
- observe(aSubject, aTopic, aData) {
+ observe() {
observedLastPBContext = true;
},
};
@@ -31,7 +31,7 @@ add_task(async function test_no_notification_when_pb_autostart() {
add_task(async function test_notification_when_about_preferences() {
let observedLastPBContext = false;
let observerExited = {
- observe(aSubject, aTopic, aData) {
+ observe() {
observedLastPBContext = true;
},
};
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js
index c46417933a..c57a482752 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js
@@ -11,7 +11,7 @@ function test() {
let expectedExiting = true;
let expectedExited = false;
let observerExiting = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
is(
aTopic,
"last-pb-context-exiting",
@@ -26,7 +26,7 @@ function test() {
},
};
let observerExited = {
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
is(
aTopic,
"last-pb-context-exited",
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js
index ab74caeb5e..70f6666589 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js
@@ -55,7 +55,7 @@ function test() {
}
function openPrivateBrowsingModeByUI(aWindow, aCallback) {
- Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.addObserver(function observer(aSubject) {
aSubject.addEventListener(
"load",
function () {
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js
index dd0e2e1b64..ac31b925ed 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js
@@ -12,7 +12,7 @@ add_task(async function test() {
function promiseLocationChange() {
return new Promise(resolve => {
- Services.obs.addObserver(function onLocationChange(subj, topic, data) {
+ Services.obs.addObserver(function onLocationChange(subj, topic) {
Services.obs.removeObserver(onLocationChange, topic);
resolve();
}, "browser-fullZoom:location-change");
@@ -59,7 +59,7 @@ add_task(async function test() {
);
}
- function testOnWindow(options, callback) {
+ function testOnWindow(options) {
return BrowserTestUtils.openNewBrowserWindow(options).then(win => {
windowsToClose.push(win);
windowsToReset.push(win);
diff --git a/browser/components/protections/content/protections.mjs b/browser/components/protections/content/protections.mjs
index 3204586a2b..412ac54d1b 100644
--- a/browser/components/protections/content/protections.mjs
+++ b/browser/components/protections/content/protections.mjs
@@ -31,7 +31,7 @@ if (searchParams.has("entrypoint")) {
searchParamsChanged = true;
}
-document.addEventListener("DOMContentLoaded", e => {
+document.addEventListener("DOMContentLoaded", () => {
if (searchParamsChanged) {
let newURL = protocol + pathname;
let params = searchParams.toString();
diff --git a/browser/components/protections/test/browser/browser_protections_monitor.js b/browser/components/protections/test/browser/browser_protections_monitor.js
index b24d8de55c..e96412edca 100644
--- a/browser/components/protections/test/browser/browser_protections_monitor.js
+++ b/browser/components/protections/test/browser/browser_protections_monitor.js
@@ -134,7 +134,7 @@ add_task(async function () {
await BrowserTestUtils.removeTab(tab);
});
-async function checkNoLoginsContentIsDisplayed(tab, expectedLinkContent) {
+async function checkNoLoginsContentIsDisplayed(tab) {
await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
await ContentTaskUtils.waitForCondition(() => {
const noLogins = content.document.querySelector(
diff --git a/browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs b/browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs
index 345046ae27..5255bfec46 100644
--- a/browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs
+++ b/browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs
@@ -543,7 +543,7 @@ WebProtocolHandlerRegistrar.prototype = {
notificationId,
{
label: {
- "l10n-id": "protocolhandler-mailto-handler-set-message",
+ "l10n-id": "protocolhandler-mailto-handler-set",
"l10n-args": { url: aURI.host },
},
priority: osDefaultNotificationBox.PRIORITY_INFO_LOW,
@@ -576,7 +576,7 @@ WebProtocolHandlerRegistrar.prototype = {
true
);
newitem.messageL10nId =
- "protocolhandler-mailto-handler-confirm-message";
+ "protocolhandler-mailto-handler-confirm";
newitem.removeChild(newitem.buttonContainer);
newitem.setAttribute("type", "success"); // from moz-message-bar.css
newitem.eventCallback = null; // disable show only once per day for success
diff --git a/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs b/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs
index ef1f4e1270..836908c7b4 100644
--- a/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs
+++ b/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs
@@ -594,7 +594,7 @@ export var ReportBrokenSite = new (class ReportBrokenSite {
const expectedBrowser = tabbrowser.getBrowserForTab(tab);
return new Promise(resolve => {
const listener = {
- onLocationChange(browser, webProgress, request, uri, flags) {
+ onLocationChange(browser, webProgress, request, uri) {
if (
browser == expectedBrowser &&
uri.spec == url &&
diff --git a/browser/components/reportbrokensite/test/browser/browser_back_buttons.js b/browser/components/reportbrokensite/test/browser/browser_back_buttons.js
index b8de5f8e95..c004442c24 100644
--- a/browser/components/reportbrokensite/test/browser/browser_back_buttons.js
+++ b/browser/components/reportbrokensite/test/browser/browser_back_buttons.js
@@ -12,26 +12,23 @@ add_common_setup();
add_task(async function testBackButtonsAreAdded() {
ensureReportBrokenSitePreffedOn();
- await BrowserTestUtils.withNewTab(
- REPORTABLE_PAGE_URL,
- async function (browser) {
- let rbs = await AppMenu().openReportBrokenSite();
- rbs.isBackButtonEnabled();
- await rbs.clickBack();
- await rbs.close();
-
- rbs = await HelpMenu().openReportBrokenSite();
- ok(!rbs.backButton, "Back button is not shown for Help Menu");
- await rbs.close();
-
- rbs = await ProtectionsPanel().openReportBrokenSite();
- rbs.isBackButtonEnabled();
- await rbs.clickBack();
- await rbs.close();
-
- rbs = await HelpMenu().openReportBrokenSite();
- ok(!rbs.backButton, "Back button is not shown for Help Menu");
- await rbs.close();
- }
- );
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ let rbs = await AppMenu().openReportBrokenSite();
+ rbs.isBackButtonEnabled();
+ await rbs.clickBack();
+ await rbs.close();
+
+ rbs = await HelpMenu().openReportBrokenSite();
+ ok(!rbs.backButton, "Back button is not shown for Help Menu");
+ await rbs.close();
+
+ rbs = await ProtectionsPanel().openReportBrokenSite();
+ rbs.isBackButtonEnabled();
+ await rbs.clickBack();
+ await rbs.close();
+
+ rbs = await HelpMenu().openReportBrokenSite();
+ ok(!rbs.backButton, "Back button is not shown for Help Menu");
+ await rbs.close();
+ });
});
diff --git a/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js b/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js
index 4c37866628..3bf9278e46 100644
--- a/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js
+++ b/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js
@@ -12,34 +12,31 @@ add_common_setup();
requestLongerTimeout(2);
async function testPressingKey(key, tabToMatch, makePromise, followUp) {
- await BrowserTestUtils.withNewTab(
- REPORTABLE_PAGE_URL,
- async function (browser) {
- for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) {
- info(
- `Opening RBS to test pressing ${key} for ${tabToMatch} on ${menu.menuDescription}`
- );
- const rbs = await menu.openReportBrokenSite();
- const promise = makePromise(rbs);
- if (tabToMatch) {
- if (await tabTo(tabToMatch)) {
- await pressKeyAndAwait(promise, key);
- followUp && (await followUp(rbs));
- await rbs.close();
- ok(true, `was able to activate ${tabToMatch} with keyboard`);
- } else {
- await rbs.close();
- ok(false, `could not tab to ${tabToMatch}`);
- }
- } else {
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) {
+ info(
+ `Opening RBS to test pressing ${key} for ${tabToMatch} on ${menu.menuDescription}`
+ );
+ const rbs = await menu.openReportBrokenSite();
+ const promise = makePromise(rbs);
+ if (tabToMatch) {
+ if (await tabTo(tabToMatch)) {
await pressKeyAndAwait(promise, key);
followUp && (await followUp(rbs));
await rbs.close();
- ok(true, `was able to use keyboard`);
+ ok(true, `was able to activate ${tabToMatch} with keyboard`);
+ } else {
+ await rbs.close();
+ ok(false, `could not tab to ${tabToMatch}`);
}
+ } else {
+ await pressKeyAndAwait(promise, key);
+ followUp && (await followUp(rbs));
+ await rbs.close();
+ ok(true, `was able to use keyboard`);
}
}
- );
+ });
}
add_task(async function testSendMoreInfo() {
@@ -98,16 +95,13 @@ add_task(async function testESCOnSent() {
add_task(async function testBackButtons() {
ensureReportBrokenSitePreffedOn();
- await BrowserTestUtils.withNewTab(
- REPORTABLE_PAGE_URL,
- async function (browser) {
- for (const menu of [AppMenu(), ProtectionsPanel()]) {
- await menu.openReportBrokenSite();
- await tabTo("#report-broken-site-popup-mainView .subviewbutton-back");
- const promise = BrowserTestUtils.waitForEvent(menu.popup, "ViewShown");
- await pressKeyAndAwait(promise, "KEY_Enter");
- menu.close();
- }
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ for (const menu of [AppMenu(), ProtectionsPanel()]) {
+ await menu.openReportBrokenSite();
+ await tabTo("#report-broken-site-popup-mainView .subviewbutton-back");
+ const promise = BrowserTestUtils.waitForEvent(menu.popup, "ViewShown");
+ await pressKeyAndAwait(promise, "KEY_Enter");
+ menu.close();
}
- );
+ });
});
diff --git a/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js b/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js
index 7097a662e5..68aeae911e 100644
--- a/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js
+++ b/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js
@@ -33,7 +33,7 @@ add_task(async function testReportSentViewBGColor() {
await SpecialPowers.pushPrefEnv({ set: HIGH_CONTRAST_MODE_OFF });
const rbs = await menu.openReportBrokenSite();
const { mainView, sentView } = rbs;
- mainView.style.backgroundColor = "var(--color-background-success)";
+ mainView.style.backgroundColor = "var(--background-color-success)";
const expectedReportSentBGColor =
defaultView.getComputedStyle(mainView).backgroundColor;
mainView.style.backgroundColor = "";
diff --git a/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js b/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js
index 0f5545fcc4..e6e6967919 100644
--- a/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js
+++ b/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js
@@ -29,45 +29,42 @@ async function clickSendAndCheckPing(rbs, expectedReason = null) {
add_task(async function testReasonDropdown() {
ensureReportBrokenSitePreffedOn();
- await BrowserTestUtils.withNewTab(
- REPORTABLE_PAGE_URL,
- async function (browser) {
- ensureReasonDisabled();
-
- let rbs = await AppMenu().openReportBrokenSite();
- await rbs.isReasonHidden();
- await rbs.isSendButtonEnabled();
- await clickSendAndCheckPing(rbs);
- await rbs.clickOkay();
-
- ensureReasonOptional();
- rbs = await AppMenu().openReportBrokenSite();
- await rbs.isReasonOptional();
- await rbs.isSendButtonEnabled();
- await clickSendAndCheckPing(rbs);
- await rbs.clickOkay();
-
- rbs = await AppMenu().openReportBrokenSite();
- await rbs.isReasonOptional();
- rbs.chooseReason("slow");
- await rbs.isSendButtonEnabled();
- await clickSendAndCheckPing(rbs, "slow");
- await rbs.clickOkay();
-
- ensureReasonRequired();
- rbs = await AppMenu().openReportBrokenSite();
- await rbs.isReasonRequired();
- await rbs.isSendButtonEnabled();
- const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window);
- EventUtils.synthesizeMouseAtCenter(rbs.sendButton, {}, window);
- await selectPromise;
- rbs.chooseReason("media");
- await rbs.dismissDropdownPopup();
- await rbs.isSendButtonEnabled();
- await clickSendAndCheckPing(rbs, "media");
- await rbs.clickOkay();
- }
- );
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ ensureReasonDisabled();
+
+ let rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonHidden();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs);
+ await rbs.clickOkay();
+
+ ensureReasonOptional();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonOptional();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs);
+ await rbs.clickOkay();
+
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonOptional();
+ rbs.chooseReason("slow");
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs, "slow");
+ await rbs.clickOkay();
+
+ ensureReasonRequired();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonRequired();
+ await rbs.isSendButtonEnabled();
+ const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ EventUtils.synthesizeMouseAtCenter(rbs.sendButton, {}, window);
+ await selectPromise;
+ rbs.chooseReason("media");
+ await rbs.dismissDropdownPopup();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs, "media");
+ await rbs.clickOkay();
+ });
});
async function getListItems(rbs) {
@@ -90,72 +87,69 @@ add_task(async function testReasonDropdownRandomized() {
undefined
);
- await BrowserTestUtils.withNewTab(
- REPORTABLE_PAGE_URL,
- async function (browser) {
- // confirm that the default order is initially used
- Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
- const rbs = await AppMenu().openReportBrokenSite();
- const defaultOrder = [
- "choose",
- "slow",
- "media",
- "content",
- "account",
- "adblockers",
- "other",
- ];
- Assert.deepEqual(
- await getListItems(rbs),
- defaultOrder,
- "non-random order is correct"
- );
-
- // confirm that a random order happens per user
- let randomOrder;
- let isRandomized = false;
- Services.prefs.setBoolPref(RANDOMIZE_PREF, true);
-
- // This becomes ClientEnvironment.randomizationId, which we can set to
- // any value which results in a different order from the default ordering.
- Services.prefs.setCharPref("app.normandy.user_id", "dummy");
-
- // clicking cancel triggers a reset, which is when the randomization
- // logic is called. so we must click cancel after pref-changes here.
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ // confirm that the default order is initially used
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
+ const rbs = await AppMenu().openReportBrokenSite();
+ const defaultOrder = [
+ "choose",
+ "slow",
+ "media",
+ "content",
+ "account",
+ "adblockers",
+ "other",
+ ];
+ Assert.deepEqual(
+ await getListItems(rbs),
+ defaultOrder,
+ "non-random order is correct"
+ );
+
+ // confirm that a random order happens per user
+ let randomOrder;
+ let isRandomized = false;
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, true);
+
+ // This becomes ClientEnvironment.randomizationId, which we can set to
+ // any value which results in a different order from the default ordering.
+ Services.prefs.setCharPref("app.normandy.user_id", "dummy");
+
+ // clicking cancel triggers a reset, which is when the randomization
+ // logic is called. so we must click cancel after pref-changes here.
+ rbs.clickCancel();
+ await AppMenu().openReportBrokenSite();
+ randomOrder = await getListItems(rbs);
+ Assert.ok(
+ randomOrder != defaultOrder,
+ "options are randomized with pref on"
+ );
+
+ // confirm that the order doesn't change per user
+ isRandomized = false;
+ for (let attempt = 0; attempt < 5; ++attempt) {
rbs.clickCancel();
await AppMenu().openReportBrokenSite();
- randomOrder = await getListItems(rbs);
- Assert.ok(
- randomOrder != defaultOrder,
- "options are randomized with pref on"
- );
+ const order = await getListItems(rbs);
- // confirm that the order doesn't change per user
- isRandomized = false;
- for (let attempt = 0; attempt < 5; ++attempt) {
- rbs.clickCancel();
- await AppMenu().openReportBrokenSite();
- const order = await getListItems(rbs);
-
- if (order != randomOrder) {
- isRandomized = true;
- break;
- }
+ if (order != randomOrder) {
+ isRandomized = true;
+ break;
}
- Assert.ok(!isRandomized, "options keep the same order per user");
-
- // confirm that the order reverts to the default if pref flipped to false
- Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
- rbs.clickCancel();
- await AppMenu().openReportBrokenSite();
- Assert.deepEqual(
- defaultOrder,
- await getListItems(rbs),
- "reverts to non-random order correctly"
- );
- rbs.clickCancel();
}
- );
+ Assert.ok(!isRandomized, "options keep the same order per user");
+
+ // confirm that the order reverts to the default if pref flipped to false
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
+ rbs.clickCancel();
+ await AppMenu().openReportBrokenSite();
+ Assert.deepEqual(
+ defaultOrder,
+ await getListItems(rbs),
+ "reverts to non-random order correctly"
+ );
+ rbs.clickCancel();
+ });
Services.prefs.setCharPref(USER_ID_PREF, origNormandyUserID);
});
diff --git a/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js b/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js
index 26101d77b9..98c6c740a5 100644
--- a/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js
+++ b/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js
@@ -40,14 +40,11 @@ async function testEnabledForValidURLs(menu) {
ensureReportBrokenSitePreffedOff();
ensureReportSiteIssuePreffedOn();
- await BrowserTestUtils.withNewTab(
- REPORTABLE_PAGE_URL,
- async function (browser) {
- await menu.open();
- menu.isReportSiteIssueEnabled();
- await menu.close();
- }
- );
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await menu.open();
+ menu.isReportSiteIssueEnabled();
+ await menu.close();
+ });
}
// AppMenu help sub-menu
diff --git a/browser/components/reportbrokensite/test/browser/browser_send_more_info.js b/browser/components/reportbrokensite/test/browser/browser_send_more_info.js
index edce03e0e0..9306f5161e 100644
--- a/browser/components/reportbrokensite/test/browser/browser_send_more_info.js
+++ b/browser/components/reportbrokensite/test/browser/browser_send_more_info.js
@@ -24,22 +24,19 @@ requestLongerTimeout(2);
add_task(async function testSendMoreInfoPref() {
ensureReportBrokenSitePreffedOn();
- await BrowserTestUtils.withNewTab(
- REPORTABLE_PAGE_URL,
- async function (browser) {
- await changeTab(gBrowser.selectedTab, REPORTABLE_PAGE_URL);
-
- ensureSendMoreInfoDisabled();
- let rbs = await AppMenu().openReportBrokenSite();
- await rbs.isSendMoreInfoHidden();
- await rbs.close();
-
- ensureSendMoreInfoEnabled();
- rbs = await AppMenu().openReportBrokenSite();
- await rbs.isSendMoreInfoShown();
- await rbs.close();
- }
- );
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await changeTab(gBrowser.selectedTab, REPORTABLE_PAGE_URL);
+
+ ensureSendMoreInfoDisabled();
+ let rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isSendMoreInfoHidden();
+ await rbs.close();
+
+ ensureSendMoreInfoEnabled();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isSendMoreInfoShown();
+ await rbs.close();
+ });
});
add_task(async function testSendingMoreInfo() {
diff --git a/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js b/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js
index 3a50c9aa51..e02c6a8394 100644
--- a/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js
+++ b/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js
@@ -124,12 +124,9 @@ add_task(async function testTabOrdering() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
- await BrowserTestUtils.withNewTab(
- REPORTABLE_PAGE_URL,
- async function (browser) {
- await testTabOrder(AppMenu());
- await testTabOrder(ProtectionsPanel());
- await testTabOrder(HelpMenu());
- }
- );
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await testTabOrder(AppMenu());
+ await testTabOrder(ProtectionsPanel());
+ await testTabOrder(HelpMenu());
+ });
});
diff --git a/browser/components/reportbrokensite/test/browser/head.js b/browser/components/reportbrokensite/test/browser/head.js
index 7cc1d51a21..84aa0f56dc 100644
--- a/browser/components/reportbrokensite/test/browser/head.js
+++ b/browser/components/reportbrokensite/test/browser/head.js
@@ -833,7 +833,7 @@ async function tabTo(match, win = window) {
return undefined;
}
-async function setupStrictETP(fn) {
+async function setupStrictETP() {
await UrlClassifierTestUtils.addTestTrackers();
registerCleanupFunction(() => {
UrlClassifierTestUtils.cleanupTestTrackers();
diff --git a/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js b/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js
index 13dcec8ea1..69443db930 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js
@@ -160,7 +160,7 @@ async function test_dynamical_window_rounding(aWindow, aCheckFunc) {
* check() functions use ok() while on Linux, we do not all ok() and instead
* rely on waitForCondition to fail).
*
- * The logging statements in this test, and RFPHelper.jsm, help narrow down and
+ * The logging statements in this test, and RFPHelper.sys.mjs, help narrow down and
* illustrate the issue.
*/
info(caseString + "We hit the weird resize bug. Resize it again.");
diff --git a/browser/components/resistfingerprinting/test/browser/browser_navigator.js b/browser/components/resistfingerprinting/test/browser/browser_navigator.js
index fb2c539194..2e7e76fdfb 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_navigator.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_navigator.js
@@ -339,7 +339,7 @@ async function testWorkerNavigator() {
// test in Fission.
if (SpecialPowers.useRemoteSubframes) {
await new Promise(resolve => {
- let observer = (subject, topic, data) => {
+ let observer = (subject, topic) => {
if (topic === "ipc:content-shutdown") {
Services.obs.removeObserver(observer, "ipc:content-shutdown");
resolve();
diff --git a/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js b/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js
index a8f9db9edc..c070c7485b 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js
@@ -2081,7 +2081,7 @@ async function testKeyEvent(aTab, aTestCase) {
// a custom event 'resultAvailable' for informing the script to check the
// result.
await new Promise(resolve => {
- function eventHandler(aEvent) {
+ function eventHandler() {
verifyKeyboardEvent(
JSON.parse(resElement.value),
result,
diff --git a/browser/components/resistfingerprinting/test/browser/browser_timezone.js b/browser/components/resistfingerprinting/test/browser/browser_timezone.js
index d2aefff01c..13deeb5b26 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_timezone.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_timezone.js
@@ -55,6 +55,15 @@ async function verifySpoofed() {
"The hours reports in UTC timezone."
);
is(date.getTimezoneOffset(), 0, "The difference with UTC timezone is 0.");
+
+ let parser = new DOMParser();
+ let doc = parser.parseFromString("<p></p>", "text/html");
+ let lastModified = new Date(
+ doc.lastModified.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2")
+ );
+ // Use ceil to account for the time passed to run the other statements
+ let offset = Math.ceil((lastModified - new Date()) / 1000);
+ is(offset, 0, "document.lastModified does not leak the timezone.");
}
// Run test in the context of the page.
diff --git a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html
index da86656bd4..758176691b 100644
--- a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html
+++ b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html
@@ -2,7 +2,7 @@
<head>
<meta charset="utf8">
<script>
-function waitForCondition(aCond, aCallback, aErrorMsg) {
+function waitForCondition(aCond, aCallback) {
var tries = 0;
var interval = setInterval(() => {
if (tries >= 30) {
diff --git a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html
index 234661a6a9..c32bd40610 100644
--- a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html
+++ b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html
@@ -5,7 +5,7 @@
<title></title>
<script src="shared_test_funcs.js"></script>
<script>
-async function runTheTest(iframe_domain, cross_origin_domain, extraData) {
+async function runTheTest(iframe_domain, cross_origin_domain) {
const iframes = document.querySelectorAll("iframe");
iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html`;
await waitForMessage("ready", `https://${iframe_domain}`);
diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html
index 23fd058c44..d8788edee9 100644
--- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html
+++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html
@@ -33,7 +33,7 @@ function createPopup() {
window.addEventListener("load", createPopup);
console.log("TKTK: Adding initial load");
-async function runTheTest(iframe_domain, cross_origin_domain, mode) {
+async function runTheTest(iframe_domain, cross_origin_domain) {
await new Promise(r => setTimeout(r, 2000));
console.log("TKTK: runTheTest() popup =", (popup === undefined ? "undefined" : "something"));
if (document.readyState !== 'complete') {
diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html
index ae08111e61..ea38234def 100644
--- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html
+++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html
@@ -3,7 +3,7 @@
<script src="shared_test_funcs.js"></script>
<script type="text/javascript">
var popup;
-async function runTheTest(iframe_domain, cross_origin_domain, mode) {
+async function runTheTest(iframe_domain, cross_origin_domain) {
let s = `<html><script>
console.log("TKTK: Loaded popup");
function give_result() {
diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html
index d03c514fc7..9c52f5774a 100644
--- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html
+++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html
@@ -5,7 +5,7 @@
<title></title>
<script src="shared_test_funcs.js"></script>
<script>
-async function runTheTest(iframe_domain, cross_origin_domain) {
+async function runTheTest(iframe_domain) {
// Set up the frame
const iframes = document.querySelectorAll("iframe");
iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframee.html`;
diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html
index 188d78ee6e..75ae15313b 100644
--- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html
+++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html
@@ -3,7 +3,7 @@
<script src="shared_test_funcs.js"></script>
<script type="text/javascript">
var popup;
-async function runTheTest(iframe_domain, cross_origin_domain, mode) {
+async function runTheTest(iframe_domain, cross_origin_domain) {
let s = `<!DOCTYPE html><html><script>
function give_result() {
return {
diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html
index 3de74bc9a3..b3eb2e6ad2 100644
--- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html
+++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html
@@ -5,7 +5,7 @@
<title></title>
<script src="shared_test_funcs.js"></script>
<script>
-async function runTheTest(iframe_domain, cross_origin_domain, mode) {
+async function runTheTest(iframe_domain, cross_origin_domain) {
var child_reference;
let url = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframee.html?mode=`
let params = new URLSearchParams(document.location.search);
diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html
index 8a4373c703..f4ea70e466 100644
--- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html
+++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html
@@ -3,7 +3,7 @@
<body>
<output id="result"></output>
<script type="text/javascript">
- window.addEventListener("load", function listener(event) {
+ window.addEventListener("load", function listener() {
parent.postMessage(["frame_ready"], "*");
});
window.addEventListener("message", function listener(event) {
diff --git a/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html b/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html
index 8e312d1d7b..350d05f6aa 100644
--- a/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html
+++ b/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html
@@ -52,7 +52,7 @@ window.addEventListener("message", async function listener(event) {
result.framee_crossOrigin_userAgentHTTPHeader = content;
});
- Promise.all([one, two]).then((values) => {
+ Promise.all([one, two]).then(() => {
parent.postMessage(result, "*")
});
}
diff --git a/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html
index 4d9c81ec8d..499d9d8194 100644
--- a/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html
+++ b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html
@@ -5,7 +5,7 @@
<title></title>
<script src="shared_test_funcs.js"></script>
<script>
-async function runTheTest(iframe_domain, cross_origin_domain, extraData) {
+async function runTheTest(iframe_domain, cross_origin_domain) {
const iframes = document.querySelectorAll("iframe");
iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframee.html`;
await waitForMessage("ready", `https://${iframe_domain}`);
diff --git a/browser/components/resistfingerprinting/test/browser/head.js b/browser/components/resistfingerprinting/test/browser/head.js
index 3c3f588960..8973839220 100644
--- a/browser/components/resistfingerprinting/test/browser/head.js
+++ b/browser/components/resistfingerprinting/test/browser/head.js
@@ -372,9 +372,7 @@ async function testWindowOpen(
aTargetWidth,
aTargetHeight,
aMaxAvailWidth,
- aMaxAvailHeight,
- aPopupChromeUIWidth,
- aPopupChromeUIHeight
+ aMaxAvailHeight
) {
// If the target size is greater than the maximum available content size,
// we set the target size to it.
@@ -687,7 +685,7 @@ async function runActualTest(uri, testFunction, expectedResults, extraData) {
let filterExtraData = function (x) {
let banned_keys = ["private_window", "etp_reload", "noopener", "await_uri"];
return Object.fromEntries(
- Object.entries(x).filter(([k, v]) => !banned_keys.includes(k))
+ Object.entries(x).filter(([k]) => !banned_keys.includes(k))
);
};
diff --git a/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html b/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html
index 95394ddb56..1c8828ee9a 100644
--- a/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html
+++ b/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html
@@ -30,7 +30,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1372069
function doTest_getCurrentPosition() {
navigator.geolocation.getCurrentPosition(
- (position) => {
+ () => {
ok(true, "Success callback is expected to be called");
doTest_watchPosition();
},
@@ -43,7 +43,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1372069
function doTest_watchPosition() {
let wid = navigator.geolocation.watchPosition(
- (position) => {
+ () => {
ok(true, "Success callback is expected to be called");
navigator.geolocation.clearWatch(wid);
SimpleTest.finish();
diff --git a/browser/components/safebrowsing/content/test/browser_whitelisted.js b/browser/components/safebrowsing/content/test/browser_whitelisted.js
index 92c42a5b52..eb217d618a 100644
--- a/browser/components/safebrowsing/content/test/browser_whitelisted.js
+++ b/browser/components/safebrowsing/content/test/browser_whitelisted.js
@@ -12,7 +12,7 @@ registerCleanupFunction(function () {
}
});
-function testBlockedPage(window) {
+function testBlockedPage() {
info("Non-whitelisted pages must be blocked");
ok(true, "about:blocked was shown");
}
diff --git a/browser/components/safebrowsing/content/test/head.js b/browser/components/safebrowsing/content/test/head.js
index 145833d010..ffbdb18d15 100644
--- a/browser/components/safebrowsing/content/test/head.js
+++ b/browser/components/safebrowsing/content/test/head.js
@@ -1,4 +1,4 @@
-// This url must sync with the table, url in SafeBrowsing.jsm addMozEntries
+// This url must sync with the table, url in SafeBrowsing.sys.mjs addMozEntries
const PHISH_TABLE = "moztest-phish-simple";
const PHISH_URL = "https://www.itisatrap.org/firefox/its-a-trap.html";
diff --git a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs
index bcb3199902..3718b6a4e0 100644
--- a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs
+++ b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs
@@ -37,6 +37,7 @@ import {
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs";
const STATES = {
CROSSHAIRS: "crosshairs",
@@ -49,7 +50,7 @@ const STATES = {
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
- return new Localization(["browser/screenshotsOverlay.ftl"], true);
+ return new Localization(["browser/screenshots.ftl"], true);
});
const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
@@ -79,13 +80,33 @@ export class ScreenshotsOverlay {
#methodsUsed;
get markup() {
- let [cancel, instructions, download, copy] =
- lazy.overlayLocalization.formatMessagesSync([
- { id: "screenshots-overlay-cancel-button" },
- { id: "screenshots-overlay-instructions" },
- { id: "screenshots-overlay-download-button" },
- { id: "screenshots-overlay-copy-button" },
- ]);
+ let accelString = ShortcutUtils.getModifierString("accel");
+ let copyShorcut = accelString + this.copyKey;
+ let downloadShortcut = accelString + this.downloadKey;
+
+ let [
+ cancelLabel,
+ cancelAttributes,
+ instructions,
+ downloadLabel,
+ downloadAttributes,
+ copyLabel,
+ copyAttributes,
+ ] = lazy.overlayLocalization.formatMessagesSync([
+ { id: "screenshots-cancel-button" },
+ { id: "screenshots-component-cancel-button" },
+ { id: "screenshots-instructions" },
+ { id: "screenshots-component-download-button-label" },
+ {
+ id: "screenshots-component-download-button",
+ args: { shortcut: downloadShortcut },
+ },
+ { id: "screenshots-component-copy-button-label" },
+ {
+ id: "screenshots-component-copy-button",
+ args: { shortcut: copyShorcut },
+ },
+ ]);
return `
<template>
@@ -98,7 +119,7 @@ export class ScreenshotsOverlay {
<div class="face"></div>
</div>
<div class="preview-instructions">${instructions.value}</div>
- <button class="screenshots-button ghost-button" id="screenshots-cancel-button">${cancel.value}</button>
+ <button class="screenshots-button ghost-button" id="screenshots-cancel-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}">${cancelLabel.value}</button>
</div>
<div id="hover-highlight" hidden></div>
<div id="selection-container" hidden>
@@ -138,9 +159,9 @@ export class ScreenshotsOverlay {
</div>
<div id="buttons-container" hidden>
<div class="buttons-wrapper">
- <button id="cancel" class="screenshots-button" title="${cancel.value}" aria-label="${cancel.value}" tabindex="0"><img/></button>
- <button id="copy" class="screenshots-button" title="${copy.value}" aria-label="${copy.value}" tabindex="0"><img/>${copy.value}</button>
- <button id="download" class="screenshots-button primary" title="${download.value}" aria-label="${download.value}" tabindex="0"><img/>${download.value}</button>
+ <button id="cancel" class="screenshots-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}"><img/></button>
+ <button id="copy" class="screenshots-button" title="${copyAttributes.attributes[0].value}" aria-label="${copyAttributes.attributes[1].value}"><img/><label>${copyLabel.value}</label></button>
+ <button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadLabel.value}</label></button>
</div>
</div>
</div>
@@ -180,6 +201,14 @@ export class ScreenshotsOverlay {
this.selectionRegion = new Region(this.windowDimensions);
this.hoverElementRegion = new Region(this.windowDimensions);
this.resetMethodsUsed();
+
+ let [downloadKey, copyKey] = lazy.overlayLocalization.formatMessagesSync([
+ { id: "screenshots-component-download-key" },
+ { id: "screenshots-component-copy-key" },
+ ]);
+
+ this.downloadKey = downloadKey.value;
+ this.copyKey = copyKey.value;
}
get content() {
@@ -204,6 +233,9 @@ export class ScreenshotsOverlay {
this.#content.root.appendChild(this.fragment);
this.initializeElements();
+ this.screenshotsContainer.dir = Services.locale.isAppLocaleRTL
+ ? "rtl"
+ : "ltr";
await this.updateWindowDimensions();
this.#setState(STATES.CROSSHAIRS);
@@ -290,10 +322,6 @@ export class ScreenshotsOverlay {
}
handleEvent(event) {
- if (event.button > 0) {
- return;
- }
-
switch (event.type) {
case "click":
this.handleClick(event);
@@ -316,21 +344,46 @@ export class ScreenshotsOverlay {
}
}
+ /**
+ * If the event came from the primary button, return false as we should not
+ * early return in the event handler function.
+ * If the event had another button, set to the crosshairs or selected state
+ * and return true to early return from the event handler function.
+ * @param {PointerEvent} event
+ * @returns true if the event button(s) was the non primary button
+ * false otherwise
+ */
+ preEventHandler(event) {
+ if (event.button > 0 || event.buttons > 1) {
+ switch (this.#state) {
+ case STATES.DRAGGING_READY:
+ this.#setState(STATES.CROSSHAIRS);
+ break;
+ case STATES.DRAGGING:
+ case STATES.RESIZING:
+ this.#setState(STATES.SELECTED);
+ break;
+ }
+ return true;
+ }
+ return false;
+ }
+
handleClick(event) {
+ if (this.preEventHandler(event)) {
+ return;
+ }
+
switch (event.originalTarget.id) {
case "screenshots-cancel-button":
case "cancel":
this.maybeCancelScreenshots();
break;
case "copy":
- this.#dispatchEvent("Screenshots:Copy", {
- region: this.selectionRegion.dimensions,
- });
+ this.copySelectedRegion();
break;
case "download":
- this.#dispatchEvent("Screenshots:Download", {
- region: this.selectionRegion.dimensions,
- });
+ this.downloadSelectedRegion();
break;
}
}
@@ -351,6 +404,16 @@ export class ScreenshotsOverlay {
* @param {Event} event The pointerown event
*/
handlePointerDown(event) {
+ // Early return if the event target is not within the screenshots component
+ // element.
+ if (!event.originalTarget.closest("#screenshots-component")) {
+ return;
+ }
+
+ if (this.preEventHandler(event)) {
+ return;
+ }
+
if (
event.originalTarget.id === "screenshots-cancel-button" ||
event.originalTarget.closest("#buttons-container") ===
@@ -379,6 +442,10 @@ export class ScreenshotsOverlay {
* @param {Event} event The pointermove event
*/
handlePointerMove(event) {
+ if (this.preEventHandler(event)) {
+ return;
+ }
+
const { pageX, pageY, clientX, clientY } =
this.getCoordinatesFromEvent(event);
@@ -450,6 +517,18 @@ export class ScreenshotsOverlay {
case "Escape":
this.maybeCancelScreenshots();
break;
+ case this.copyKey.toLowerCase():
+ if (this.state === "selected" && this.getAccelKey(event)) {
+ event.preventDefault();
+ this.copySelectedRegion();
+ }
+ break;
+ case this.downloadKey.toLowerCase():
+ if (this.state === "selected" && this.getAccelKey(event)) {
+ event.preventDefault();
+ this.downloadSelectedRegion();
+ }
+ break;
}
}
@@ -780,9 +859,9 @@ export class ScreenshotsOverlay {
*/
setFocusToActionButton() {
if (lazy.SCREENSHOTS_LAST_SAVED_METHOD === "copy") {
- this.copyButton.focus({ focusVisible: true });
+ this.copyButton.focus({ focusVisible: true, preventScroll: true });
} else {
- this.downloadButton.focus({ focusVisible: true });
+ this.downloadButton.focus({ focusVisible: true, preventScroll: true });
}
}
@@ -868,6 +947,18 @@ export class ScreenshotsOverlay {
}
}
+ copySelectedRegion() {
+ this.#dispatchEvent("Screenshots:Copy", {
+ region: this.selectionRegion.dimensions,
+ });
+ }
+
+ downloadSelectedRegion() {
+ this.#dispatchEvent("Screenshots:Download", {
+ region: this.selectionRegion.dimensions,
+ });
+ }
+
/**
* Hide hover element, selection and buttons containers.
* Show the preview container and the panel.
@@ -1285,17 +1376,21 @@ export class ScreenshotsOverlay {
this.updateSelectionSizeText();
}
+ /**
+ * Update the size of the selected region. Use the zoom to correctly display
+ * the region dimensions.
+ */
updateSelectionSizeText() {
- let dpr = this.windowDimensions.devicePixelRatio;
let { width, height } = this.selectionRegion.dimensions;
+ let zoom = Math.round(this.window.browsingContext.fullZoom * 100) / 100;
let [selectionSizeTranslation] =
lazy.overlayLocalization.formatMessagesSync([
{
- id: "screenshots-overlay-selection-region-size",
+ id: "screenshots-overlay-selection-region-size-2",
args: {
- width: Math.floor(width * dpr),
- height: Math.floor(height * dpr),
+ width: Math.floor(width * zoom),
+ height: Math.floor(height * zoom),
},
},
]);
diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
index fc84facee3..9df74a4359 100644
--- a/browser/components/screenshots/ScreenshotsUtils.sys.mjs
+++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
@@ -817,8 +817,9 @@ export var ScreenshotsUtils = {
let dialog = await this.openPreviewDialog(browser);
await dialog._dialogReady;
- let screenshotsUI =
- dialog._frame.contentDocument.createElement("screenshots-ui");
+ let screenshotsUI = dialog._frame.contentDocument.createElement(
+ "screenshots-preview"
+ );
dialog._frame.contentDocument.body.appendChild(screenshotsUI);
screenshotsUI.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);
diff --git a/browser/components/screenshots/content/screenshots.css b/browser/components/screenshots/content/screenshots.css
index 506f3658c9..b155c294f8 100644
--- a/browser/components/screenshots/content/screenshots.css
+++ b/browser/components/screenshots/content/screenshots.css
@@ -30,13 +30,12 @@ body {
display: flex;
align-items: center;
justify-content: center;
+ gap: var(--space-xsmall);
cursor: pointer;
text-align: center;
user-select: none;
white-space: nowrap;
- min-height: 36px;
- font-size: 15px;
- min-width: 36px;
+ min-width: 32px;
}
.preview-button > img {
@@ -44,11 +43,23 @@ body {
fill: currentColor;
width: 16px;
height: 16px;
+ pointer-events: none;
+}
+
+#retry > img {
+ content: url("chrome://global/skin/icons/reload.svg");
+}
+
+#cancel > img {
+ content: url("chrome://global/skin/icons/close.svg");
}
-#download > img,
#copy > img {
- margin-inline-end: 5px;
+ content: url("chrome://global/skin/icons/edit-copy.svg");
+}
+
+#download > img {
+ content: url("chrome://browser/skin/downloads/downloads.svg");
}
.preview-image {
diff --git a/browser/components/screenshots/content/screenshots.html b/browser/components/screenshots/content/screenshots.html
index 88c71fb4fe..fea032700c 100644
--- a/browser/components/screenshots/content/screenshots.html
+++ b/browser/components/screenshots/content/screenshots.html
@@ -31,32 +31,34 @@
<button
id="retry"
class="preview-button"
- data-l10n-id="screenshots-retry-button-title"
+ data-l10n-id="screenshots-component-retry-button"
>
- <img src="chrome://global/skin/icons/reload.svg" />
+ <img />
</button>
<button
id="cancel"
class="preview-button"
- data-l10n-id="screenshots-cancel-button-title"
+ data-l10n-id="screenshots-component-cancel-button"
>
- <img src="chrome://global/skin/icons/close.svg" />
+ <img />
</button>
<button
id="copy"
class="preview-button"
- data-l10n-id="screenshots-copy-button-title"
+ data-l10n-id="screenshots-component-copy-button"
>
- <img src="chrome://global/skin/icons/edit-copy.svg" />
- <span data-l10n-id="screenshots-copy-button" />
+ <img /><label
+ data-l10n-id="screenshots-component-copy-button-label"
+ ></label>
</button>
<button
id="download"
class="preview-button primary"
- data-l10n-id="screenshots-download-button-title"
+ data-l10n-id="screenshots-component-download-button"
>
- <img src="chrome://browser/skin/downloads/downloads.svg" />
- <span data-l10n-id="screenshots-download-button" />
+ <img /><label
+ data-l10n-id="screenshots-component-download-button-label"
+ ></label>
</button>
</div>
<div class="preview-image">
diff --git a/browser/components/screenshots/content/screenshots.js b/browser/components/screenshots/content/screenshots.js
index 9e47570e07..8159206d18 100644
--- a/browser/components/screenshots/content/screenshots.js
+++ b/browser/components/screenshots/content/screenshots.js
@@ -6,15 +6,35 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
+ ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
});
-class ScreenshotsUI extends HTMLElement {
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => {
+ return new Localization(["browser/screenshots.ftl"], true);
+});
+
+class ScreenshotsPreview extends HTMLElement {
constructor() {
super();
// we get passed the <browser> as a param via TabDialogBox.open()
this.openerBrowser = window.arguments[0];
+
+ window.ensureCustomElements("moz-button");
+
+ let [downloadKey, copyKey] =
+ lazy.screenshotsLocalization.formatMessagesSync([
+ { id: "screenshots-component-download-key" },
+ { id: "screenshots-component-copy-key" },
+ ]);
+
+ this.downloadKey = downloadKey.value;
+ this.copyKey = copyKey.value;
}
+
async connectedCallback() {
this.initialize();
}
@@ -38,6 +58,29 @@ class ScreenshotsUI extends HTMLElement {
this._copyButton.addEventListener("click", this);
this._downloadButton = this.querySelector("#download");
this._downloadButton.addEventListener("click", this);
+
+ let accelString = ShortcutUtils.getModifierString("accel");
+ let copyShorcut = accelString + this.copyKey;
+ let downloadShortcut = accelString + this.downloadKey;
+
+ document.l10n.setAttributes(
+ this._cancelButton,
+ "screenshots-component-cancel-button"
+ );
+
+ document.l10n.setAttributes(
+ this._copyButton,
+ "screenshots-component-copy-button",
+ { shortcut: copyShorcut }
+ );
+
+ document.l10n.setAttributes(
+ this._downloadButton,
+ "screenshots-component-download-button",
+ { shortcut: downloadShortcut }
+ );
+
+ window.addEventListener("keydown", this, true);
}
close() {
@@ -45,31 +88,68 @@ class ScreenshotsUI extends HTMLElement {
window.close();
}
- async handleEvent(event) {
- if (event.type == "click" && event.currentTarget == this._cancelButton) {
- this.close();
- ScreenshotsUtils.recordTelemetryEvent("canceled", "preview_cancel", {});
- } else if (
- event.type == "click" &&
- event.currentTarget == this._copyButton
- ) {
- this.saveToClipboard(
- this.ownerDocument.getElementById("placeholder-image").src
- );
- } else if (
- event.type == "click" &&
- event.currentTarget == this._downloadButton
- ) {
- await this.saveToFile(
- this.ownerDocument.getElementById("placeholder-image").src
- );
- } else if (
- event.type == "click" &&
- event.currentTarget == this._retryButton
- ) {
- ScreenshotsUtils.scheduleRetry(this.openerBrowser, "preview_retry");
- this.close();
+ handleEvent(event) {
+ switch (event.type) {
+ case "click":
+ this.handleClick(event);
+ break;
+ case "keydown":
+ this.handleKeydown(event);
+ break;
+ }
+ }
+
+ handleClick(event) {
+ switch (event.target.id) {
+ case "retry":
+ ScreenshotsUtils.scheduleRetry(this.openerBrowser, "preview_retry");
+ this.close();
+ break;
+ case "cancel":
+ this.close();
+ ScreenshotsUtils.recordTelemetryEvent("canceled", "preview_cancel", {});
+ break;
+ case "copy":
+ this.saveToClipboard(
+ this.ownerDocument.getElementById("placeholder-image").src
+ );
+ break;
+ case "download":
+ this.saveToFile(
+ this.ownerDocument.getElementById("placeholder-image").src
+ );
+ break;
+ }
+ }
+
+ handleKeydown(event) {
+ switch (event.key) {
+ case this.copyKey.toLowerCase():
+ if (this.getAccelKey(event)) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.saveToClipboard(
+ this.ownerDocument.getElementById("placeholder-image").src
+ );
+ }
+ break;
+ case this.downloadKey.toLowerCase():
+ if (this.getAccelKey(event)) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.saveToFile(
+ this.ownerDocument.getElementById("placeholder-image").src
+ );
+ }
+ break;
+ }
+ }
+
+ getAccelKey(event) {
+ if (AppConstants.platform === "macosx") {
+ return event.metaKey;
}
+ return event.ctrlKey;
}
async saveToFile(dataUrl) {
@@ -102,4 +182,4 @@ class ScreenshotsUI extends HTMLElement {
}
}
}
-customElements.define("screenshots-ui", ScreenshotsUI);
+customElements.define("screenshots-preview", ScreenshotsPreview);
diff --git a/browser/components/screenshots/overlay/overlay.css b/browser/components/screenshots/overlay/overlay.css
index 6eeda8b44c..b042f0b0c2 100644
--- a/browser/components/screenshots/overlay/overlay.css
+++ b/browser/components/screenshots/overlay/overlay.css
@@ -6,6 +6,12 @@
:host {
display: contents;
+
+ /* These z-indexes are used to correctly layer elements in the screenshots overlay */
+ --screenshots-lowest-layer: 1;
+ --screenshots-low-layer: 2;
+ --screenshots-high-layer: 3;
+ --screenshots-highest-layer: 4;
}
[hidden] {
@@ -57,6 +63,7 @@
position: absolute;
margin: 10px 0;
cursor: auto;
+ z-index: var(--screenshots-highest-layer);
}
#selection-size,
@@ -77,11 +84,13 @@
.screenshots-button {
display: inline-flex;
align-items: center;
+ justify-content: center;
+ gap: var(--space-xsmall);
cursor: pointer;
text-align: center;
user-select: none;
white-space: nowrap;
- z-index: 6;
+ z-index: var(--screenshots-highest-layer);
min-width: 32px;
margin-inline: 4px;
}
@@ -90,6 +99,7 @@
width: 100%;
height: 100%;
pointer-events: none;
+ z-index: var(--screenshots-lowest-layer);
}
#screenshots-cancel-button {
@@ -123,6 +133,10 @@
pointer-events: none;
}
+.screenshots-button > label {
+ pointer-events: none;
+}
+
#cancel > img {
content: url("chrome://global/skin/icons/close.svg");
}
@@ -135,15 +149,14 @@
content: url("chrome://browser/skin/downloads/downloads.svg");
}
-#download > img,
-#copy > img {
- margin-inline-end: 5px;
-}
-
.face-container {
position: relative;
width: 64px;
height: 64px;
+
+ @media (prefers-contrast) {
+ display: none;
+ }
}
.face {
@@ -172,7 +185,7 @@
border-radius: 50%;
inset-inline-start: 2px;
top: 4px;
- z-index: 10;
+ z-index: var(--screenshots-high-layer);
}
.left {
@@ -209,7 +222,7 @@
box-sizing: border-box;
pointer-events: none;
position: absolute;
- z-index: 11;
+ z-index: var(--screenshots-high-layer);
}
#top-background {
@@ -242,7 +255,7 @@
cursor: move;
position: absolute;
pointer-events: auto;
- z-index: 2;
+ z-index: var(--screenshots-lowest-layer);
outline-offset: 8px;
}
@@ -251,7 +264,7 @@
align-items: center;
justify-content: center;
position: absolute;
- z-index: 5;
+ z-index: var(--screenshots-high-layer);
pointer-events: auto;
outline-offset: -15px;
}
@@ -270,7 +283,7 @@
inset-inline-start: 0;
top: -30px;
width: 100%;
- z-index: 4;
+ z-index: var(--screenshots-low-layer);
}
.mover-target.direction-topRight {
@@ -287,7 +300,7 @@
left: -30px;
top: 0;
width: 60px;
- z-index: 4;
+ z-index: var(--screenshots-low-layer);
}
.mover-target.direction-right {
@@ -296,7 +309,7 @@
right: -30px;
top: 0;
width: 60px;
- z-index: 4;
+ z-index: var(--screenshots-low-layer);
}
.mover-target.direction-bottomLeft {
@@ -313,7 +326,7 @@
height: 60px;
inset-inline-start: 0;
width: 100%;
- z-index: 4;
+ z-index: var(--screenshots-low-layer);
}
.mover-target.direction-bottomRight {
diff --git a/browser/components/screenshots/screenshots-buttons.css b/browser/components/screenshots/screenshots-buttons.css
index 82b075bccb..b63308d8b4 100644
--- a/browser/components/screenshots/screenshots-buttons.css
+++ b/browser/components/screenshots/screenshots-buttons.css
@@ -25,7 +25,8 @@
.full-page, .visible-page {
-moz-context-properties: fill, stroke;
fill: currentColor;
- stroke: var(--color-accent-primary);
+ /* stroke is the secondary fill color used to define the viewport shape in the SVGs */
+ stroke: var(--color-gray-60);
background-position: center top;
background-repeat: no-repeat;
background-size: 46px 46px;
diff --git a/browser/components/screenshots/screenshots-buttons.js b/browser/components/screenshots/screenshots-buttons.js
index 864505ae2f..9ac8dab2cf 100644
--- a/browser/components/screenshots/screenshots-buttons.js
+++ b/browser/components/screenshots/screenshots-buttons.js
@@ -13,28 +13,40 @@
});
class ScreenshotsButtons extends MozXULElement {
+ static #template = null;
+
static get markup() {
return `
- <html:link rel="stylesheet" href="chrome://global/skin/global.css"/>
- <html:link rel="stylesheet" href="chrome://browser/content/screenshots/screenshots-buttons.css"/>
- <html:button class="visible-page footer-button" data-l10n-id="screenshots-save-visible-button"></html:button>
- <html:button class="full-page footer-button" data-l10n-id="screenshots-save-page-button"></html:button>
+ <html:link rel="stylesheet" href="chrome://global/skin/global.css" />
+ <html:link rel="stylesheet" href="chrome://browser/content/screenshots/screenshots-buttons.css" />
+ <html:moz-button-group>
+ <html:button class="visible-page footer-button" data-l10n-id="screenshots-save-visible-button"></html:button>
+ <html:button class="full-page footer-button primary" data-l10n-id="screenshots-save-page-button"></html:button>
+ </html:moz-button-group>
`;
}
+ static get fragment() {
+ if (!ScreenshotsButtons.#template) {
+ ScreenshotsButtons.#template = MozXULElement.parseXULToFragment(
+ ScreenshotsButtons.markup
+ );
+ }
+ return ScreenshotsButtons.#template;
+ }
+
connectedCallback() {
const shadowRoot = this.attachShadow({ mode: "open" });
document.l10n.connectRoot(shadowRoot);
- let fragment = MozXULElement.parseXULToFragment(this.constructor.markup);
- this.shadowRoot.append(fragment);
+ this.shadowRoot.append(ScreenshotsButtons.fragment);
- let visibleButton = shadowRoot.querySelector(".visible-page");
+ let visibleButton = this.shadowRoot.querySelector(".visible-page");
visibleButton.onclick = function () {
ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "visible");
};
- let fullpageButton = shadowRoot.querySelector(".full-page");
+ let fullpageButton = this.shadowRoot.querySelector(".full-page");
fullpageButton.onclick = function () {
ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "full_page");
};
@@ -49,7 +61,8 @@
* This will default to the visible page button.
* @param {String} buttonToFocus
*/
- focusButton(buttonToFocus) {
+ async focusButton(buttonToFocus) {
+ await this.shadowRoot.querySelector("moz-button-group").updateComplete;
if (buttonToFocus === "fullpage") {
this.shadowRoot
.querySelector(".full-page")
diff --git a/browser/components/screenshots/tests/browser/browser.toml b/browser/components/screenshots/tests/browser/browser.toml
index b27d28c677..97e7474fa3 100644
--- a/browser/components/screenshots/tests/browser/browser.toml
+++ b/browser/components/screenshots/tests/browser/browser.toml
@@ -18,6 +18,8 @@ prefs = [
["browser_iframe_test.js"]
skip-if = ["os == 'linux'"]
+["browser_keyboard_shortcuts.js"]
+
["browser_overlay_keyboard_test.js"]
["browser_screenshots_drag_scroll_test.js"]
@@ -63,3 +65,5 @@ skip-if = ["!crashreporter"]
["browser_test_moving_tab_to_new_window.js"]
["browser_test_resize.js"]
+
+["browser_test_selection_size_text.js"]
diff --git a/browser/components/screenshots/tests/browser/browser_iframe_test.js b/browser/components/screenshots/tests/browser/browser_iframe_test.js
index bb853fbe28..24f7a71dca 100644
--- a/browser/components/screenshots/tests/browser/browser_iframe_test.js
+++ b/browser/components/screenshots/tests/browser/browser_iframe_test.js
@@ -90,7 +90,7 @@ add_task(async function test_selectingElementsInIframes() {
await helper.waitForHoverElementRect(el.width, el.height);
mouse.click(x, y);
- await helper.waitForStateChange("selected");
+ await helper.waitForStateChange(["selected"]);
let dimensions = await helper.getSelectionRegionDimensions();
@@ -116,7 +116,7 @@ add_task(async function test_selectingElementsInIframes() {
);
mouse.click(500, 500);
- await helper.waitForStateChange("crosshairs");
+ await helper.waitForStateChange(["crosshairs"]);
}
}
);
diff --git a/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js b/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js
new file mode 100644
index 0000000000..bca96f333f
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_download_shortcut() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.useDownloadDir", true]],
+ });
+
+ let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+ // First ensure we catch the download finishing.
+ let downloadFinishedPromise = new Promise(resolve => {
+ publicDownloads.addView({
+ onDownloadChanged(download) {
+ info("Download changed!");
+ if (download.succeeded || download.error) {
+ info("Download succeeded or errored");
+ publicDownloads.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ EventUtils.synthesizeKey("s", { accelKey: true }, content);
+ });
+
+ info("wait for download to finish");
+ let download = await downloadFinishedPromise;
+
+ ok(download.succeeded, "Download should succeed");
+
+ await publicDownloads.removeFinished();
+ await screenshotExit;
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ let visibleButton = await helper.getPanelButton(".visible-page");
+ visibleButton.click();
+
+ await screenshotReady;
+
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+
+ EventUtils.synthesizeKey("s", { accelKey: true });
+
+ info("wait for download to finish");
+ download = await downloadFinishedPromise;
+
+ ok(download.succeeded, "Download should succeed");
+
+ await publicDownloads.removeFinished();
+ await screenshotExit;
+ }
+ );
+});
+
+add_task(async function test_copy_shortcut() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ let clipboardChanged = helper.waitForRawClipboardChange(490, 490);
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ EventUtils.synthesizeKey("c", { accelKey: true }, content);
+ });
+
+ await clipboardChanged;
+ await screenshotExit;
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ let visibleButton = await helper.getPanelButton(".visible-page");
+ visibleButton.click();
+
+ await screenshotReady;
+
+ clipboardChanged = helper.waitForRawClipboardChange(
+ contentInfo.clientWidth,
+ contentInfo.clientHeight
+ );
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+
+ EventUtils.synthesizeKey("c", { accelKey: true });
+
+ await clipboardChanged;
+ await screenshotExit;
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js
index 757d721268..86940a5203 100644
--- a/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js
@@ -353,8 +353,6 @@ add_task(async function test_scrollIfByEdge() {
await helper.scrollContentWindow(windowX, windowY);
- await TestUtils.waitForTick();
-
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
@@ -363,17 +361,18 @@ add_task(async function test_scrollIfByEdge() {
is(scrollX, windowX, "Window x position is 1000");
is(scrollY, windowY, "Window y position is 1000");
- let startX = 1100;
- let startY = 1100;
+ let startX = 1200;
+ let startY = 1200;
let endX = 1010;
let endY = 1010;
- // The window won't scroll if the state is draggingReady so we move to
- // get into the dragging state and then move again to scroll the window
- mouse.down(startX, startY);
- await helper.assertStateChange("draggingReady");
- mouse.move(1050, 1050);
- await helper.assertStateChange("dragging");
+ await helper.dragOverlay(startX, startY, endX + 20, endY + 20);
+ await helper.scrollContentWindow(windowX, windowY);
+
+ await TestUtils.waitForTick();
+
+ mouse.down(endX + 20, endY + 20);
+ await helper.assertStateChange("resizing");
mouse.move(endX, endY);
mouse.up(endX, endY);
await helper.assertStateChange("selected");
@@ -387,26 +386,64 @@ add_task(async function test_scrollIfByEdge() {
is(scrollX, windowX, "Window x position is 990");
is(scrollY, windowY, "Window y position is 990");
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ helper.triggerUIFromToolbar();
+ await screenshotExit;
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let windowX = 1000;
+ let windowY = 1000;
+
+ await helper.scrollContentWindow(windowX, windowY);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
let contentInfo = await helper.getContentDimensions();
+ let { scrollX, scrollY, clientWidth, clientHeight } = contentInfo;
+
+ let startX = windowX + clientWidth - 200;
+ let startY = windowX + clientHeight - 200;
+ let endX = windowX + clientWidth - 10;
+ let endY = windowY + clientHeight - 10;
- endX = windowX + contentInfo.clientWidth - 10;
- endY = windowY + contentInfo.clientHeight - 10;
+ await helper.dragOverlay(startX, startY, endX - 20, endY - 20);
+ await helper.scrollContentWindow(windowX, windowY);
+
+ await TestUtils.waitForTick();
info(
`starting to drag overlay to ${endX}, ${endY} in test\nclientInfo: ${JSON.stringify(
contentInfo
)}\n`
);
- await helper.dragOverlay(startX, startY, endX, endY, "selected");
+ mouse.down(endX - 20, endY - 20);
+ await helper.assertStateChange("resizing");
+ mouse.move(endX, endY);
+ mouse.up(endX, endY);
+ await helper.assertStateChange("selected");
- windowX = 1000;
- windowY = 1000;
+ windowX = 1010;
+ windowY = 1010;
await helper.waitForScrollTo(windowX, windowY);
({ scrollX, scrollY } = await helper.getContentDimensions());
- is(scrollX, windowX, "Window x position is 1000");
- is(scrollY, windowY, "Window y position is 1000");
+ is(scrollX, windowX, "Window x position is 1010");
+ is(scrollY, windowY, "Window y position is 1010");
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ helper.triggerUIFromToolbar();
+ await screenshotExit;
}
);
});
@@ -428,13 +465,19 @@ add_task(async function test_scrollIfByEdgeWithKeyboard() {
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
- let { scrollX, scrollY, clientWidth, clientHeight } =
- await helper.getContentDimensions();
+ let { scrollX, scrollY } = await helper.getContentDimensions();
is(scrollX, windowX, "Window x position is 1000");
is(scrollY, windowY, "Window y position is 1000");
- await helper.dragOverlay(1020, 1020, 1120, 1120);
+ await helper.dragOverlay(
+ scrollX + 20,
+ scrollY + 20,
+ scrollX + 120,
+ scrollY + 120
+ );
+
+ await helper.scrollContentWindow(windowX, windowY);
await helper.moveOverlayViaKeyboard("highlight", [
{ key: "ArrowLeft", options: { shiftKey: true } },
@@ -447,14 +490,36 @@ add_task(async function test_scrollIfByEdgeWithKeyboard() {
windowY = 989;
await helper.waitForScrollTo(windowX, windowY);
- ({ scrollX, scrollY, clientWidth, clientHeight } =
- await helper.getContentDimensions());
+ ({ scrollX, scrollY } = await helper.getContentDimensions());
is(scrollX, windowX, "Window x position is 989");
is(scrollY, windowY, "Window y position is 989");
- mouse.click(1200, 1200);
- await helper.assertStateChange("crosshairs");
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ helper.triggerUIFromToolbar();
+ await screenshotExit;
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let windowX = 989;
+ let windowY = 989;
+
+ await helper.scrollContentWindow(windowX, windowY);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let { scrollX, scrollY, clientWidth, clientHeight } =
+ await helper.getContentDimensions();
+
await helper.dragOverlay(
scrollX + clientWidth - 100 - 20,
scrollY + clientHeight - 100 - 20,
@@ -477,6 +542,10 @@ add_task(async function test_scrollIfByEdgeWithKeyboard() {
is(scrollX, windowX, "Window x position is 1000");
is(scrollY, windowY, "Window y position is 1000");
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ helper.triggerUIFromToolbar();
+ await screenshotExit;
}
);
});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js
index 605e0ae75c..3cef2dbd72 100644
--- a/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js
@@ -442,7 +442,7 @@ add_task(async function resizeAllCorners() {
/**
* This function tests clicking the overlay with the different mouse buttons
*/
-add_task(async function test_otherMouseButtons() {
+add_task(async function test_clickingOtherMouseButtons() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
@@ -478,6 +478,7 @@ add_task(async function test_otherMouseButtons() {
mouse.down(10, 10, { button: 2 });
mouse.move(100, 100, { button: 2 });
+
mouse.up(100, 100, { button: 2 });
await TestUtils.waitForTick();
@@ -486,3 +487,66 @@ add_task(async function test_otherMouseButtons() {
}
);
});
+
+/**
+ * This function tests dragging the overlay with the different mouse buttons
+ */
+add_task(async function test_draggingOtherMouseButtons() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ // Click with button 1 in dragging state
+ mouse.down(100, 100);
+ await helper.assertStateChange("draggingReady");
+ mouse.move(200, 200);
+ await helper.assertStateChange("dragging");
+ mouse.click(200, 200, { button: 1 });
+ await helper.assertStateChange("selected");
+
+ // Reset
+ mouse.click(10, 10);
+ await helper.assertStateChange("crosshairs");
+
+ // Mouse down with button 2 in draggingReady state
+ mouse.down(100, 100);
+ await helper.assertStateChange("draggingReady");
+ mouse.down(200, 200, { button: 2 });
+ await helper.assertStateChange("crosshairs");
+
+ await helper.dragOverlay(100, 100, 200, 200);
+
+ // Click with button 1 in resizing state
+ mouse.down(200, 200);
+ await helper.assertStateChange("resizing");
+ mouse.click(200, 200, { button: 1 });
+
+ // Reset
+ mouse.click(10, 10);
+ await helper.assertStateChange("crosshairs");
+
+ await helper.dragOverlay(100, 100, 200, 200);
+
+ // Mouse down with button 2 in dragging state
+ mouse.down(200, 200);
+ await helper.assertStateChange("resizing");
+ mouse.down(200, 200, { button: 2 });
+
+ // Reset
+ mouse.click(10, 10);
+ await helper.assertStateChange("crosshairs");
+
+ // Mouse move with button 2 in draggingReady state
+ mouse.down(100, 100);
+ await helper.assertStateChange("draggingReady");
+ mouse.move(100, 100, { button: 2 });
+ await helper.assertStateChange("crosshairs");
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js
index ad262a7e67..021a37b5c9 100644
--- a/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js
@@ -31,7 +31,7 @@ add_task(async function test_toggling_screenshots_pref() {
.callsFake(observerSpy);
let notifierStub = sinon
.stub(ScreenshotsUtils, "notify")
- .callsFake(function (window, type) {
+ .callsFake(function () {
notifierSpy();
ScreenshotsUtils.notify.wrappedMethod.apply(this, arguments);
});
diff --git a/browser/components/screenshots/tests/browser/browser_test_element_picker.js b/browser/components/screenshots/tests/browser/browser_test_element_picker.js
index 17ed2a0190..3e2069134e 100644
--- a/browser/components/screenshots/tests/browser/browser_test_element_picker.js
+++ b/browser/components/screenshots/tests/browser/browser_test_element_picker.js
@@ -43,14 +43,14 @@ add_task(async function test_element_picker() {
);
mouse.click(10, 10);
- await helper.waitForStateChange("crosshairs");
+ await helper.waitForStateChange(["crosshairs"]);
let hoverElementRegionValid = await helper.isHoverElementRegionValid();
ok(!hoverElementRegionValid, "Hover element rect is null");
mouse.click(10, 10);
- await helper.waitForStateChange("crosshairs");
+ await helper.waitForStateChange(["crosshairs"]);
}
);
});
diff --git a/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js b/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js
new file mode 100644
index 0000000000..38d1acbea9
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_selectionSizeTest() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ const dpr = browser.ownerGlobal.devicePixelRatio;
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+ await helper.dragOverlay(100, 100, 500, 500);
+
+ let actualText = await helper.getOverlaySelectionSizeText();
+
+ Assert.equal(
+ actualText,
+ `${400 * dpr} x ${400 * dpr}`,
+ "The selection size text is the same"
+ );
+ }
+ );
+});
+
+add_task(async function test_selectionSizeTestAt1Point5Zoom() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ const zoom = 1.5;
+ const dpr = browser.ownerGlobal.devicePixelRatio;
+ let helper = new ScreenshotsHelper(browser);
+ helper.zoomBrowser(zoom);
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+ await helper.dragOverlay(100, 100, 500, 500);
+
+ let actualText = await helper.getOverlaySelectionSizeText();
+
+ Assert.equal(
+ actualText,
+ `${400 * dpr * zoom} x ${400 * dpr * zoom}`,
+ "The selection size text is the same"
+ );
+ }
+ );
+});
+
+add_task(async function test_selectionSizeTestAtPoint5Zoom() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ const zoom = 0.5;
+ const dpr = browser.ownerGlobal.devicePixelRatio;
+ let helper = new ScreenshotsHelper(browser);
+ helper.zoomBrowser(zoom);
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+ await helper.dragOverlay(100, 100, 500, 500);
+
+ let actualText = await helper.getOverlaySelectionSizeText();
+
+ Assert.equal(
+ actualText,
+ `${400 * dpr * zoom} x ${400 * dpr * zoom}`,
+ "The selection size text is the same"
+ );
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/head.js b/browser/components/screenshots/tests/browser/head.js
index a36e955830..762da5f866 100644
--- a/browser/components/screenshots/tests/browser/head.js
+++ b/browser/components/screenshots/tests/browser/head.js
@@ -159,23 +159,23 @@ class ScreenshotsHelper {
});
}
- waitForStateChange(newState) {
- return SpecialPowers.spawn(this.browser, [newState], async state => {
+ waitForStateChange(newStateArr) {
+ return SpecialPowers.spawn(this.browser, [newStateArr], async stateArr => {
let screenshotsChild = content.windowGlobalChild.getActor(
"ScreenshotsComponent"
);
await ContentTaskUtils.waitForCondition(() => {
- info(`got ${screenshotsChild.overlay.state}. expected ${state}`);
- return screenshotsChild.overlay.state === state;
- }, `Wait for overlay state to be ${state}`);
+ info(`got ${screenshotsChild.overlay.state}. expected ${stateArr}`);
+ return stateArr.includes(screenshotsChild.overlay.state);
+ }, `Wait for overlay state to be ${stateArr}`);
return screenshotsChild.overlay.state;
});
}
async assertStateChange(newState) {
- let currentState = await this.waitForStateChange(newState);
+ let currentState = await this.waitForStateChange([newState]);
is(
currentState,
@@ -269,18 +269,13 @@ class ScreenshotsHelper {
mouse.down(startX, startY);
- await Promise.any([
- this.waitForStateChange("draggingReady"),
- this.waitForStateChange("resizing"),
- ]);
+ await this.waitForStateChange(["draggingReady", "resizing"]);
Assert.ok(true, "The overlay is in the draggingReady or resizing state");
mouse.move(endX, endY);
- await Promise.any([
- this.waitForStateChange("dragging"),
- this.waitForStateChange("resizing"),
- ]);
+ await this.waitForStateChange(["dragging", "resizing"]);
+
Assert.ok(true, "The overlay is in the dragging or resizing state");
// We intentionally turn off this a11y check, because the following mouse
// event is emitted at the end of the dragging event. Its keyboard
@@ -324,7 +319,6 @@ class ScreenshotsHelper {
overlay.topRightMover.focus({ focusVisible: true });
break;
}
- screenshotsChild.overlay.highlightEl.focus();
for (let event of eventsArr) {
EventUtils.synthesizeKey(
@@ -354,7 +348,6 @@ class ScreenshotsHelper {
}
async scrollContentWindow(x, y) {
- let promise = BrowserTestUtils.waitForContentEvent(this.browser, "scroll");
let contentDims = await this.getContentDimensions();
await ContentTask.spawn(
this.browser,
@@ -404,7 +397,6 @@ class ScreenshotsHelper {
}, `Waiting for window to scroll to ${xPos}, ${yPos}`);
}
);
- await promise;
}
async waitForScrollTo(x, y) {
@@ -521,6 +513,15 @@ class ScreenshotsHelper {
});
}
+ getOverlaySelectionSizeText(elementId = "testPageElement") {
+ return ContentTask.spawn(this.browser, [elementId], async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ return screenshotsChild.overlay.selectionSize.textContent;
+ });
+ }
+
async clickTestPageElement(elementId = "testPageElement") {
let rect = await this.getTestPageElementRect(elementId);
let dims = await this.getContentDimensions();
@@ -909,6 +910,21 @@ add_setup(async () => {
);
let screenshotBtn = document.getElementById("screenshot-button");
Assert.ok(screenshotBtn, "The screenshots button was added to the nav bar");
+
+ registerCleanupFunction(async () => {
+ info(`downloads panel should be visible: ${DownloadsPanel.isPanelShowing}`);
+ if (DownloadsPanel.isPanelShowing) {
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+ info(
+ `downloads panel should not be visible: ${DownloadsPanel.isPanelShowing}`
+ );
+ }
+ });
});
function getContentDevicePixelRatio(browser) {
diff --git a/browser/components/search/.eslintrc.js b/browser/components/search/.eslintrc.js
index 39079432e7..7224dc6eb7 100644
--- a/browser/components/search/.eslintrc.js
+++ b/browser/components/search/.eslintrc.js
@@ -5,8 +5,6 @@
"use strict";
module.exports = {
- extends: ["plugin:mozilla/require-jsdoc"],
-
rules: {
"mozilla/var-only-at-top-level": "error",
},
diff --git a/browser/components/search/DomainToCategoriesMap.worker.mjs b/browser/components/search/DomainToCategoriesMap.worker.mjs
deleted file mode 100644
index 07dc52cfb8..0000000000
--- a/browser/components/search/DomainToCategoriesMap.worker.mjs
+++ /dev/null
@@ -1,101 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-import { PromiseWorker } from "resource://gre/modules/workers/PromiseWorker.mjs";
-
-/**
- * Boilerplate to connect with the main thread PromiseWorker.
- */
-const worker = new PromiseWorker.AbstractWorker();
-worker.dispatch = function (method, args = []) {
- return agent[method](...args);
-};
-worker.postMessage = function (message, ...transfers) {
- self.postMessage(message, ...transfers);
-};
-worker.close = function () {
- self.close();
-};
-
-self.addEventListener("message", msg => worker.handleMessage(msg));
-self.addEventListener("unhandledrejection", function (error) {
- throw error.reason;
-});
-
-/**
- * Stores and manages the Domain-to-Categories Map.
- */
-class Agent {
- /**
- * @type {Map<string, Array<number>>} Hashes mapped to categories and values.
- */
- #map = new Map();
-
- /**
- * Converts data from the array directly into a Map.
- *
- * @param {Array<ArrayBuffer>} fileContents Files
- * @returns {boolean} Returns whether the Map contains results.
- */
- populateMap(fileContents) {
- this.#map.clear();
-
- for (let fileContent of fileContents) {
- let obj;
- try {
- obj = JSON.parse(new TextDecoder().decode(fileContent));
- } catch (ex) {
- return false;
- }
- for (let objKey in obj) {
- if (Object.hasOwn(obj, objKey)) {
- this.#map.set(objKey, obj[objKey]);
- }
- }
- }
- return this.#map.size > 0;
- }
-
- /**
- * Retrieves scores for the hash from the map.
- *
- * @param {string} hash Key to look up in the map.
- * @returns {Array<number>}
- */
- getScores(hash) {
- if (this.#map.has(hash)) {
- return this.#map.get(hash);
- }
- return [];
- }
-
- /**
- * Empties the internal map.
- *
- * @returns {boolean}
- */
- emptyMap() {
- this.#map.clear();
- return true;
- }
-
- /**
- * Test only function to allow the map to contain information without
- * having to go through Remote Settings.
- *
- * @param {object} obj The data to directly import into the Map.
- * @returns {boolean} Whether the map contains values.
- */
- overrideMapForTests(obj) {
- this.#map.clear();
- for (let objKey in obj) {
- if (Object.hasOwn(obj, objKey)) {
- this.#map.set(objKey, obj[objKey]);
- }
- }
- return this.#map.size > 0;
- }
-}
-
-const agent = new Agent();
diff --git a/browser/components/search/SearchSERPTelemetry.sys.mjs b/browser/components/search/SearchSERPTelemetry.sys.mjs
index fa593be08c..2a9ed88db1 100644
--- a/browser/components/search/SearchSERPTelemetry.sys.mjs
+++ b/browser/components/search/SearchSERPTelemetry.sys.mjs
@@ -7,12 +7,12 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "gCryptoHash", () => {
@@ -52,11 +52,15 @@ export const SEARCH_TELEMETRY_SHARED = {
const impressionIdsWithoutEngagementsSet = new Set();
export const CATEGORIZATION_SETTINGS = {
+ STORE_SCHEMA: 1,
+ STORE_FILE: "domain_to_categories.sqlite",
+ STORE_NAME: "domain_to_categories",
MAX_DOMAINS_TO_CATEGORIZE: 10,
MINIMUM_SCORE: 0,
STARTING_RANK: 2,
IDLE_TIMEOUT_SECONDS: 60 * 60,
WAKE_TIMEOUT_MS: 60 * 60 * 1000,
+ PING_SUBMISSION_THRESHOLD: 10,
};
ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
@@ -83,15 +87,20 @@ XPCOMUtils.defineLazyPreferenceGetter(
false,
(aPreference, previousValue, newValue) => {
if (newValue) {
- SearchSERPDomainToCategoriesMap.init();
- SearchSERPCategorizationEventScheduler.init();
+ SearchSERPCategorization.init();
} else {
- SearchSERPDomainToCategoriesMap.uninit();
- SearchSERPCategorizationEventScheduler.uninit();
+ SearchSERPCategorization.uninit({ deleteMap: true });
}
}
);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "activityLimit",
+ "telemetry.fog.test.activity_limit",
+ 120
+);
+
export const SearchSERPTelemetryUtils = {
ACTIONS: {
CLICKED: "clicked",
@@ -380,7 +389,7 @@ class TelemetryHandler {
* unit tests can set it to easy to test values.
*
* @param {Array} providerInfo
- * See {@link https://searchfox.org/mozilla-central/search?q=search-telemetry-schema.json}
+ * See {@link https://searchfox.org/mozilla-central/search?q=search-telemetry-v2-schema.json}
* for type information.
*/
overrideSearchTelemetryForTests(providerInfo) {
@@ -1641,7 +1650,10 @@ class ContentHandler {
!telemetryState.adImpressionsReported
) {
for (let [componentType, data] of info.adImpressions.entries()) {
- telemetryState.adsVisible += data.adsVisible;
+ // Not all ad impressions are sponsored.
+ if (AD_COMPONENTS.includes(componentType)) {
+ telemetryState.adsVisible += data.adsVisible;
+ }
lazy.logConsole.debug("Counting ad:", { type: componentType, ...data });
Glean.serp.adImpression.record({
@@ -1772,6 +1784,8 @@ class ContentHandler {
let item = this._findItemForBrowser(browser);
let telemetryState = item.browserTelemetryStateMap.get(browser);
if (lazy.serpEventTelemetryCategorization && telemetryState) {
+ lazy.logConsole.debug("Ad domains:", Array.from(info.adDomains));
+ lazy.logConsole.debug("Non ad domains:", Array.from(info.nonAdDomains));
let result = await SearchSERPCategorization.maybeCategorizeSERP(
info.nonAdDomains,
info.adDomains,
@@ -1789,6 +1803,7 @@ class ContentHandler {
partner_code: impressionInfo.partnerCode,
provider: impressionInfo.provider,
tagged: impressionInfo.tagged,
+ is_shopping_page: impressionInfo.isShoppingPage,
num_ads_clicked: telemetryState.adsClicked,
num_ads_visible: telemetryState.adsVisible,
});
@@ -1843,6 +1858,22 @@ class ContentHandler {
* Categorizes SERPs.
*/
class SERPCategorizer {
+ async init() {
+ if (lazy.serpEventTelemetryCategorization) {
+ lazy.logConsole.debug("Initialize SERP categorizer.");
+ await SearchSERPDomainToCategoriesMap.init();
+ SearchSERPCategorizationEventScheduler.init();
+ SERPCategorizationRecorder.init();
+ }
+ }
+
+ async uninit({ deleteMap = false } = {}) {
+ lazy.logConsole.debug("Uninit SERP categorizer.");
+ await SearchSERPDomainToCategoriesMap.uninit(deleteMap);
+ SearchSERPCategorizationEventScheduler.uninit();
+ SERPCategorizationRecorder.uninit();
+ }
+
/**
* Categorizes domains extracted from SERPs. Note that we don't process
* domains if the domain-to-categories map is empty (if the client couldn't
@@ -1999,12 +2030,8 @@ class CategorizationEventScheduler {
*/
#mostRecentMs = null;
- constructor() {
- this.init();
- }
-
init() {
- if (!lazy.serpEventTelemetryCategorization || this.#init) {
+ if (this.#init) {
return;
}
@@ -2114,6 +2141,61 @@ class CategorizationEventScheduler {
* Handles reporting SERP categorization telemetry to Glean.
*/
class CategorizationRecorder {
+ #init = false;
+
+ // The number of SERP categorizations that have been recorded but not yet
+ // reported in a Glean ping.
+ #serpCategorizationsCount = 0;
+
+ // When the user started interacting with the SERP.
+ #userInteractionStartTime = null;
+
+ async init() {
+ if (this.#init) {
+ return;
+ }
+
+ Services.obs.addObserver(this, "user-interaction-active");
+ Services.obs.addObserver(this, "user-interaction-inactive");
+ this.#init = true;
+ this.submitPing("startup");
+ Services.obs.notifyObservers(null, "categorization-recorder-init");
+ }
+
+ uninit() {
+ if (this.#init) {
+ Services.obs.removeObserver(this, "user-interaction-active");
+ Services.obs.removeObserver(this, "user-interaction-inactive");
+ this.#resetCategorizationRecorderData();
+ this.#init = false;
+ }
+ }
+
+ observe(subject, topic, _data) {
+ switch (topic) {
+ case "user-interaction-active": {
+ // If the user is already active, we don't want to overwrite the start
+ // time.
+ if (this.#userInteractionStartTime == null) {
+ this.#userInteractionStartTime = Date.now();
+ }
+ break;
+ }
+ case "user-interaction-inactive": {
+ let currentTime = Date.now();
+ let activityLimitInMs = lazy.activityLimit * 1000;
+ if (
+ this.#userInteractionStartTime &&
+ currentTime - this.#userInteractionStartTime >= activityLimitInMs
+ ) {
+ this.submitPing("inactivity");
+ }
+ this.#userInteractionStartTime = null;
+ break;
+ }
+ }
+ }
+
/**
* Helper function for recording the SERP categorization event.
*
@@ -2125,7 +2207,37 @@ class CategorizationRecorder {
"Reporting the following categorization result:",
resultToReport
);
- // TODO: Bug 1868476 - Report result to Glean.
+ Glean.serp.categorization.record(resultToReport);
+
+ this.#serpCategorizationsCount++;
+ if (
+ this.#serpCategorizationsCount >=
+ CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD
+ ) {
+ this.submitPing("threshold_reached");
+ this.#serpCategorizationsCount = 0;
+ }
+ }
+
+ submitPing(reason) {
+ lazy.logConsole.debug("Submitting SERP categorization ping:", reason);
+ GleanPings.serpCategorization.submit(reason);
+ }
+
+ /**
+ * Tests are able to clear telemetry on demand. When that happens, we need to
+ * ensure we're doing to the same here or else the internal count in tests
+ * will be inaccurate.
+ */
+ testReset() {
+ if (Cu.isInAutomation) {
+ this.#resetCategorizationRecorderData();
+ }
+ }
+
+ #resetCategorizationRecorderData() {
+ this.#serpCategorizationsCount = 0;
+ this.#userInteractionStartTime = null;
}
}
@@ -2144,10 +2256,8 @@ class CategorizationRecorder {
*/
/**
- * Maps domain to categories, with its data synced using Remote Settings. The
- * data is downloaded from Remote Settings and stored in a map in a worker
- * thread to avoid processing the data from the attachments from occupying
- * the main thread.
+ * Maps domain to categories. Data is downloaded from Remote Settings and
+ * stored inside DomainToCategoriesStore.
*/
class DomainToCategoriesMap {
/**
@@ -2195,40 +2305,63 @@ class DomainToCategoriesMap {
#downloadRetries = 0;
/**
- * Whether the mappings are empty.
- */
- #empty = true;
-
- /**
- * @type {BasePromiseWorker|null} Worker used to access the raw domain
- * to categories map data.
+ * A reference to the data store.
+ *
+ * @type {DomainToCategoriesStore | null}
*/
- #worker = null;
+ #store = null;
/**
* Runs at application startup with startup idle tasks. If the SERP
* categorization preference is enabled, it creates a Remote Settings
- * client to listen to updates, and populates the map.
+ * client to listen to updates, and populates the store.
*/
async init() {
- if (!lazy.serpEventTelemetryCategorization || this.#init) {
+ if (this.#init) {
return;
}
lazy.logConsole.debug("Initializing domain-to-categories map.");
- this.#worker = new lazy.BasePromiseWorker(
- "resource:///modules/DomainToCategoriesMap.worker.mjs",
- { type: "module" }
- );
- await this.#setupClientAndMap();
+
+ // Set early to allow un-init from an initialization.
this.#init = true;
+
+ try {
+ await this.#setupClientAndStore();
+ } catch (ex) {
+ lazy.logConsole.error(ex);
+ await this.uninit();
+ return;
+ }
+
+ // If we don't have a client and store, it likely means an un-init process
+ // started during the initialization process.
+ if (this.#client && this.#store) {
+ lazy.logConsole.debug("Initialized domain-to-categories map.");
+ Services.obs.notifyObservers(null, "domain-to-categories-map-init");
+ }
}
- uninit() {
+ async uninit(shouldDeleteStore) {
if (this.#init) {
lazy.logConsole.debug("Un-initializing domain-to-categories map.");
- this.#clearClientAndWorker();
+ this.#clearClient();
this.#cancelAndNullifyTimer();
+
+ if (this.#store) {
+ if (shouldDeleteStore) {
+ try {
+ await this.#store.dropData();
+ } catch (ex) {
+ lazy.logConsole.error(ex);
+ }
+ }
+ await this.#store.uninit();
+ this.#store = null;
+ }
+
+ lazy.logConsole.debug("Un-initialized domain-to-categories map.");
this.#init = false;
+ Services.obs.notifyObservers(null, "domain-to-categories-map-uninit");
}
}
@@ -2241,14 +2374,14 @@ class DomainToCategoriesMap {
* for the domain is available, return an empty array.
*/
async get(domain) {
- if (this.empty) {
+ if (!this.#store || this.#store.empty || !this.#store.ready) {
return [];
}
lazy.gCryptoHash.init(lazy.gCryptoHash.SHA256);
let bytes = new TextEncoder().encode(domain);
lazy.gCryptoHash.update(bytes, domain.length);
let hash = lazy.gCryptoHash.finish(true);
- let rawValues = await this.#worker.post("getScores", [hash]);
+ let rawValues = await this.#store.getCategories(hash);
if (rawValues?.length) {
let output = [];
// Transform data into a more readable format.
@@ -2275,12 +2408,15 @@ class DomainToCategoriesMap {
}
/**
- * Whether the map is empty of data.
+ * Whether the store is empty of data.
*
* @returns {boolean}
*/
get empty() {
- return this.#empty;
+ if (!this.#store) {
+ return true;
+ }
+ return this.#store.empty;
}
/**
@@ -2290,15 +2426,26 @@ class DomainToCategoriesMap {
* @param {object} domainToCategoriesMap
* An object where the key is a hashed domain and the value is an array
* containing an arbitrary number of DomainCategoryScores.
+ * @param {number} version
+ * The version number for the store.
*/
- async overrideMapForTests(domainToCategoriesMap) {
- let hasResults = await this.#worker.post("overrideMapForTests", [
- domainToCategoriesMap,
- ]);
- this.#empty = !hasResults;
+ async overrideMapForTests(domainToCategoriesMap, version = 1) {
+ if (Cu.isInAutomation || Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
+ await this.#store.init();
+ await this.#store.dropData();
+ await this.#store.insertObject(domainToCategoriesMap, version);
+ }
}
- async #setupClientAndMap() {
+ /**
+ * Connect with Remote Settings and retrieve the records associated with
+ * categorization. Then, check if the records match the store version. If
+ * no records exist, return early. If records exist but the version stored
+ * on the records differ from the store version, then attempt to
+ * empty the store and fill it with data from downloaded attachments. Only
+ * reuse the store if the version in each record matches the store.
+ */
+ async #setupClientAndStore() {
if (this.#client && !this.empty) {
return;
}
@@ -2308,11 +2455,33 @@ class DomainToCategoriesMap {
this.#onSettingsSync = event => this.#sync(event.data);
this.#client.on("sync", this.#onSettingsSync);
+ this.#store = new DomainToCategoriesStore();
+ await this.#store.init();
+
let records = await this.#client.get();
- await this.#clearAndPopulateMap(records);
+ // Even though records don't exist, this is still technically initialized
+ // since the next sync from Remote Settings will populate the store with
+ // records.
+ if (!records.length) {
+ lazy.logConsole.debug("No records found for domain-to-categories map.");
+ return;
+ }
+
+ this.#version = this.#retrieveLatestVersion(records);
+ let storeVersion = await this.#store.getVersion();
+ if (storeVersion == this.#version && !this.#store.empty) {
+ lazy.logConsole.debug("Reuse existing domain-to-categories map.");
+ Services.obs.notifyObservers(
+ null,
+ "domain-to-categories-map-update-complete"
+ );
+ return;
+ }
+
+ await this.#clearAndPopulateStore(records);
}
- #clearClientAndWorker() {
+ #clearClient() {
if (this.#client) {
lazy.logConsole.debug("Removing Remote Settings client.");
this.#client.off("sync", this.#onSettingsSync);
@@ -2320,17 +2489,6 @@ class DomainToCategoriesMap {
this.#onSettingsSync = null;
this.#downloadRetries = 0;
}
-
- if (!this.#empty) {
- lazy.logConsole.debug("Clearing domain-to-categories map.");
- this.#empty = true;
- this.#version = null;
- }
-
- if (this.#worker) {
- this.#worker.terminate();
- this.#worker = null;
- }
}
/**
@@ -2377,27 +2535,50 @@ class DomainToCategoriesMap {
// again in case there's a new download error.
this.#downloadRetries = 0;
- this.#clearAndPopulateMap(data?.current);
+ try {
+ await this.#clearAndPopulateStore(data?.current);
+ } catch (ex) {
+ lazy.logConsole.error("Error populating map: ", ex);
+ await this.uninit();
+ }
}
/**
- * Clear the existing map and populate it with attachments found in the
+ * Clear the existing store and populate it with attachments found in the
* records. If no attachments are found, or no record containing an
* attachment contained the latest version, then nothing will change.
*
* @param {Array<DomainToCategoriesRecord>} records
* The records containing attachments.
- *
+ * @throws {Error}
+ * Will throw if it was not able to drop the store data, or it was unable
+ * to insert data into the store.
*/
- async #clearAndPopulateMap(records) {
- // Empty map so that if there are errors in the download process, callers
- // querying the map won't use information we know is already outdated.
- await this.#worker.post("emptyMap");
+ async #clearAndPopulateStore(records) {
+ // If we don't have a handle to a store, it would mean that it was removed
+ // during an uninitialization process.
+ if (!this.#store) {
+ lazy.logConsole.debug(
+ "Could not populate store because no store was available."
+ );
+ return;
+ }
+
+ if (!this.#store.ready) {
+ lazy.logConsole.debug(
+ "Could not populate store because it was not ready."
+ );
+ return;
+ }
+
+ // Empty table so that if there are errors in the download process, callers
+ // querying the map won't use information we know is probably outdated.
+ await this.#store.dropData();
- this.#empty = true;
this.#version = null;
this.#cancelAndNullifyTimer();
+ // A collection with no records is still a valid init state.
if (!records?.length) {
lazy.logConsole.debug("No records found for domain-to-categories map.");
return;
@@ -2418,41 +2599,24 @@ class DomainToCategoriesMap {
fileContents.push(result.buffer);
}
ChromeUtils.addProfilerMarker(
- "SearchSERPTelemetry.#clearAndPopulateMap",
+ "SearchSERPTelemetry.#clearAndPopulateStore",
start,
"Download attachments."
);
- // Attachments should have a version number.
this.#version = this.#retrieveLatestVersion(records);
-
if (!this.#version) {
lazy.logConsole.debug("Could not find a version number for any record.");
return;
}
- Services.tm.idleDispatchToMainThread(async () => {
- start = Cu.now();
- let hasResults;
- try {
- hasResults = await this.#worker.post("populateMap", [fileContents]);
- } catch (ex) {
- console.error(ex);
- }
+ await this.#store.insertFileContents(fileContents, this.#version);
- this.#empty = !hasResults;
-
- ChromeUtils.addProfilerMarker(
- "SearchSERPTelemetry.#clearAndPopulateMap",
- start,
- "Convert contents to JSON."
- );
- lazy.logConsole.debug("Updated domain-to-categories map.");
- Services.obs.notifyObservers(
- null,
- "domain-to-categories-map-update-complete"
- );
- });
+ lazy.logConsole.debug("Finished updating domain-to-categories store.");
+ Services.obs.notifyObservers(
+ null,
+ "domain-to-categories-map-update-complete"
+ );
}
#cancelAndNullifyTimer() {
@@ -2466,7 +2630,8 @@ class DomainToCategoriesMap {
#createTimerToPopulateMap() {
if (
this.#downloadRetries >=
- TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxTriesPerSession
+ TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxTriesPerSession ||
+ !this.#client
) {
return;
}
@@ -2486,7 +2651,12 @@ class DomainToCategoriesMap {
async () => {
this.#downloadRetries += 1;
let records = await this.#client.get();
- this.#clearAndPopulateMap(records);
+ try {
+ await this.#clearAndPopulateStore(records);
+ } catch (ex) {
+ lazy.logConsole.error("Error populating store: ", ex);
+ await this.uninit();
+ }
},
delay,
Ci.nsITimer.TYPE_ONE_SHOT
@@ -2494,6 +2664,514 @@ class DomainToCategoriesMap {
}
}
+/**
+ * Handles the storage of data containing domains to categories.
+ */
+export class DomainToCategoriesStore {
+ #init = false;
+
+ /**
+ * The connection to the store.
+ *
+ * @type {object | null}
+ */
+ #connection = null;
+
+ /**
+ * Reference for the shutdown blocker in case we need to remove it before
+ * shutdown.
+ *
+ * @type {Function | null}
+ */
+ #asyncShutdownBlocker = null;
+
+ /**
+ * Whether the store is empty of data.
+ *
+ * @type {boolean}
+ */
+ #empty = true;
+
+ /**
+ * For a particular subset of errors, we'll attempt to rebuild the database
+ * from scratch.
+ */
+ #rebuildableErrors = ["NS_ERROR_FILE_CORRUPTED"];
+
+ /**
+ * Initializes the store. If the store is initialized it should have cached
+ * a connection to the store and ensured the store exists.
+ */
+ async init() {
+ if (this.#init) {
+ return;
+ }
+ lazy.logConsole.debug("Initializing domain-to-categories store.");
+
+ // Attempts to cache a connection to the store.
+ // If a failure occured, try to re-build the store.
+ let rebuiltStore = false;
+ try {
+ await this.#initConnection();
+ } catch (ex1) {
+ lazy.logConsole.error(`Error initializing a connection: ${ex1}`);
+ if (this.#rebuildableErrors.includes(ex1.name)) {
+ try {
+ await this.#rebuildStore();
+ } catch (ex2) {
+ await this.#closeConnection();
+ lazy.logConsole.error(`Could not rebuild store: ${ex2}`);
+ return;
+ }
+ rebuiltStore = true;
+ }
+ }
+
+ // If we don't have a connection, bail because the browser could be
+ // shutting down ASAP, or re-creating the store is impossible.
+ if (!this.#connection) {
+ lazy.logConsole.debug(
+ "Bailing from DomainToCategoriesStore.init because connection doesn't exist."
+ );
+ return;
+ }
+
+ // If we weren't forced to re-build the store, we only have the connection.
+ // We want to ensure the store exists so calls to public methods can pass
+ // without throwing errors due to the absence of the store.
+ if (!rebuiltStore) {
+ try {
+ await this.#initSchema();
+ } catch (ex) {
+ lazy.logConsole.error(`Error trying to create store: ${ex}`);
+ await this.#closeConnection();
+ return;
+ }
+ }
+
+ lazy.logConsole.debug("Initialized domain-to-categories store.");
+ this.#init = true;
+ }
+
+ async uninit() {
+ if (this.#init) {
+ lazy.logConsole.debug("Un-initializing domain-to-categories store.");
+ await this.#closeConnection();
+ this.#asyncShutdownBlocker = null;
+ lazy.logConsole.debug("Un-initialized domain-to-categories store.");
+ }
+ }
+
+ /**
+ * Whether the store has an open connection to the physical store.
+ *
+ * @returns {boolean}
+ */
+ get ready() {
+ return this.#init;
+ }
+
+ /**
+ * Whether the store is devoid of data.
+ *
+ * @returns {boolean}
+ */
+ get empty() {
+ return this.#empty;
+ }
+
+ /**
+ * Clears information in the store. If dropping data encountered a failure,
+ * try to delete the file containing the store and re-create it.
+ *
+ * @throws {Error} Will throw if it was unable to clear information from the
+ * store.
+ */
+ async dropData() {
+ if (!this.#connection) {
+ return;
+ }
+ let tableExists = await this.#connection.tableExists(
+ CATEGORIZATION_SETTINGS.STORE_NAME
+ );
+ if (tableExists) {
+ lazy.logConsole.debug("Drop domain_to_categories.");
+ // This can fail if the permissions of the store are read-only.
+ await this.#connection.executeTransaction(async () => {
+ await this.#connection.execute(`DROP TABLE domain_to_categories`);
+ const createDomainToCategoriesTable = `
+ CREATE TABLE IF NOT EXISTS
+ domain_to_categories (
+ string_id
+ TEXT PRIMARY KEY NOT NULL,
+ categories
+ TEXT
+ );
+ `;
+ await this.#connection.execute(createDomainToCategoriesTable);
+ await this.#connection.execute(`DELETE FROM moz_meta`);
+ await this.#connection.executeCached(
+ `
+ INSERT INTO
+ moz_meta (key, value)
+ VALUES
+ (:key, :value)
+ ON CONFLICT DO UPDATE SET
+ value = :value
+ `,
+ { key: "version", value: 0 }
+ );
+ });
+
+ this.#empty = true;
+ }
+ }
+
+ /**
+ * Given file contents, try moving them into the store. If a failure occurs,
+ * it will attempt to drop existing data to ensure callers aren't accessing
+ * a partially filled store.
+ *
+ * @param {Array<ArrayBuffer>} fileContents
+ * Contents to convert.
+ * @param {number} version
+ * The version for the store.
+ * @throws {Error}
+ * Will throw if the insertion failed and dropData was unable to run
+ * successfully.
+ */
+ async insertFileContents(fileContents, version) {
+ if (!this.#init || !fileContents?.length || !version) {
+ return;
+ }
+
+ try {
+ await this.#insert(fileContents, version);
+ } catch (ex) {
+ lazy.logConsole.error(`Could not insert file contents: ${ex}`);
+ await this.dropData();
+ }
+ }
+
+ /**
+ * Convenience function to make it trivial to insert Javascript objects into
+ * the store. This avoids having to set up the collection in Remote Settings.
+ *
+ * @param {object} domainToCategoriesMap
+ * An object whose keys should be hashed domains with values containing
+ * an array of integers.
+ * @param {number} version
+ * The version for the store.
+ * @returns {boolean}
+ * Whether the operation was successful.
+ */
+ async insertObject(domainToCategoriesMap, version) {
+ if (!Cu.isInAutomation || !this.#init) {
+ return false;
+ }
+ let buffer = new TextEncoder().encode(
+ JSON.stringify(domainToCategoriesMap)
+ ).buffer;
+ await this.insertFileContents([buffer], version);
+ return true;
+ }
+
+ /**
+ * Retrieves domains mapped to the key.
+ *
+ * @param {string} key
+ * The value to lookup in the store.
+ * @returns {Array<number>}
+ * An array of numbers corresponding to the category and score. If the key
+ * does not exist in the store or the store is having issues retrieving the
+ * value, returns an empty array.
+ */
+ async getCategories(key) {
+ if (!this.#init) {
+ return [];
+ }
+
+ let rows;
+ try {
+ rows = await this.#connection.executeCached(
+ `
+ SELECT
+ categories
+ FROM
+ domain_to_categories
+ WHERE
+ string_id = :key
+ `,
+ {
+ key,
+ }
+ );
+ } catch (ex) {
+ lazy.logConsole.error(`Could not retrieve from the store: ${ex}`);
+ return [];
+ }
+
+ if (!rows.length) {
+ return [];
+ }
+ return JSON.parse(rows[0].getResultByName("categories")) ?? [];
+ }
+
+ /**
+ * Retrieves the version number of the store.
+ *
+ * @returns {number}
+ * The version number. Returns 0 if the version was never set or if there
+ * was an issue accessing the version number.
+ */
+ async getVersion() {
+ if (this.#connection) {
+ let rows;
+ try {
+ rows = await this.#connection.executeCached(
+ `
+ SELECT
+ value
+ FROM
+ moz_meta
+ WHERE
+ key = "version"
+ `
+ );
+ } catch (ex) {
+ lazy.logConsole.error(`Could not retrieve version of the store: ${ex}`);
+ return 0;
+ }
+ if (rows.length) {
+ return parseInt(rows[0].getResultByName("value")) ?? 0;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Test only function allowing tests to delete the store.
+ */
+ async testDelete() {
+ if (Cu.isInAutomation) {
+ await this.#closeConnection();
+ await this.#delete();
+ }
+ }
+
+ /**
+ * If a connection is available, close it and remove shutdown blockers.
+ */
+ async #closeConnection() {
+ this.#init = false;
+ this.#empty = true;
+ if (this.#asyncShutdownBlocker) {
+ lazy.Sqlite.shutdown.removeBlocker(this.#asyncShutdownBlocker);
+ this.#asyncShutdownBlocker = null;
+ }
+
+ if (this.#connection) {
+ lazy.logConsole.debug("Closing connection.");
+ // An error could occur while closing the connection. We suppress the
+ // error since it is not a critical part of the browser.
+ try {
+ await this.#connection.close();
+ } catch (ex) {
+ lazy.logConsole.error(ex);
+ }
+ this.#connection = null;
+ }
+ }
+
+ /**
+ * Initialize the schema for the store.
+ *
+ * @throws {Error}
+ * Will throw if a permissions error prevents creating the store.
+ */
+ async #initSchema() {
+ if (!this.#connection) {
+ return;
+ }
+ lazy.logConsole.debug("Create store.");
+ // Creation can fail if the store is read only.
+ await this.#connection.executeTransaction(async () => {
+ // Let outer try block handle the exception.
+ const createDomainToCategoriesTable = `
+ CREATE TABLE IF NOT EXISTS
+ domain_to_categories (
+ string_id
+ TEXT PRIMARY KEY NOT NULL,
+ categories
+ TEXT
+ ) WITHOUT ROWID;
+ `;
+ await this.#connection.execute(createDomainToCategoriesTable);
+ const createMetaTable = `
+ CREATE TABLE IF NOT EXISTS
+ moz_meta (
+ key
+ TEXT PRIMARY KEY NOT NULL,
+ value
+ INTEGER
+ ) WITHOUT ROWID;
+ `;
+ await this.#connection.execute(createMetaTable);
+ await this.#connection.setSchemaVersion(
+ CATEGORIZATION_SETTINGS.STORE_SCHEMA
+ );
+ });
+
+ let rows = await this.#connection.executeCached(
+ "SELECT count(*) = 0 FROM domain_to_categories"
+ );
+ this.#empty = !!rows[0].getResultByIndex(0);
+ }
+
+ /**
+ * Attempt to delete the store.
+ *
+ * @throws {Error}
+ * Will throw if the permissions for the file prevent its deletion.
+ */
+ async #delete() {
+ lazy.logConsole.debug("Attempt to delete the store.");
+ try {
+ await IOUtils.remove(
+ PathUtils.join(
+ PathUtils.profileDir,
+ CATEGORIZATION_SETTINGS.STORE_FILE
+ ),
+ { ignoreAbsent: true }
+ );
+ } catch (ex) {
+ lazy.logConsole.error(ex);
+ }
+ this.#empty = true;
+ lazy.logConsole.debug("Store was deleted.");
+ }
+
+ /**
+ * Tries to establish a connection to the store.
+ *
+ * @throws {Error}
+ * Will throw if there was an issue establishing a connection or adding
+ * adding a shutdown blocker.
+ */
+ async #initConnection() {
+ if (this.#connection) {
+ return;
+ }
+
+ // This could fail if the store is corrupted.
+ this.#connection = await lazy.Sqlite.openConnection({
+ path: PathUtils.join(
+ PathUtils.profileDir,
+ CATEGORIZATION_SETTINGS.STORE_FILE
+ ),
+ });
+
+ await this.#connection.execute("PRAGMA journal_mode = TRUNCATE");
+
+ this.#asyncShutdownBlocker = async () => {
+ await this.#connection.close();
+ this.#connection = null;
+ };
+
+ // This could fail if we're adding it during shutdown. In this case,
+ // don't throw but close the connection.
+ try {
+ lazy.Sqlite.shutdown.addBlocker(
+ "SearchSERPTelemetry:DomainToCategoriesSqlite closing",
+ this.#asyncShutdownBlocker
+ );
+ } catch (ex) {
+ lazy.logConsole.error(ex);
+ await this.#closeConnection();
+ }
+ }
+
+ /**
+ * Inserts into the store.
+ *
+ * @param {Array<ArrayBuffer>} fileContents
+ * The data that should be converted and inserted into the store.
+ * @param {number} version
+ * The version number that should be inserted into the store.
+ * @throws {Error}
+ * Will throw if a connection is not present, if the store is not
+ * able to be updated (permissions error, corrupted file), or there is
+ * something wrong with the file contents.
+ */
+ async #insert(fileContents, version) {
+ let start = Cu.now();
+ await this.#connection.executeTransaction(async () => {
+ lazy.logConsole.debug("Insert into domain_to_categories table.");
+ for (let fileContent of fileContents) {
+ await this.#connection.executeCached(
+ `
+ INSERT INTO
+ domain_to_categories (string_id, categories)
+ SELECT
+ json_each.key AS string_id,
+ json_each.value AS categories
+ FROM
+ json_each(json(:obj))
+ `,
+ {
+ obj: new TextDecoder().decode(fileContent),
+ }
+ );
+ }
+ // Once the insertions have successfully completed, update the version.
+ await this.#connection.executeCached(
+ `
+ INSERT INTO
+ moz_meta (key, value)
+ VALUES
+ (:key, :value)
+ ON CONFLICT DO UPDATE SET
+ value = :value
+ `,
+ { key: "version", value: version }
+ );
+ });
+ ChromeUtils.addProfilerMarker(
+ "DomainToCategoriesSqlite.#insert",
+ start,
+ "Move file contents into table."
+ );
+
+ if (fileContents?.length) {
+ this.#empty = false;
+ }
+ }
+
+ /**
+ * Deletes and re-build's the store. Used in cases where we encounter a
+ * failure and we want to try fixing the error by starting with an
+ * entirely fresh store.
+ *
+ * @throws {Error}
+ * Will throw if a connection could not be established, if it was
+ * unable to delete the store, or it was unable to build a new store.
+ */
+ async #rebuildStore() {
+ lazy.logConsole.debug("Try rebuilding store.");
+ // Step 1. Close all connections.
+ await this.#closeConnection();
+
+ // Step 2. Delete the existing store.
+ await this.#delete();
+
+ // Step 3. Re-establish the connection.
+ await this.#initConnection();
+
+ // Step 4. If a connection exists, try creating the store.
+ await this.#initSchema();
+ }
+}
+
function randomInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
diff --git a/browser/components/search/metrics.yaml b/browser/components/search/metrics.yaml
index 12fd44a0e2..c7636b9d04 100644
--- a/browser/components/search/metrics.yaml
+++ b/browser/components/search/metrics.yaml
@@ -331,6 +331,110 @@ serp:
- fx-search-telemetry@mozilla.com
expires: never
+ categorization:
+ type: event
+ description: >
+ A high-level categorization of a SERP (a best guess as to its topic),
+ using buckets such as "sports" or "travel".
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1868476
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1869064
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887686
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1868476
+ data_sensitivity:
+ - stored_content
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ expires: never
+ extra_keys:
+ sponsored_category:
+ description: >
+ An index corresponding to a broad category for the SERP, derived from
+ sponsored domains.
+ type: quantity
+ sponsored_num_domains:
+ description: >
+ The total number of sponsored domains used in the categorization
+ process for the SERP.
+ type: quantity
+ sponsored_num_unknown:
+ description: >
+ The count of sponsored domains extracted from the SERP that are not
+ found in the domain-to-categories mapping.
+ type: quantity
+ sponsored_num_inconclusive:
+ description: >
+ The count of sponsored domains extracted from the SERP that are found
+ in the domain-to-categories mapping but are deemed inconclusive.
+ type: quantity
+ organic_category:
+ description: >
+ An index corresponding to a broad category for the SERP, derived from
+ organic domains.
+ type: quantity
+ organic_num_domains:
+ description: >
+ The total number of organic domains used in the categorization
+ process for the SERP.
+ type: quantity
+ organic_num_unknown:
+ description: >
+ The count of organic domains extracted from the SERP that are not
+ found in the domain-to-categories mapping.
+ type: quantity
+ organic_num_inconclusive:
+ description: >
+ The count of organic domains extracted from the SERP that are found
+ in the domain-to-categories mapping but are deemed inconclusive.
+ type: quantity
+ region:
+ description: >
+ A two-letter country code indicating where the SERP was loaded.
+ type: string
+ channel:
+ description: >
+ The type of update channel, for example: “nightly”, “beta”, “release”.
+ type: string
+ provider:
+ description: >
+ The name of the provider.
+ type: string
+ tagged:
+ description: >
+ Whether the search is tagged (true) or organic (false).
+ type: boolean
+ partner_code:
+ description: >
+ Any partner_code parsing in the URL or an empty string if not
+ available.
+ type: string
+ app_version:
+ description: >
+ The Firefox major version used, for example: 126.
+ type: quantity
+ mappings_version:
+ description: >
+ Version number for the Remote Settings attachments used to generate
+ the domain-to-categories map used in the SERP categorization process.
+ type: quantity
+ is_shopping_page:
+ description: >
+ Indicates if the page is a shopping page.
+ type: boolean
+ num_ads_visible:
+ description: >
+ Number of ads visible on the page at the time of categorizing the
+ page.
+ type: quantity
+ num_ads_clicked:
+ description: >
+ Number of ads clicked on the page.
+ type: quantity
+ send_in_pings:
+ - serp-categorization
+
search_with:
reporting_url:
type: url
diff --git a/browser/components/search/moz.build b/browser/components/search/moz.build
index 0289f32979..ff49a259ed 100644
--- a/browser/components/search/moz.build
+++ b/browser/components/search/moz.build
@@ -6,7 +6,6 @@
EXTRA_JS_MODULES += [
"BrowserSearchTelemetry.sys.mjs",
- "DomainToCategoriesMap.worker.mjs",
"SearchOneOffs.sys.mjs",
"SearchSERPTelemetry.sys.mjs",
"SearchUIUtils.sys.mjs",
@@ -18,7 +17,10 @@ BROWSER_CHROME_MANIFESTS += [
"test/browser/telemetry/browser.toml",
]
-MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"]
+MARIONETTE_MANIFESTS += [
+ "test/marionette/manifest.toml",
+ "test/marionette/telemetry/manifest.toml",
+]
XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"]
diff --git a/browser/components/search/schema/search-telemetry-schema.json b/browser/components/search/schema/search-telemetry-v2-schema.json
index 50b6e124fc..50b6e124fc 100644
--- a/browser/components/search/schema/search-telemetry-schema.json
+++ b/browser/components/search/schema/search-telemetry-v2-schema.json
diff --git a/browser/components/search/schema/search-telemetry-ui-schema.json b/browser/components/search/schema/search-telemetry-v2-ui-schema.json
index 781da5a626..749063db72 100644
--- a/browser/components/search/schema/search-telemetry-ui-schema.json
+++ b/browser/components/search/schema/search-telemetry-v2-ui-schema.json
@@ -11,10 +11,12 @@
"organicCodes",
"followOnParamNames",
"followOnCookies",
+ "ignoreLinkRegexps",
"extraAdServersRegexps",
"adServerAttributes",
"components",
"nonAdsLinkRegexps",
+ "nonAdsLinkQueryParamNames",
"shoppingTab",
"domainExtraction",
"isSPA",
diff --git a/browser/components/search/test/browser/telemetry/browser.toml b/browser/components/search/test/browser/telemetry/browser.toml
index 660fc4eae2..5e42a9187d 100644
--- a/browser/components/search/test/browser/telemetry/browser.toml
+++ b/browser/components/search/test/browser/telemetry/browser.toml
@@ -50,6 +50,15 @@ support-files = ["searchTelemetryDomainCategorizationReporting.html"]
["browser_search_telemetry_domain_categorization_extraction.js"]
support-files = ["searchTelemetryDomainExtraction.html"]
+["browser_search_telemetry_domain_categorization_no_sponsored_values.js"]
+support-files = ["searchTelemetryDomainCategorizationReportingWithoutAds.html"]
+
+["browser_search_telemetry_domain_categorization_ping_submission.js"]
+support-files = [
+ "searchTelemetryDomainCategorizationReporting.html",
+ "searchTelemetryDomainExtraction.html",
+]
+
["browser_search_telemetry_domain_categorization_region.js"]
support-files = ["searchTelemetryDomainCategorizationReporting.html"]
@@ -103,13 +112,6 @@ support-files = [
"searchTelemetryAd_searchbox_with_content.html^headers^",
]
-["browser_search_telemetry_engagement_non_ad.js"]
-support-files = [
- "searchTelemetryAd_searchbox_with_content.html",
- "searchTelemetryAd_searchbox_with_content.html^headers^",
- "serp.css",
-]
-
["browser_search_telemetry_engagement_nonAdsLinkQueryParamNames.js"]
support-files = [
"searchTelemetryAd_searchbox_with_redirecting_links.html",
@@ -118,6 +120,13 @@ support-files = [
"serp.css",
]
+["browser_search_telemetry_engagement_non_ad.js"]
+support-files = [
+ "searchTelemetryAd_searchbox_with_content.html",
+ "searchTelemetryAd_searchbox_with_content.html^headers^",
+ "serp.css",
+]
+
["browser_search_telemetry_engagement_query_params.js"]
support-files = [
"searchTelemetryAd_components_query_parameters.html",
diff --git a/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js b/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js
index e73a9601d4..8e9db64fae 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js
@@ -74,11 +74,14 @@ add_setup(async function () {
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await insertRecordIntoCollectionAndSync();
// If the categorization preference is enabled, we should also wait for the
// sync event to update the domain to categories map.
if (lazy.serpEventsCategorizationEnabled) {
- await waitForDomainToCategoriesUpdate();
+ let promise = waitForDomainToCategoriesUpdate();
+ await insertRecordIntoCollectionAndSync();
+ await promise;
+ } else {
+ await insertRecordIntoCollectionAndSync();
}
registerCleanupFunction(async () => {
@@ -99,6 +102,11 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() {
// the default branch, and not overwrite the user branch.
prefBranch.setBoolPref(TELEMETRY_PREF, false);
+ // If it was true, we should wait until the map is fully un-inited.
+ if (originalPrefValue) {
+ await waitForDomainToCategoriesUninit();
+ }
+
Assert.equal(
lazy.serpEventsCategorizationEnabled,
false,
@@ -152,6 +160,7 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
@@ -160,6 +169,7 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() {
info("End experiment.");
await doExperimentCleanup();
+ await waitForDomainToCategoriesUninit();
Assert.equal(
lazy.serpEventsCategorizationEnabled,
@@ -179,6 +189,7 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() {
await new Promise(resolve => setTimeout(resolve, 1500));
BrowserTestUtils.removeTab(tab);
+ // We should not record telemetry if the experiment is un-enrolled.
assertCategorizationValues([]);
// Clean up.
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js
index 246caf6f47..daccbf0c93 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js
@@ -71,6 +71,16 @@ add_setup(async function () {
await promise;
registerCleanupFunction(async () => {
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be un-initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesUninit();
+ }
SearchSERPTelemetry.overrideSearchTelemetryForTests();
resetTelemetry();
});
@@ -103,6 +113,7 @@ add_task(async function test_load_serp_and_categorize() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
@@ -143,6 +154,7 @@ add_task(async function test_load_serp_and_categorize_and_click_organic() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
@@ -181,6 +193,7 @@ add_task(async function test_load_serp_and_categorize_and_click_sponsored() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "1",
num_ads_visible: "2",
},
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js
index b8dd85da97..9bd215f697 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js
@@ -82,11 +82,20 @@ add_setup(async function () {
await db.clear();
- // Set the state of the pref to false so that tests toggle the preference,
- // triggering the map to be updated.
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetryCategorization.enabled", false]],
- });
+ // If the pref is by default on, disable it as the following tests toggle
+ // the preference to check what happens when the preference is off and the
+ // preference is turned on.
+ if (
+ Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ let promise = waitForDomainToCategoriesUninit();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.serpEventTelemetryCategorization.enabled", false]],
+ });
+ await promise;
+ }
let defaultDownloadSettings = {
...TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS,
@@ -104,6 +113,16 @@ add_setup(async function () {
TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxAdjust = 0;
registerCleanupFunction(async () => {
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesInit();
+ }
SearchSERPTelemetry.overrideSearchTelemetryForTests();
resetTelemetry();
TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS = {
@@ -159,6 +178,7 @@ add_task(async function test_download_after_failure() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_visible: "2",
num_ads_clicked: "0",
},
@@ -166,6 +186,7 @@ add_task(async function test_download_after_failure() {
// Clean up.
await SpecialPowers.popPrefEnv();
+ await waitForDomainToCategoriesUninit();
await resetCategorizationCollection(record);
});
@@ -214,6 +235,7 @@ add_task(async function test_download_after_multiple_failures() {
// Clean up.
await SpecialPowers.popPrefEnv();
+ await waitForDomainToCategoriesUninit();
await resetCategorizationCollection(record);
});
@@ -245,6 +267,7 @@ add_task(async function test_cancel_download_timer() {
});
await SpecialPowers.popPrefEnv();
await observeCancel;
+ await waitForDomainToCategoriesUninit();
// To ensure we don't attempt another download, wait a bit over how long the
// the download error should take.
@@ -263,7 +286,6 @@ add_task(async function test_cancel_download_timer() {
Assert.ok(SearchSERPDomainToCategoriesMap.empty, "Map is empty");
// Clean up.
- await SpecialPowers.popPrefEnv();
await resetCategorizationCollection(record);
});
@@ -310,6 +332,7 @@ add_task(async function test_download_adjust() {
// Clean up.
await SpecialPowers.popPrefEnv();
+ await waitForDomainToCategoriesUninit();
await resetCategorizationCollection(record);
TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.base = TIMEOUT_IN_MS;
TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.minAdjust = 0;
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js
index e653be6c48..2d13b147a2 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js
@@ -362,19 +362,57 @@ const TESTS = [
],
expectedDomains: ["organic.com"],
},
+ {
+ title: "Bing organic result with a path in the URL.",
+ extractorInfos: [
+ {
+ selectors: "#test26 #b_results .b_algo .b_attribution cite",
+ method: "textContent",
+ },
+ ],
+ expectedDomains: ["organic.com"],
+ },
+ {
+ title: "Bing organic result with a path and query param in the URL.",
+ extractorInfos: [
+ {
+ selectors: "#test27 #b_results .b_algo .b_attribution cite",
+ method: "textContent",
+ },
+ ],
+ expectedDomains: ["organic.com"],
+ },
+ {
+ title:
+ "Bing organic result with a path in the URL, but protocol appears in separate HTML element.",
+ extractorInfos: [
+ {
+ selectors: "#test28 #b_results .b_algo .b_attribution cite",
+ method: "textContent",
+ },
+ ],
+ expectedDomains: ["wikipedia.org"],
+ },
];
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
- set: [
- ["browser.search.serpEventTelemetry.enabled", true],
- ["browser.search.serpEventTelemetryCategorization.enabled", true],
- ],
+ set: [["browser.search.serpEventTelemetryCategorization.enabled", true]],
});
await SearchSERPTelemetry.init();
registerCleanupFunction(async () => {
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be un-initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesUninit();
+ }
resetTelemetry();
});
});
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js
new file mode 100644
index 0000000000..2375cad82a
--- /dev/null
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * Checks reporting of pages without ads is accurate.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+});
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp:
+ /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetry/,
+ queryParamNames: ["s"],
+ codeParamName: "abc",
+ taggedCodes: ["ff"],
+ adServerAttributes: ["mozAttr"],
+ nonAdsLinkRegexps: [],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad/],
+ shoppingTab: {
+ selector: "#shopping",
+ },
+ // The search telemetry entry responsible for targeting the specific results.
+ domainExtraction: {
+ ads: [],
+ nonAds: [
+ {
+ selectors: "#results .organic a",
+ method: "href",
+ },
+ ],
+ },
+ components: [
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ default: true,
+ },
+ ],
+ },
+];
+
+add_setup(async function () {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+
+ let promise = waitForDomainToCategoriesUpdate();
+ await insertRecordIntoCollectionAndSync();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.serpEventTelemetryCategorization.enabled", true]],
+ });
+ await promise;
+
+ registerCleanupFunction(async () => {
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be un-initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesUninit();
+ }
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ resetTelemetry();
+ });
+});
+
+add_task(
+ async function test_load_serp_without_sponsored_links_and_categorize() {
+ resetTelemetry();
+
+ let url = getSERPUrl(
+ "searchTelemetryDomainCategorizationReportingWithoutAds.html"
+ );
+ info("Load a SERP with organic and ad components that are non-sponsored.");
+ let promise = waitForPageWithCategorizedDomains();
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await promise;
+
+ info("Assert there is a non-sponsored component on the page.");
+ assertSERPTelemetry([
+ {
+ impression: {
+ shopping_tab_displayed: "true",
+ provider: "example",
+ source: "unknown",
+ tagged: "true",
+ is_private: "false",
+ is_shopping_page: "false",
+ partner_code: "ff",
+ },
+ adImpressions: [
+ {
+ component: SearchSERPTelemetryUtils.COMPONENTS.SHOPPING_TAB,
+ ads_loaded: "1",
+ ads_visible: "1",
+ ads_hidden: "0",
+ },
+ ],
+ },
+ ]);
+
+ info("Click on the non-sponsored component.");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#shopping",
+ {},
+ tab.linkedBrowser
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+ info("Assert no ads were visible or clicked on.");
+ assertCategorizationValues([
+ {
+ organic_category: "3",
+ organic_num_domains: "1",
+ organic_num_inconclusive: "0",
+ organic_num_unknown: "0",
+ sponsored_category: "0",
+ sponsored_num_domains: "0",
+ sponsored_num_inconclusive: "0",
+ sponsored_num_unknown: "0",
+ mappings_version: "1",
+ app_version: APP_MAJOR_VERSION,
+ channel: CHANNEL,
+ region: REGION,
+ partner_code: "ff",
+ provider: "example",
+ tagged: "true",
+ is_shopping_page: "false",
+ num_ads_clicked: "0",
+ num_ads_visible: "0",
+ },
+ ]);
+ }
+);
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js
new file mode 100644
index 0000000000..0196483b8c
--- /dev/null
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js
@@ -0,0 +1,302 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * This test ensures we are correctly submitting the custom ping for SERP
+ * categorization. (Please see the search component's Marionette tests for
+ * a test of the ping's submission upon startup.)
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ SERPCategorizationRecorder: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ TELEMETRY_CATEGORIZATION_KEY:
+ "resource:///modules/SearchSERPTelemetry.sys.mjs",
+});
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp:
+ /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetry/,
+ queryParamNames: ["s"],
+ codeParamName: "abc",
+ taggedCodes: ["ff"],
+ adServerAttributes: ["mozAttr"],
+ nonAdsLinkRegexps: [/^https:\/\/example.com/],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad/],
+ // The search telemetry entry responsible for targeting the specific results.
+ domainExtraction: {
+ ads: [
+ {
+ selectors: "[data-ad-domain]",
+ method: "data-attribute",
+ options: {
+ dataAttributeKey: "adDomain",
+ },
+ },
+ {
+ selectors: ".ad",
+ method: "href",
+ options: {
+ queryParamKey: "ad_domain",
+ },
+ },
+ ],
+ nonAds: [
+ {
+ selectors: "#results .organic a",
+ method: "href",
+ },
+ ],
+ },
+ components: [
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ default: true,
+ },
+ ],
+ },
+];
+
+const client = RemoteSettings(TELEMETRY_CATEGORIZATION_KEY);
+const db = client.db;
+
+function sleep(ms) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_setup(async function () {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ await db.clear();
+
+ let promise = waitForDomainToCategoriesUpdate();
+ await insertRecordIntoCollectionAndSync();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.serpEventTelemetryCategorization.enabled", true]],
+ });
+ await promise;
+
+ registerCleanupFunction(async () => {
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be un-initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesUninit();
+ }
+
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ resetTelemetry();
+ });
+});
+
+add_task(async function test_threshold_reached() {
+ resetTelemetry();
+
+ let oldThreshold = CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD;
+ // For testing, it's fine to categorize fewer SERPs before sending the ping.
+ CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD = 2;
+ SERPCategorizationRecorder.uninit();
+ SERPCategorizationRecorder.init();
+
+ Assert.equal(
+ null,
+ Glean.serp.categorization.testGetValue(),
+ "Should not have recorded any metrics yet."
+ );
+
+ let submitted = false;
+ GleanPings.serpCategorization.testBeforeNextSubmit(reason => {
+ submitted = true;
+ Assert.equal(
+ "threshold_reached",
+ reason,
+ "Ping submission reason should be 'threshold_reached'."
+ );
+ });
+
+ // Categorize first SERP, which results in one organic and one sponsored
+ // reporting.
+ let url = getSERPUrl("searchTelemetryDomainCategorizationReporting.html");
+ info("Load a sample SERP with organic and sponsored results.");
+ let promise = waitForPageWithCategorizedDomains();
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await promise;
+
+ Assert.equal(
+ false,
+ submitted,
+ "Ping should not be submitted before threshold is reached."
+ );
+
+ // Categorize second SERP, which results in one organic and one sponsored
+ // reporting.
+ url = getSERPUrl("searchTelemetryDomainExtraction.html");
+ info("Load a sample SERP with organic and sponsored results.");
+ promise = waitForPageWithCategorizedDomains();
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await promise;
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ Assert.equal(
+ true,
+ submitted,
+ "Ping should be submitted once threshold is reached."
+ );
+
+ CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD = oldThreshold;
+});
+
+add_task(async function test_quick_activity_to_inactivity_alternation() {
+ resetTelemetry();
+
+ Assert.equal(
+ null,
+ Glean.serp.categorization.testGetValue(),
+ "Should not have recorded any metrics yet."
+ );
+
+ let submitted = false;
+ GleanPings.serpCategorization.testBeforeNextSubmit(() => {
+ submitted = true;
+ });
+
+ let url = getSERPUrl("searchTelemetryDomainCategorizationReporting.html");
+ info("Load a sample SERP with organic and sponsored results.");
+ let promise = waitForPageWithCategorizedDomains();
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await promise;
+
+ let activityDetectedPromise = TestUtils.topicObserved(
+ "user-interaction-active"
+ );
+ // Simulate ~2.5 seconds of activity.
+ for (let i = 0; i < 25; i++) {
+ EventUtils.synthesizeKey("KEY_Enter");
+ await sleep(100);
+ }
+ await activityDetectedPromise;
+
+ let inactivityDetectedPromise = TestUtils.topicObserved(
+ "user-interaction-inactive"
+ );
+ await inactivityDetectedPromise;
+
+ Assert.equal(
+ false,
+ submitted,
+ "Ping should not be submitted after a quick alternation from activity to inactivity."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_submit_after_activity_then_inactivity() {
+ resetTelemetry();
+ let oldActivityLimit = Services.prefs.getIntPref(
+ "telemetry.fog.test.activity_limit"
+ );
+ Services.prefs.setIntPref("telemetry.fog.test.activity_limit", 2);
+
+ Assert.equal(
+ null,
+ Glean.serp.categorization.testGetValue(),
+ "Should not have recorded any metrics yet."
+ );
+
+ let submitted = false;
+ GleanPings.serpCategorization.testBeforeNextSubmit(reason => {
+ submitted = true;
+ Assert.equal(
+ "inactivity",
+ reason,
+ "Ping submission reason should be 'inactivity'."
+ );
+ });
+
+ let url = getSERPUrl("searchTelemetryDomainCategorizationReporting.html");
+ info("Load a sample SERP with organic and sponsored results.");
+ let promise = waitForPageWithCategorizedDomains();
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await promise;
+
+ let activityDetectedPromise = TestUtils.topicObserved(
+ "user-interaction-active"
+ );
+ // Simulate ~2.5 seconds of activity.
+ for (let i = 0; i < 25; i++) {
+ EventUtils.synthesizeKey("KEY_Enter");
+ await sleep(100);
+ }
+ await activityDetectedPromise;
+
+ let inactivityDetectedPromise = TestUtils.topicObserved(
+ "user-interaction-inactive"
+ );
+ await inactivityDetectedPromise;
+
+ Assert.equal(
+ true,
+ submitted,
+ "Ping should be submitted after 2+ seconds of activity, followed by inactivity."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ Services.prefs.setIntPref(
+ "telemetry.fog.test.activity_limit",
+ oldActivityLimit
+ );
+});
+
+add_task(async function test_no_observers_added_if_pref_is_off() {
+ resetTelemetry();
+
+ let prefOnActiveObserverCount = Array.from(
+ Services.obs.enumerateObservers("user-interaction-active")
+ ).length;
+ let prefOnInactiveObserverCount = Array.from(
+ Services.obs.enumerateObservers("user-interaction-inactive")
+ ).length;
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.serpEventTelemetryCategorization.enabled", false]],
+ });
+ await waitForDomainToCategoriesUninit();
+
+ let prefOffActiveObserverCount = Array.from(
+ Services.obs.enumerateObservers("user-interaction-active")
+ ).length;
+ let prefOffInactiveObserverCount = Array.from(
+ Services.obs.enumerateObservers("user-interaction-inactive")
+ ).length;
+
+ Assert.equal(
+ prefOnActiveObserverCount - prefOffActiveObserverCount,
+ 1,
+ "There should be one fewer active observer when the pref is off."
+ );
+ Assert.equal(
+ prefOnInactiveObserverCount - prefOffInactiveObserverCount,
+ 1,
+ "There should be one fewer inactive observer when the pref is off."
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await waitForDomainToCategoriesInit();
+});
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js
index 4c47b0b14a..7dbf605396 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js
@@ -78,6 +78,17 @@ add_setup(async function () {
Assert.equal(Region.home, "DE", "Region");
registerCleanupFunction(async () => {
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be un-initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesUninit();
+ }
+
Region._setHomeRegion(originalHomeRegion);
Region._setCurrentRegion(originalCurrentRegion);
@@ -113,6 +124,7 @@ add_task(async function test_categorize_page_with_different_region() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js
index 973f17b760..3c439844d7 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js
@@ -19,6 +19,7 @@ const TEST_PROVIDER_INFO = [
queryParamNames: ["s"],
codeParamName: "abc",
taggedCodes: ["ff"],
+ organicCodes: [],
adServerAttributes: ["mozAttr"],
nonAdsLinkRegexps: [],
extraAdServersRegexps: [
@@ -56,6 +57,9 @@ const TEST_PROVIDER_INFO = [
default: true,
},
],
+ shoppingTab: {
+ regexp: "&page=shop",
+ },
},
];
@@ -69,6 +73,10 @@ add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
let { record, attachment } = await insertRecordIntoCollection();
categorizationRecord = record;
categorizationAttachment = attachment;
@@ -82,7 +90,18 @@ add_setup(async function () {
await promise;
registerCleanupFunction(async () => {
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be un-initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesUninit();
+ }
SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ Services.telemetry.canRecordExtended = oldCanRecord;
resetTelemetry();
await db.clear();
});
@@ -115,6 +134,7 @@ add_task(async function test_categorization_reporting() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
@@ -147,6 +167,7 @@ add_task(async function test_no_reporting_if_download_failure() {
await promise;
await BrowserTestUtils.removeTab(tab);
+ // We should not record telemetry if attachments weren't downloaded.
assertCategorizationValues([]);
// Re-insert the attachment for other tests.
@@ -177,6 +198,7 @@ add_task(async function test_no_reporting_if_no_records() {
await promise;
await BrowserTestUtils.removeTab(tab);
+ // We should not record telemetry if there are no records.
assertCategorizationValues([]);
});
@@ -218,8 +240,46 @@ add_task(async function test_reporting_limited_to_10_domains_of_each_kind() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "12",
},
]);
});
+
+add_task(async function test_categorization_reporting_for_shopping_page() {
+ resetTelemetry();
+
+ let url = getSERPUrl("searchTelemetryDomainCategorizationReporting.html");
+ let shoppingUrl = new URL(url);
+ shoppingUrl.searchParams.set("page", "shop");
+ shoppingUrl = shoppingUrl.toString();
+ info("Load a sample shopping page SERP with organic and sponsored results.");
+ let promise = waitForPageWithCategorizedDomains();
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, shoppingUrl);
+ await promise;
+
+ await BrowserTestUtils.removeTab(tab);
+ assertCategorizationValues([
+ {
+ organic_category: "3",
+ organic_num_domains: "1",
+ organic_num_inconclusive: "0",
+ organic_num_unknown: "0",
+ sponsored_category: "4",
+ sponsored_num_domains: "2",
+ sponsored_num_inconclusive: "0",
+ sponsored_num_unknown: "0",
+ mappings_version: "1",
+ app_version: APP_MAJOR_VERSION,
+ channel: CHANNEL,
+ region: REGION,
+ partner_code: "ff",
+ provider: "example",
+ tagged: "true",
+ is_shopping_page: "true",
+ num_ads_clicked: "0",
+ num_ads_visible: "2",
+ },
+ ]);
+});
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js
index 9d3ac2c931..0e2d1c07fd 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js
@@ -87,9 +87,20 @@ add_setup(async function () {
await promise;
registerCleanupFunction(async () => {
- // The scheduler uses the mock idle service.
- SearchSERPCategorizationEventScheduler.uninit();
- SearchSERPCategorizationEventScheduler.init();
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be un-initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesUninit();
+ } else {
+ // The scheduler uses the mock idle service.
+ SearchSERPCategorizationEventScheduler.uninit();
+ SearchSERPCategorizationEventScheduler.init();
+ }
SearchSERPTelemetry.overrideSearchTelemetryForTests();
resetTelemetry();
});
@@ -126,6 +137,7 @@ add_task(async function test_categorize_serp_and_wait() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
@@ -170,6 +182,7 @@ add_task(async function test_categorize_serp_open_multiple_tabs() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
});
@@ -223,6 +236,7 @@ add_task(async function test_categorize_serp_close_tab_and_wait() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
@@ -276,6 +290,7 @@ add_task(async function test_categorize_serp_open_ad_and_wait() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "1",
num_ads_visible: "2",
},
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js
index c73e224eae..43c520a8d0 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js
@@ -92,9 +92,20 @@ add_setup(async function () {
await promise;
registerCleanupFunction(async () => {
- // The scheduler uses the mock idle service.
- SearchSERPCategorizationEventScheduler.uninit();
- SearchSERPCategorizationEventScheduler.init();
+ // Manually unload the pref so that we can check if we should wait for the
+ // the categories map to be un-initialized.
+ await SpecialPowers.popPrefEnv();
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.search.serpEventTelemetryCategorization.enabled"
+ )
+ ) {
+ await waitForDomainToCategoriesUninit();
+ } else {
+ // The scheduler uses the mock idle service.
+ SearchSERPCategorizationEventScheduler.uninit();
+ SearchSERPCategorizationEventScheduler.init();
+ }
CATEGORIZATION_SETTINGS.WAKE_TIMEOUT_MS = oldWakeTimeout;
SearchSERPTelemetry.overrideSearchTelemetryForTests();
resetTelemetry();
@@ -138,6 +149,7 @@ add_task(async function test_categorize_serp_and_sleep() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
@@ -195,6 +207,7 @@ add_task(async function test_categorize_serp_and_sleep_not_long_enough() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
num_ads_visible: "2",
},
diff --git a/browser/components/search/test/browser/telemetry/head.js b/browser/components/search/test/browser/telemetry/head.js
index b798099bdd..ecc6e38fa9 100644
--- a/browser/components/search/test/browser/telemetry/head.js
+++ b/browser/components/search/test/browser/telemetry/head.js
@@ -4,11 +4,14 @@
ChromeUtils.defineESModuleGetters(this, {
ADLINK_CHECK_TIMEOUT_MS:
"resource:///actors/SearchSERPTelemetryChild.sys.mjs",
+ CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs",
CustomizableUITestUtils:
"resource://testing-common/CustomizableUITestUtils.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
SEARCH_TELEMETRY_SHARED: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ SearchSERPDomainToCategoriesMap:
+ "resource:///modules/SearchSERPTelemetry.sys.mjs",
SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs",
SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
@@ -193,11 +196,10 @@ async function assertSearchSourcesTelemetry(
}
function resetTelemetry() {
- // TODO Bug 1868476: Replace when we're using Glean telemetry.
- fakeTelemetryStorage = [];
searchCounts.clear();
Services.telemetry.clearScalars();
Services.fog.testResetFOG();
+ SERPCategorizationRecorder.testReset();
}
/**
@@ -377,23 +379,6 @@ function assertSERPTelemetry(expectedEvents) {
);
}
-// TODO Bug 1868476: Replace when we're using Glean telemetry.
-let categorizationSandbox;
-let fakeTelemetryStorage = [];
-add_setup(function () {
- categorizationSandbox = sinon.createSandbox();
- categorizationSandbox
- .stub(SERPCategorizationRecorder, "recordCategorizationTelemetry")
- .callsFake(input => {
- fakeTelemetryStorage.push(input);
- });
-
- registerCleanupFunction(() => {
- categorizationSandbox.restore();
- fakeTelemetryStorage = [];
- });
-});
-
async function openSerpInNewTab(url, expectedAds = true) {
let promise;
if (expectedAds) {
@@ -435,12 +420,11 @@ async function synthesizePageAction({
}
function assertCategorizationValues(expectedResults) {
- // TODO Bug 1868476: Replace with calls to Glean telemetry.
- let actualResults = [...fakeTelemetryStorage];
+ let actualResults = Glean.serp.categorization.testGetValue() ?? [];
Assert.equal(
- expectedResults.length,
actualResults.length,
+ expectedResults.length,
"Should have the correct number of categorization impressions."
);
@@ -458,7 +442,7 @@ function assertCategorizationValues(expectedResults) {
}
}
for (let actual of actualResults) {
- for (let key in actual) {
+ for (let key in actual.extra) {
keys.add(key);
}
}
@@ -467,14 +451,21 @@ function assertCategorizationValues(expectedResults) {
for (let index = 0; index < expectedResults.length; ++index) {
info(`Checking categorization at index: ${index}`);
let expected = expectedResults[index];
- let actual = actualResults[index];
+ let actual = actualResults[index].extra;
+
+ Assert.ok(
+ Number(actual?.organic_num_domains) <=
+ CATEGORIZATION_SETTINGS.MAX_DOMAINS_TO_CATEGORIZE,
+ "Number of organic domains categorized should not exceed threshold."
+ );
+
+ Assert.ok(
+ Number(actual?.sponsored_num_domains) <=
+ CATEGORIZATION_SETTINGS.MAX_DOMAINS_TO_CATEGORIZE,
+ "Number of sponsored domains categorized should not exceed threshold."
+ );
+
for (let key of keys) {
- // TODO Bug 1868476: This conversion to strings is to mimic Glean
- // converting all values into strings. Once we receive real values from
- // Glean, it can be removed.
- if (actual[key] != null && typeof actual[key] !== "string") {
- actual[key] = actual[key].toString();
- }
Assert.equal(
actual[key],
expected[key],
@@ -508,6 +499,14 @@ function waitForDomainToCategoriesUpdate() {
return TestUtils.topicObserved("domain-to-categories-map-update-complete");
}
+function waitForDomainToCategoriesInit() {
+ return TestUtils.topicObserved("domain-to-categories-map-init");
+}
+
+function waitForDomainToCategoriesUninit() {
+ return TestUtils.topicObserved("domain-to-categories-map-uninit");
+}
+
registerCleanupFunction(async () => {
await PlacesUtils.history.clear();
});
diff --git a/browser/components/search/test/browser/telemetry/searchTelemetryDomainCategorizationReportingWithoutAds.html b/browser/components/search/test/browser/telemetry/searchTelemetryDomainCategorizationReportingWithoutAds.html
new file mode 100644
index 0000000000..13d023e45d
--- /dev/null
+++ b/browser/components/search/test/browser/telemetry/searchTelemetryDomainCategorizationReportingWithoutAds.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <div>
+ <a id="shopping" href="https://www.example.org/shopping">Shopping</a>
+ </div>
+ <div id="results">
+ <div class="organic">
+ <a href="https://www.foobar.org">Link</a>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html b/browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html
index 28c31af959..fe52bb8b48 100644
--- a/browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html
+++ b/browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html
@@ -256,6 +256,37 @@
</div>
</div>
</div>
+
+ <div id="test26">
+ <div id="b_results">
+ <div class="b_algo">
+ <div class="b_attribution">
+ <cite>https://organic.com/cats</cite>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="test27">
+ <div id="b_results">
+ <div class="b_algo">
+ <div class="b_attribution">
+ <cite>https://organic.com/testing?q=cats</cite>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="test28">
+ <div id="b_results">
+ <div class="b_algo">
+ <div class="b_attribution">
+ <span>HTTPS</span>
+ <cite>en.wikipedia.org/wiki/Cat</cite>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</body>
</html>
diff --git a/browser/components/search/test/marionette/manifest.toml b/browser/components/search/test/marionette/manifest.toml
index 152442bc5b..9cc88e9f84 100644
--- a/browser/components/search/test/marionette/manifest.toml
+++ b/browser/components/search/test/marionette/manifest.toml
@@ -1,4 +1,6 @@
[DEFAULT]
run-if = ["buildapp == 'browser'"]
+["include:telemetry/manifest.toml"]
+
["test_engines_on_restart.py"]
diff --git a/browser/components/search/test/marionette/telemetry/manifest.toml b/browser/components/search/test/marionette/telemetry/manifest.toml
new file mode 100644
index 0000000000..1fe35945c9
--- /dev/null
+++ b/browser/components/search/test/marionette/telemetry/manifest.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+run-if = ["buildapp == 'browser'"]
+
+["test_ping_submitted.py"]
diff --git a/browser/components/search/test/marionette/telemetry/test_ping_submitted.py b/browser/components/search/test/marionette/telemetry/test_ping_submitted.py
new file mode 100644
index 0000000000..cefe2d72d1
--- /dev/null
+++ b/browser/components/search/test/marionette/telemetry/test_ping_submitted.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_driver import Wait
+from marionette_harness.marionette_test import MarionetteTestCase
+
+
+class TestPingSubmitted(MarionetteTestCase):
+ def setUp(self):
+ super(TestPingSubmitted, self).setUp()
+
+ self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+
+ self.marionette.enforce_gecko_prefs(
+ {
+ "datareporting.healthreport.uploadEnabled": True,
+ "telemetry.fog.test.localhost_port": 3000,
+ "browser.search.log": True,
+ }
+ )
+ # The categorization ping is submitted on startup. If anything delays
+ # its initialization, turning the preference on and immediately
+ # attaching a categorization event could result in the ping being
+ # submitted after the test event is reported but before the browser
+ # restarts.
+ script = """
+ let [outerResolve] = arguments;
+ (async () => {
+ if (!Services.prefs.getBoolPref("browser.search.serpEventTelemetryCategorization.enabled")) {
+ let inited = new Promise(innerResolve => {
+ Services.obs.addObserver(function callback() {
+ Services.obs.removeObserver(callback, "categorization-recorder-init");
+ innerResolve();
+ }, "categorization-recorder-init");
+ });
+ Services.prefs.setBoolPref("browser.search.serpEventTelemetryCategorization.enabled", true);
+ await inited;
+ }
+ })().then(outerResolve);
+ """
+ self.marionette.execute_async_script(script)
+
+ def test_ping_submit_on_start(self):
+ # Record an event for the ping to eventually submit.
+ self.marionette.execute_script(
+ """
+ Glean.serp.categorization.record({
+ organic_category: "3",
+ organic_num_domains: "1",
+ organic_num_inconclusive: "0",
+ organic_num_unknown: "0",
+ sponsored_category: "4",
+ sponsored_num_domains: "2",
+ sponsored_num_inconclusive: "0",
+ sponsored_num_unknown: "0",
+ mappings_version: "1",
+ app_version: "124",
+ channel: "nightly",
+ region: "US",
+ partner_code: "ff",
+ provider: "example",
+ tagged: "true",
+ num_ads_clicked: "0",
+ num_ads_visible: "2",
+ });
+ """
+ )
+
+ Wait(self.marionette, timeout=60).until(
+ lambda _: self.marionette.execute_script(
+ """
+ return (Glean.serp.categorization.testGetValue()?.length ?? 0) == 1;
+ """
+ ),
+ message="Should have recorded a SERP categorization event before restart.",
+ )
+
+ self.marionette.restart(clean=False, in_app=True)
+
+ Wait(self.marionette, timeout=60).until(
+ lambda _: self.marionette.execute_script(
+ """
+ return (Glean.serp.categorization.testGetValue()?.length ?? 0) == 0;
+ """
+ ),
+ message="SERP categorization should have been sent some time after restart.",
+ )
diff --git a/browser/components/search/test/unit/corruptDB.sqlite b/browser/components/search/test/unit/corruptDB.sqlite
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..3396f38238
--- /dev/null
+++ b/browser/components/search/test/unit/test_ui_schemas_valid.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let schemas = [
+ ["search-telemetry-v2-schema.json", "search-telemetry-v2-ui-schema.json"],
+];
+
+async function checkUISchemaValid(configSchema, uiSchema) {
+ for (let key of Object.keys(configSchema.properties)) {
+ Assert.ok(
+ uiSchema["ui:order"].includes(key),
+ `Should have ${key} listed at the top-level of the ui schema`
+ );
+ }
+}
+
+add_task(async function test_ui_schemas_valid() {
+ for (let [schema, uiSchema] of schemas) {
+ info(`Validating ${uiSchema} has every top-level from ${schema}`);
+ let schemaData = await IOUtils.readJSON(
+ PathUtils.join(do_get_cwd().path, schema)
+ );
+ let uiSchemaData = await IOUtils.readJSON(
+ PathUtils.join(do_get_cwd().path, uiSchema)
+ );
+
+ await checkUISchemaValid(schemaData, uiSchemaData);
+ }
+});
diff --git a/browser/components/search/test/unit/xpcshell.toml b/browser/components/search/test/unit/xpcshell.toml
index 423d218d19..24e1d78eb5 100644
--- a/browser/components/search/test/unit/xpcshell.toml
+++ b/browser/components/search/test/unit/xpcshell.toml
@@ -6,6 +6,9 @@ prefs = ["browser.search.log=true"]
skip-if = ["os == 'android'"] # bug 1730213
firefox-appdir = "browser"
+["test_domain_to_categories_store.js"]
+support-files = ["corruptDB.sqlite"]
+
["test_search_telemetry_categorization_logic.js"]
["test_search_telemetry_categorization_sync.js"]
@@ -14,7 +17,13 @@ prefs = ["browser.search.serpEventTelemetryCategorization.enabled=true"]
["test_search_telemetry_compare_urls.js"]
["test_search_telemetry_config_validation.js"]
-support-files = ["../../schema/search-telemetry-schema.json"]
+support-files = ["../../schema/search-telemetry-v2-schema.json"]
+
+["test_ui_schemas_valid.js"]
+support-files = [
+ "../../schema/search-telemetry-v2-schema.json",
+ "../../schema/search-telemetry-v2-ui-schema.json",
+]
["test_urlTelemetry.js"]
diff --git a/browser/components/sessionstore/ContentRestore.sys.mjs b/browser/components/sessionstore/ContentRestore.sys.mjs
deleted file mode 100644
index e55772cab3..0000000000
--- a/browser/components/sessionstore/ContentRestore.sys.mjs
+++ /dev/null
@@ -1,435 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-const lazy = {};
-
-ChromeUtils.defineESModuleGetters(lazy, {
- SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
- Utils: "resource://gre/modules/sessionstore/Utils.sys.mjs",
-});
-
-/**
- * This module implements the content side of session restoration. The chrome
- * side is handled by SessionStore.sys.mjs. The functions in this module are called
- * by content-sessionStore.js based on messages received from SessionStore.sys.mjs
- * (or, in one case, based on a "load" event). Each tab has its own
- * ContentRestore instance, constructed by content-sessionStore.js.
- *
- * In a typical restore, content-sessionStore.js will call the following based
- * on messages and events it receives:
- *
- * restoreHistory(tabData, loadArguments, callbacks)
- * Restores the tab's history and session cookies.
- * restoreTabContent(loadArguments, finishCallback)
- * Starts loading the data for the current page to restore.
- * restoreDocument()
- * Restore form and scroll data.
- *
- * When the page has been loaded from the network, we call finishCallback. It
- * should send a message to SessionStore.sys.mjs, which may cause other tabs to be
- * restored.
- *
- * When the page has finished loading, a "load" event will trigger in
- * content-sessionStore.js, which will call restoreDocument. At that point,
- * form data is restored and the restore is complete.
- *
- * At any time, SessionStore.sys.mjs can cancel the ongoing restore by sending a
- * reset message, which causes resetRestore to be called. At that point it's
- * legal to begin another restore.
- */
-export function ContentRestore(chromeGlobal) {
- let internal = new ContentRestoreInternal(chromeGlobal);
- let external = {};
-
- let EXPORTED_METHODS = [
- "restoreHistory",
- "restoreTabContent",
- "restoreDocument",
- "resetRestore",
- ];
-
- for (let method of EXPORTED_METHODS) {
- external[method] = internal[method].bind(internal);
- }
-
- return Object.freeze(external);
-}
-
-function ContentRestoreInternal(chromeGlobal) {
- this.chromeGlobal = chromeGlobal;
-
- // The following fields are only valid during certain phases of the restore
- // process.
-
- // The tabData for the restore. Set in restoreHistory and removed in
- // restoreTabContent.
- this._tabData = null;
-
- // Contains {entry, scrollPositions, formdata}, where entry is a
- // single entry from the tabData.entries array. Set in
- // restoreTabContent and removed in restoreDocument.
- this._restoringDocument = null;
-
- // This listener is used to detect reloads on restoring tabs. Set in
- // restoreHistory and removed in restoreTabContent.
- this._historyListener = null;
-
- // This listener detects when a pending tab starts loading (when not
- // initiated by sessionstore) and when a restoring tab has finished loading
- // data from the network. Set in restoreHistory() and restoreTabContent(),
- // removed in resetRestore().
- this._progressListener = null;
-}
-
-/**
- * The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are
- * public.
- */
-ContentRestoreInternal.prototype = {
- get docShell() {
- return this.chromeGlobal.docShell;
- },
-
- /**
- * Starts the process of restoring a tab. The tabData to be restored is passed
- * in here and used throughout the restoration. The epoch (which must be
- * non-zero) is passed through to all the callbacks. If a load in the tab
- * is started while it is pending, the appropriate callbacks are called.
- */
- restoreHistory(tabData, loadArguments, callbacks) {
- this._tabData = tabData;
-
- // In case about:blank isn't done yet.
- let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
- webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
-
- // Make sure currentURI is set so that switch-to-tab works before the tab is
- // restored. We'll reset this to about:blank when we try to restore the tab
- // to ensure that docshell doeesn't get confused. Don't bother doing this if
- // we're restoring immediately due to a process switch. It just causes the
- // URL bar to be temporarily blank.
- let activeIndex = tabData.index - 1;
- let activePageData = tabData.entries[activeIndex] || {};
- let uri = activePageData.url || null;
- if (uri && !loadArguments) {
- webNavigation.setCurrentURIForSessionStore(Services.io.newURI(uri));
- }
-
- lazy.SessionHistory.restore(this.docShell, tabData);
-
- // Add a listener to watch for reloads.
- let listener = new HistoryListener(this.docShell, () => {
- // On reload, restore tab contents.
- this.restoreTabContent(null, false, callbacks.onLoadFinished);
- });
-
- webNavigation.sessionHistory.legacySHistory.addSHistoryListener(listener);
- this._historyListener = listener;
-
- // Make sure to reset the capabilities and attributes in case this tab gets
- // reused.
- SessionStoreUtils.restoreDocShellCapabilities(
- this.docShell,
- tabData.disallow
- );
-
- // Add a progress listener to correctly handle browser.loadURI()
- // calls from foreign code.
- this._progressListener = new ProgressListener(this.docShell, {
- onStartRequest: () => {
- // Some code called browser.loadURI() on a pending tab. It's safe to
- // assume we don't care about restoring scroll or form data.
- this._tabData = null;
-
- // Listen for the tab to finish loading.
- this.restoreTabContentStarted(callbacks.onLoadFinished);
-
- // Notify the parent.
- callbacks.onLoadStarted();
- },
- });
- },
-
- /**
- * Start loading the current page. When the data has finished loading from the
- * network, finishCallback is called. Returns true if the load was successful.
- */
- restoreTabContent(loadArguments, isRemotenessUpdate, finishCallback) {
- let tabData = this._tabData;
- this._tabData = null;
-
- let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
-
- // Listen for the tab to finish loading.
- this.restoreTabContentStarted(finishCallback);
-
- // Reset the current URI to about:blank. We changed it above for
- // switch-to-tab, but now it must go back to the correct value before the
- // load happens. Don't bother doing this if we're restoring immediately
- // due to a process switch.
- if (!isRemotenessUpdate) {
- webNavigation.setCurrentURIForSessionStore(
- Services.io.newURI("about:blank")
- );
- }
-
- try {
- if (loadArguments) {
- // If the load was started in another process, and the in-flight channel
- // was redirected into this process, resume that load within our process.
- //
- // NOTE: In this case `isRemotenessUpdate` must be true.
- webNavigation.resumeRedirectedLoad(
- loadArguments.redirectLoadSwitchId,
- loadArguments.redirectHistoryIndex
- );
- } else if (tabData.userTypedValue && tabData.userTypedClear) {
- // If the user typed a URL into the URL bar and hit enter right before
- // we crashed, we want to start loading that page again. A non-zero
- // userTypedClear value means that the load had started.
- // Load userTypedValue and fix up the URL if it's partial/broken.
- let loadURIOptions = {
- triggeringPrincipal:
- Services.scriptSecurityManager.getSystemPrincipal(),
- loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
- };
- webNavigation.fixupAndLoadURIString(
- tabData.userTypedValue,
- loadURIOptions
- );
- } else if (tabData.entries.length) {
- // Stash away the data we need for restoreDocument.
- this._restoringDocument = {
- formdata: tabData.formdata || {},
- scrollPositions: tabData.scroll || {},
- };
-
- // In order to work around certain issues in session history, we need to
- // force session history to update its internal index and call reload
- // instead of gotoIndex. See bug 597315.
- let history = webNavigation.sessionHistory.legacySHistory;
- history.reloadCurrentEntry();
- } else {
- // If there's nothing to restore, we should still blank the page.
- let loadURIOptions = {
- triggeringPrincipal:
- Services.scriptSecurityManager.getSystemPrincipal(),
- loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
- // Specify an override to force the load to finish in the current
- // process, as tests rely on this behaviour for non-fission session
- // restore.
- remoteTypeOverride: Services.appinfo.remoteType,
- };
- webNavigation.loadURI(
- Services.io.newURI("about:blank"),
- loadURIOptions
- );
- }
-
- return true;
- } catch (ex) {
- if (ex instanceof Ci.nsIException) {
- // Ignore page load errors, but return false to signal that the load never
- // happened.
- return false;
- }
- }
- return null;
- },
-
- /**
- * To be called after restoreHistory(). Removes all listeners needed for
- * pending tabs and makes sure to notify when the tab finished loading.
- */
- restoreTabContentStarted(finishCallback) {
- // The reload listener is no longer needed.
- this._historyListener.uninstall();
- this._historyListener = null;
-
- // Remove the old progress listener.
- this._progressListener.uninstall();
-
- // We're about to start a load. This listener will be called when the load
- // has finished getting everything from the network.
- this._progressListener = new ProgressListener(this.docShell, {
- onStopRequest: () => {
- // Call resetRestore() to reset the state back to normal. The data
- // needed for restoreDocument() (which hasn't happened yet) will
- // remain in _restoringDocument.
- this.resetRestore();
-
- finishCallback();
- },
- });
- },
-
- /**
- * Finish restoring the tab by filling in form data and setting the scroll
- * position. The restore is complete when this function exits. It should be
- * called when the "load" event fires for the restoring tab. Returns true
- * if we're restoring a document.
- */
- restoreDocument() {
- if (!this._restoringDocument) {
- return;
- }
-
- let { formdata, scrollPositions } = this._restoringDocument;
- this._restoringDocument = null;
-
- let window = this.docShell.domWindow;
-
- // Restore form data.
- lazy.Utils.restoreFrameTreeData(window, formdata, (frame, data) => {
- // restore() will return false, and thus abort restoration for the
- // current |frame| and its descendants, if |data.url| is given but
- // doesn't match the loaded document's URL.
- return SessionStoreUtils.restoreFormData(frame.document, data);
- });
-
- // Restore scroll data.
- lazy.Utils.restoreFrameTreeData(window, scrollPositions, (frame, data) => {
- if (data.scroll) {
- SessionStoreUtils.restoreScrollPosition(frame, data);
- }
- });
- },
-
- /**
- * Cancel an ongoing restore. This function can be called any time between
- * restoreHistory and restoreDocument.
- *
- * This function is called externally (if a restore is canceled) and
- * internally (when the loads for a restore have finished). In the latter
- * case, it's called before restoreDocument, so it cannot clear
- * _restoringDocument.
- */
- resetRestore() {
- this._tabData = null;
-
- if (this._historyListener) {
- this._historyListener.uninstall();
- }
- this._historyListener = null;
-
- if (this._progressListener) {
- this._progressListener.uninstall();
- }
- this._progressListener = null;
- },
-};
-
-/*
- * This listener detects when a page being restored is reloaded. It triggers a
- * callback and cancels the reload. The callback will send a message to
- * SessionStore.sys.mjs so that it can restore the content immediately.
- */
-function HistoryListener(docShell, callback) {
- let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
- webNavigation.sessionHistory.legacySHistory.addSHistoryListener(this);
-
- this.webNavigation = webNavigation;
- this.callback = callback;
-}
-HistoryListener.prototype = {
- QueryInterface: ChromeUtils.generateQI([
- "nsISHistoryListener",
- "nsISupportsWeakReference",
- ]),
-
- uninstall() {
- let shistory = this.webNavigation.sessionHistory.legacySHistory;
- if (shistory) {
- shistory.removeSHistoryListener(this);
- }
- },
-
- OnHistoryGotoIndex() {},
- OnHistoryPurge() {},
- OnHistoryReplaceEntry() {},
-
- // This will be called for a pending tab when loadURI(uri) is called where
- // the given |uri| only differs in the fragment.
- OnHistoryNewEntry(newURI) {
- let currentURI = this.webNavigation.currentURI;
-
- // Ignore new SHistory entries with the same URI as those do not indicate
- // a navigation inside a document by changing the #hash part of the URL.
- // We usually hit this when purging session history for browsers.
- if (currentURI && currentURI.spec == newURI.spec) {
- return;
- }
-
- // Reset the tab's URL to what it's actually showing. Without this loadURI()
- // would use the current document and change the displayed URL only.
- this.webNavigation.setCurrentURIForSessionStore(
- Services.io.newURI("about:blank")
- );
-
- // Kick off a new load so that we navigate away from about:blank to the
- // new URL that was passed to loadURI(). The new load will cause a
- // STATE_START notification to be sent and the ProgressListener will then
- // notify the parent and do the rest.
- let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
- let loadURIOptions = {
- triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
- loadFlags,
- };
- this.webNavigation.loadURI(newURI, loadURIOptions);
- },
-
- OnHistoryReload() {
- this.callback();
-
- // Cancel the load.
- return false;
- },
-};
-
-/**
- * This class informs SessionStore.sys.mjs whenever the network requests for a
- * restoring page have completely finished. We only restore three tabs
- * simultaneously, so this is the signal for SessionStore.sys.mjs to kick off
- * another restore (if there are more to do).
- *
- * The progress listener is also used to be notified when a load not initiated
- * by sessionstore starts. Pending tabs will then need to be marked as no
- * longer pending.
- */
-function ProgressListener(docShell, callbacks) {
- let webProgress = docShell
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebProgress);
- webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
-
- this.webProgress = webProgress;
- this.callbacks = callbacks;
-}
-
-ProgressListener.prototype = {
- QueryInterface: ChromeUtils.generateQI([
- "nsIWebProgressListener",
- "nsISupportsWeakReference",
- ]),
-
- uninstall() {
- this.webProgress.removeProgressListener(this);
- },
-
- onStateChange(webProgress, request, stateFlags, status) {
- let { STATE_IS_WINDOW, STATE_STOP, STATE_START } =
- Ci.nsIWebProgressListener;
- if (!webProgress.isTopLevel || !(stateFlags & STATE_IS_WINDOW)) {
- return;
- }
-
- if (stateFlags & STATE_START && this.callbacks.onStartRequest) {
- this.callbacks.onStartRequest();
- }
-
- if (stateFlags & STATE_STOP && this.callbacks.onStopRequest) {
- this.callbacks.onStopRequest();
- }
- },
-};
diff --git a/browser/components/sessionstore/ContentSessionStore.sys.mjs b/browser/components/sessionstore/ContentSessionStore.sys.mjs
deleted file mode 100644
index 44f59cd39d..0000000000
--- a/browser/components/sessionstore/ContentSessionStore.sys.mjs
+++ /dev/null
@@ -1,685 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-import {
- clearTimeout,
- setTimeoutWithTarget,
-} from "resource://gre/modules/Timer.sys.mjs";
-
-const lazy = {};
-
-ChromeUtils.defineESModuleGetters(lazy, {
- ContentRestore: "resource:///modules/sessionstore/ContentRestore.sys.mjs",
- SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
-});
-
-// This pref controls whether or not we send updates to the parent on a timeout
-// or not, and should only be used for tests or debugging.
-const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates";
-
-const PREF_INTERVAL = "browser.sessionstore.interval";
-
-const kNoIndex = Number.MAX_SAFE_INTEGER;
-const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
-
-class Handler {
- constructor(store) {
- this.store = store;
- }
-
- get contentRestore() {
- return this.store.contentRestore;
- }
-
- get contentRestoreInitialized() {
- return this.store.contentRestoreInitialized;
- }
-
- get mm() {
- return this.store.mm;
- }
-
- get messageQueue() {
- return this.store.messageQueue;
- }
-}
-
-/**
- * Listens for and handles content events that we need for the
- * session store service to be notified of state changes in content.
- */
-class EventListener extends Handler {
- constructor(store) {
- super(store);
-
- SessionStoreUtils.addDynamicFrameFilteredListener(
- this.mm,
- "load",
- this,
- true
- );
- }
-
- handleEvent(event) {
- let { content } = this.mm;
-
- // Ignore load events from subframes.
- if (event.target != content.document) {
- return;
- }
-
- if (content.document.documentURI.startsWith("about:reader")) {
- if (
- event.type == "load" &&
- !content.document.body.classList.contains("loaded")
- ) {
- // Don't restore the scroll position of an about:reader page at this
- // point; listen for the custom event dispatched from AboutReader.sys.mjs.
- content.addEventListener("AboutReaderContentReady", this);
- return;
- }
-
- content.removeEventListener("AboutReaderContentReady", this);
- }
-
- if (this.contentRestoreInitialized) {
- // Restore the form data and scroll position.
- this.contentRestore.restoreDocument();
- }
- }
-}
-
-/**
- * Listens for changes to the session history. Whenever the user navigates
- * we will collect URLs and everything belonging to session history.
- *
- * Causes a SessionStore:update message to be sent that contains the current
- * session history.
- *
- * Example:
- * {entries: [{url: "about:mozilla", ...}, ...], index: 1}
- */
-class SessionHistoryListener extends Handler {
- constructor(store) {
- super(store);
-
- this._fromIdx = kNoIndex;
-
- // By adding the SHistoryListener immediately, we will unfortunately be
- // notified of every history entry as the tab is restored. We don't bother
- // waiting to add the listener later because these notifications are cheap.
- // We will likely only collect once since we are batching collection on
- // a delay.
- this.mm.docShell
- .QueryInterface(Ci.nsIWebNavigation)
- .sessionHistory.legacySHistory.addSHistoryListener(this); // OK in non-geckoview
-
- let webProgress = this.mm.docShell
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebProgress);
-
- webProgress.addProgressListener(
- this,
- Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
- );
-
- // Collect data if we start with a non-empty shistory.
- if (!lazy.SessionHistory.isEmpty(this.mm.docShell)) {
- this.collect();
- // When a tab is detached from the window, for the new window there is a
- // new SessionHistoryListener created. Normally it is empty at this point
- // but in a test env. the initial about:blank might have a children in which
- // case we fire off a history message here with about:blank in it. If we
- // don't do it ASAP then there is going to be a browser swap and the parent
- // will be all confused by that message.
- this.store.messageQueue.send();
- }
-
- // Listen for page title changes.
- this.mm.addEventListener("DOMTitleChanged", this);
- }
-
- get mm() {
- return this.store.mm;
- }
-
- uninit() {
- let sessionHistory = this.mm.docShell.QueryInterface(
- Ci.nsIWebNavigation
- ).sessionHistory;
- if (sessionHistory) {
- sessionHistory.legacySHistory.removeSHistoryListener(this); // OK in non-geckoview
- }
- }
-
- collect() {
- // We want to send down a historychange even for full collects in case our
- // session history is a partial session history, in which case we don't have
- // enough information for a full update. collectFrom(-1) tells the collect
- // function to collect all data avaliable in this process.
- if (this.mm.docShell) {
- this.collectFrom(-1);
- }
- }
-
- // History can grow relatively big with the nested elements, so if we don't have to, we
- // don't want to send the entire history all the time. For a simple optimization
- // we keep track of the smallest index from after any change has occured and we just send
- // the elements from that index. If something more complicated happens we just clear it
- // and send the entire history. We always send the additional info like the current selected
- // index (so for going back and forth between history entries we set the index to kLastIndex
- // if nothing else changed send an empty array and the additonal info like the selected index)
- collectFrom(idx) {
- if (this._fromIdx <= idx) {
- // If we already know that we need to update history fromn index N we can ignore any changes
- // tha happened with an element with index larger than N.
- // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything
- // here, and in case of navigation in the history back and forth we use kLastIndex which ignores
- // only the subsequent navigations, but not any new elements added.
- return;
- }
-
- this._fromIdx = idx;
- this.store.messageQueue.push("historychange", () => {
- if (this._fromIdx === kNoIndex) {
- return null;
- }
-
- let history = lazy.SessionHistory.collect(
- this.mm.docShell,
- this._fromIdx
- );
- this._fromIdx = kNoIndex;
- return history;
- });
- }
-
- handleEvent(event) {
- this.collect();
- }
-
- OnHistoryNewEntry(newURI, oldIndex) {
- // Collect the current entry as well, to make sure to collect any changes
- // that were made to the entry while the document was active.
- this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1);
- }
-
- OnHistoryGotoIndex() {
- // We ought to collect the previously current entry as well, see bug 1350567.
- this.collectFrom(kLastIndex);
- }
-
- OnHistoryPurge() {
- this.collect();
- }
-
- OnHistoryReload() {
- this.collect();
- return true;
- }
-
- OnHistoryReplaceEntry() {
- this.collect();
- }
-
- /**
- * @see nsIWebProgressListener.onStateChange
- */
- onStateChange(webProgress, request, stateFlags, status) {
- // Ignore state changes for subframes because we're only interested in the
- // top-document starting or stopping its load.
- if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) {
- return;
- }
-
- // onStateChange will be fired when loading the initial about:blank URI for
- // a browser, which we don't actually care about. This is particularly for
- // the case of unrestored background tabs, where the content has not yet
- // been restored: we don't want to accidentally send any updates to the
- // parent when the about:blank placeholder page has loaded.
- if (!this.mm.docShell.hasLoadedNonBlankURI) {
- return;
- }
-
- if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
- this.collect();
- } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
- this.collect();
- }
- }
-}
-SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([
- "nsIWebProgressListener",
- "nsISHistoryListener",
- "nsISupportsWeakReference",
-]);
-
-/**
- * A message queue that takes collected data and will take care of sending it
- * to the chrome process. It allows flushing using synchronous messages and
- * takes care of any race conditions that might occur because of that. Changes
- * will be batched if they're pushed in quick succession to avoid a message
- * flood.
- */
-class MessageQueue extends Handler {
- constructor(store) {
- super(store);
-
- /**
- * A map (string -> lazy fn) holding lazy closures of all queued data
- * collection routines. These functions will return data collected from the
- * docShell.
- */
- this._data = new Map();
-
- /**
- * The delay (in ms) used to delay sending changes after data has been
- * invalidated.
- */
- this.BATCH_DELAY_MS = 1000;
-
- /**
- * The minimum idle period (in ms) we need for sending data to chrome process.
- */
- this.NEEDED_IDLE_PERIOD_MS = 5;
-
- /**
- * Timeout for waiting an idle period to send data. We will set this from
- * the pref "browser.sessionstore.interval".
- */
- this._timeoutWaitIdlePeriodMs = null;
-
- /**
- * The current timeout ID, null if there is no queue data. We use timeouts
- * to damp a flood of data changes and send lots of changes as one batch.
- */
- this._timeout = null;
-
- /**
- * Whether or not sending batched messages on a timer is disabled. This should
- * only be used for debugging or testing. If you need to access this value,
- * you should probably use the timeoutDisabled getter.
- */
- this._timeoutDisabled = false;
-
- /**
- * True if there is already a send pending idle dispatch, set to prevent
- * scheduling more than one. If false there may or may not be one scheduled.
- */
- this._idleScheduled = false;
-
- this.timeoutDisabled = Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
- this._timeoutWaitIdlePeriodMs = Services.prefs.getIntPref(PREF_INTERVAL);
-
- Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this);
- Services.prefs.addObserver(PREF_INTERVAL, this);
- }
-
- /**
- * True if batched messages are not being fired on a timer. This should only
- * ever be true when debugging or during tests.
- */
- get timeoutDisabled() {
- return this._timeoutDisabled;
- }
-
- /**
- * Disables sending batched messages on a timer. Also cancels any pending
- * timers.
- */
- set timeoutDisabled(val) {
- this._timeoutDisabled = val;
-
- if (val && this._timeout) {
- clearTimeout(this._timeout);
- this._timeout = null;
- }
- }
-
- uninit() {
- Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this);
- Services.prefs.removeObserver(PREF_INTERVAL, this);
- this.cleanupTimers();
- }
-
- /**
- * Cleanup pending idle callback and timer.
- */
- cleanupTimers() {
- this._idleScheduled = false;
- if (this._timeout) {
- clearTimeout(this._timeout);
- this._timeout = null;
- }
- }
-
- observe(subject, topic, data) {
- if (topic == "nsPref:changed") {
- switch (data) {
- case TIMEOUT_DISABLED_PREF:
- this.timeoutDisabled = Services.prefs.getBoolPref(
- TIMEOUT_DISABLED_PREF
- );
- break;
- case PREF_INTERVAL:
- this._timeoutWaitIdlePeriodMs =
- Services.prefs.getIntPref(PREF_INTERVAL);
- break;
- default:
- console.error("received unknown message '" + data + "'");
- break;
- }
- }
- }
-
- /**
- * Pushes a given |value| onto the queue. The given |key| represents the type
- * of data that is stored and can override data that has been queued before
- * but has not been sent to the parent process, yet.
- *
- * @param key (string)
- * A unique identifier specific to the type of data this is passed.
- * @param fn (function)
- * A function that returns the value that will be sent to the parent
- * process.
- */
- push(key, fn) {
- this._data.set(key, fn);
-
- if (!this._timeout && !this._timeoutDisabled) {
- // Wait a little before sending the message to batch multiple changes.
- this._timeout = setTimeoutWithTarget(
- () => this.sendWhenIdle(),
- this.BATCH_DELAY_MS,
- this.mm.tabEventTarget
- );
- }
- }
-
- /**
- * Sends queued data when the remaining idle time is enough or waiting too
- * long; otherwise, request an idle time again. If the |deadline| is not
- * given, this function is going to schedule the first request.
- *
- * @param deadline (object)
- * An IdleDeadline object passed by idleDispatch().
- */
- sendWhenIdle(deadline) {
- if (!this.mm.content) {
- // The frameloader is being torn down. Nothing more to do.
- return;
- }
-
- if (deadline) {
- if (
- deadline.didTimeout ||
- deadline.timeRemaining() > this.NEEDED_IDLE_PERIOD_MS
- ) {
- this.send();
- return;
- }
- } else if (this._idleScheduled) {
- // Bail out if there's a pending run.
- return;
- }
- ChromeUtils.idleDispatch(deadline_ => this.sendWhenIdle(deadline_), {
- timeout: this._timeoutWaitIdlePeriodMs,
- });
- this._idleScheduled = true;
- }
-
- /**
- * Sends queued data to the chrome process.
- *
- * @param options (object)
- * {flushID: 123} to specify that this is a flush
- * {isFinal: true} to signal this is the final message sent on unload
- */
- send(options = {}) {
- // Looks like we have been called off a timeout after the tab has been
- // closed. The docShell is gone now and we can just return here as there
- // is nothing to do.
- if (!this.mm.docShell) {
- return;
- }
-
- this.cleanupTimers();
-
- let flushID = (options && options.flushID) || 0;
- let histID = "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_MS";
-
- let data = {};
- for (let [key, func] of this._data) {
- if (key != "isPrivate") {
- TelemetryStopwatch.startKeyed(histID, key);
- }
-
- let value = func();
-
- if (key != "isPrivate") {
- TelemetryStopwatch.finishKeyed(histID, key);
- }
-
- if (value || (key != "storagechange" && key != "historychange")) {
- data[key] = value;
- }
- }
-
- this._data.clear();
-
- try {
- // Send all data to the parent process.
- this.mm.sendAsyncMessage("SessionStore:update", {
- data,
- flushID,
- isFinal: options.isFinal || false,
- epoch: this.store.epoch,
- });
- } catch (ex) {
- if (ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) {
- Services.telemetry
- .getHistogramById("FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM")
- .add(1);
- this.mm.sendAsyncMessage("SessionStore:error");
- }
- }
- }
-}
-
-/**
- * Listens for and handles messages sent by the session store service.
- */
-const MESSAGES = [
- "SessionStore:restoreHistory",
- "SessionStore:restoreTabContent",
- "SessionStore:resetRestore",
- "SessionStore:flush",
- "SessionStore:prepareForProcessChange",
-];
-
-export class ContentSessionStore {
- constructor(mm) {
- if (Services.appinfo.sessionHistoryInParent) {
- throw new Error("This frame script should not be loaded for SHIP");
- }
-
- this.mm = mm;
- this.messageQueue = new MessageQueue(this);
-
- this.epoch = 0;
-
- this.contentRestoreInitialized = false;
-
- this.handlers = [
- this.messageQueue,
- new EventListener(this),
- new SessionHistoryListener(this),
- ];
-
- ChromeUtils.defineLazyGetter(this, "contentRestore", () => {
- this.contentRestoreInitialized = true;
- return new lazy.ContentRestore(mm);
- });
-
- MESSAGES.forEach(m => mm.addMessageListener(m, this));
-
- mm.addEventListener("unload", this);
- }
-
- receiveMessage({ name, data }) {
- // The docShell might be gone. Don't process messages,
- // that will just lead to errors anyway.
- if (!this.mm.docShell) {
- return;
- }
-
- // A fresh tab always starts with epoch=0. The parent has the ability to
- // override that to signal a new era in this tab's life. This enables it
- // to ignore async messages that were already sent but not yet received
- // and would otherwise confuse the internal tab state.
- if (data && data.epoch && data.epoch != this.epoch) {
- this.epoch = data.epoch;
- }
-
- switch (name) {
- case "SessionStore:restoreHistory":
- this.restoreHistory(data);
- break;
- case "SessionStore:restoreTabContent":
- this.restoreTabContent(data);
- break;
- case "SessionStore:resetRestore":
- this.contentRestore.resetRestore();
- break;
- case "SessionStore:flush":
- this.flush(data);
- break;
- case "SessionStore:prepareForProcessChange":
- // During normal in-process navigations, the DocShell would take
- // care of automatically persisting layout history state to record
- // scroll positions on the nsSHEntry. Unfortunately, process switching
- // is not a normal navigation, so for now we do this ourselves. This
- // is a workaround until session history state finally lives in the
- // parent process.
- this.mm.docShell.persistLayoutHistoryState();
- break;
- default:
- console.error("received unknown message '" + name + "'");
- break;
- }
- }
-
- // non-SHIP only
- restoreHistory(data) {
- let { epoch, tabData, loadArguments, isRemotenessUpdate } = data;
-
- this.contentRestore.restoreHistory(tabData, loadArguments, {
- // Note: The callbacks passed here will only be used when a load starts
- // that was not initiated by sessionstore itself. This can happen when
- // some code calls browser.loadURI() or browser.reload() on a pending
- // browser/tab.
-
- onLoadStarted: () => {
- // Notify the parent that the tab is no longer pending.
- this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", {
- epoch,
- });
- },
-
- onLoadFinished: () => {
- // Tell SessionStore.sys.mjs that it may want to restore some more tabs,
- // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
- this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {
- epoch,
- });
- },
- });
-
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
- // For non-remote tabs, when restoreHistory finishes, we send a synchronous
- // message to SessionStore.sys.mjs so that it can run SSTabRestoring. Users of
- // SSTabRestoring seem to get confused if chrome and content are out of
- // sync about the state of the restore (particularly regarding
- // docShell.currentURI). Using a synchronous message is the easiest way
- // to temporarily synchronize them.
- //
- // For remote tabs, because all nsIWebProgress notifications are sent
- // asynchronously using messages, we get the same-order guarantees of the
- // message manager, and can use an async message.
- this.mm.sendSyncMessage("SessionStore:restoreHistoryComplete", {
- epoch,
- isRemotenessUpdate,
- });
- } else {
- this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", {
- epoch,
- isRemotenessUpdate,
- });
- }
- }
-
- restoreTabContent({ loadArguments, isRemotenessUpdate, reason }) {
- let epoch = this.epoch;
-
- // We need to pass the value of didStartLoad back to SessionStore.sys.mjs.
- let didStartLoad = this.contentRestore.restoreTabContent(
- loadArguments,
- isRemotenessUpdate,
- () => {
- // Tell SessionStore.sys.mjs that it may want to restore some more tabs,
- // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
- this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {
- epoch,
- isRemotenessUpdate,
- });
- }
- );
-
- this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", {
- epoch,
- isRemotenessUpdate,
- reason,
- });
-
- if (!didStartLoad) {
- // Pretend that the load succeeded so that event handlers fire correctly.
- this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {
- epoch,
- isRemotenessUpdate,
- });
- }
- }
-
- flush({ id }) {
- // Flush the message queue, send the latest updates.
- this.messageQueue.send({ flushID: id });
- }
-
- handleEvent(event) {
- if (event.type == "unload") {
- this.onUnload();
- }
- }
-
- onUnload() {
- // Upon frameLoader destruction, send a final update message to
- // the parent and flush all data currently held in the child.
- this.messageQueue.send({ isFinal: true });
-
- for (let handler of this.handlers) {
- if (handler.uninit) {
- handler.uninit();
- }
- }
-
- if (this.contentRestoreInitialized) {
- // Remove progress listeners.
- this.contentRestore.resetRestore();
- }
-
- // We don't need to take care of any StateChangeNotifier observers as they
- // will die with the content script. The same goes for the privacy transition
- // observer that will die with the docShell when the tab is closed.
- }
-}
diff --git a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs
index 4d53b166c0..d0627180f0 100644
--- a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs
+++ b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs
@@ -182,7 +182,7 @@ export var RecentlyClosedTabsAndWindowsMenuUtils = {
* @param aEvent
* The command event when the user clicks the restore all menu item
*/
- onRestoreAllWindowsCommand(aEvent) {
+ onRestoreAllWindowsCommand() {
const count = lazy.SessionStore.getClosedWindowCount();
for (let index = 0; index < count; index++) {
lazy.SessionStore.undoCloseWindow(index);
@@ -265,7 +265,7 @@ function createEntry(
element.removeAttribute("oncommand");
element.addEventListener(
"command",
- event => {
+ () => {
lazy.SessionStore.undoClosedTabFromClosedWindow(
{ sourceClosedId },
aClosedTab.closedId
diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
index 1e5a3bf718..077529d739 100644
--- a/browser/components/sessionstore/SessionFile.sys.mjs
+++ b/browser/components/sessionstore/SessionFile.sys.mjs
@@ -194,6 +194,7 @@ var SessionFileInternal = {
},
async _readInternal(useOldExtension) {
+ Services.telemetry.setEventRecordingEnabled("session_restore", true);
let result;
let noFilesFound = true;
this._usingOldExtension = useOldExtension;
@@ -251,6 +252,18 @@ var SessionFileInternal = {
path,
". Wrong format/version: " + JSON.stringify(parsed.version) + "."
);
+ Services.telemetry.recordEvent(
+ "session_restore",
+ "backup_can_be_loaded",
+ "session_file",
+ null,
+ {
+ can_load: "false",
+ path_key: key,
+ loadfail_reason:
+ "Wrong format/version: " + JSON.stringify(parsed.version) + ".",
+ }
+ );
continue;
}
result = {
@@ -259,6 +272,17 @@ var SessionFileInternal = {
parsed,
useOldExtension,
};
+ Services.telemetry.recordEvent(
+ "session_restore",
+ "backup_can_be_loaded",
+ "session_file",
+ null,
+ {
+ can_load: "true",
+ path_key: key,
+ loadfail_reason: "N/A",
+ }
+ );
Services.telemetry
.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE")
.add(false);
@@ -269,6 +293,17 @@ var SessionFileInternal = {
} catch (ex) {
if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
exists = false;
+ Services.telemetry.recordEvent(
+ "session_restore",
+ "backup_can_be_loaded",
+ "session_file",
+ null,
+ {
+ can_load: "false",
+ path_key: key,
+ loadfail_reason: "File doesn't exist.",
+ }
+ );
} else if (
DOMException.isInstance(ex) &&
ex.name == "NotAllowedError"
@@ -277,6 +312,17 @@ var SessionFileInternal = {
// or similar failures. We'll just count it as "corrupted".
console.error("Could not read session file ", ex);
corrupted = true;
+ Services.telemetry.recordEvent(
+ "session_restore",
+ "backup_can_be_loaded",
+ "session_file",
+ null,
+ {
+ can_load: "false",
+ path_key: key,
+ loadfail_reason: ` ${ex.name}: Could not read session file`,
+ }
+ );
} else if (ex instanceof SyntaxError) {
console.error(
"Corrupt session file (invalid JSON found) ",
@@ -285,6 +331,17 @@ var SessionFileInternal = {
);
// File is corrupted, try next file
corrupted = true;
+ Services.telemetry.recordEvent(
+ "session_restore",
+ "backup_can_be_loaded",
+ "session_file",
+ null,
+ {
+ can_load: "false",
+ path_key: key,
+ loadfail_reason: ` ${ex.name}: Corrupt session file (invalid JSON found)`,
+ }
+ );
}
} finally {
if (exists) {
@@ -292,6 +349,17 @@ var SessionFileInternal = {
Services.telemetry
.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE")
.add(corrupted);
+ Services.telemetry.recordEvent(
+ "session_restore",
+ "backup_can_be_loaded",
+ "session_file",
+ null,
+ {
+ can_load: (!corrupted).toString(),
+ path_key: key,
+ loadfail_reason: "N/A",
+ }
+ );
}
}
}
diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs
index 2f08bb2243..1237e3f970 100644
--- a/browser/components/sessionstore/SessionSaver.sys.mjs
+++ b/browser/components/sessionstore/SessionSaver.sys.mjs
@@ -210,7 +210,7 @@ var SessionSaverInternal = {
/**
* Observe idle/ active notifications.
*/
- observe(subject, topic, data) {
+ observe(subject, topic) {
switch (topic) {
case "idle":
this._isIdle = true;
diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs
index ff3ba55176..0d017ac035 100644
--- a/browser/components/sessionstore/SessionStartup.sys.mjs
+++ b/browser/components/sessionstore/SessionStartup.sys.mjs
@@ -158,6 +158,11 @@ export var SessionStartup = {
*/
_onSessionFileRead({ source, parsed, noFilesFound }) {
this._initialized = true;
+ const crashReasons = {
+ FINAL_STATE_WRITING_INCOMPLETE: "final-state-write-incomplete",
+ SESSION_STATE_FLAG_MISSING:
+ "session-state-missing-or-running-at-last-write",
+ };
// Let observers modify the state before it is used
let supportsStateString = this._createSupportsString(source);
@@ -210,12 +215,17 @@ export var SessionStartup = {
delete this._initialState.lastSessionState;
}
+ let previousSessionCrashedReason = "N/A";
lazy.CrashMonitor.previousCheckpoints.then(checkpoints => {
if (checkpoints) {
// If the previous session finished writing the final state, we'll
// assume there was no crash.
this._previousSessionCrashed =
!checkpoints["sessionstore-final-state-write-complete"];
+ if (!checkpoints["sessionstore-final-state-write-complete"]) {
+ previousSessionCrashedReason =
+ crashReasons.FINAL_STATE_WRITING_INCOMPLETE;
+ }
} else if (noFilesFound) {
// If the Crash Monitor could not load a checkpoints file it will
// provide null. This could occur on the first run after updating to
@@ -241,6 +251,13 @@ export var SessionStartup = {
this._previousSessionCrashed =
!stateFlagPresent ||
this._initialState.session.state == STATE_RUNNING_STR;
+ if (
+ !stateFlagPresent ||
+ this._initialState.session.state == STATE_RUNNING_STR
+ ) {
+ previousSessionCrashedReason =
+ crashReasons.SESSION_STATE_FLAG_MISSING;
+ }
}
// Report shutdown success via telemetry. Shortcoming here are
@@ -249,6 +266,16 @@ export var SessionStartup = {
Services.telemetry
.getHistogramById("SHUTDOWN_OK")
.add(!this._previousSessionCrashed);
+ Services.telemetry.recordEvent(
+ "session_restore",
+ "shutdown_success",
+ "session_startup",
+ null,
+ {
+ shutdown_ok: this._previousSessionCrashed.toString(),
+ shutdown_reason: previousSessionCrashedReason,
+ }
+ );
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
@@ -268,7 +295,7 @@ export var SessionStartup = {
/**
* Handle notifications
*/
- observe(subject, topic, data) {
+ observe(subject, topic) {
switch (topic) {
case "sessionstore-windows-restored":
Services.obs.removeObserver(this, "sessionstore-windows-restored");
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index f269251f54..16137b8388 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -109,59 +109,6 @@ const WINDOW_OPEN_FEATURES_MAP = {
statusbar: "status",
};
-// Messages that will be received via the Frame Message Manager.
-const MESSAGES = [
- // The content script sends us data that has been invalidated and needs to
- // be saved to disk.
- "SessionStore:update",
-
- // The restoreHistory code has run. This is a good time to run SSTabRestoring.
- "SessionStore:restoreHistoryComplete",
-
- // The load for the restoring tab has begun. We update the URL bar at this
- // time; if we did it before, the load would overwrite it.
- "SessionStore:restoreTabContentStarted",
-
- // All network loads for a restoring tab are done, so we should
- // consider restoring another tab in the queue. The document has
- // been restored, and forms have been filled. We trigger
- // SSTabRestored at this time.
- "SessionStore:restoreTabContentComplete",
-
- // The content script encountered an error.
- "SessionStore:error",
-];
-
-// The list of messages we accept from <xul:browser>s that have no tab
-// assigned, or whose windows have gone away. Those are for example the
-// ones that preload about:newtab pages, or from browsers where the window
-// has just been closed.
-const NOTAB_MESSAGES = new Set([
- // For a description see above.
- "SessionStore:update",
-
- // For a description see above.
- "SessionStore:error",
-]);
-
-// The list of messages we accept without an "epoch" parameter.
-// See getCurrentEpoch() and friends to find out what an "epoch" is.
-const NOEPOCH_MESSAGES = new Set([
- // For a description see above.
- "SessionStore:error",
-]);
-
-// The list of messages we want to receive even during the short period after a
-// frame has been removed from the DOM and before its frame script has finished
-// unloading.
-const CLOSED_MESSAGES = new Set([
- // For a description see above.
- "SessionStore:update",
-
- // For a description see above.
- "SessionStore:error",
-]);
-
// These are tab events that we listen to.
const TAB_EVENTS = [
"TabOpen",
@@ -645,10 +592,6 @@ export var SessionStore = {
SessionStoreInternal.deleteCustomGlobalValue(aKey);
},
- persistTabAttribute: function ss_persistTabAttribute(aName) {
- SessionStoreInternal.persistTabAttribute(aName);
- },
-
restoreLastSession: function ss_restoreLastSession() {
SessionStoreInternal.restoreLastSession();
},
@@ -813,18 +756,6 @@ export var SessionStore = {
},
/**
- * Prepares to change the remoteness of the given browser, by ensuring that
- * the local instance of session history is up-to-date.
- */
- async prepareToChangeRemoteness(aTab) {
- await SessionStoreInternal.prepareToChangeRemoteness(aTab);
- },
-
- finishTabRemotenessChange(aTab, aSwitchId) {
- SessionStoreInternal.finishTabRemotenessChange(aTab, aSwitchId);
- },
-
- /**
* Clear session store data for a given private browsing window.
* @param {ChromeWindow} win - Open private browsing window to clear data for.
*/
@@ -1354,8 +1285,6 @@ var SessionStoreInternal = {
"privacy.resistFingerprinting"
);
Services.prefs.addObserver("privacy.resistFingerprinting", this);
-
- this._shistoryInParent = Services.appinfo.sessionHistoryInParent;
},
/**
@@ -1434,33 +1363,26 @@ var SessionStoreInternal = {
}
break;
case "browsing-context-did-set-embedder":
- if (Services.appinfo.sessionHistoryInParent) {
- if (
- aSubject &&
- aSubject === aSubject.top &&
- aSubject.isContent &&
- aSubject.embedderElement &&
- aSubject.embedderElement.permanentKey
- ) {
- let permanentKey = aSubject.embedderElement.permanentKey;
- this._browserSHistoryListener.get(permanentKey)?.unregister();
- this.getOrCreateSHistoryListener(permanentKey, aSubject, true);
- }
+ if (
+ aSubject &&
+ aSubject === aSubject.top &&
+ aSubject.isContent &&
+ aSubject.embedderElement &&
+ aSubject.embedderElement.permanentKey
+ ) {
+ let permanentKey = aSubject.embedderElement.permanentKey;
+ this._browserSHistoryListener.get(permanentKey)?.unregister();
+ this.getOrCreateSHistoryListener(permanentKey, aSubject, true);
}
break;
case "browsing-context-discarded":
- if (Services.appinfo.sessionHistoryInParent) {
- let permanentKey = aSubject?.embedderElement?.permanentKey;
- if (permanentKey) {
- this._browserSHistoryListener.get(permanentKey)?.unregister();
- }
+ let permanentKey = aSubject?.embedderElement?.permanentKey;
+ if (permanentKey) {
+ this._browserSHistoryListener.get(permanentKey)?.unregister();
}
break;
case "browser-shutdown-tabstate-updated":
- if (Services.appinfo.sessionHistoryInParent) {
- // Non-SHIP code calls this when the frame script is unloaded.
- this.onFinalTabStateUpdateComplete(aSubject);
- }
+ this.onFinalTabStateUpdateComplete(aSubject);
this._notifyOfClosedObjectsChange();
break;
}
@@ -1573,10 +1495,6 @@ var SessionStoreInternal = {
}
}
- if (!Services.appinfo.sessionHistoryInParent) {
- throw new Error("This function should only be used with SHIP");
- }
-
if (!permanentKey || browsingContext !== browsingContext.top) {
return null;
}
@@ -1691,29 +1609,27 @@ var SessionStoreInternal = {
return;
}
- if (Services.appinfo.sessionHistoryInParent) {
- let listener = this.getOrCreateSHistoryListener(
- permanentKey,
- browsingContext
- );
+ let listener = this.getOrCreateSHistoryListener(
+ permanentKey,
+ browsingContext
+ );
- if (listener) {
- let historychange =
- // If it is not the scheduled update (tab closed, window closed etc),
- // try to store the loading non-web-controlled page opened in _blank
- // first.
- (forStorage &&
- lazy.SessionHistory.collectNonWebControlledBlankLoadingSession(
- browsingContext
- )) ||
- listener.collect(permanentKey, browsingContext, {
- collectFull: !!update.sHistoryNeeded,
- writeToCache: false,
- });
+ if (listener) {
+ let historychange =
+ // If it is not the scheduled update (tab closed, window closed etc),
+ // try to store the loading non-web-controlled page opened in _blank
+ // first.
+ (forStorage &&
+ lazy.SessionHistory.collectNonWebControlledBlankLoadingSession(
+ browsingContext
+ )) ||
+ listener.collect(permanentKey, browsingContext, {
+ collectFull: !!update.sHistoryNeeded,
+ writeToCache: false,
+ });
- if (historychange) {
- update.data.historychange = historychange;
- }
+ if (historychange) {
+ update.data.historychange = historychange;
}
}
@@ -1724,98 +1640,6 @@ var SessionStoreInternal = {
this.onTabStateUpdate(permanentKey, win, update);
},
- /**
- * This method handles incoming messages sent by the session store content
- * script via the Frame Message Manager or Parent Process Message Manager,
- * and thus enables communication with OOP tabs.
- */
- receiveMessage(aMessage) {
- if (Services.appinfo.sessionHistoryInParent) {
- throw new Error(
- `received unexpected message '${aMessage.name}' with ` +
- `sessionHistoryInParent enabled`
- );
- }
-
- // If we got here, that means we're dealing with a frame message
- // manager message, so the target will be a <xul:browser>.
- var browser = aMessage.target;
- let win = browser.ownerGlobal;
- let tab = win ? win.gBrowser.getTabForBrowser(browser) : null;
-
- // Ensure we receive only specific messages from <xul:browser>s that
- // have no tab or window assigned, e.g. the ones that preload
- // about:newtab pages, or windows that have closed.
- if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
- throw new Error(
- `received unexpected message '${aMessage.name}' ` +
- `from a browser that has no tab or window`
- );
- }
-
- let data = aMessage.data || {};
- let hasEpoch = data.hasOwnProperty("epoch");
-
- // Most messages sent by frame scripts require to pass an epoch.
- if (!hasEpoch && !NOEPOCH_MESSAGES.has(aMessage.name)) {
- throw new Error(`received message '${aMessage.name}' without an epoch`);
- }
-
- // Ignore messages from previous epochs.
- if (hasEpoch && !this.isCurrentEpoch(browser.permanentKey, data.epoch)) {
- return;
- }
-
- switch (aMessage.name) {
- case "SessionStore:update":
- // |browser.frameLoader| might be empty if the browser was already
- // destroyed and its tab removed. In that case we still have the last
- // frameLoader we know about to compare.
- let frameLoader =
- browser.frameLoader ||
- this._lastKnownFrameLoader.get(browser.permanentKey);
-
- // If the message isn't targeting the latest frameLoader discard it.
- if (frameLoader != aMessage.targetFrameLoader) {
- return;
- }
-
- this.onTabStateUpdate(browser.permanentKey, browser.ownerGlobal, data);
-
- // SHIP code will call this when it receives "browser-shutdown-tabstate-updated"
- if (data.isFinal) {
- if (!Services.appinfo.sessionHistoryInParent) {
- this.onFinalTabStateUpdateComplete(browser);
- }
- } else if (data.flushID) {
- // This is an update kicked off by an async flush request. Notify the
- // TabStateFlusher so that it can finish the request and notify its
- // consumer that's waiting for the flush to be done.
- lazy.TabStateFlusher.resolve(browser, data.flushID);
- }
-
- break;
- case "SessionStore:restoreHistoryComplete":
- this._restoreHistoryComplete(browser, data);
- break;
- case "SessionStore:restoreTabContentStarted":
- this._restoreTabContentStarted(browser, data);
- break;
- case "SessionStore:restoreTabContentComplete":
- this._restoreTabContentComplete(browser, data);
- break;
- case "SessionStore:error":
- lazy.TabStateFlusher.resolveAll(
- browser,
- false,
- "Received error from the content process"
- );
- break;
- default:
- throw new Error(`received unknown message '${aMessage.name}'`);
- }
- },
-
/* ........ Window Event Handlers .............. */
/**
@@ -1917,21 +1741,6 @@ var SessionStoreInternal = {
// internal data about the window.
aWindow.__SSi = this._generateWindowID();
- if (!Services.appinfo.sessionHistoryInParent) {
- let mm = aWindow.getGroupMessageManager("browsers");
- MESSAGES.forEach(msg => {
- let listenWhenClosed = CLOSED_MESSAGES.has(msg);
- mm.addMessageListener(msg, this, listenWhenClosed);
- });
-
- // Load the frame script after registering listeners.
- mm.loadFrameScript(
- "chrome://browser/content/content-sessionStore.js",
- true,
- true
- );
- }
-
// and create its data object
this._windows[aWindow.__SSi] = {
tabs: [],
@@ -2347,7 +2156,7 @@ var SessionStoreInternal = {
// Save non-private windows if they have at
// least one saveable tab or are the last window.
if (!winData.isPrivate) {
- this.maybeSaveClosedWindow(winData, isLastWindow, true);
+ this.maybeSaveClosedWindow(winData, isLastWindow);
if (!isLastWindow && winData.closedId > -1) {
this._addClosedAction(
@@ -2402,11 +2211,6 @@ var SessionStoreInternal = {
// Cache the window state until it is completely gone.
DyingWindowCache.set(aWindow, winData);
- if (!Services.appinfo.sessionHistoryInParent) {
- let mm = aWindow.getGroupMessageManager("browsers");
- MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
- }
-
this._saveableClosedWindowData.delete(winData);
delete aWindow.__SSi;
},
@@ -2428,7 +2232,7 @@ var SessionStoreInternal = {
* to call this method again asynchronously (for example, after
* a window flush).
*/
- maybeSaveClosedWindow(winData, isLastWindow, recordTelemetry = false) {
+ maybeSaveClosedWindow(winData, isLastWindow) {
// Make sure SessionStore is still running, and make sure that we
// haven't chosen to forget this window.
if (
@@ -2489,13 +2293,9 @@ var SessionStoreInternal = {
this._removeClosedWindow(winIndex);
return;
}
- // we only do this after the TabStateFlusher promise resolves in ssi_onClose
- if (recordTelemetry) {
- let closedTabsHistogram = Services.telemetry.getHistogramById(
- "FX_SESSION_RESTORE_CLOSED_TABS_NOT_SAVED"
- );
- closedTabsHistogram.add(winData._closedTabs.length);
- }
+ this._log.warn(
+ `Discarding window with 0 saveable tabs and ${winData._closedTabs.length} closed tabs`
+ );
}
}
},
@@ -3644,7 +3444,7 @@ var SessionStoreInternal = {
}
// Create a new tab.
- let userContextId = aTab.getAttribute("usercontextid");
+ let userContextId = aTab.getAttribute("usercontextid") || "";
let tabOptions = {
userContextId,
@@ -4273,12 +4073,6 @@ var SessionStoreInternal = {
this.saveStateDelayed();
},
- persistTabAttribute: function ssi_persistTabAttribute(aName) {
- if (lazy.TabAttributes.persist(aName)) {
- this.saveStateDelayed();
- }
- },
-
/**
* Undoes the closing of a tab or window which corresponds
* to the closedId passed in.
@@ -5480,13 +5274,6 @@ var SessionStoreInternal = {
tab.updateLastAccessed(tabData.lastAccessed);
}
- if ("attributes" in tabData) {
- // Ensure that we persist tab attributes restored from previous sessions.
- Object.keys(tabData.attributes).forEach(a =>
- lazy.TabAttributes.persist(a)
- );
- }
-
if (!tabData.entries) {
tabData.entries = [];
}
@@ -5656,7 +5443,6 @@ var SessionStoreInternal = {
let browser = aTab.linkedBrowser;
let window = aTab.ownerGlobal;
- let tabbrowser = window.gBrowser;
let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
let activeIndex = tabData.index - 1;
let activePageData = tabData.entries[activeIndex] || null;
@@ -5664,36 +5450,9 @@ var SessionStoreInternal = {
this.markTabAsRestoring(aTab);
- let isRemotenessUpdate = aOptions.isRemotenessUpdate;
- let explicitlyUpdateRemoteness = !Services.appinfo.sessionHistoryInParent;
- // If we aren't already updating the browser's remoteness, check if it's
- // necessary.
- if (explicitlyUpdateRemoteness && !isRemotenessUpdate) {
- isRemotenessUpdate = tabbrowser.updateBrowserRemotenessByURL(
- browser,
- uri
- );
-
- if (isRemotenessUpdate) {
- // We updated the remoteness, so we need to send the history down again.
- //
- // Start a new epoch to discard all frame script messages relating to a
- // previous epoch. All async messages that are still on their way to chrome
- // will be ignored and don't override any tab data set when restoring.
- let epoch = this.startNextEpoch(browser.permanentKey);
-
- this._sendRestoreHistory(browser, {
- tabData,
- epoch,
- loadArguments,
- isRemotenessUpdate,
- });
- }
- }
-
this._sendRestoreTabContent(browser, {
loadArguments,
- isRemotenessUpdate,
+ isRemotenessUpdate: aOptions.isRemotenessUpdate,
reason:
aOptions.restoreContentReason || RESTORE_TAB_CONTENT_REASON.SET_STATE,
});
@@ -6828,10 +6587,8 @@ var SessionStoreInternal = {
// The browser is no longer in any sort of restoring state.
TAB_STATE_FOR_BROWSER.delete(browser);
- if (Services.appinfo.sessionHistoryInParent) {
- this._restoreListeners.get(browser.permanentKey)?.unregister();
- browser.browsingContext.clearRestoreState();
- }
+ this._restoreListeners.get(browser.permanentKey)?.unregister();
+ browser.browsingContext.clearRestoreState();
aTab.removeAttribute("pending");
@@ -6855,9 +6612,6 @@ var SessionStoreInternal = {
return;
}
- if (!Services.appinfo.sessionHistoryInParent) {
- browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
- }
this._resetLocalTabRestoringState(tab);
},
@@ -7048,7 +6802,7 @@ var SessionStoreInternal = {
} catch {} // May have already gotten rid of the browser's webProgress.
},
- onStateChange(webProgress, request, stateFlags, status) {
+ onStateChange(webProgress, request, stateFlags) {
if (
webProgress.isTopLevel &&
stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
@@ -7109,7 +6863,7 @@ var SessionStoreInternal = {
OnHistoryPurge() {},
OnHistoryReplaceEntry() {},
- onStateChange(webProgress, request, stateFlags, status) {
+ onStateChange(webProgress, request, stateFlags) {
if (
webProgress.isTopLevel &&
stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
@@ -7143,10 +6897,6 @@ var SessionStoreInternal = {
* history restores.
*/
_restoreHistory(browser, data) {
- if (!Services.appinfo.sessionHistoryInParent) {
- throw new Error("This function should only be used with SHIP");
- }
-
this._tabStateToRestore.set(browser.permanentKey, data);
// In case about:blank isn't done yet.
@@ -7190,7 +6940,7 @@ var SessionStoreInternal = {
this._tabStateRestorePromises.delete(browser.permanentKey);
- this._restoreHistoryComplete(browser, data);
+ this._restoreHistoryComplete(browser);
};
promise.then(onResolve).catch(() => {});
@@ -7238,10 +6988,6 @@ var SessionStoreInternal = {
* history restores.
*/
_restoreTabContent(browser, options = {}) {
- if (!Services.appinfo.sessionHistoryInParent) {
- throw new Error("This function should only be used with SHIP");
- }
-
this._restoreListeners.get(browser.permanentKey)?.unregister();
this._restoreTabContentStarted(browser, options);
@@ -7266,17 +7012,10 @@ var SessionStoreInternal = {
},
_sendRestoreTabContent(browser, options) {
- if (Services.appinfo.sessionHistoryInParent) {
- this._restoreTabContent(browser, options);
- } else {
- browser.messageManager.sendAsyncMessage(
- "SessionStore:restoreTabContent",
- options
- );
- }
+ this._restoreTabContent(browser, options);
},
- _restoreHistoryComplete(browser, data) {
+ _restoreHistoryComplete(browser) {
let win = browser.ownerGlobal;
let tab = win?.gBrowser.getTabForBrowser(browser);
if (!tab) {
@@ -7417,68 +7156,12 @@ var SessionStoreInternal = {
delete options.tabData.storage;
}
- if (Services.appinfo.sessionHistoryInParent) {
- this._restoreHistory(browser, options);
- } else {
- browser.messageManager.sendAsyncMessage(
- "SessionStore:restoreHistory",
- options
- );
- }
+ this._restoreHistory(browser, options);
if (browser && browser.frameLoader) {
browser.frameLoader.requestEpochUpdate(options.epoch);
}
},
-
- // Flush out session history state so that it can be used to restore the state
- // into a new process in `finishTabRemotenessChange`.
- //
- // NOTE: This codepath is temporary while the Fission Session History rewrite
- // is in process, and will be removed & replaced once that rewrite is
- // complete. (bug 1645062)
- async prepareToChangeRemoteness(aBrowser) {
- aBrowser.messageManager.sendAsyncMessage(
- "SessionStore:prepareForProcessChange"
- );
- await lazy.TabStateFlusher.flush(aBrowser);
- },
-
- // Handle finishing the remoteness change for a tab by restoring session
- // history state into it, and resuming the ongoing network load.
- //
- // NOTE: This codepath is temporary while the Fission Session History rewrite
- // is in process, and will be removed & replaced once that rewrite is
- // complete. (bug 1645062)
- finishTabRemotenessChange(aTab, aSwitchId) {
- let window = aTab.ownerGlobal;
- if (!window || !window.__SSi || window.closed) {
- return;
- }
-
- let tabState = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
- let options = {
- restoreImmediately: true,
- restoreContentReason: RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE,
- isRemotenessUpdate: true,
- loadArguments: {
- redirectLoadSwitchId: aSwitchId,
- // As we're resuming a load which has been redirected from another
- // process, record the history index which is currently being requested.
- // It has to be offset by 1 to get back to native history indices from
- // SessionStore history indicies.
- redirectHistoryIndex: tabState.requestedIndex - 1,
- },
- };
-
- // Need to reset restoring tabs.
- if (TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser)) {
- this._resetLocalTabRestoringState(aTab);
- }
-
- // Restore the state into the tab.
- this.restoreTab(aTab, tabState, options);
- },
};
/**
@@ -7689,7 +7372,7 @@ var DirtyWindows = {
this._data.delete(window);
},
- clear(window) {
+ clear(_window) {
this._data = new WeakMap();
},
};
diff --git a/browser/components/sessionstore/StartupPerformance.sys.mjs b/browser/components/sessionstore/StartupPerformance.sys.mjs
index a13333d9d1..c2b791609b 100644
--- a/browser/components/sessionstore/StartupPerformance.sys.mjs
+++ b/browser/components/sessionstore/StartupPerformance.sys.mjs
@@ -153,7 +153,7 @@ export var StartupPerformance = {
}, COLLECT_RESULTS_AFTER_MS);
},
- observe(subject, topic, details) {
+ observe(subject, topic) {
try {
switch (topic) {
case "sessionstore-restoring-on-startup":
diff --git a/browser/components/sessionstore/TabAttributes.sys.mjs b/browser/components/sessionstore/TabAttributes.sys.mjs
index 1c7f54b6ab..ea53156d12 100644
--- a/browser/components/sessionstore/TabAttributes.sys.mjs
+++ b/browser/components/sessionstore/TabAttributes.sys.mjs
@@ -2,27 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-// We never want to directly read or write these attributes.
-// 'image' should not be accessed directly but handled by using the
-// gBrowser.getIcon()/setIcon() methods.
-// 'muted' should not be accessed directly but handled by using the
-// tab.linkedBrowser.audioMuted/toggleMuteAudio methods.
-// 'pending' is used internal by sessionstore and managed accordingly.
-const ATTRIBUTES_TO_SKIP = new Set([
- "image",
- "muted",
- "pending",
- "skipbackgroundnotify",
-]);
+// Tab attributes which are persisted & restored by SessionStore.
+const PERSISTED_ATTRIBUTES = ["customizemode"];
// A set of tab attributes to persist. We will read a given list of tab
// attributes when collecting tab data and will re-set those attributes when
// the given tab data is restored to a new tab.
export var TabAttributes = Object.freeze({
- persist(name) {
- return TabAttributesInternal.persist(name);
- },
-
get(tab) {
return TabAttributesInternal.get(tab);
},
@@ -33,21 +19,10 @@ export var TabAttributes = Object.freeze({
});
var TabAttributesInternal = {
- _attrs: new Set(),
-
- persist(name) {
- if (this._attrs.has(name) || ATTRIBUTES_TO_SKIP.has(name)) {
- return false;
- }
-
- this._attrs.add(name);
- return true;
- },
-
get(tab) {
let data = {};
- for (let name of this._attrs) {
+ for (let name of PERSISTED_ATTRIBUTES) {
if (tab.hasAttribute(name)) {
data[name] = tab.getAttribute(name);
}
@@ -57,15 +32,11 @@ var TabAttributesInternal = {
},
set(tab, data = {}) {
- // Clear attributes.
- for (let name of this._attrs) {
+ // Clear & Set attributes.
+ for (let name of PERSISTED_ATTRIBUTES) {
tab.removeAttribute(name);
- }
-
- // Set attributes.
- for (let [name, value] of Object.entries(data)) {
- if (!ATTRIBUTES_TO_SKIP.has(name)) {
- tab.setAttribute(name, value);
+ if (name in data) {
+ tab.setAttribute(name, data[name]);
}
}
},
diff --git a/browser/components/sessionstore/TabStateFlusher.sys.mjs b/browser/components/sessionstore/TabStateFlusher.sys.mjs
index e391abc970..ed7953e41e 100644
--- a/browser/components/sessionstore/TabStateFlusher.sys.mjs
+++ b/browser/components/sessionstore/TabStateFlusher.sys.mjs
@@ -2,11 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-const lazy = {};
-ChromeUtils.defineESModuleGetters(lazy, {
- SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
-});
-
/**
* A module that enables async flushes. Updates from frame scripts are
* throttled to be sent only once per second. If an action wants a tab's latest
@@ -33,23 +28,6 @@ export var TabStateFlusher = Object.freeze({
},
/**
- * Resolves the flush request with the given flush ID.
- *
- * @param browser (<xul:browser>)
- * The browser for which the flush is being resolved.
- * @param flushID (int)
- * The ID of the flush that was sent to the browser.
- * @param success (bool, optional)
- * Whether or not the flush succeeded.
- * @param message (string, optional)
- * An error message that will be sent to the Console in the
- * event that a flush failed.
- */
- resolve(browser, flushID, success = true, message = "") {
- TabStateFlusherInternal.resolve(browser, flushID, success, message);
- },
-
- /**
* Resolves all active flush requests for a given browser. This should be
* used when the content process crashed or the final update message was
* seen. In those cases we can't guarantee to ever hear back from the frame
@@ -69,9 +47,6 @@ export var TabStateFlusher = Object.freeze({
});
var TabStateFlusherInternal = {
- // Stores the last request ID.
- _lastRequestID: 0,
-
// A map storing all active requests per browser. A request is a
// triple of a map containing all flush requests, a promise that
// resolve when a request for a browser is canceled, and the
@@ -79,7 +54,6 @@ var TabStateFlusherInternal = {
_requests: new WeakMap(),
initEntry(entry) {
- entry.perBrowserRequests = new Map();
entry.cancelPromise = new Promise(resolve => {
entry.cancel = resolve;
}).then(result => {
@@ -96,7 +70,6 @@ var TabStateFlusherInternal = {
* all the latest data.
*/
flush(browser) {
- let id = ++this._lastRequestID;
let nativePromise = Promise.resolve();
if (browser && browser.frameLoader) {
/*
@@ -106,24 +79,6 @@ var TabStateFlusherInternal = {
nativePromise = browser.frameLoader.requestTabStateFlush();
}
- if (!Services.appinfo.sessionHistoryInParent) {
- /*
- In the event that we have to trigger a process switch and thus change
- browser remoteness, session store needs to register and track the new
- browser window loaded and to have message manager listener registered
- ** before ** TabStateFlusher send "SessionStore:flush" message. This fixes
- the race where we send the message before the message listener is
- registered for it.
- */
- lazy.SessionStore.ensureInitialized(browser.ownerGlobal);
-
- let mm = browser.messageManager;
- mm.sendAsyncMessage("SessionStore:flush", {
- id,
- epoch: lazy.SessionStore.getCurrentEpoch(browser),
- });
- }
-
// Retrieve active requests for given browser.
let permanentKey = browser.permanentKey;
let request = this._requests.get(permanentKey);
@@ -134,22 +89,10 @@ var TabStateFlusherInternal = {
this._requests.set(permanentKey, request);
}
- // Non-SHIP flushes resolve this after the "SessionStore:update" message. We
- // don't use that message for SHIP, so it's fine to resolve the request
- // immediately after the native promise resolves, since SessionStore will
- // have processed all updates from this browser by that point.
- let requestPromise = Promise.resolve();
- if (!Services.appinfo.sessionHistoryInParent) {
- requestPromise = new Promise(resolve => {
- // Store resolve() so that we can resolve the promise later.
- request.perBrowserRequests.set(id, resolve);
- });
- }
-
- return Promise.race([
- nativePromise.then(_ => requestPromise),
- request.cancelPromise,
- ]);
+ // It's fine to resolve the request immediately after the native promise
+ // resolves, since SessionStore will have processed all updates from this
+ // browser by that point.
+ return Promise.race([nativePromise, request.cancelPromise]);
},
/**
@@ -167,41 +110,6 @@ var TabStateFlusherInternal = {
},
/**
- * Resolves the flush request with the given flush ID.
- *
- * @param browser (<xul:browser>)
- * The browser for which the flush is being resolved.
- * @param flushID (int)
- * The ID of the flush that was sent to the browser.
- * @param success (bool, optional)
- * Whether or not the flush succeeded.
- * @param message (string, optional)
- * An error message that will be sent to the Console in the
- * event that a flush failed.
- */
- resolve(browser, flushID, success = true, message = "") {
- // Nothing to do if there are no pending flushes for the given browser.
- if (!this._requests.has(browser.permanentKey)) {
- return;
- }
-
- // Retrieve active requests for given browser.
- let { perBrowserRequests } = this._requests.get(browser.permanentKey);
- if (!perBrowserRequests.has(flushID)) {
- return;
- }
-
- if (!success) {
- console.error("Failed to flush browser: ", message);
- }
-
- // Resolve the request with the given id.
- let resolve = perBrowserRequests.get(flushID);
- perBrowserRequests.delete(flushID);
- resolve(success);
- },
-
- /**
* Resolves all active flush requests for a given browser. This should be
* used when the content process crashed or the final update message was
* seen. In those cases we can't guarantee to ever hear back from the frame
diff --git a/browser/components/sessionstore/content/aboutSessionRestore.js b/browser/components/sessionstore/content/aboutSessionRestore.js
index 51bed7c51b..2dfa45d40f 100644
--- a/browser/components/sessionstore/content/aboutSessionRestore.js
+++ b/browser/components/sessionstore/content/aboutSessionRestore.js
@@ -204,7 +204,7 @@ function startNewSession() {
),
});
} else {
- getBrowserWindow().BrowserHome();
+ getBrowserWindow().BrowserCommands.home();
}
}
@@ -325,31 +325,31 @@ var treeView = {
setTree(treeBox) {
this.treeBox = treeBox;
},
- getCellText(idx, column) {
+ getCellText(idx) {
return gTreeData[idx].label;
},
isContainer(idx) {
return "open" in gTreeData[idx];
},
- getCellValue(idx, column) {
+ getCellValue(idx) {
return gTreeData[idx].checked;
},
isContainerOpen(idx) {
return gTreeData[idx].open;
},
- isContainerEmpty(idx) {
+ isContainerEmpty() {
return false;
},
- isSeparator(idx) {
+ isSeparator() {
return false;
},
isSorted() {
return false;
},
- isEditable(idx, column) {
+ isEditable() {
return false;
},
- canDrop(idx, orientation, dt) {
+ canDrop() {
return false;
},
getLevel(idx) {
@@ -438,10 +438,10 @@ var treeView = {
return null;
},
- cycleHeader(column) {},
- cycleCell(idx, column) {},
+ cycleHeader() {},
+ cycleCell() {},
selectionChanged() {},
- getColumnProperties(column) {
+ getColumnProperties() {
return "";
},
};
diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js
deleted file mode 100644
index a4bdea0bdc..0000000000
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* eslint-env mozilla/frame-script */
-
-"use strict";
-
-const { ContentSessionStore } = ChromeUtils.importESModule(
- "resource:///modules/sessionstore/ContentSessionStore.sys.mjs"
-);
-
-void new ContentSessionStore(this);
diff --git a/browser/components/sessionstore/jar.mn b/browser/components/sessionstore/jar.mn
index 7e5bc07dc6..b31a4fb351 100644
--- a/browser/components/sessionstore/jar.mn
+++ b/browser/components/sessionstore/jar.mn
@@ -5,4 +5,3 @@
browser.jar:
* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml)
content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js)
- content/browser/content-sessionStore.js (content/content-sessionStore.js)
diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build
index 1536826733..cd3a0ad6fc 100644
--- a/browser/components/sessionstore/moz.build
+++ b/browser/components/sessionstore/moz.build
@@ -5,14 +5,12 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"]
-BROWSER_CHROME_MANIFESTS += ["test/browser.toml"]
+BROWSER_CHROME_MANIFESTS += ["test/browser.toml", "test/browser_oldformat.toml"]
MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"]
JAR_MANIFESTS += ["jar.mn"]
EXTRA_JS_MODULES.sessionstore = [
- "ContentRestore.sys.mjs",
- "ContentSessionStore.sys.mjs",
"GlobalState.sys.mjs",
"RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs",
"RunState.sys.mjs",
diff --git a/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs b/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs
index dd2885cee4..eecb1240e2 100644
--- a/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs
+++ b/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs
@@ -100,7 +100,7 @@ export var SessionStoreTestUtils = {
expectedTabsRestored = aState.windows.length;
}
- function onSSTabRestored(aEvent) {
+ function onSSTabRestored() {
if (++tabsRestored == expectedTabsRestored) {
// Remove the event listener from each window
windows.forEach(function (win) {
@@ -118,7 +118,7 @@ export var SessionStoreTestUtils = {
// Used to add our listener to further windows so we can catch SSTabRestored
// coming from them when creating a multi-window state.
- function windowObserver(aSubject, aTopic, aData) {
+ function windowObserver(aSubject, aTopic) {
if (aTopic == "domwindowopened") {
let newWindow = aSubject;
newWindow.addEventListener(
diff --git a/browser/components/sessionstore/test/browser.toml b/browser/components/sessionstore/test/browser.toml
index 26fb4b4550..7d5b407d22 100644
--- a/browser/components/sessionstore/test/browser.toml
+++ b/browser/components/sessionstore/test/browser.toml
@@ -22,30 +22,11 @@ support-files = [
"browser_scrollPositions_readerModeArticle.html",
"browser_sessionStorage.html",
"browser_speculative_connect.html",
- "browser_248970_b_sample.html",
- "browser_339445_sample.html",
- "browser_423132_sample.html",
- "browser_447951_sample.html",
- "browser_454908_sample.html",
- "browser_456342_sample.xhtml",
- "browser_463205_sample.html",
- "browser_463206_sample.html",
- "browser_466937_sample.html",
- "browser_485482_sample.html",
- "browser_637020_slow.sjs",
- "browser_662743_sample.html",
- "browser_739531_sample.html",
- "browser_739531_frame.html",
- "browser_911547_sample.html",
- "browser_911547_sample.html^headers^",
"coopHeaderCommon.sjs",
"restore_redirect_http.html",
"restore_redirect_http.html^headers^",
"restore_redirect_js.html",
"restore_redirect_target.html",
- "browser_1234021_page.html",
- "browser_1284886_suspend_tab.html",
- "browser_1284886_suspend_tab_2.html",
"empty.html",
"coop_coep.html",
"coop_coep.html^headers^",
@@ -58,248 +39,6 @@ prefs = [
"browser.sessionstore.closedTabsFromClosedWindows=true",
]
-#NB: the following are disabled
-# browser_464620_a.html
-# browser_464620_b.html
-# browser_464620_xd.html
-
-#disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html
-#disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html
-#disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html
-
-["browser_1234021.js"]
-
-["browser_1284886_suspend_tab.js"]
-
-["browser_1446343-windowsize.js"]
-skip-if = ["os == 'linux'"] # Bug 1600180
-
-["browser_248970_b_perwindowpb.js"]
-# Disabled because of leaks.
-# Re-enabling and rewriting this test is tracked in bug 936919.
-skip-if = ["true"]
-
-["browser_339445.js"]
-
-["browser_345898.js"]
-
-["browser_350525.js"]
-
-["browser_354894_perwindowpb.js"]
-
-["browser_367052.js"]
-
-["browser_393716.js"]
-skip-if = ["debug"] # Bug 1507747
-
-["browser_394759_basic.js"]
-# Disabled for intermittent failures, bug 944372.
-skip-if = ["true"]
-
-["browser_394759_behavior.js"]
-https_first_disabled = true
-
-["browser_394759_perwindowpb.js"]
-
-["browser_394759_purge.js"]
-
-["browser_423132.js"]
-
-["browser_447951.js"]
-
-["browser_454908.js"]
-
-["browser_456342.js"]
-
-["browser_461634.js"]
-
-["browser_463205.js"]
-
-["browser_463206.js"]
-
-["browser_464199.js"]
-# Disabled for frequent intermittent failures
-
-["browser_464620_a.js"]
-skip-if = ["true"]
-
-["browser_464620_b.js"]
-skip-if = ["true"]
-
-["browser_465215.js"]
-
-["browser_465223.js"]
-
-["browser_466937.js"]
-
-["browser_467409-backslashplosion.js"]
-
-["browser_477657.js"]
-skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1610668 for ubuntu 18.04
-
-["browser_480893.js"]
-
-["browser_485482.js"]
-
-["browser_485563.js"]
-
-["browser_490040.js"]
-
-["browser_491168.js"]
-
-["browser_491577.js"]
-skip-if = [
- "verify && debug && os == 'mac'",
- "verify && debug && os == 'win'",
-]
-
-["browser_495495.js"]
-
-["browser_500328.js"]
-
-["browser_514751.js"]
-
-["browser_522375.js"]
-
-["browser_522545.js"]
-skip-if = ["true"] # Bug 1380968
-
-["browser_524745.js"]
-skip-if = [
- "win10_2009 && !ccov", # Bug 1418627
- "os == 'linux'", # Bug 1803187
-]
-
-["browser_528776.js"]
-
-["browser_579868.js"]
-
-["browser_579879.js"]
-skip-if = ["os == 'linux' && (debug || asan)"] # Bug 1234404
-
-["browser_581937.js"]
-
-["browser_586068-apptabs.js"]
-
-["browser_586068-apptabs_ondemand.js"]
-skip-if = ["verify && (os == 'mac' || os == 'win')"]
-
-["browser_586068-browser_state_interrupted.js"]
-
-["browser_586068-cascade.js"]
-
-["browser_586068-multi_window.js"]
-
-["browser_586068-reload.js"]
-https_first_disabled = true
-
-["browser_586068-select.js"]
-
-["browser_586068-window_state.js"]
-
-["browser_586068-window_state_override.js"]
-
-["browser_586147.js"]
-
-["browser_588426.js"]
-
-["browser_590268.js"]
-
-["browser_590563.js"]
-
-["browser_595601-restore_hidden.js"]
-
-["browser_597071.js"]
-skip-if = ["true"] # Needs to be rewritten as Marionette test, bug 995916
-
-["browser_600545.js"]
-
-["browser_601955.js"]
-
-["browser_607016.js"]
-
-["browser_615394-SSWindowState_events_duplicateTab.js"]
-
-["browser_615394-SSWindowState_events_setBrowserState.js"]
-skip-if = ["verify && debug && os == 'mac'"]
-
-["browser_615394-SSWindowState_events_setTabState.js"]
-
-["browser_615394-SSWindowState_events_setWindowState.js"]
-https_first_disabled = true
-
-["browser_615394-SSWindowState_events_undoCloseTab.js"]
-
-["browser_615394-SSWindowState_events_undoCloseWindow.js"]
-skip-if = [
- "os == 'win' && !debug", # Bug 1572554
- "os == 'linux'", # Bug 1572554
-]
-
-["browser_618151.js"]
-
-["browser_623779.js"]
-
-["browser_624727.js"]
-
-["browser_625016.js"]
-skip-if = [
- "os == 'mac'", # Disabled on OS X:
- "os == 'linux'", # linux, Bug 1348583
- "os == 'win' && debug", # Bug 1430977
-]
-
-["browser_628270.js"]
-
-["browser_635418.js"]
-
-["browser_636279.js"]
-
-["browser_637020.js"]
-
-["browser_645428.js"]
-
-["browser_659591.js"]
-
-["browser_662743.js"]
-
-["browser_662812.js"]
-skip-if = ["verify"]
-
-["browser_665702-state_session.js"]
-
-["browser_682507.js"]
-
-["browser_687710.js"]
-
-["browser_687710_2.js"]
-https_first_disabled = true
-
-["browser_694378.js"]
-
-["browser_701377.js"]
-skip-if = [
- "verify && debug && os == 'win'",
- "verify && debug && os == 'mac'",
-]
-
-["browser_705597.js"]
-
-["browser_707862.js"]
-
-["browser_739531.js"]
-
-["browser_739805.js"]
-
-["browser_819510_perwindowpb.js"]
-skip-if = ["true"] # Bug 1284312, Bug 1341980, bug 1381451
-
-["browser_906076_lazy_tabs.js"]
-https_first_disabled = true
-skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1446464
-
-["browser_911547.js"]
-
["browser_aboutPrivateBrowsing.js"]
["browser_aboutSessionRestore.js"]
@@ -316,7 +55,6 @@ support-files = ["file_async_flushes.html"]
run-if = ["crashreporter"]
["browser_async_remove_tab.js"]
-skip-if = ["!sessionHistoryInParent"]
["browser_async_window_flushing.js"]
https_first_disabled = true
@@ -393,6 +131,7 @@ https_first_disabled = true
skip-if = ["verify && debug"]
["browser_formdata_cc.js"]
+skip-if = ["asan"] # test runs too long
["browser_formdata_face.js"]
@@ -466,12 +205,12 @@ skip-if = [
["browser_privatetabs.js"]
["browser_purge_shistory.js"]
-skip-if = ["!sessionHistoryInParent"] # Bug 1271024
["browser_remoteness_flip_on_restore.js"]
["browser_reopen_all_windows.js"]
https_first_disabled = true
+skip-if = ["asan"] # high memory
["browser_replace_load.js"]
skip-if = ["true"] # Bug 1646894
@@ -516,9 +255,6 @@ skip-if = [
["browser_scrollPositionsReaderMode.js"]
-["browser_send_async_message_oom.js"]
-skip-if = ["sessionHistoryInParent"] # Tests that the frame script OOMs, which is unused when SHIP is enabled.
-
["browser_sessionHistory.js"]
https_first_disabled = true
support-files = ["file_sessionHistory_hashchange.html"]
diff --git a/browser/components/sessionstore/test/browser_354894_perwindowpb.js b/browser/components/sessionstore/test/browser_354894_perwindowpb.js
index 90368536dc..30a065c1af 100644
--- a/browser/components/sessionstore/test/browser_354894_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_354894_perwindowpb.js
@@ -21,7 +21,7 @@
* not enabled on that platform (platform shim; the application is kept running
* although there are no windows left)
* @note There is a difference when closing a browser window with
- * BrowserTryToCloseWindow() as opposed to close(). The former will make
+ * BrowserCommands.tryToCloseWindow() as opposed to close(). The former will make
* nsSessionStore restore a window next time it gets a chance and will post
* notifications. The latter won't.
*/
@@ -133,7 +133,7 @@ let setupTest = async function (options, testFunction) {
* Helper: Will observe and handle the notifications for us
*/
let hitCount = 0;
- function observer(aCancel, aTopic, aData) {
+ function observer(aCancel, aTopic) {
// count so that we later may compare
observing[aTopic]++;
@@ -182,7 +182,7 @@ function injectTestTabs(win) {
}
/**
- * Attempts to close a window via BrowserTryToCloseWindow so that
+ * Attempts to close a window via BrowserCommands.tryToCloseWindow so that
* we get the browser-lastwindow-close-requested and
* browser-lastwindow-close-granted observer notifications.
*
@@ -195,7 +195,7 @@ function injectTestTabs(win) {
function closeWindowForRestoration(win) {
return new Promise(resolve => {
let closePromise = BrowserTestUtils.windowClosed(win);
- win.BrowserTryToCloseWindow();
+ win.BrowserCommands.tryToCloseWindow();
if (!win.closed) {
resolve(false);
return;
@@ -415,7 +415,7 @@ add_task(async function test_open_close_restore_from_popup() {
return;
}
- await setupTest({}, async function (newWin, obs) {
+ await setupTest({}, async function (newWin) {
let newWin2 = await promiseNewWindowLoaded();
await injectTestTabs(newWin2);
diff --git a/browser/components/sessionstore/test/browser_394759_basic.js b/browser/components/sessionstore/test/browser_394759_basic.js
index 62d5c40e17..cc1c335165 100644
--- a/browser/components/sessionstore/test/browser_394759_basic.js
+++ b/browser/components/sessionstore/test/browser_394759_basic.js
@@ -74,7 +74,7 @@ function test() {
let expectedTabs = data[0].tabs.length;
newWin2.addEventListener(
"SSTabRestored",
- function sstabrestoredListener(aEvent) {
+ function sstabrestoredListener() {
++restoredTabs;
info("Restored tab " + restoredTabs + "/" + expectedTabs);
if (restoredTabs < expectedTabs) {
diff --git a/browser/components/sessionstore/test/browser_394759_behavior.js b/browser/components/sessionstore/test/browser_394759_behavior.js
index ee4b121e84..01217f86c9 100644
--- a/browser/components/sessionstore/test/browser_394759_behavior.js
+++ b/browser/components/sessionstore/test/browser_394759_behavior.js
@@ -34,7 +34,7 @@ function testWindows(windowsToOpen, expectedResults) {
}
let closedWindowData = ss.getClosedWindowData();
- let numPopups = closedWindowData.filter(function (el, i, arr) {
+ let numPopups = closedWindowData.filter(function (el) {
return el.isPopup;
}).length;
let numNormal = ss.getClosedWindowCount() - numPopups;
@@ -50,7 +50,7 @@ function testWindows(windowsToOpen, expectedResults) {
is(
numNormal,
oResults.normal,
- "There were " + oResults.normal + " normal windows to repoen"
+ "There were " + oResults.normal + " normal windows to reopen"
);
})();
}
@@ -63,14 +63,15 @@ add_task(async function test_closed_window_states() {
let windowsToOpen = [
{ isPopup: false },
- { isPopup: false },
+ { isPopup: true },
+ { isPopup: true },
{ isPopup: true },
{ isPopup: true },
{ isPopup: true },
];
let expectedResults = {
- mac: { popup: 3, normal: 0 },
- other: { popup: 3, normal: 1 },
+ mac: { popup: 5, normal: 0 },
+ other: { popup: 5, normal: 1 },
};
await testWindows(windowsToOpen, expectedResults);
@@ -81,10 +82,11 @@ add_task(async function test_closed_window_states() {
{ isPopup: false },
{ isPopup: false },
{ isPopup: false },
+ { isPopup: false },
];
let expectedResults2 = {
- mac: { popup: 0, normal: 3 },
- other: { popup: 0, normal: 3 },
+ mac: { popup: 0, normal: 5 },
+ other: { popup: 0, normal: 5 },
};
await testWindows(windowsToOpen2, expectedResults2);
diff --git a/browser/components/sessionstore/test/browser_394759_purge.js b/browser/components/sessionstore/test/browser_394759_purge.js
index e5218c9936..ea75d6e4b2 100644
--- a/browser/components/sessionstore/test/browser_394759_purge.js
+++ b/browser/components/sessionstore/test/browser_394759_purge.js
@@ -9,7 +9,7 @@ let { ForgetAboutSite } = ChromeUtils.importESModule(
function promiseClearHistory() {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe() {
Services.obs.removeObserver(
this,
"browser:purge-session-history-for-domain"
diff --git a/browser/components/sessionstore/test/browser_459906.js b/browser/components/sessionstore/test/browser_459906.js
index 6827f6ad1d..5a0c1aeea3 100644
--- a/browser/components/sessionstore/test/browser_459906.js
+++ b/browser/components/sessionstore/test/browser_459906.js
@@ -17,7 +17,7 @@ function test() {
let tab = BrowserTestUtils.addTab(gBrowser, testURL);
tab.linkedBrowser.addEventListener(
"load",
- function listener(aEvent) {
+ function listener() {
// wait for all frames to load completely
if (frameCount++ < 2) {
return;
@@ -31,7 +31,7 @@ function test() {
let tab2 = gBrowser.duplicateTab(tab);
tab2.linkedBrowser.addEventListener(
"load",
- function loadListener(eventTab2) {
+ function loadListener() {
// wait for all frames to load (and reload!) completely
if (frameCount++ < 2) {
return;
diff --git a/browser/components/sessionstore/test/browser_461743.js b/browser/components/sessionstore/test/browser_461743.js
index fd4501b5ac..a27ccc7721 100644
--- a/browser/components/sessionstore/test/browser_461743.js
+++ b/browser/components/sessionstore/test/browser_461743.js
@@ -24,7 +24,7 @@ function test() {
let tab2 = gBrowser.duplicateTab(tab);
tab2.linkedBrowser.addEventListener(
"461743",
- function listener(eventTab2) {
+ function listener() {
tab2.linkedBrowser.removeEventListener("461743", listener, true);
is(aEvent.data, "done", "XSS injection was attempted");
diff --git a/browser/components/sessionstore/test/browser_464199.js b/browser/components/sessionstore/test/browser_464199.js
index 4ac8fba1a5..98a17c4955 100644
--- a/browser/components/sessionstore/test/browser_464199.js
+++ b/browser/components/sessionstore/test/browser_464199.js
@@ -9,7 +9,7 @@ let { ForgetAboutSite } = ChromeUtils.importESModule(
function promiseClearHistory() {
return new Promise(resolve => {
let observer = {
- observe(aSubject, aTopic, aData) {
+ observe() {
Services.obs.removeObserver(
this,
"browser:purge-session-history-for-domain"
diff --git a/browser/components/sessionstore/test/browser_464620_a.js b/browser/components/sessionstore/test/browser_464620_a.js
index 9052d7bec0..6a3b56f767 100644
--- a/browser/components/sessionstore/test/browser_464620_a.js
+++ b/browser/components/sessionstore/test/browser_464620_a.js
@@ -27,7 +27,7 @@ function test() {
let tab2 = gBrowser.duplicateTab(tab);
tab2.linkedBrowser.addEventListener(
"464620_a",
- function listener(eventTab2) {
+ function listener() {
tab2.linkedBrowser.removeEventListener("464620_a", listener, true);
is(aEvent.data, "done", "XSS injection was attempted");
diff --git a/browser/components/sessionstore/test/browser_464620_b.js b/browser/components/sessionstore/test/browser_464620_b.js
index 005bb4cc27..3e2b46d685 100644
--- a/browser/components/sessionstore/test/browser_464620_b.js
+++ b/browser/components/sessionstore/test/browser_464620_b.js
@@ -27,7 +27,7 @@ function test() {
let tab2 = gBrowser.duplicateTab(tab);
tab2.linkedBrowser.addEventListener(
"464620_b",
- function listener(eventTab2) {
+ function listener() {
tab2.linkedBrowser.removeEventListener("464620_b", listener, true);
is(aEvent.data, "done", "XSS injection was attempted");
diff --git a/browser/components/sessionstore/test/browser_526613.js b/browser/components/sessionstore/test/browser_526613.js
index ba3f03ef32..784febd3d5 100644
--- a/browser/components/sessionstore/test/browser_526613.js
+++ b/browser/components/sessionstore/test/browser_526613.js
@@ -45,7 +45,7 @@ function test() {
};
let pass = 1;
- function observer(aSubject, aTopic, aData) {
+ function observer(aSubject, aTopic) {
is(
aTopic,
"sessionstore-browser-state-restored",
diff --git a/browser/components/sessionstore/test/browser_580512.js b/browser/components/sessionstore/test/browser_580512.js
index 1dfd696277..e27dc61ba3 100644
--- a/browser/components/sessionstore/test/browser_580512.js
+++ b/browser/components/sessionstore/test/browser_580512.js
@@ -32,10 +32,10 @@ function closeFirstWin(win) {
win.gBrowser.pinTab(win.gBrowser.tabs[1]);
let winClosed = BrowserTestUtils.windowClosed(win);
- // We need to call BrowserTryToCloseWindow in order to trigger
+ // We need to call BrowserCommands.tryToCloseWindow in order to trigger
// the machinery that chooses whether or not to save the session
// for the last window.
- win.BrowserTryToCloseWindow();
+ win.BrowserCommands.tryToCloseWindow();
ok(win.closed, "window closed");
winClosed.then(() => {
@@ -88,7 +88,7 @@ function openWinWithCb(cb, argURIs, expectedURIs) {
var expectedLoads = expectedURIs.length;
win.gBrowser.addTabsProgressListener({
- onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, _aStatus) {
if (
aRequest &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
diff --git a/browser/components/sessionstore/test/browser_586068-apptabs.js b/browser/components/sessionstore/test/browser_586068-apptabs.js
index b2f92f760c..24878ba267 100644
--- a/browser/components/sessionstore/test/browser_586068-apptabs.js
+++ b/browser/components/sessionstore/test/browser_586068-apptabs.js
@@ -69,12 +69,7 @@ add_task(async function test() {
let loadCount = 0;
let promiseRestoringTabs = new Promise(resolve => {
- gProgressListener.setCallback(function (
- aBrowser,
- aNeedRestore,
- aRestoring,
- aRestored
- ) {
+ gProgressListener.setCallback(function (aBrowser) {
loadCount++;
// We'll make sure that the loads we get come from pinned tabs or the
diff --git a/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
index b729555ff1..6ef18e2b3a 100644
--- a/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
+++ b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
@@ -147,12 +147,7 @@ add_task(async function test() {
let loadCount = 0;
let promiseRestoringTabs = new Promise(resolve => {
- gProgressListener.setCallback(function (
- aBrowser,
- aNeedRestore,
- aRestoring,
- aRestored
- ) {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore) {
loadCount++;
if (
@@ -188,7 +183,7 @@ add_task(async function test() {
});
// We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
- Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+ Services.ww.registerNotification(function observer(aSubject, aTopic) {
if (aTopic == "domwindowopened") {
let win = aSubject;
win.addEventListener(
diff --git a/browser/components/sessionstore/test/browser_586068-multi_window.js b/browser/components/sessionstore/test/browser_586068-multi_window.js
index bf5d839812..352c5bcefb 100644
--- a/browser/components/sessionstore/test/browser_586068-multi_window.js
+++ b/browser/components/sessionstore/test/browser_586068-multi_window.js
@@ -72,12 +72,7 @@ add_task(async function test() {
let loadCount = 0;
let promiseRestoringTabs = new Promise(resolve => {
- gProgressListener.setCallback(function (
- aBrowser,
- aNeedRestore,
- aRestoring,
- aRestored
- ) {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore) {
if (++loadCount == numTabs) {
// We don't actually care about load order in this test, just that they all
// do load.
@@ -91,7 +86,7 @@ add_task(async function test() {
});
// We also want to catch the 2nd window, so we need to observe domwindowopened
- Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+ Services.ww.registerNotification(function observer(aSubject, aTopic) {
if (aTopic == "domwindowopened") {
let win = aSubject;
win.addEventListener(
diff --git a/browser/components/sessionstore/test/browser_586068-window_state.js b/browser/components/sessionstore/test/browser_586068-window_state.js
index 69c3742a66..25066a2db4 100644
--- a/browser/components/sessionstore/test/browser_586068-window_state.js
+++ b/browser/components/sessionstore/test/browser_586068-window_state.js
@@ -82,12 +82,7 @@ add_task(async function test() {
let loadCount = 0;
let promiseRestoringTabs = new Promise(resolve => {
- gProgressListener.setCallback(function (
- aBrowser,
- aNeedRestore,
- aRestoring,
- aRestored
- ) {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore) {
// When loadCount == 2, we'll also restore state2 into the window
if (++loadCount == 2) {
ss.setWindowState(window, JSON.stringify(state2), false);
diff --git a/browser/components/sessionstore/test/browser_586068-window_state_override.js b/browser/components/sessionstore/test/browser_586068-window_state_override.js
index 8a6eac6de2..eb3d2c709b 100644
--- a/browser/components/sessionstore/test/browser_586068-window_state_override.js
+++ b/browser/components/sessionstore/test/browser_586068-window_state_override.js
@@ -82,12 +82,7 @@ add_task(async function test() {
let loadCount = 0;
let promiseRestoringTabs = new Promise(resolve => {
- gProgressListener.setCallback(function (
- aBrowser,
- aNeedRestore,
- aRestoring,
- aRestored
- ) {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore) {
// When loadCount == 2, we'll also restore state2 into the window
if (++loadCount == 2) {
executeSoon(() =>
diff --git a/browser/components/sessionstore/test/browser_589246.js b/browser/components/sessionstore/test/browser_589246.js
index 2fd92b2b82..34d9dc97a8 100644
--- a/browser/components/sessionstore/test/browser_589246.js
+++ b/browser/components/sessionstore/test/browser_589246.js
@@ -164,7 +164,7 @@ function setupForTest(aConditions) {
ss.setBrowserState(JSON.stringify(testState));
}
-function onStateRestored(aSubject, aTopic, aData) {
+function onStateRestored() {
info("test #" + testNum + ": onStateRestored");
Services.obs.removeObserver(
onStateRestored,
@@ -183,7 +183,7 @@ function onStateRestored(aSubject, aTopic, aData) {
);
newWin.addEventListener(
"load",
- function (aEvent) {
+ function () {
promiseBrowserLoaded(newWin.gBrowser.selectedBrowser).then(() => {
// pin this tab
if (shouldPinTab) {
@@ -216,12 +216,12 @@ function onStateRestored(aSubject, aTopic, aData) {
newWin.gBrowser.removeTab(newTab);
newWin.gBrowser.removeTab(newTab2);
}
- newWin.BrowserTryToCloseWindow();
+ newWin.BrowserCommands.tryToCloseWindow();
},
{ capture: true, once: true }
);
} else {
- newWin.BrowserTryToCloseWindow();
+ newWin.BrowserCommands.tryToCloseWindow();
}
});
},
@@ -230,7 +230,7 @@ function onStateRestored(aSubject, aTopic, aData) {
}
// This will be called before the window is actually closed
-function onLastWindowClosed(aSubject, aTopic, aData) {
+function onLastWindowClosed() {
info("test #" + testNum + ": onLastWindowClosed");
Services.obs.removeObserver(
onLastWindowClosed,
@@ -261,7 +261,7 @@ function onWindowUnloaded() {
);
newWin.addEventListener(
"load",
- function (aEvent) {
+ function () {
newWin.gBrowser.selectedBrowser.addEventListener(
"load",
function () {
diff --git a/browser/components/sessionstore/test/browser_590268.js b/browser/components/sessionstore/test/browser_590268.js
index cde1a1cafa..eb1940e35d 100644
--- a/browser/components/sessionstore/test/browser_590268.js
+++ b/browser/components/sessionstore/test/browser_590268.js
@@ -52,7 +52,7 @@ function test() {
}
}
- function onSSTabRestored(aEvent) {
+ function onSSTabRestored() {
if (++restoredTabsCount < NUM_TABS) {
return;
}
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js
index b3ad6d240a..b2f7692b7c 100644
--- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js
@@ -29,11 +29,11 @@ function test_duplicateTab() {
// We'll look to make sure this value is on the duplicated tab
ss.setCustomTabValue(tab, "foo", "bar");
- function onSSWindowStateBusy(aEvent) {
+ function onSSWindowStateBusy() {
busyEventCount++;
}
- function onSSWindowStateReady(aEvent) {
+ function onSSWindowStateReady() {
newTab = gBrowser.tabs[2];
readyEventCount++;
is(ss.getCustomTabValue(newTab, "foo"), "bar");
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js
index 4dfcbc844d..fbca3301e6 100644
--- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js
@@ -84,7 +84,7 @@ function test() {
// waitForBrowserState does it's own observing for windows, but doesn't attach
// the listeners we want here, so do it ourselves.
let newWindow;
- function windowObserver(aSubject, aTopic, aData) {
+ function windowObserver(aSubject, aTopic) {
if (aTopic == "domwindowopened") {
Services.ww.unregisterNotification(windowObserver);
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js
index a76a8b3dd5..4b0c256388 100644
--- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js
@@ -29,17 +29,17 @@ function test_setTabState() {
let busyEventCount = 0;
let readyEventCount = 0;
- function onSSWindowStateBusy(aEvent) {
+ function onSSWindowStateBusy() {
busyEventCount++;
}
- function onSSWindowStateReady(aEvent) {
+ function onSSWindowStateReady() {
readyEventCount++;
is(ss.getCustomTabValue(tab, "foo"), "bar");
ss.setCustomTabValue(tab, "baz", "qux");
}
- function onSSTabRestoring(aEvent) {
+ function onSSTabRestoring() {
is(busyEventCount, 1);
is(readyEventCount, 1);
is(ss.getCustomTabValue(tab, "baz"), "qux");
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js
index c9d4bd00f5..daa40bd75a 100644
--- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js
@@ -29,17 +29,17 @@ function test() {
readyEventCount = 0,
tabRestoredCount = 0;
- function onSSWindowStateBusy(aEvent) {
+ function onSSWindowStateBusy() {
busyEventCount++;
}
- function onSSWindowStateReady(aEvent) {
+ function onSSWindowStateReady() {
readyEventCount++;
is(ss.getCustomTabValue(gBrowser.tabs[0], "foo"), "bar");
is(ss.getCustomTabValue(gBrowser.tabs[1], "baz"), "qux");
}
- function onSSTabRestored(aEvent) {
+ function onSSTabRestored() {
if (++tabRestoredCount < 2) {
return;
}
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js
index 345bba516c..b5d5af2835 100644
--- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js
@@ -24,11 +24,11 @@ add_task(async function test_undoCloseTab() {
ss.setCustomTabValue(tab, "foo", "bar");
- function onSSWindowStateBusy(aEvent) {
+ function onSSWindowStateBusy() {
busyEventCount++;
}
- function onSSWindowStateReady(aEvent) {
+ function onSSWindowStateReady() {
Assert.equal(gBrowser.tabs.length, 2, "Should only have 2 tabs");
lastTab = gBrowser.tabs[1];
readyEventCount++;
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js
index 0a5b07da29..7483583e5a 100644
--- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js
@@ -71,7 +71,7 @@ function test() {
let newWindow, reopenedWindow;
- function firstWindowObserver(aSubject, aTopic, aData) {
+ function firstWindowObserver(aSubject, aTopic) {
if (aTopic == "domwindowopened") {
newWindow = aSubject;
Services.ww.unregisterNotification(firstWindowObserver);
@@ -107,15 +107,15 @@ function test() {
readyEventCount = 0,
tabRestoredCount = 0;
// These will listen to the reopened closed window...
- function onSSWindowStateBusy(aEvent) {
+ function onSSWindowStateBusy() {
busyEventCount++;
}
- function onSSWindowStateReady(aEvent) {
+ function onSSWindowStateReady() {
readyEventCount++;
}
- function onSSTabRestored(aEvent) {
+ function onSSTabRestored() {
if (++tabRestoredCount < 4) {
return;
}
diff --git a/browser/components/sessionstore/test/browser_618151.js b/browser/components/sessionstore/test/browser_618151.js
index c38a349818..f3c44d1e88 100644
--- a/browser/components/sessionstore/test/browser_618151.js
+++ b/browser/components/sessionstore/test/browser_618151.js
@@ -46,7 +46,7 @@ function runNextTest() {
}
function test_setup() {
- function onSSTabRestored(aEvent) {
+ function onSSTabRestored() {
gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored);
runNextTest();
}
diff --git a/browser/components/sessionstore/test/browser_636279.js b/browser/components/sessionstore/test/browser_636279.js
index 3b71fcbb4c..4842f145b2 100644
--- a/browser/components/sessionstore/test/browser_636279.js
+++ b/browser/components/sessionstore/test/browser_636279.js
@@ -129,7 +129,7 @@ var TabsProgressListener = {
delete this.callback;
},
- observe(browser, topic, data) {
+ observe(browser) {
TabsProgressListener.onRestored(browser);
},
diff --git a/browser/components/sessionstore/test/browser_645428.js b/browser/components/sessionstore/test/browser_645428.js
index bbb3b1b299..3916c44a7e 100644
--- a/browser/components/sessionstore/test/browser_645428.js
+++ b/browser/components/sessionstore/test/browser_645428.js
@@ -6,7 +6,7 @@ const NOTIFICATION = "sessionstore-browser-state-restored";
function test() {
waitForExplicitFinish();
- function observe(subject, topic, data) {
+ function observe(subject, topic) {
if (NOTIFICATION == topic) {
finish();
ok(true, "TOPIC received");
diff --git a/browser/components/sessionstore/test/browser_687710_2.js b/browser/components/sessionstore/test/browser_687710_2.js
index 81d3c55379..190b5a718a 100644
--- a/browser/components/sessionstore/test/browser_687710_2.js
+++ b/browser/components/sessionstore/test/browser_687710_2.js
@@ -38,61 +38,31 @@ var state = {
add_task(async function test() {
let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
await promiseTabState(tab, state);
- if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
- await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
- function compareEntries(i, j, history) {
- let e1 = history.getEntryAtIndex(i);
- let e2 = history.getEntryAtIndex(j);
- ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
- is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
+ function compareEntries(i, j, history) {
+ let e1 = history.getEntryAtIndex(i);
+ let e2 = history.getEntryAtIndex(j);
- for (let c = 0; c < e1.childCount; c++) {
- let c1 = e1.GetChildAt(c);
- let c2 = e2.GetChildAt(c);
+ ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
+ is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
- ok(
- c1.sharesDocumentWith(c2),
- `Cousins should share documents. (${i}, ${j}, ${c})`
- );
- }
- }
+ for (let c = 0; c < e1.childCount; c++) {
+ let c1 = e1.GetChildAt(c);
+ let c2 = e2.GetChildAt(c);
- let history = docShell.browsingContext.childSessionHistory.legacySHistory;
-
- is(history.count, 2, "history.count");
- for (let i = 0; i < history.count; i++) {
- for (let j = 0; j < history.count; j++) {
- compareEntries(i, j, history);
- }
- }
- });
- } else {
- function compareEntries(i, j, history) {
- let e1 = history.getEntryAtIndex(i);
- let e2 = history.getEntryAtIndex(j);
-
- ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
- is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
-
- for (let c = 0; c < e1.childCount; c++) {
- let c1 = e1.GetChildAt(c);
- let c2 = e2.GetChildAt(c);
-
- ok(
- c1.sharesDocumentWith(c2),
- `Cousins should share documents. (${i}, ${j}, ${c})`
- );
- }
+ ok(
+ c1.sharesDocumentWith(c2),
+ `Cousins should share documents. (${i}, ${j}, ${c})`
+ );
}
+ }
- let history = tab.linkedBrowser.browsingContext.sessionHistory;
+ let history = tab.linkedBrowser.browsingContext.sessionHistory;
- is(history.count, 2, "history.count");
- for (let i = 0; i < history.count; i++) {
- for (let j = 0; j < history.count; j++) {
- compareEntries(i, j, history);
- }
+ is(history.count, 2, "history.count");
+ for (let i = 0; i < history.count; i++) {
+ for (let j = 0; j < history.count; j++) {
+ compareEntries(i, j, history);
}
}
diff --git a/browser/components/sessionstore/test/browser_705597.js b/browser/components/sessionstore/test/browser_705597.js
index d497e46a97..10f4f08863 100644
--- a/browser/components/sessionstore/test/browser_705597.js
+++ b/browser/components/sessionstore/test/browser_705597.js
@@ -26,14 +26,8 @@ function test() {
let browser = tab.linkedBrowser;
promiseTabState(tab, tabState).then(() => {
- let entry;
- if (!Services.appinfo.sessionHistoryInParent) {
- let sessionHistory = browser.sessionHistory;
- entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
- } else {
- let sessionHistory = browser.browsingContext.sessionHistory;
- entry = sessionHistory.getEntryAtIndex(0);
- }
+ let sessionHistory = browser.browsingContext.sessionHistory;
+ let entry = sessionHistory.getEntryAtIndex(0);
whenChildCount(entry, 1, function () {
whenChildCount(entry, 2, function () {
diff --git a/browser/components/sessionstore/test/browser_707862.js b/browser/components/sessionstore/test/browser_707862.js
index 765c63257f..4559362e21 100644
--- a/browser/components/sessionstore/test/browser_707862.js
+++ b/browser/components/sessionstore/test/browser_707862.js
@@ -26,26 +26,14 @@ function test() {
let browser = tab.linkedBrowser;
promiseTabState(tab, tabState).then(() => {
- let entry;
- if (!Services.appinfo.sessionHistoryInParent) {
- let sessionHistory = browser.sessionHistory;
- entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
- } else {
- let sessionHistory = browser.browsingContext.sessionHistory;
- entry = sessionHistory.getEntryAtIndex(0);
- }
+ let sessionHistory = browser.browsingContext.sessionHistory;
+ let entry = sessionHistory.getEntryAtIndex(0);
whenChildCount(entry, 1, function () {
whenChildCount(entry, 2, function () {
promiseBrowserLoaded(browser).then(() => {
- let newEntry;
- if (!Services.appinfo.sessionHistoryInParent) {
- let newSessionHistory = browser.sessionHistory;
- newEntry = newSessionHistory.legacySHistory.getEntryAtIndex(0);
- } else {
- let newSessionHistory = browser.browsingContext.sessionHistory;
- newEntry = newSessionHistory.getEntryAtIndex(0);
- }
+ let newSessionHistory = browser.browsingContext.sessionHistory;
+ let newEntry = newSessionHistory.getEntryAtIndex(0);
whenChildCount(newEntry, 0, function () {
// Make sure that we reset the state.
diff --git a/browser/components/sessionstore/test/browser_739531.js b/browser/components/sessionstore/test/browser_739531.js
index 507d10a5f1..e02a94d9a7 100644
--- a/browser/components/sessionstore/test/browser_739531.js
+++ b/browser/components/sessionstore/test/browser_739531.js
@@ -19,7 +19,7 @@ function test() {
removeFunc = BrowserTestUtils.addContentEventListener(
tab.linkedBrowser,
"load",
- function onLoad(aEvent) {
+ function onLoad() {
// make sure both the page and the frame are loaded
if (++loadCount < 2) {
return;
diff --git a/browser/components/sessionstore/test/browser_async_flushes.js b/browser/components/sessionstore/test/browser_async_flushes.js
index e35593dc30..d0bf039ff2 100644
--- a/browser/components/sessionstore/test/browser_async_flushes.js
+++ b/browser/components/sessionstore/test/browser_async_flushes.js
@@ -44,58 +44,6 @@ add_task(async function test_flush() {
gBrowser.removeTab(tab);
});
-add_task(async function test_crash() {
- if (Services.appinfo.sessionHistoryInParent) {
- // This test relies on frame script message ordering. Since the frame script
- // is unused with SHIP, there's no guarantee that we'll crash the frame
- // before we've started the flush.
- ok(true, "Test relies on frame script message ordering.");
- return;
- }
-
- // Create new tab.
- let tab = BrowserTestUtils.addTab(gBrowser, URL);
- gBrowser.selectedTab = tab;
- let browser = tab.linkedBrowser;
- await promiseBrowserLoaded(browser);
-
- // Flush to empty any queued update messages.
- await TabStateFlusher.flush(browser);
-
- // There should be one history entry.
- let { entries } = JSON.parse(ss.getTabState(tab));
- is(entries.length, 1, "there is a single history entry");
-
- // Click the link to navigate.
- await SpecialPowers.spawn(browser, [], async function () {
- return new Promise(resolve => {
- docShell.chromeEventHandler.addEventListener(
- "hashchange",
- () => resolve(),
- { once: true, capture: true }
- );
-
- // Click the link.
- content.document.querySelector("a").click();
- });
- });
-
- // Crash the browser and flush. Both messages are async and will be sent to
- // the content process. The "crash" message makes it first so that we don't
- // get a chance to process the flush. The TabStateFlusher however should be
- // notified so that the flush still completes.
- let promise1 = BrowserTestUtils.crashFrame(browser);
- let promise2 = TabStateFlusher.flush(browser);
- await Promise.all([promise1, promise2]);
-
- // The pending update should be lost.
- ({ entries } = JSON.parse(ss.getTabState(tab)));
- is(entries.length, 1, "still only one history entry");
-
- // Cleanup.
- gBrowser.removeTab(tab);
-});
-
add_task(async function test_remove() {
// Create new tab.
let tab = BrowserTestUtils.addTab(gBrowser, URL);
diff --git a/browser/components/sessionstore/test/browser_async_remove_tab.js b/browser/components/sessionstore/test/browser_async_remove_tab.js
index 7f74c57b40..1e3a75adfa 100644
--- a/browser/components/sessionstore/test/browser_async_remove_tab.js
+++ b/browser/components/sessionstore/test/browser_async_remove_tab.js
@@ -92,15 +92,7 @@ add_task(async function save_worthy_tabs_remote_final() {
ok(browser.isRemoteBrowser, "browser is still remote");
// Remove the tab before the update arrives.
- let promise = promiseRemoveTabAndSessionState(tab);
-
- // With SHIP, we'll do the final tab state update sooner than we did before.
- if (!Services.appinfo.sessionHistoryInParent) {
- // No tab state worth saving (that we know about yet).
- ok(!isValueInClosedData(r), "closed tab not saved");
- }
-
- await promise;
+ await promiseRemoveTabAndSessionState(tab);
// Turns out there is a tab state worth saving.
ok(isValueInClosedData(r), "closed tab saved");
@@ -117,15 +109,7 @@ add_task(async function save_worthy_tabs_nonremote_final() {
ok(!browser.isRemoteBrowser, "browser is not remote anymore");
// Remove the tab before the update arrives.
- let promise = promiseRemoveTabAndSessionState(tab);
-
- // With SHIP, we'll do the final tab state update sooner than we did before.
- if (!Services.appinfo.sessionHistoryInParent) {
- // No tab state worth saving (that we know about yet).
- ok(!isValueInClosedData(r), "closed tab not saved");
- }
-
- await promise;
+ await promiseRemoveTabAndSessionState(tab);
// Turns out there is a tab state worth saving.
ok(isValueInClosedData(r), "closed tab saved");
@@ -151,15 +135,7 @@ add_task(async function dont_save_empty_tabs_final() {
await entryReplaced;
// Remove the tab before the update arrives.
- let promise = promiseRemoveTabAndSessionState(tab);
-
- // With SHIP, we'll do the final tab state update sooner than we did before.
- if (!Services.appinfo.sessionHistoryInParent) {
- // Tab state deemed worth saving (yet).
- ok(isValueInClosedData(r), "closed tab saved");
- }
-
- await promise;
+ await promiseRemoveTabAndSessionState(tab);
// Turns out we don't want to save the tab state.
ok(!isValueInClosedData(r), "closed tab not saved");
diff --git a/browser/components/sessionstore/test/browser_async_window_flushing.js b/browser/components/sessionstore/test/browser_async_window_flushing.js
index d346f9eb1f..42e24bdd83 100644
--- a/browser/components/sessionstore/test/browser_async_window_flushing.js
+++ b/browser/components/sessionstore/test/browser_async_window_flushing.js
@@ -116,17 +116,10 @@ add_task(async function test_remove_uninteresting_window() {
await SpecialPowers.spawn(browser, [], async function () {
// Epic hackery to make this browser seem suddenly boring.
docShell.setCurrentURIForSessionStore(Services.io.newURI("about:blank"));
-
- if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
- let { sessionHistory } = docShell.QueryInterface(Ci.nsIWebNavigation);
- sessionHistory.legacySHistory.purgeHistory(sessionHistory.count);
- }
});
- if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
- let { sessionHistory } = browser.browsingContext;
- sessionHistory.purgeHistory(sessionHistory.count);
- }
+ let { sessionHistory } = browser.browsingContext;
+ sessionHistory.purgeHistory(sessionHistory.count);
// Once this windowClosed Promise resolves, we should have finished
// the flush and revisited our decision to put this window into
diff --git a/browser/components/sessionstore/test/browser_attributes.js b/browser/components/sessionstore/test/browser_attributes.js
index a0ee6d5b0c..491ec5db22 100644
--- a/browser/components/sessionstore/test/browser_attributes.js
+++ b/browser/components/sessionstore/test/browser_attributes.js
@@ -38,45 +38,68 @@ add_task(async function test() {
ok(tab.hasAttribute("muted"), "tab.muted exists");
// Make sure we do not persist 'image' and 'muted' attributes.
- ss.persistTabAttribute("image");
- ss.persistTabAttribute("muted");
let { attributes } = JSON.parse(ss.getTabState(tab));
ok(!("image" in attributes), "'image' attribute not saved");
ok(!("muted" in attributes), "'muted' attribute not saved");
- ok(!("custom" in attributes), "'custom' attribute not saved");
-
- // Test persisting a custom attribute.
- tab.setAttribute("custom", "foobar");
- ss.persistTabAttribute("custom");
-
- ({ attributes } = JSON.parse(ss.getTabState(tab)));
- is(attributes.custom, "foobar", "'custom' attribute is correct");
-
- // Make sure we're backwards compatible and restore old 'image' attributes.
+ ok(!("customizemode" in attributes), "'customizemode' attribute not saved");
+
+ // Test persisting a customizemode attribute.
+ {
+ let customizationReady = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gCustomizeMode.enter();
+ await customizationReady;
+ }
+
+ let customizeIcon = gBrowser.getIcon(gBrowser.selectedTab);
+ ({ attributes } = JSON.parse(ss.getTabState(gBrowser.selectedTab)));
+ ok(!("image" in attributes), "'image' attribute not saved");
+ is(attributes.customizemode, "true", "'customizemode' attribute is correct");
+
+ {
+ let afterCustomization = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "aftercustomization"
+ );
+ gCustomizeMode.exit();
+ await afterCustomization;
+ }
+
+ // Test restoring a customizemode tab.
let state = {
- entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
- attributes: { custom: "foobaz" },
- image: gBrowser.getIcon(tab),
+ entries: [],
+ attributes: { customizemode: "true", nonpersisted: "true" },
};
+ // Customize mode doesn't like being restored on top of a non-blank tab.
+ // For the moment, it appears it isn't possible to restore customizemode onto
+ // an existing non-blank tab outside of tests, however this may be a latent
+ // bug if we ever try to do that in the future.
+ let principal = Services.scriptSecurityManager.createNullPrincipal({});
+ tab.linkedBrowser.createAboutBlankDocumentViewer(principal, principal);
+
// Prepare a pending tab waiting to be restored.
let promise = promiseTabRestoring(tab);
ss.setTabState(tab, JSON.stringify(state));
await promise;
ok(tab.hasAttribute("pending"), "tab is pending");
- is(gBrowser.getIcon(tab), state.image, "tab has correct icon");
+ ok(tab.hasAttribute("customizemode"), "tab is in customizemode");
+ ok(!tab.hasAttribute("nonpersisted"), "tab has no nonpersisted attribute");
+ is(gBrowser.getIcon(tab), customizeIcon, "tab has correct icon");
ok(!state.attributes.image, "'image' attribute not saved");
// Let the pending tab load.
gBrowser.selectedTab = tab;
- await promiseTabRestored(tab);
// Ensure no 'image' or 'pending' attributes are stored.
({ attributes } = JSON.parse(ss.getTabState(tab)));
ok(!("image" in attributes), "'image' attribute not saved");
ok(!("pending" in attributes), "'pending' attribute not saved");
- is(attributes.custom, "foobaz", "'custom' attribute is correct");
+ ok(!("nonpersisted" in attributes), "'nonpersisted' attribute not saved");
+ is(attributes.customizemode, "true", "'customizemode' attribute is correct");
// Clean up.
gBrowser.removeTab(tab);
diff --git a/browser/components/sessionstore/test/browser_bfcache_telemetry.js b/browser/components/sessionstore/test/browser_bfcache_telemetry.js
index 5faa2822ea..c1e9877505 100644
--- a/browser/components/sessionstore/test/browser_bfcache_telemetry.js
+++ b/browser/components/sessionstore/test/browser_bfcache_telemetry.js
@@ -39,7 +39,6 @@ async function test_bfcache_telemetry(probeInParent) {
add_task(async () => {
await test_bfcache_telemetry(
- Services.appinfo.sessionHistoryInParent &&
- Services.prefs.getBoolPref("fission.bfcacheInParent")
+ Services.prefs.getBoolPref("fission.bfcacheInParent")
);
});
diff --git a/browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js b/browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js
index 081167acfa..c80e63df04 100644
--- a/browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js
+++ b/browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js
@@ -81,10 +81,6 @@ async function prepareClosedData() {
const testWindow7 = await BrowserTestUtils.openNewBrowserWindow();
await openAndCloseTab(testWindow7, TEST_URLS[4]);
- let closedTabsHistogram = TelemetryTestUtils.getAndClearHistogram(
- "FX_SESSION_RESTORE_CLOSED_TABS_NOT_SAVED"
- );
-
await BrowserTestUtils.closeWindow(testWindow1);
closedIds.testWindow1 = SessionStore.getClosedWindowData()[0].closedId;
await BrowserTestUtils.closeWindow(testWindow2);
@@ -100,13 +96,7 @@ async function prepareClosedData() {
);
await BrowserTestUtils.closeWindow(testWindow6);
- TelemetryTestUtils.assertHistogram(closedTabsHistogram, 0, 1);
- closedTabsHistogram.clear();
-
await BrowserTestUtils.closeWindow(testWindow7);
- TelemetryTestUtils.assertHistogram(closedTabsHistogram, 1, 1);
- closedTabsHistogram.clear();
-
return closedIds;
}
diff --git a/browser/components/sessionstore/test/browser_cookies.js b/browser/components/sessionstore/test/browser_cookies.js
index f514efc777..96244dda1a 100644
--- a/browser/components/sessionstore/test/browser_cookies.js
+++ b/browser/components/sessionstore/test/browser_cookies.js
@@ -14,7 +14,7 @@ function promiseSetCookie(cookie) {
function waitForCookieChanged() {
return new Promise(resolve => {
- Services.obs.addObserver(function observer(subj, topic, data) {
+ Services.obs.addObserver(function observer(subj, topic) {
Services.obs.removeObserver(observer, topic);
resolve();
}, "session-cookie-changed");
diff --git a/browser/components/sessionstore/test/browser_crashedTabs.js b/browser/components/sessionstore/test/browser_crashedTabs.js
index 32c064dd81..797cf5ecf8 100644
--- a/browser/components/sessionstore/test/browser_crashedTabs.js
+++ b/browser/components/sessionstore/test/browser_crashedTabs.js
@@ -82,7 +82,7 @@ function promiseTabCrashedReady(browser) {
return new Promise(resolve => {
browser.addEventListener(
"AboutTabCrashedReady",
- function ready(e) {
+ function ready() {
browser.removeEventListener("AboutTabCrashedReady", ready, false, true);
resolve();
},
diff --git a/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js b/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js
index 1b152139d7..6fc212eb2b 100644
--- a/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js
+++ b/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js
@@ -4,38 +4,18 @@ add_task(async function duplicateTab() {
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
- if (!Services.appinfo.sessionHistoryInParent) {
- await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
- let docshell = content.window.docShell.QueryInterface(
- Ci.nsIWebNavigation
- );
- let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0);
- is(shEntry.docshellID.toString(), docshell.historyID.toString());
- });
- } else {
- let historyID = tab.linkedBrowser.browsingContext.historyID;
- let shEntry =
- tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
- is(shEntry.docshellID.toString(), historyID.toString());
- }
+ let historyID = tab.linkedBrowser.browsingContext.historyID;
+ let shEntry =
+ tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
+ is(shEntry.docshellID.toString(), historyID.toString());
let tab2 = gBrowser.duplicateTab(tab);
await BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
- if (!Services.appinfo.sessionHistoryInParent) {
- await SpecialPowers.spawn(tab2.linkedBrowser, [], function () {
- let docshell = content.window.docShell.QueryInterface(
- Ci.nsIWebNavigation
- );
- let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0);
- is(shEntry.docshellID.toString(), docshell.historyID.toString());
- });
- } else {
- let historyID = tab2.linkedBrowser.browsingContext.historyID;
- let shEntry =
- tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
- is(shEntry.docshellID.toString(), historyID.toString());
- }
+ historyID = tab2.linkedBrowser.browsingContext.historyID;
+ shEntry =
+ tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
+ is(shEntry.docshellID.toString(), historyID.toString());
BrowserTestUtils.removeTab(tab);
BrowserTestUtils.removeTab(tab2);
@@ -47,24 +27,10 @@ add_task(async function contentToChromeNavigate() {
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
- if (!Services.appinfo.sessionHistoryInParent) {
- await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
- let docshell = content.window.docShell.QueryInterface(
- Ci.nsIWebNavigation
- );
- let sh = docshell.sessionHistory;
- is(sh.count, 1);
- is(
- sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(),
- docshell.historyID.toString()
- );
- });
- } else {
- let historyID = tab.linkedBrowser.browsingContext.historyID;
- let sh = tab.linkedBrowser.browsingContext.sessionHistory;
- is(sh.count, 1);
- is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
- }
+ let historyID = tab.linkedBrowser.browsingContext.historyID;
+ let sh = tab.linkedBrowser.browsingContext.sessionHistory;
+ is(sh.count, 1);
+ is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
// Force the browser to navigate to the chrome process.
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:config");
@@ -74,31 +40,17 @@ add_task(async function contentToChromeNavigate() {
let docShell = tab.linkedBrowser.frameLoader.docShell;
// 'cause we're in the chrome process, we can just directly poke at the shistory.
- if (!Services.appinfo.sessionHistoryInParent) {
- let sh = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
-
- is(sh.count, 2);
- is(
- sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(),
- docShell.historyID.toString()
- );
- is(
- sh.legacySHistory.getEntryAtIndex(1).docshellID.toString(),
- docShell.historyID.toString()
- );
- } else {
- let sh = docShell.browsingContext.sessionHistory;
-
- is(sh.count, 2);
- is(
- sh.getEntryAtIndex(0).docshellID.toString(),
- docShell.historyID.toString()
- );
- is(
- sh.getEntryAtIndex(1).docshellID.toString(),
- docShell.historyID.toString()
- );
- }
+ sh = docShell.browsingContext.sessionHistory;
+
+ is(sh.count, 2);
+ is(
+ sh.getEntryAtIndex(0).docshellID.toString(),
+ docShell.historyID.toString()
+ );
+ is(
+ sh.getEntryAtIndex(1).docshellID.toString(),
+ docShell.historyID.toString()
+ );
BrowserTestUtils.removeTab(tab);
});
diff --git a/browser/components/sessionstore/test/browser_frame_history.js b/browser/components/sessionstore/test/browser_frame_history.js
index 1db32e74ab..eeb6de177c 100644
--- a/browser/components/sessionstore/test/browser_frame_history.js
+++ b/browser/components/sessionstore/test/browser_frame_history.js
@@ -206,7 +206,7 @@ function waitForLoadsInBrowser(aBrowser, aLoadCount) {
let loadCount = 0;
aBrowser.addEventListener(
"load",
- function listener(aEvent) {
+ function listener() {
if (++loadCount < aLoadCount) {
info(
"Got " + loadCount + " loads, waiting until we have " + aLoadCount
diff --git a/browser/components/sessionstore/test/browser_frametree.js b/browser/components/sessionstore/test/browser_frametree.js
index ce1f5cdf0b..06e0379c59 100644
--- a/browser/components/sessionstore/test/browser_frametree.js
+++ b/browser/components/sessionstore/test/browser_frametree.js
@@ -98,7 +98,7 @@ add_task(async function test_frametree_dynamic() {
is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
// Remopve a non-dynamic iframe.
- await SpecialPowers.spawn(browser, [URL], async ([url]) => {
+ await SpecialPowers.spawn(browser, [URL], async () => {
// Remove the first iframe, which should be a non-dynamic iframe.
content.document.body.removeChild(
content.document.getElementsByTagName("iframe")[0]
diff --git a/browser/components/sessionstore/test/browser_history_persist.js b/browser/components/sessionstore/test/browser_history_persist.js
index f6749b02e3..1cf8bf1b8d 100644
--- a/browser/components/sessionstore/test/browser_history_persist.js
+++ b/browser/components/sessionstore/test/browser_history_persist.js
@@ -25,54 +25,27 @@ add_task(async function check_history_not_persisted() {
browser = tab.linkedBrowser;
await promiseTabState(tab, state);
- if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
- await SpecialPowers.spawn(browser, [], function () {
- let sessionHistory =
- docShell.browsingContext.childSessionHistory.legacySHistory;
-
- is(sessionHistory.count, 1, "Should be a single history entry");
- is(
- sessionHistory.getEntryAtIndex(0).URI.spec,
- "about:blank",
- "Should be the right URL"
- );
- });
- } else {
- let sessionHistory = browser.browsingContext.sessionHistory;
-
- is(sessionHistory.count, 1, "Should be a single history entry");
- is(
- sessionHistory.getEntryAtIndex(0).URI.spec,
- "about:blank",
- "Should be the right URL"
- );
- }
+ let sessionHistory = browser.browsingContext.sessionHistory;
+
+ is(sessionHistory.count, 1, "Should be a single history entry");
+ is(
+ sessionHistory.getEntryAtIndex(0).URI.spec,
+ "about:blank",
+ "Should be the right URL"
+ );
// Load a new URL into the tab, it should replace the about:blank history entry
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
await promiseBrowserLoaded(browser, false, "about:robots");
- if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
- await SpecialPowers.spawn(browser, [], function () {
- let sessionHistory =
- docShell.browsingContext.childSessionHistory.legacySHistory;
-
- is(sessionHistory.count, 1, "Should be a single history entry");
- is(
- sessionHistory.getEntryAtIndex(0).URI.spec,
- "about:robots",
- "Should be the right URL"
- );
- });
- } else {
- let sessionHistory = browser.browsingContext.sessionHistory;
-
- is(sessionHistory.count, 1, "Should be a single history entry");
- is(
- sessionHistory.getEntryAtIndex(0).URI.spec,
- "about:robots",
- "Should be the right URL"
- );
- }
+
+ sessionHistory = browser.browsingContext.sessionHistory;
+
+ is(sessionHistory.count, 1, "Should be a single history entry");
+ is(
+ sessionHistory.getEntryAtIndex(0).URI.spec,
+ "about:robots",
+ "Should be the right URL"
+ );
// Cleanup.
BrowserTestUtils.removeTab(tab);
@@ -99,64 +72,33 @@ add_task(async function check_history_default_persisted() {
tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
browser = tab.linkedBrowser;
await promiseTabState(tab, state);
- if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
- await SpecialPowers.spawn(browser, [], function () {
- let sessionHistory =
- docShell.browsingContext.childSessionHistory.legacySHistory;
-
- is(sessionHistory.count, 1, "Should be a single history entry");
- is(
- sessionHistory.getEntryAtIndex(0).URI.spec,
- "about:blank",
- "Should be the right URL"
- );
- });
- } else {
- let sessionHistory = browser.browsingContext.sessionHistory;
-
- is(sessionHistory.count, 1, "Should be a single history entry");
- is(
- sessionHistory.getEntryAtIndex(0).URI.spec,
- "about:blank",
- "Should be the right URL"
- );
- }
+
+ let sessionHistory = browser.browsingContext.sessionHistory;
+
+ is(sessionHistory.count, 1, "Should be a single history entry");
+ is(
+ sessionHistory.getEntryAtIndex(0).URI.spec,
+ "about:blank",
+ "Should be the right URL"
+ );
// Load a new URL into the tab, it should replace the about:blank history entry
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
await promiseBrowserLoaded(browser, false, "about:robots");
- if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
- await SpecialPowers.spawn(browser, [], function () {
- let sessionHistory =
- docShell.browsingContext.childSessionHistory.legacySHistory;
-
- is(sessionHistory.count, 2, "Should be two history entries");
- is(
- sessionHistory.getEntryAtIndex(0).URI.spec,
- "about:blank",
- "Should be the right URL"
- );
- is(
- sessionHistory.getEntryAtIndex(1).URI.spec,
- "about:robots",
- "Should be the right URL"
- );
- });
- } else {
- let sessionHistory = browser.browsingContext.sessionHistory;
-
- is(sessionHistory.count, 2, "Should be two history entries");
- is(
- sessionHistory.getEntryAtIndex(0).URI.spec,
- "about:blank",
- "Should be the right URL"
- );
- is(
- sessionHistory.getEntryAtIndex(1).URI.spec,
- "about:robots",
- "Should be the right URL"
- );
- }
+
+ sessionHistory = browser.browsingContext.sessionHistory;
+
+ is(sessionHistory.count, 2, "Should be two history entries");
+ is(
+ sessionHistory.getEntryAtIndex(0).URI.spec,
+ "about:blank",
+ "Should be the right URL"
+ );
+ is(
+ sessionHistory.getEntryAtIndex(1).URI.spec,
+ "about:robots",
+ "Should be the right URL"
+ );
// Cleanup.
BrowserTestUtils.removeTab(tab);
diff --git a/browser/components/sessionstore/test/browser_newtab_userTypedValue.js b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js
index 755a1f2859..cd17c9a9f0 100644
--- a/browser/components/sessionstore/test/browser_newtab_userTypedValue.js
+++ b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js
@@ -16,7 +16,7 @@ add_task(async function () {
);
// This opens about:newtab:
- win.BrowserOpenTab();
+ win.BrowserCommands.openTab();
let tab = await tabOpenedAndSwitchedTo;
is(win.gURLBar.value, "", "URL bar should be empty");
is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
@@ -55,7 +55,7 @@ add_task(async function () {
for (let url of gInitialPages) {
if (url == BROWSER_NEW_TAB_URL) {
- continue; // We tested about:newtab using BrowserOpenTab() above.
+ continue; // We tested about:newtab using BrowserCommands.openTab() above.
}
info("Testing " + url + " - " + new Date());
await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
diff --git a/browser/components/sessionstore/test/browser_oldformat.toml b/browser/components/sessionstore/test/browser_oldformat.toml
new file mode 100644
index 0000000000..7edc51dc67
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_oldformat.toml
@@ -0,0 +1,301 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "browser_formdata_sample.html",
+ "browser_formdata_xpath_sample.html",
+ "browser_frametree_sample.html",
+ "browser_frametree_sample_frameset.html",
+ "browser_frametree_sample_iframes.html",
+ "browser_frame_history_index.html",
+ "browser_frame_history_index2.html",
+ "browser_frame_history_index_blank.html",
+ "browser_frame_history_a.html",
+ "browser_frame_history_b.html",
+ "browser_frame_history_c.html",
+ "browser_frame_history_c1.html",
+ "browser_frame_history_c2.html",
+ "browser_formdata_format_sample.html",
+ "browser_sessionHistory_slow.sjs",
+ "browser_scrollPositions_sample.html",
+ "browser_scrollPositions_sample2.html",
+ "browser_scrollPositions_sample_frameset.html",
+ "browser_scrollPositions_readerModeArticle.html",
+ "browser_sessionStorage.html",
+ "browser_speculative_connect.html",
+ "browser_248970_b_sample.html",
+ "browser_339445_sample.html",
+ "browser_423132_sample.html",
+ "browser_447951_sample.html",
+ "browser_454908_sample.html",
+ "browser_456342_sample.xhtml",
+ "browser_463205_sample.html",
+ "browser_463206_sample.html",
+ "browser_466937_sample.html",
+ "browser_485482_sample.html",
+ "browser_637020_slow.sjs",
+ "browser_662743_sample.html",
+ "browser_739531_sample.html",
+ "browser_739531_frame.html",
+ "browser_911547_sample.html",
+ "browser_911547_sample.html^headers^",
+ "coopHeaderCommon.sjs",
+ "restore_redirect_http.html",
+ "restore_redirect_http.html^headers^",
+ "restore_redirect_js.html",
+ "restore_redirect_target.html",
+ "browser_1234021_page.html",
+ "browser_1284886_suspend_tab.html",
+ "browser_1284886_suspend_tab_2.html",
+ "empty.html",
+ "coop_coep.html",
+ "coop_coep.html^headers^",
+]
+# remove this after bug 1628486 is landed
+prefs = [
+ "network.cookie.cookieBehavior=5",
+ "gfx.font_rendering.fallback.async=false",
+ "browser.sessionstore.closedTabsFromAllWindows=true",
+ "browser.sessionstore.closedTabsFromClosedWindows=true",
+]
+
+#NB: the following are disabled
+# browser_464620_a.html
+# browser_464620_b.html
+# browser_464620_xd.html
+
+#disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html
+#disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html
+#disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html
+
+["browser_1234021.js"]
+
+["browser_1284886_suspend_tab.js"]
+
+["browser_1446343-windowsize.js"]
+skip-if = ["os == 'linux'"] # Bug 1600180
+
+["browser_248970_b_perwindowpb.js"]
+# Disabled because of leaks.
+# Re-enabling and rewriting this test is tracked in bug 936919.
+skip-if = ["true"]
+
+["browser_339445.js"]
+
+["browser_345898.js"]
+
+["browser_350525.js"]
+
+["browser_354894_perwindowpb.js"]
+
+["browser_367052.js"]
+
+["browser_393716.js"]
+skip-if = ["debug"] # Bug 1507747
+
+["browser_394759_basic.js"]
+# Disabled for intermittent failures, bug 944372.
+skip-if = ["true"]
+
+["browser_394759_behavior.js"]
+https_first_disabled = true
+
+["browser_394759_perwindowpb.js"]
+
+["browser_394759_purge.js"]
+
+["browser_423132.js"]
+
+["browser_447951.js"]
+
+["browser_454908.js"]
+
+["browser_456342.js"]
+
+["browser_461634.js"]
+
+["browser_463205.js"]
+
+["browser_463206.js"]
+
+["browser_464199.js"]
+# Disabled for frequent intermittent failures
+
+["browser_464620_a.js"]
+skip-if = ["true"]
+
+["browser_464620_b.js"]
+skip-if = ["true"]
+
+["browser_465215.js"]
+
+["browser_465223.js"]
+
+["browser_466937.js"]
+
+["browser_467409-backslashplosion.js"]
+
+["browser_477657.js"]
+skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1610668 for ubuntu 18.04
+
+["browser_480893.js"]
+
+["browser_485482.js"]
+
+["browser_485563.js"]
+
+["browser_490040.js"]
+
+["browser_491168.js"]
+
+["browser_491577.js"]
+skip-if = [
+ "verify && debug && os == 'mac'",
+ "verify && debug && os == 'win'",
+]
+
+["browser_495495.js"]
+
+["browser_500328.js"]
+
+["browser_514751.js"]
+
+["browser_522375.js"]
+
+["browser_522545.js"]
+skip-if = ["true"] # Bug 1380968
+
+["browser_524745.js"]
+skip-if = [
+ "win10_2009 && !ccov", # Bug 1418627
+ "os == 'linux'", # Bug 1803187
+]
+
+["browser_528776.js"]
+
+["browser_579868.js"]
+
+["browser_579879.js"]
+skip-if = ["os == 'linux' && (debug || asan)"] # Bug 1234404
+
+["browser_581937.js"]
+
+["browser_586068-apptabs.js"]
+
+["browser_586068-apptabs_ondemand.js"]
+skip-if = ["verify && (os == 'mac' || os == 'win')"]
+
+["browser_586068-browser_state_interrupted.js"]
+
+["browser_586068-cascade.js"]
+
+["browser_586068-multi_window.js"]
+
+["browser_586068-reload.js"]
+https_first_disabled = true
+
+["browser_586068-select.js"]
+
+["browser_586068-window_state.js"]
+
+["browser_586068-window_state_override.js"]
+
+["browser_586147.js"]
+
+["browser_588426.js"]
+
+["browser_590268.js"]
+
+["browser_590563.js"]
+
+["browser_595601-restore_hidden.js"]
+
+["browser_597071.js"]
+skip-if = ["true"] # Needs to be rewritten as Marionette test, bug 995916
+
+["browser_600545.js"]
+
+["browser_601955.js"]
+
+["browser_607016.js"]
+
+["browser_615394-SSWindowState_events_duplicateTab.js"]
+
+["browser_615394-SSWindowState_events_setBrowserState.js"]
+skip-if = ["verify && debug && os == 'mac'"]
+
+["browser_615394-SSWindowState_events_setTabState.js"]
+
+["browser_615394-SSWindowState_events_setWindowState.js"]
+https_first_disabled = true
+
+["browser_615394-SSWindowState_events_undoCloseTab.js"]
+
+["browser_615394-SSWindowState_events_undoCloseWindow.js"]
+skip-if = [
+ "os == 'win' && !debug", # Bug 1572554
+ "os == 'linux'", # Bug 1572554
+]
+
+["browser_618151.js"]
+
+["browser_623779.js"]
+
+["browser_624727.js"]
+
+["browser_625016.js"]
+skip-if = [
+ "os == 'mac'", # Disabled on OS X:
+ "os == 'linux'", # linux, Bug 1348583
+ "os == 'win' && debug", # Bug 1430977
+]
+
+["browser_628270.js"]
+
+["browser_635418.js"]
+
+["browser_636279.js"]
+
+["browser_637020.js"]
+
+["browser_645428.js"]
+
+["browser_659591.js"]
+
+["browser_662743.js"]
+
+["browser_662812.js"]
+skip-if = ["verify"]
+
+["browser_665702-state_session.js"]
+
+["browser_682507.js"]
+
+["browser_687710.js"]
+
+["browser_687710_2.js"]
+https_first_disabled = true
+
+["browser_694378.js"]
+
+["browser_701377.js"]
+skip-if = [
+ "verify && debug && os == 'win'",
+ "verify && debug && os == 'mac'",
+]
+
+["browser_705597.js"]
+
+["browser_707862.js"]
+
+["browser_739531.js"]
+
+["browser_739805.js"]
+
+["browser_819510_perwindowpb.js"]
+skip-if = ["true"] # Bug 1284312, Bug 1341980, bug 1381451
+
+["browser_906076_lazy_tabs.js"]
+https_first_disabled = true
+skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1446464
+
+["browser_911547.js"]
diff --git a/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js b/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js
index 442914d580..ad8144f864 100644
--- a/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js
+++ b/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js
@@ -12,7 +12,7 @@ const TESTURL = "about:testpageforsessionrestore#foo";
let TestAboutPage = {
QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]),
- getURIFlags(aURI) {
+ getURIFlags() {
// No CAN_ or MUST_LOAD_IN_CHILD means this loads in the parent:
return (
Ci.nsIAboutModule.ALLOW_SCRIPT |
@@ -73,7 +73,7 @@ add_task(async function () {
r => (resolveLocationChangePromise = r)
);
let wpl = {
- onStateChange(listener, request, state, status) {
+ onStateChange(listener, request, state, _status) {
let location = request.QueryInterface(Ci.nsIChannel).originalURI;
// Ignore about:blank loads.
let docStop =
diff --git a/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js b/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js
index cc340c4617..10551238f5 100644
--- a/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js
+++ b/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js
@@ -208,7 +208,7 @@ add_task(async function test_reopen_last_tab_if_no_closed_actions() {
gBrowser,
url: "about:blank",
},
- async browser => {
+ async () => {
const TEST_URL = "https://example.com/";
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
let update = BrowserTestUtils.waitForSessionStoreUpdate(tab);
diff --git a/browser/components/sessionstore/test/browser_send_async_message_oom.js b/browser/components/sessionstore/test/browser_send_async_message_oom.js
deleted file mode 100644
index 7e807f2fbd..0000000000
--- a/browser/components/sessionstore/test/browser_send_async_message_oom.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-/* eslint-disable mozilla/no-arbitrary-setTimeout */
-
-const HISTOGRAM_NAME = "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM";
-
-/**
- * Test that an OOM in sendAsyncMessage in a framescript will be reported
- * to Telemetry.
- */
-
-add_setup(async function () {
- Services.telemetry.canRecordExtended = true;
-});
-
-function frameScript() {
- // Make send[A]syncMessage("SessionStore:update", ...) simulate OOM.
- // Other operations are unaffected.
- let mm = docShell.messageManager;
-
- let wrap = function (original) {
- return function (name, ...args) {
- if (name != "SessionStore:update") {
- return original(name, ...args);
- }
- throw new Components.Exception(
- "Simulated OOM",
- Cr.NS_ERROR_OUT_OF_MEMORY
- );
- };
- };
-
- mm.sendAsyncMessage = wrap(mm.sendAsyncMessage.bind(mm));
- mm.sendSyncMessage = wrap(mm.sendSyncMessage.bind(mm));
-}
-
-add_task(async function () {
- // Capture original state.
- let snapshot = Services.telemetry.getHistogramById(HISTOGRAM_NAME).snapshot();
-
- // Open a browser, configure it to cause OOM.
- let newTab = BrowserTestUtils.addTab(gBrowser, "about:robots");
- let browser = newTab.linkedBrowser;
- await ContentTask.spawn(browser, null, frameScript);
-
- let promiseReported = new Promise(resolve => {
- browser.messageManager.addMessageListener("SessionStore:error", resolve);
- });
-
- // Attempt to flush. This should fail.
- let promiseFlushed = TabStateFlusher.flush(browser);
- promiseFlushed.then(success => {
- if (success) {
- throw new Error("Flush should have failed");
- }
- });
-
- // The frame script should report an error.
- await promiseReported;
-
- // Give us some time to handle that error.
- await new Promise(resolve => setTimeout(resolve, 10));
-
- // By now, Telemetry should have been updated.
- let snapshot2 = Services.telemetry
- .getHistogramById(HISTOGRAM_NAME)
- .snapshot();
- gBrowser.removeTab(newTab);
-
- Assert.ok(snapshot2.sum > snapshot.sum);
-});
-
-add_task(async function cleanup() {
- Services.telemetry.canRecordExtended = false;
-});
diff --git a/browser/components/sessionstore/test/browser_sessionHistory.js b/browser/components/sessionstore/test/browser_sessionHistory.js
index 69dcc4995b..34b1ef7d09 100644
--- a/browser/components/sessionstore/test/browser_sessionHistory.js
+++ b/browser/components/sessionstore/test/browser_sessionHistory.js
@@ -296,12 +296,9 @@ add_task(async function test_slow_subframe_load() {
* Ensure that document wireframes can be persisted when they're enabled.
*/
add_task(async function test_wireframes() {
- // Wireframes only works when Fission and SHIP are enabled.
- if (
- !Services.appinfo.fissionAutostart ||
- !Services.appinfo.sessionHistoryInParent
- ) {
- ok(true, "Skipping test_wireframes when Fission or SHIP is not enabled.");
+ // Wireframes only works when Fission is enabled.
+ if (!Services.appinfo.fissionAutostart) {
+ ok(true, "Skipping test_wireframes when Fission is not enabled.");
return;
}
diff --git a/browser/components/sessionstore/test/browser_sessionStoreContainer.js b/browser/components/sessionstore/test/browser_sessionStoreContainer.js
index 86833dea82..e4f3ecea9f 100644
--- a/browser/components/sessionstore/test/browser_sessionStoreContainer.js
+++ b/browser/components/sessionstore/test/browser_sessionStoreContainer.js
@@ -14,7 +14,7 @@ add_task(async function () {
await promiseBrowserLoaded(browser);
let tab2 = gBrowser.duplicateTab(tab);
- Assert.equal(tab2.getAttribute("usercontextid"), i);
+ Assert.equal(tab2.getAttribute("usercontextid") || "", i);
let browser2 = tab2.linkedBrowser;
await promiseTabRestored(tab2);
diff --git a/browser/components/sessionstore/test/browser_should_restore_tab.js b/browser/components/sessionstore/test/browser_should_restore_tab.js
index ab9513083a..958222141e 100644
--- a/browser/components/sessionstore/test/browser_should_restore_tab.js
+++ b/browser/components/sessionstore/test/browser_should_restore_tab.js
@@ -13,7 +13,7 @@ async function check_tab_close_notification(openedTab, expectNotification) {
let tabClosed = BrowserTestUtils.waitForTabClosing(openedTab);
let notified = false;
- function topicObserver(_, topic) {
+ function topicObserver() {
notified = true;
}
Services.obs.addObserver(topicObserver, NOTIFY_CLOSED_OBJECTS_CHANGED);
@@ -73,7 +73,7 @@ add_task(async function test_about_new_tab() {
() => {}
);
// This opens about:newtab:
- win.BrowserOpenTab();
+ win.BrowserCommands.openTab();
let tab = await tabOpenedAndSwitchedTo;
await check_tab_close_notification(tab, false);
});
diff --git a/browser/components/sessionstore/test/browser_windowStateContainer.js b/browser/components/sessionstore/test/browser_windowStateContainer.js
index f0d6f42d39..e2d2d256eb 100644
--- a/browser/components/sessionstore/test/browser_windowStateContainer.js
+++ b/browser/components/sessionstore/test/browser_windowStateContainer.js
@@ -11,7 +11,7 @@ add_setup(async function () {
function promiseTabsRestored(win, nExpected) {
return new Promise(resolve => {
let nReceived = 0;
- function handler(event) {
+ function handler() {
if (++nReceived === nExpected) {
win.gBrowser.tabContainer.removeEventListener(
"SSTabRestored",
diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js
index d475fa86a1..85db6e9d5e 100644
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -144,7 +144,7 @@ function waitForTopic(aTopic, aTimeout, aCallback) {
aCallback(false);
}, aTimeout);
- function observer(subject, topic, data) {
+ function observer() {
removeObserver();
timeout = clearTimeout(timeout);
executeSoon(() => aCallback(true));
@@ -268,7 +268,7 @@ var gWebProgressListener = {
}
},
- onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, _aStatus) {
if (
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
@@ -298,7 +298,7 @@ var gProgressListener = {
}
},
- observe(browser, topic, data) {
+ observe(browser) {
gProgressListener.onRestored(browser);
},
@@ -451,7 +451,7 @@ function modifySessionStorage(browser, storageData, storageOptions = {}) {
return SpecialPowers.spawn(
browsingContext,
[[storageData, storageOptions]],
- async function ([data, options]) {
+ async function ([data]) {
let frame = content;
let keys = new Set(Object.keys(data));
let isClearing = !keys.size;
@@ -558,35 +558,9 @@ function setPropertyOfFormField(browserContext, selector, propName, newValue) {
}
function promiseOnHistoryReplaceEntry(browser) {
- if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
- return new Promise(resolve => {
- let sessionHistory = browser.browsingContext?.sessionHistory;
- if (sessionHistory) {
- var historyListener = {
- OnHistoryNewEntry() {},
- OnHistoryGotoIndex() {},
- OnHistoryPurge() {},
- OnHistoryReload() {
- return true;
- },
-
- OnHistoryReplaceEntry() {
- resolve();
- },
-
- QueryInterface: ChromeUtils.generateQI([
- "nsISHistoryListener",
- "nsISupportsWeakReference",
- ]),
- };
-
- sessionHistory.addSHistoryListener(historyListener);
- }
- });
- }
-
- return SpecialPowers.spawn(browser, [], () => {
- return new Promise(resolve => {
+ return new Promise(resolve => {
+ let sessionHistory = browser.browsingContext?.sessionHistory;
+ if (sessionHistory) {
var historyListener = {
OnHistoryNewEntry() {},
OnHistoryGotoIndex() {},
@@ -605,13 +579,8 @@ function promiseOnHistoryReplaceEntry(browser) {
]),
};
- var { sessionHistory } = this.docShell.QueryInterface(
- Ci.nsIWebNavigation
- );
- if (sessionHistory) {
- sessionHistory.legacySHistory.addSHistoryListener(historyListener);
- }
- });
+ sessionHistory.addSHistoryListener(historyListener);
+ }
});
}
diff --git a/browser/components/shell/HeadlessShell.sys.mjs b/browser/components/shell/HeadlessShell.sys.mjs
index c87a7a6d56..7882031613 100644
--- a/browser/components/shell/HeadlessShell.sys.mjs
+++ b/browser/components/shell/HeadlessShell.sys.mjs
@@ -35,7 +35,7 @@ function loadContentWindow(browser, url) {
}
const principal = Services.scriptSecurityManager.getSystemPrincipal();
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
let oa = E10SUtils.predictOriginAttributes({
browser,
});
diff --git a/browser/components/shell/ShellService.sys.mjs b/browser/components/shell/ShellService.sys.mjs
index c4af0be7de..ed0c86d1a3 100644
--- a/browser/components/shell/ShellService.sys.mjs
+++ b/browser/components/shell/ShellService.sys.mjs
@@ -9,6 +9,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
@@ -18,6 +19,13 @@ XPCOMUtils.defineLazyServiceGetter(
"nsIXREDirProvider"
);
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "BackgroundTasks",
+ "@mozilla.org/backgroundtasks;1",
+ "nsIBackgroundTasks"
+);
+
ChromeUtils.defineLazyGetter(lazy, "log", () => {
let { ConsoleAPI } = ChromeUtils.importESModule(
"resource://gre/modules/Console.sys.mjs"
@@ -337,6 +345,16 @@ let ShellServiceInternal = {
}
this.shellService.setDefaultBrowser(forAllUsers);
+
+ // Disable showing toast notification from Firefox Background Tasks.
+ if (!lazy.BackgroundTasks?.isBackgroundTaskMode) {
+ await lazy.ASRouter.waitForInitialized;
+ const win = Services.wm.getMostRecentBrowserWindow() ?? null;
+ lazy.ASRouter.sendTriggerMessage({
+ browser: win,
+ id: "deeplinkedToWindowsSettingsUI",
+ });
+ }
},
async setAsDefault() {
diff --git a/browser/components/shell/content/setDesktopBackground.js b/browser/components/shell/content/setDesktopBackground.js
index 7448a3e076..70ab825354 100644
--- a/browser/components/shell/content/setDesktopBackground.js
+++ b/browser/components/shell/content/setDesktopBackground.js
@@ -234,7 +234,7 @@ if (AppConstants.platform != "macosx") {
);
};
} else {
- gSetBackground.observe = function (aSubject, aTopic, aData) {
+ gSetBackground.observe = function (aSubject, aTopic) {
if (aTopic == "shell:desktop-background-changed") {
document.getElementById("setDesktopBackground").hidden = true;
document.getElementById("showDesktopPreferences").hidden = false;
diff --git a/browser/components/shell/nsIWindowsShellService.idl b/browser/components/shell/nsIWindowsShellService.idl
index 13c824f39c..d28b713a78 100644
--- a/browser/components/shell/nsIWindowsShellService.idl
+++ b/browser/components/shell/nsIWindowsShellService.idl
@@ -112,7 +112,7 @@ interface nsIWindowsShellService : nsISupports
* successful or rejects with an nserror.
*/
[implicit_jscontext]
- Promise pinCurrentAppToTaskbarAsync(in bool aPrivateBrowsing);
+ Promise pinCurrentAppToTaskbarAsync(in boolean aPrivateBrowsing);
/*
* Do a dry run of pinCurrentAppToTaskbar().
@@ -128,7 +128,7 @@ interface nsIWindowsShellService : nsISupports
* @returns same as pinCurrentAppToTaskbarAsync()
*/
[implicit_jscontext]
- Promise checkPinCurrentAppToTaskbarAsync(in bool aPrivateBrowsing);
+ Promise checkPinCurrentAppToTaskbarAsync(in boolean aPrivateBrowsing);
/*
* Search for the current executable among taskbar pins
@@ -247,7 +247,7 @@ interface nsIWindowsShellService : nsISupports
AString classifyShortcut(in AString aPath);
[implicit_jscontext]
- Promise hasMatchingShortcut(in AString aAUMID, in bool aPrivateBrowsing);
+ Promise hasMatchingShortcut(in AString aAUMID, in boolean aPrivateBrowsing);
/*
* Check if setDefaultBrowserUserChoice() is expected to succeed.
@@ -257,7 +257,7 @@ interface nsIWindowsShellService : nsISupports
*
* @return true if the check succeeds, false otherwise.
*/
- bool canSetDefaultBrowserUserChoice();
+ boolean canSetDefaultBrowserUserChoice();
/*
* checkAllProgIDsExist() and checkBrowserUserChoiceHashes() are components
@@ -265,8 +265,8 @@ interface nsIWindowsShellService : nsISupports
*
* @return true if the check succeeds, false otherwise.
*/
- bool checkAllProgIDsExist();
- bool checkBrowserUserChoiceHashes();
+ boolean checkAllProgIDsExist();
+ boolean checkBrowserUserChoiceHashes();
/*
* Determines whether or not Firefox is the "Default Handler", i.e.,
diff --git a/browser/components/shell/test/browser_1119088.js b/browser/components/shell/test/browser_1119088.js
index bc0995fe51..62fc953f44 100644
--- a/browser/components/shell/test/browser_1119088.js
+++ b/browser/components/shell/test/browser_1119088.js
@@ -101,7 +101,7 @@ add_task(async function () {
gBrowser,
url: "about:logo",
},
- async browser => {
+ async () => {
let dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
Ci.nsIDirectoryServiceProvider
);
diff --git a/browser/components/shell/test/browser_420786.js b/browser/components/shell/test/browser_420786.js
index 025cd87943..9cbdc8c4b7 100644
--- a/browser/components/shell/test/browser_420786.js
+++ b/browser/components/shell/test/browser_420786.js
@@ -14,7 +14,7 @@ add_task(async function () {
gBrowser,
url: "about:logo",
},
- browser => {
+ () => {
var brandName = Services.strings
.createBundle("chrome://branding/locale/brand.properties")
.GetStringFromName("brandShortName");
diff --git a/browser/components/shell/test/browser_setDesktopBackgroundPreview.js b/browser/components/shell/test/browser_setDesktopBackgroundPreview.js
index b2dbe13db8..b8e2a38dd5 100644
--- a/browser/components/shell/test/browser_setDesktopBackgroundPreview.js
+++ b/browser/components/shell/test/browser_setDesktopBackgroundPreview.js
@@ -12,7 +12,7 @@ add_task(async function () {
gBrowser,
url: "about:logo",
},
- async browser => {
+ async () => {
const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => {
await BrowserTestUtils.waitForEvent(win, "load");
Assert.equal(
diff --git a/browser/components/shell/test/head.js b/browser/components/shell/test/head.js
index db1f8811fd..692ba918d0 100644
--- a/browser/components/shell/test/head.js
+++ b/browser/components/shell/test/head.js
@@ -89,7 +89,7 @@ async function testWindowSizePositive(width, height) {
}
let data = await IOUtils.read(screenshotPath);
- await new Promise((resolve, reject) => {
+ await new Promise(resolve => {
let blob = new Blob([data], { type: "image/png" });
let reader = new FileReader();
reader.onloadend = function () {
@@ -126,7 +126,7 @@ async function testGreen(url, path) {
}
let data = await IOUtils.read(path);
- let image = await new Promise((resolve, reject) => {
+ let image = await new Promise(resolve => {
let blob = new Blob([data], { type: "image/png" });
let reader = new FileReader();
reader.onloadend = function () {
diff --git a/browser/components/shopping/tests/browser/browser_exposure_telemetry.js b/browser/components/shopping/tests/browser/browser_exposure_telemetry.js
index 51334ce722..f76126aa9d 100644
--- a/browser/components/shopping/tests/browser/browser_exposure_telemetry.js
+++ b/browser/components/shopping/tests/browser/browser_exposure_telemetry.js
@@ -30,7 +30,7 @@ async function setup(pref) {
Services.fog.testResetFOG();
}
-async function teardown(pref) {
+async function teardown() {
await SpecialPowers.popPrefEnv();
await Services.fog.testFlushAllChildren();
Services.fog.testResetFOG();
diff --git a/browser/components/shopping/tests/browser/browser_shopping_settings.js b/browser/components/shopping/tests/browser/browser_shopping_settings.js
index 2508be05c7..a32488239e 100644
--- a/browser/components/shopping/tests/browser/browser_shopping_settings.js
+++ b/browser/components/shopping/tests/browser/browser_shopping_settings.js
@@ -16,7 +16,7 @@ add_task(async function test_shopping_settings_fakespot_learn_more() {
await SpecialPowers.spawn(
browser,
[MOCK_ANALYZED_PRODUCT_RESPONSE],
- async mockData => {
+ async () => {
let shoppingContainer =
content.document.querySelector(
"shopping-container"
@@ -55,7 +55,7 @@ add_task(async function test_shopping_settings_ads_learn_more() {
await SpecialPowers.spawn(
browser,
[MOCK_ANALYZED_PRODUCT_RESPONSE],
- async mockData => {
+ async () => {
let shoppingContainer =
content.document.querySelector(
"shopping-container"
@@ -404,7 +404,7 @@ add_task(
await SpecialPowers.spawn(
sidebar.querySelector("browser"),
[MOCK_ANALYZED_PRODUCT_RESPONSE],
- async mockData => {
+ async () => {
let shoppingContainer =
content.document.querySelector(
"shopping-container"
@@ -490,7 +490,7 @@ add_task(
await SpecialPowers.spawn(
sidebar.querySelector("browser"),
[MOCK_ANALYZED_PRODUCT_RESPONSE],
- async mockData => {
+ async () => {
let shoppingContainer =
content.document.querySelector(
"shopping-container"
diff --git a/browser/components/shopping/tests/browser/browser_shopping_urlbar.js b/browser/components/shopping/tests/browser/browser_shopping_urlbar.js
index 9eb396e846..d89db3ebcb 100644
--- a/browser/components/shopping/tests/browser/browser_shopping_urlbar.js
+++ b/browser/components/shopping/tests/browser/browser_shopping_urlbar.js
@@ -7,7 +7,7 @@ const CONTENT_PAGE = "https://example.com";
const PRODUCT_PAGE = "https://example.com/product/B09TJGHL5F";
add_task(async function test_button_hidden() {
- await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function () {
let shoppingButton = document.getElementById("shopping-sidebar-button");
ok(
BrowserTestUtils.isHidden(shoppingButton),
@@ -17,7 +17,7 @@ add_task(async function test_button_hidden() {
});
add_task(async function test_button_shown() {
- await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function () {
let shoppingButton = document.getElementById("shopping-sidebar-button");
ok(
BrowserTestUtils.isVisible(shoppingButton),
@@ -52,7 +52,7 @@ add_task(async function test_button_changes_with_location() {
add_task(async function test_button_active() {
Services.prefs.setBoolPref("browser.shopping.experience2023.active", true);
- await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function () {
let shoppingButton = document.getElementById("shopping-sidebar-button");
Assert.equal(
shoppingButton.getAttribute("shoppingsidebaropen"),
@@ -65,7 +65,7 @@ add_task(async function test_button_active() {
add_task(async function test_button_inactive() {
Services.prefs.setBoolPref("browser.shopping.experience2023.active", false);
- await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function () {
let shoppingButton = document.getElementById("shopping-sidebar-button");
Assert.equal(
shoppingButton.getAttribute("shoppingsidebaropen"),
@@ -245,7 +245,7 @@ add_task(async function test_button_right_click_doesnt_affect_sidebars() {
add_task(async function test_button_deals_with_tabswitches() {
Services.prefs.setBoolPref("browser.shopping.experience2023.active", true);
- await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function () {
let shoppingButton = document.getElementById("shopping-sidebar-button");
ok(
diff --git a/browser/components/shopping/tests/browser/browser_ui_telemetry.js b/browser/components/shopping/tests/browser/browser_ui_telemetry.js
index b97aca1963..69bdf50bd1 100644
--- a/browser/components/shopping/tests/browser/browser_ui_telemetry.js
+++ b/browser/components/shopping/tests/browser/browser_ui_telemetry.js
@@ -259,7 +259,7 @@ add_task(async function test_close_telemetry_recorded() {
set: [["browser.shopping.experience2023.active", true]],
});
- await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function () {
let shoppingButton = document.getElementById("shopping-sidebar-button");
shoppingButton.click();
});
diff --git a/browser/components/sidebar/browser-sidebar.js b/browser/components/sidebar/browser-sidebar.js
new file mode 100644
index 0000000000..55664f8cfc
--- /dev/null
+++ b/browser/components/sidebar/browser-sidebar.js
@@ -0,0 +1,744 @@
+/* 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/. */
+
+/**
+ * SidebarUI controls showing and hiding the browser sidebar.
+ */
+var SidebarUI = {
+ 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",
+ }),
+ ],
+ [
+ "viewHistorySidebar",
+ makeSidebar({
+ elementId: "sidebar-switcher-history",
+ url: this.sidebarRevampEnabled
+ ? "chrome://browser/content/sidebar/sidebar-history.html"
+ : "chrome://browser/content/places/historySidebar.xhtml",
+ menuId: "menu_historySidebar",
+ triggerButtonId: "appMenuViewHistorySidebar",
+ }),
+ ],
+ [
+ "viewTabsSidebar",
+ makeSidebar({
+ elementId: "sidebar-switcher-tabs",
+ url: this.sidebarRevampEnabled
+ ? "chrome://browser/content/sidebar/sidebar-syncedtabs.html"
+ : "chrome://browser/content/syncedtabs/sidebar.xhtml",
+ menuId: "menu_tabsSidebar",
+ }),
+ ],
+ [
+ "viewMegalistSidebar",
+ makeSidebar({
+ elementId: "sidebar-switcher-megalist",
+ url: "chrome://global/content/megalist/megalist.html",
+ menuId: "menu_megalistSidebar",
+ }),
+ ],
+ ]));
+ },
+
+ // Avoid getting the browser element from init() to avoid triggering the
+ // <browser> constructor during startup if the sidebar is hidden.
+ get browser() {
+ if (this._browser) {
+ return this._browser;
+ }
+ return (this._browser = document.getElementById("sidebar"));
+ },
+ POSITION_START_PREF: "sidebar.position_start",
+ DEFAULT_SIDEBAR_ID: "viewBookmarksSidebar",
+
+ // lastOpenedId is set in show() but unlike currentID it's not cleared out on hide
+ // and isn't persisted across windows
+ lastOpenedId: null,
+
+ _box: null,
+ // The constructor of this label accesses the browser element due to the
+ // control="sidebar" attribute, so avoid getting this label during startup.
+ get _title() {
+ if (this.__title) {
+ return this.__title;
+ }
+ return (this.__title = document.getElementById("sidebar-title"));
+ },
+ _splitter: null,
+ _reversePositionButton: null,
+ _switcherPanel: null,
+ _switcherTarget: null,
+ _switcherArrow: null,
+ _inited: false,
+
+ /**
+ * @type {MutationObserver | null}
+ */
+ _observer: null,
+
+ _initDeferred: Promise.withResolvers(),
+
+ get promiseInitialized() {
+ return this._initDeferred.promise;
+ },
+
+ get initialized() {
+ return this._inited;
+ },
+
+ async init() {
+ this._box = document.getElementById("sidebar-box");
+ this._splitter = document.getElementById("sidebar-splitter");
+ this._reversePositionButton = document.getElementById(
+ "sidebar-reverse-position"
+ );
+ this._switcherPanel = document.getElementById("sidebarMenu-popup");
+ this._switcherTarget = document.getElementById("sidebar-switcher-target");
+ this._switcherArrow = document.getElementById("sidebar-switcher-arrow");
+
+ if (this.sidebarRevampEnabled) {
+ await import("chrome://browser/content/sidebar/sidebar-launcher.mjs");
+ document.getElementById("sidebar-launcher").hidden = false;
+ document.getElementById("sidebar-header").hidden = true;
+ } else {
+ this._switcherTarget.addEventListener("command", () => {
+ this.toggleSwitcherPanel();
+ });
+ this._switcherTarget.addEventListener("keydown", event => {
+ this.handleKeydown(event);
+ });
+ }
+
+ this._inited = true;
+
+ Services.obs.addObserver(this, "intl:app-locales-changed");
+
+ this._initDeferred.resolve();
+ },
+
+ toggleMegalistItem() {
+ const sideMenuPopupItem = document.getElementById(
+ "sidebar-switcher-megalist"
+ );
+ sideMenuPopupItem.style.display = Services.prefs.getBoolPref(
+ "browser.megalist.enabled",
+ false
+ )
+ ? ""
+ : "none";
+ },
+
+ setMegalistMenubarVisibility(aEvent) {
+ const popup = aEvent.target;
+ if (popup != aEvent.currentTarget) {
+ return;
+ }
+
+ // Show the megalist item if enabled
+ const megalistItem = popup.querySelector("#menu_megalistSidebar");
+ megalistItem.hidden = !Services.prefs.getBoolPref(
+ "browser.megalist.enabled",
+ false
+ );
+ },
+
+ uninit() {
+ // If this is the last browser window, persist various values that should be
+ // remembered for after a restart / reopening a browser window.
+ 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");
+ }
+
+ Services.obs.removeObserver(this, "intl:app-locales-changed");
+
+ if (this._observer) {
+ this._observer.disconnect();
+ this._observer = null;
+ }
+ },
+
+ /**
+ * The handler for Services.obs.addObserver.
+ */
+ observe(_subject, topic, _data) {
+ switch (topic) {
+ case "intl:app-locales-changed": {
+ if (this.isOpen) {
+ // The <tree> component used in history and bookmarks, but it does not
+ // support live switching the app locale. Reload the entire sidebar to
+ // invalidate any old text.
+ this.hide();
+ this.showInitially(this.lastOpenedId);
+ break;
+ }
+ }
+ }
+ },
+
+ /**
+ * Ensure the title stays in sync with the source element, which updates for
+ * l10n changes.
+ *
+ * @param {HTMLElement} [element]
+ */
+ observeTitleChanges(element) {
+ if (!element) {
+ return;
+ }
+ let observer = this._observer;
+ if (!observer) {
+ observer = new MutationObserver(() => {
+ this.title = this.sidebars.get(this.lastOpenedId).title;
+ });
+ // Re-use the observer.
+ this._observer = observer;
+ }
+ observer.disconnect();
+ observer.observe(element, {
+ attributes: true,
+ attributeFilter: ["label"],
+ });
+ },
+
+ /**
+ * Opens the switcher panel if it's closed, or closes it if it's open.
+ */
+ toggleSwitcherPanel() {
+ if (
+ this._switcherPanel.state == "open" ||
+ this._switcherPanel.state == "showing"
+ ) {
+ this.hideSwitcherPanel();
+ } else if (this._switcherPanel.state == "closed") {
+ this.showSwitcherPanel();
+ }
+ },
+
+ /**
+ * Handles keydown on the the switcherTarget button
+ *
+ * @param {Event} event
+ */
+ handleKeydown(event) {
+ switch (event.key) {
+ case "Enter":
+ case " ": {
+ this.toggleSwitcherPanel();
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ }
+ case "Escape": {
+ this.hideSwitcherPanel();
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ }
+ }
+ },
+
+ hideSwitcherPanel() {
+ this._switcherPanel.hidePopup();
+ },
+
+ showSwitcherPanel() {
+ this.toggleMegalistItem();
+ this._switcherPanel.addEventListener(
+ "popuphiding",
+ () => {
+ this._switcherTarget.classList.remove("active");
+ this._switcherTarget.setAttribute("aria-expanded", false);
+ },
+ { once: true }
+ );
+
+ // Combine start/end position with ltr/rtl to set the label in the popup appropriately.
+ let label =
+ this._positionStart == RTL_UI
+ ? gNavigatorBundle.getString("sidebar.moveToLeft")
+ : gNavigatorBundle.getString("sidebar.moveToRight");
+ this._reversePositionButton.setAttribute("label", label);
+
+ // Open the sidebar switcher popup, anchored off the switcher toggle
+ this._switcherPanel.hidden = false;
+ this._switcherPanel.openPopup(this._switcherTarget);
+
+ this._switcherTarget.classList.add("active");
+ this._switcherTarget.setAttribute("aria-expanded", true);
+ },
+
+ updateShortcut({ keyId }) {
+ let menuitem = this._switcherPanel?.querySelector(`[key="${keyId}"]`);
+ if (!menuitem) {
+ // If the menu item doesn't exist yet then the accel text will be set correctly
+ // upon creation so there's nothing to do now.
+ return;
+ }
+ menuitem.removeAttribute("acceltext");
+ },
+
+ /**
+ * Change the pref that will trigger a call to setPosition
+ */
+ reversePosition() {
+ Services.prefs.setBoolPref(this.POSITION_START_PREF, !this._positionStart);
+ },
+
+ /**
+ * Read the positioning pref and position the sidebar and the splitter
+ * appropriately within the browser container.
+ */
+ setPosition() {
+ // First reset all ordinals to match DOM ordering.
+ let browser = document.getElementById("browser");
+ [...browser.children].forEach((node, i) => {
+ node.style.order = i + 1;
+ });
+ let sidebarLauncher = document.querySelector("sidebar-launcher");
+
+ if (!this._positionStart) {
+ // DOM ordering is: sidebar-launcher | sidebar-box | splitter | appcontent |
+ // Want to display as: | appcontent | splitter | sidebar-box | sidebar-launcher
+ // So we just swap box and appcontent ordering and move sidebar-launcher to the end
+ let appcontent = document.getElementById("appcontent");
+ let boxOrdinal = this._box.style.order;
+ this._box.style.order = appcontent.style.order;
+
+ appcontent.style.order = boxOrdinal;
+ // the launcher should be on the right of the sidebar-box
+ sidebarLauncher.style.order = parseInt(this._box.style.order) + 1;
+ // Indicate we've switched ordering to the box
+ this._box.setAttribute("positionend", true);
+ sidebarLauncher.setAttribute("positionend", true);
+ } else {
+ this._box.removeAttribute("positionend");
+ sidebarLauncher.removeAttribute("positionend");
+ }
+
+ this.hideSwitcherPanel();
+
+ let content = SidebarUI.browser.contentWindow;
+ if (content && content.updatePosition) {
+ content.updatePosition();
+ }
+ },
+
+ /**
+ * Try and adopt the status of the sidebar from another window.
+ *
+ * @param {Window} sourceWindow - Window to use as a source for sidebar status.
+ * @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;
+ if (!sourceUI || !sourceUI._box) {
+ // no source UI or no _box means we also can't adopt the state.
+ return false;
+ }
+
+ // Set sidebar command even if hidden, so that we keep the same sidebar
+ // even if it's currently closed.
+ let commandID = sourceUI._box.getAttribute("sidebarcommand");
+ if (commandID) {
+ this._box.setAttribute("sidebarcommand", commandID);
+ }
+
+ if (sourceUI._box.hidden) {
+ // just hidden means we have adopted the hidden state.
+ return true;
+ }
+
+ // dynamically generated sidebars will fail this check, but we still
+ // consider it adopted.
+ if (!this.sidebars.has(commandID)) {
+ return true;
+ }
+
+ this._box.style.width = sourceUI._box.getBoundingClientRect().width + "px";
+ this.showInitially(commandID);
+
+ return true;
+ },
+
+ windowPrivacyMatches(w1, w2) {
+ return (
+ PrivateBrowsingUtils.isWindowPrivate(w1) ===
+ PrivateBrowsingUtils.isWindowPrivate(w2)
+ );
+ },
+
+ /**
+ * If loading a sidebar was delayed on startup, start the load now.
+ */
+ startDelayedLoad() {
+ let sourceWindow = window.opener;
+ // No source window means this is the initial window. If we're being
+ // opened from another window, check that it is one we might open a sidebar
+ // for.
+ if (sourceWindow) {
+ if (
+ sourceWindow.closed ||
+ sourceWindow.location.protocol != "chrome:" ||
+ !this.windowPrivacyMatches(sourceWindow, window)
+ ) {
+ return;
+ }
+ // Try to adopt the sidebar state from the source window
+ if (this.adoptFromWindow(sourceWindow)) {
+ return;
+ }
+ }
+
+ // If we're not adopting settings from a parent window, set them now.
+ let wasOpen = this._box.getAttribute("checked");
+ if (!wasOpen) {
+ return;
+ }
+
+ let commandID = this._box.getAttribute("sidebarcommand");
+ if (commandID && this.sidebars.has(commandID)) {
+ this.showInitially(commandID);
+ } else {
+ this._box.removeAttribute("checked");
+ // Remove the |sidebarcommand| attribute, because the element it
+ // refers to no longer exists, so we should assume this sidebar
+ // panel has been uninstalled. (249883)
+ // We use setAttribute rather than removeAttribute so it persists
+ // correctly.
+ this._box.setAttribute("sidebarcommand", "");
+ // On a startup in which the startup cache was invalidated (e.g. app update)
+ // extensions will not be started prior to delayedLoad, thus the
+ // sidebarcommand element will not exist yet. Store the commandID so
+ // extensions may reopen if necessary. A startup cache invalidation
+ // can be forced (for testing) by deleting compatibility.ini from the
+ // profile.
+ this.lastOpenedId = commandID;
+ }
+ },
+
+ /**
+ * Fire a "SidebarShown" event on the sidebar to give any interested parties
+ * a chance to update the button or whatever.
+ */
+ _fireShowEvent() {
+ let event = new CustomEvent("SidebarShown", { bubbles: true });
+ this._switcherTarget.dispatchEvent(event);
+ },
+
+ /**
+ * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
+ * a chance to adjust focus as needed. An additional event is needed, because
+ * we don't want to focus the sidebar when it's opened on startup or in a new
+ * window, only when the user opens the sidebar.
+ */
+ _fireFocusedEvent() {
+ let event = new CustomEvent("SidebarFocused", { bubbles: true });
+ this.browser.contentWindow.dispatchEvent(event);
+ },
+
+ /**
+ * True if the sidebar is currently open.
+ */
+ get isOpen() {
+ return !this._box.hidden;
+ },
+
+ /**
+ * The ID of the current sidebar.
+ */
+ get currentID() {
+ return this.isOpen ? this._box.getAttribute("sidebarcommand") : "";
+ },
+
+ get title() {
+ return this._title.value;
+ },
+
+ set title(value) {
+ this._title.value = value;
+ },
+
+ /**
+ * Toggle the visibility of the sidebar. If the sidebar is hidden or is open
+ * with a different commandID, then the sidebar will be opened using the
+ * specified commandID. Otherwise the sidebar will be hidden.
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * visibility toggling of the sidebar.
+ * @returns {Promise}
+ */
+ toggle(commandID = this.lastOpenedId, triggerNode) {
+ if (
+ CustomizationHandler.isCustomizing() ||
+ CustomizationHandler.isExitingCustomizeMode
+ ) {
+ return Promise.resolve();
+ }
+ // First priority for a default value is this.lastOpenedId which is set during show()
+ // and not reset in hide(), unlike currentID. If show() hasn't been called and we don't
+ // have a persisted command either, or the command doesn't exist anymore, then
+ // fallback to a default sidebar.
+ if (!commandID) {
+ commandID = this._box.getAttribute("sidebarcommand");
+ }
+ if (!commandID || !this.sidebars.has(commandID)) {
+ commandID = this.DEFAULT_SIDEBAR_ID;
+ }
+
+ if (this.isOpen && commandID == this.currentID) {
+ this.hide(triggerNode);
+ return Promise.resolve();
+ }
+ return this.show(commandID, triggerNode);
+ },
+
+ _loadSidebarExtension(commandID) {
+ let sidebar = this.sidebars.get(commandID);
+ let { extensionId } = sidebar;
+ if (extensionId) {
+ SidebarUI.browser.contentWindow.loadPanel(
+ extensionId,
+ sidebar.panel,
+ sidebar.browserStyle
+ );
+ }
+ },
+
+ /**
+ * Show the sidebar.
+ *
+ * This wraps the internal method, including a ping to telemetry.
+ *
+ * @param {string} commandID ID of the sidebar to use.
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * showing of the sidebar.
+ * @returns {Promise<boolean>}
+ */
+ async show(commandID, triggerNode) {
+ let panelType = commandID.substring(4, commandID.length - 7);
+ Services.telemetry.keyedScalarAdd("sidebar.opened", panelType, 1);
+
+ // Extensions without private window access wont be in the
+ // sidebars map.
+ if (!this.sidebars.has(commandID)) {
+ return false;
+ }
+ return this._show(commandID).then(() => {
+ this._loadSidebarExtension(commandID);
+
+ if (triggerNode) {
+ updateToggleControlLabel(triggerNode);
+ }
+
+ this._fireFocusedEvent();
+ return true;
+ });
+ },
+
+ /**
+ * Show the sidebar, without firing the focused event or logging telemetry.
+ * This is intended to be used when the sidebar is opened automatically
+ * when a window opens (not triggered by user interaction).
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @returns {Promise<boolean>}
+ */
+ async showInitially(commandID) {
+ let panelType = commandID.substring(4, commandID.length - 7);
+ Services.telemetry.keyedScalarAdd("sidebar.opened", panelType, 1);
+
+ // Extensions without private window access wont be in the
+ // sidebars map.
+ if (!this.sidebars.has(commandID)) {
+ return false;
+ }
+ return this._show(commandID).then(() => {
+ this._loadSidebarExtension(commandID);
+ return true;
+ });
+ },
+
+ /**
+ * Implementation for show. Also used internally for sidebars that are shown
+ * when a window is opened and we don't want to ping telemetry.
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @returns {Promise<void>}
+ */
+ _show(commandID) {
+ return new Promise(resolve => {
+ 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._box.setAttribute("checked", "true");
+ this._box.setAttribute("sidebarcommand", commandID);
+
+ let { url, title, sourceL10nEl } = this.sidebars.get(commandID);
+
+ // use to live update <tree> elements if the locale changes
+ this.lastOpenedId = commandID;
+ this.title = title;
+ // Keep the title element in 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",
+ () => {
+ // We're handling the 'load' event before it bubbles up to the usual
+ // (non-capturing) event handlers. Let it bubble up before resolving.
+ setTimeout(() => {
+ resolve();
+
+ // Now that the currentId is updated, fire a show event.
+ this._fireShowEvent();
+ }, 0);
+ },
+ { capture: true, once: true }
+ );
+ } else {
+ resolve();
+
+ // Now that the currentId is updated, fire a show event.
+ this._fireShowEvent();
+ }
+ });
+ },
+
+ /**
+ * Hide the sidebar.
+ *
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * hiding of the sidebar.
+ */
+ hide(triggerNode) {
+ if (!this.isOpen) {
+ return;
+ }
+
+ 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
+ // so that we can free memory by unloading the page. We need to explicitly
+ // create a new content viewer because the old one doesn't get destroyed
+ // until about:blank has loaded (which does not happen as long as the
+ // element is hidden).
+ this.browser.setAttribute("src", "about:blank");
+ this.browser.docShell.createAboutBlankDocumentViewer(null, null);
+
+ this._box.removeAttribute("checked");
+ this._box.hidden = this._splitter.hidden = true;
+
+ let selBrowser = gBrowser.selectedBrowser;
+ selBrowser.focus();
+ if (triggerNode) {
+ updateToggleControlLabel(triggerNode);
+ }
+ },
+
+ /**
+ * Sets the checked state only on the menu items of the specified sidebar, or
+ * none if the argument is an empty string.
+ */
+ selectMenuItem(commandID) {
+ for (let [id, { menuId, triggerButtonId }] of this.sidebars) {
+ let menu = document.getElementById(menuId);
+ let triggerbutton =
+ triggerButtonId && document.getElementById(triggerButtonId);
+ if (id == commandID) {
+ menu.setAttribute("checked", "true");
+ if (triggerbutton) {
+ triggerbutton.setAttribute("checked", "true");
+ updateToggleControlLabel(triggerbutton);
+ }
+ } else {
+ menu.removeAttribute("checked");
+ if (triggerbutton) {
+ triggerbutton.removeAttribute("checked");
+ updateToggleControlLabel(triggerbutton);
+ }
+ }
+ }
+ },
+};
+
+// Add getters related to the position here, since we will want them
+// available for both startDelayedLoad and init.
+XPCOMUtils.defineLazyPreferenceGetter(
+ SidebarUI,
+ "_positionStart",
+ SidebarUI.POSITION_START_PREF,
+ true,
+ SidebarUI.setPosition.bind(SidebarUI)
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ SidebarUI,
+ "sidebarRevampEnabled",
+ "sidebar.revamp",
+ false
+);
diff --git a/browser/components/sidebar/jar.mn b/browser/components/sidebar/jar.mn
index c3d7f0cbcf..8a7071ca72 100644
--- a/browser/components/sidebar/jar.mn
+++ b/browser/components/sidebar/jar.mn
@@ -3,3 +3,12 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
+ content/browser/sidebar/browser-sidebar.js
+ content/browser/sidebar/sidebar-launcher.css
+ content/browser/sidebar/sidebar-launcher.mjs
+ content/browser/sidebar/sidebar-history.html
+ content/browser/sidebar/sidebar-history.mjs
+ content/browser/sidebar/sidebar-page.mjs
+ content/browser/sidebar/sidebar-syncedtabs.html
+ content/browser/sidebar/sidebar-syncedtabs.mjs
+ content/browser/sidebar/sidebar.css
diff --git a/browser/components/sidebar/sidebar-history.html b/browser/components/sidebar/sidebar-history.html
new file mode 100644
index 0000000000..f1df5c507a
--- /dev/null
+++ b/browser/components/sidebar/sidebar-history.html
@@ -0,0 +1,34 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <title data-l10n-id="firefoxview-page-title"></title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="toolkit/branding/accounts.ftl" />
+ <link rel="localization" href="browser/firefoxView.ftl" />
+ <link rel="localization" href="preview/sidebar.ftl" />
+ <link rel="localization" href="toolkit/branding/brandings.ftl" />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/sidebar/sidebar.css"
+ />
+ <script
+ type="module"
+ src="chrome://browser/content/sidebar/sidebar-history.mjs"
+ ></script>
+ <script src="chrome://browser/content/contentTheme.js"></script>
+ </head>
+
+ <body>
+ <sidebar-history />
+ </body>
+</html>
diff --git a/browser/components/sidebar/sidebar-history.mjs b/browser/components/sidebar/sidebar-history.mjs
new file mode 100644
index 0000000000..6c662b2c6f
--- /dev/null
+++ b/browser/components/sidebar/sidebar-history.mjs
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { html, when } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { SidebarPage } from "./sidebar-page.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/firefoxview/fxview-search-textbox.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-card.mjs";
+import { HistoryController } from "chrome://browser/content/firefoxview/HistoryController.mjs";
+import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs";
+
+const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
+
+export class SidebarHistory extends SidebarPage {
+ constructor() {
+ super();
+ this._started = false;
+ // Setting maxTabsLength to -1 for no max
+ this.maxTabsLength = -1;
+ }
+
+ controller = new HistoryController(this, {
+ component: "sidebar",
+ });
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.controller.updateAllHistoryItems();
+ }
+
+ onPrimaryAction(e) {
+ navigateToLink(e);
+ }
+
+ deleteFromHistory() {
+ this.controller.deleteFromHistory();
+ }
+
+ /**
+ * The template to use for cards-container.
+ */
+ get cardsTemplate() {
+ if (this.controller.searchResults) {
+ return this.#searchResultsTemplate();
+ } else if (this.controller.allHistoryItems.size) {
+ return this.#historyCardsTemplate();
+ }
+ return this.#emptyMessageTemplate();
+ }
+
+ #historyCardsTemplate() {
+ let cardsTemplate = [];
+ this.controller.historyMapByDate.forEach(historyItem => {
+ if (historyItem.items.length) {
+ let dateArg = JSON.stringify({ date: historyItem.items[0].time });
+ cardsTemplate.push(html`<moz-card
+ type="accordion"
+ data-l10n-attrs="heading"
+ data-l10n-id=${historyItem.l10nId}
+ data-l10n-args=${dateArg}
+ >
+ <div>
+ <fxview-tab-list
+ compactRows
+ class="with-context-menu"
+ maxTabsLength=${this.maxTabsLength}
+ .tabItems=${this.getTabItems(historyItem.items)}
+ @fxview-tab-list-primary-action=${this.onPrimaryAction}
+ .updatesPaused=${false}
+ >
+ </fxview-tab-list>
+ </div>
+ </moz-card>`);
+ }
+ });
+ return cardsTemplate;
+ }
+
+ #emptyMessageTemplate() {
+ let descriptionHeader;
+ let descriptionLabels;
+ let descriptionLink;
+ if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) {
+ // History pref set to never remember history
+ descriptionHeader = "firefoxview-dont-remember-history-empty-header";
+ descriptionLabels = [
+ "firefoxview-dont-remember-history-empty-description",
+ "firefoxview-dont-remember-history-empty-description-two",
+ ];
+ descriptionLink = {
+ url: "about:preferences#privacy",
+ name: "history-settings-url-two",
+ };
+ } else {
+ descriptionHeader = "firefoxview-history-empty-header";
+ descriptionLabels = [
+ "firefoxview-history-empty-description",
+ "firefoxview-history-empty-description-two",
+ ];
+ descriptionLink = {
+ url: "about:preferences#privacy",
+ name: "history-settings-url",
+ };
+ }
+ return html`
+ <fxview-empty-state
+ headerLabel=${descriptionHeader}
+ .descriptionLabels=${descriptionLabels}
+ .descriptionLink=${descriptionLink}
+ class="empty-state history"
+ ?isSelectedTab=${this.selectedTab}
+ mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg"
+ >
+ </fxview-empty-state>
+ `;
+ }
+
+ #searchResultsTemplate() {
+ return html` <moz-card
+ data-l10n-attrs="heading"
+ data-l10n-id="sidebar-search-results-header"
+ data-l10n-args=${JSON.stringify({
+ query: this.controller.searchQuery,
+ })}
+ >
+ <div>
+ ${when(
+ this.controller.searchResults.length,
+ () =>
+ html`<h3
+ slot="secondary-header"
+ data-l10n-id="firefoxview-search-results-count"
+ data-l10n-args="${JSON.stringify({
+ count: this.controller.searchResults.length,
+ })}"
+ ></h3>`
+ )}
+ <fxview-tab-list
+ compactRows
+ maxTabsLength="-1"
+ .searchQuery=${this.controller.searchQuery}
+ .tabItems=${this.getTabItems(this.controller.searchResults)}
+ @fxview-tab-list-primary-action=${this.onPrimaryAction}
+ .updatesPaused=${false}
+ >
+ </fxview-tab-list>
+ </div>
+ </moz-card>`;
+ }
+
+ async onChangeSortOption(e) {
+ await this.controller.onChangeSortOption(e);
+ }
+
+ async onSearchQuery(e) {
+ await this.controller.onSearchQuery(e);
+ }
+
+ getTabItems(items) {
+ return items.map(item => ({
+ ...item,
+ secondaryL10nId: null,
+ secondaryL10nArgs: null,
+ }));
+ }
+
+ render() {
+ return html`
+ ${this.stylesheet()}
+ <div class="container">
+ <div class="history-sort-option">
+ <div class="history-sort-option">
+ <fxview-search-textbox
+ data-l10n-id="firefoxview-search-text-box-history"
+ data-l10n-attrs="placeholder"
+ @fxview-search-textbox-query=${this.onSearchQuery}
+ .size=${15}
+ ></fxview-search-textbox>
+ </div>
+ </div>
+ ${this.cardsTemplate}
+ </div>
+ `;
+ }
+
+ willUpdate() {
+ if (this.controller.allHistoryItems.size) {
+ // onChangeSortOption() will update history data once it has been fetched
+ // from the API.
+ this.controller.createHistoryMaps();
+ }
+ }
+}
+
+customElements.define("sidebar-history", SidebarHistory);
diff --git a/browser/components/sidebar/sidebar-launcher.css b/browser/components/sidebar/sidebar-launcher.css
new file mode 100644
index 0000000000..b033a650b3
--- /dev/null
+++ b/browser/components/sidebar/sidebar-launcher.css
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+.wrapper {
+ display: grid;
+ grid-template-rows: auto 1fr;
+ box-sizing: border-box;
+ height: 100%;
+ padding: var(--space-medium);
+ border-inline-end: 1px solid var(--chrome-content-separator-color);
+ background-color: var(--sidebar-background-color);
+ color: var(--sidebar-text-color);
+ :host([positionend]) & {
+ border-inline-start: 1px solid var(--chrome-content-separator-color);
+ border-inline-end: none;;
+ }
+}
+
+:host([positionend]) {
+ .wrapper {
+ border-inline-start: 1px solid var(--chrome-content-separator-color);
+ }
+}
+
+.actions-list {
+ display: flex;
+ flex-direction: column;
+ justify-content: end;
+}
+
+.icon-button::part(button) {
+ background-image: var(--action-icon);
+}
diff --git a/browser/components/sidebar/sidebar-launcher.mjs b/browser/components/sidebar/sidebar-launcher.mjs
new file mode 100644
index 0000000000..85eb94b6ca
--- /dev/null
+++ b/browser/components/sidebar/sidebar-launcher.mjs
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import {
+ html,
+ ifDefined,
+ styleMap,
+} from "chrome://global/content/vendor/lit.all.mjs";
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
+
+/**
+ * Vertical strip attached to the launcher that provides an entry point
+ * to various sidebar panels.
+ *
+ */
+export default class SidebarLauncher extends MozLitElement {
+ static properties = {
+ topActions: { type: Array },
+ bottomActions: { type: Array },
+ selectedView: { type: String },
+ open: { type: Boolean },
+ };
+
+ constructor() {
+ super();
+ this.topActions = [
+ {
+ icon: `url("chrome://browser/skin/insights.svg")`,
+ view: null,
+ l10nId: "sidebar-launcher-insights",
+ },
+ ];
+
+ this.bottomActions = [
+ {
+ l10nId: "sidebar-menu-history",
+ icon: `url("chrome://browser/content/firefoxview/view-history.svg")`,
+ view: "viewHistorySidebar",
+ },
+ {
+ l10nId: "sidebar-menu-bookmarks",
+ icon: `url("chrome://browser/skin/bookmark-hollow.svg")`,
+ view: "viewBookmarksSidebar",
+ },
+ {
+ l10nId: "sidebar-menu-synced-tabs",
+ icon: `url("chrome://browser/skin/device-phone.svg")`,
+ view: "viewTabsSidebar",
+ },
+ ];
+
+ this.selectedView = window.SidebarUI.currentID;
+ this.open = window.SidebarUI.isOpen;
+ this.menuMutationObserver = new MutationObserver(() =>
+ this.#setExtensionItems()
+ );
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this._sidebarBox = document.getElementById("sidebar-box");
+ this._sidebarBox.addEventListener("sidebar-show", this);
+ this._sidebarBox.addEventListener("sidebar-hide", this);
+ this._sidebarMenu = document.getElementById("viewSidebarMenu");
+
+ this.menuMutationObserver.observe(this._sidebarMenu, {
+ childList: true,
+ subtree: true,
+ });
+ this.#setExtensionItems();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this._sidebarBox.removeEventListener("sidebar-show", this);
+ this._sidebarBox.removeEventListener("sidebar-hide", this);
+ this.menuMutationObserver.disconnect();
+ }
+
+ getImageUrl(icon, targetURI) {
+ if (window.IS_STORYBOOK) {
+ return `chrome://global/skin/icons/defaultFavicon.svg`;
+ }
+ if (!icon) {
+ if (targetURI?.startsWith("moz-extension")) {
+ return "chrome://mozapps/skin/extensions/extension.svg";
+ }
+ return `chrome://global/skin/icons/defaultFavicon.svg`;
+ }
+ // If the icon is not for website (doesn't begin with http), we
+ // display it directly. Otherwise we go through the page-icon
+ // protocol to try to get a cached version. We don't load
+ // favicons directly.
+ if (icon.startsWith("http")) {
+ return `page-icon:${targetURI}`;
+ }
+ return icon;
+ }
+
+ #setExtensionItems() {
+ for (let item of this._sidebarMenu.children) {
+ if (item.id.endsWith("-sidebar-action")) {
+ this.topActions.push({
+ tooltiptext: item.label,
+ icon: item.style.getPropertyValue("--webextension-menuitem-image"),
+ view: item.id.slice("menubar_menu_".length),
+ });
+ }
+ }
+ }
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "sidebar-show":
+ this.selectedView = e.detail.viewId;
+ this.open = true;
+ break;
+ case "sidebar-hide":
+ this.open = false;
+ break;
+ }
+ }
+
+ showView(e) {
+ let view = e.target.getAttribute("view");
+ window.SidebarUI.toggle(view);
+ }
+
+ buttonType(action) {
+ return this.open && action.view == this.selectedView
+ ? "icon"
+ : "icon ghost";
+ }
+
+ entrypointTemplate(action) {
+ return html`<moz-button
+ class="icon-button"
+ type=${this.buttonType(action)}
+ view=${action.view}
+ @click=${action.view ? this.showView : null}
+ title=${ifDefined(action.tooltiptext)}
+ data-l10n-id=${ifDefined(action.l10nId)}
+ style=${styleMap({ "--action-icon": action.icon })}
+ >
+ </moz-button>`;
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/sidebar/sidebar-launcher.css"
+ />
+ <div class="wrapper">
+ <div class="top-actions actions-list">
+ ${this.topActions.map(action => this.entrypointTemplate(action))}
+ </div>
+ <div class="bottom-actions actions-list">
+ ${this.bottomActions.map(action => this.entrypointTemplate(action))}
+ </div>
+ </div>
+ `;
+ }
+}
+customElements.define("sidebar-launcher", SidebarLauncher);
diff --git a/browser/components/sidebar/sidebar-page.mjs b/browser/components/sidebar/sidebar-page.mjs
new file mode 100644
index 0000000000..157298a561
--- /dev/null
+++ b/browser/components/sidebar/sidebar-page.mjs
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+import { html } from "chrome://global/content/vendor/lit.all.mjs";
+
+export class SidebarPage extends MozLitElement {
+ constructor() {
+ super();
+ this.clearDocument = this.clearDocument.bind(this);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.ownerGlobal.addEventListener("beforeunload", this.clearDocument);
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.ownerGlobal.removeEventListener("beforeunload", this.clearDocument);
+ }
+
+ /**
+ * Clear out the document so the disconnectedCallback() will trigger properly
+ * and all of the custom elements can cleanup.
+ */
+ clearDocument() {
+ this.ownerGlobal.document.body.textContent = "";
+ }
+
+ /**
+ * The common stylesheet for all sidebar pages.
+ *
+ * @returns {TemplateResult}
+ */
+ stylesheet() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/sidebar/sidebar.css"
+ />
+ `;
+ }
+}
diff --git a/browser/components/sidebar/sidebar-syncedtabs.html b/browser/components/sidebar/sidebar-syncedtabs.html
new file mode 100644
index 0000000000..6c4874b9ea
--- /dev/null
+++ b/browser/components/sidebar/sidebar-syncedtabs.html
@@ -0,0 +1,45 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="browser/firefoxView.ftl" />
+ <link rel="localization" href="toolkit/branding/accounts.ftl" />
+ <link rel="localization" href="toolkit/branding/brandings.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/global.css" />
+ <script
+ type="module"
+ src="chrome://global/content/elements/moz-card.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/firefoxview/fxview-empty-state.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/firefoxview/fxview-search-textbox.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/firefoxview/fxview-tab-list.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/sidebar/sidebar-syncedtabs.mjs"
+ ></script>
+ <script src="chrome://browser/content/contentTheme.js"></script>
+ </head>
+
+ <body>
+ <sidebar-syncedtabs />
+ </body>
+</html>
diff --git a/browser/components/sidebar/sidebar-syncedtabs.mjs b/browser/components/sidebar/sidebar-syncedtabs.mjs
new file mode 100644
index 0000000000..4c3bd9dc46
--- /dev/null
+++ b/browser/components/sidebar/sidebar-syncedtabs.mjs
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs",
+});
+
+import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
+import {
+ escapeHtmlEntities,
+ navigateToLink,
+} from "chrome://browser/content/firefoxview/helpers.mjs";
+
+import { SidebarPage } from "./sidebar-page.mjs";
+
+class SyncedTabsInSidebar extends SidebarPage {
+ controller = new lazy.SyncedTabsController(this);
+
+ constructor() {
+ super();
+ this.onSearchQuery = this.onSearchQuery.bind(this);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.controller.addSyncObservers();
+ this.controller.updateStates();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.controller.removeSyncObservers();
+ }
+
+ /**
+ * The template shown when the list of synced devices is currently
+ * unavailable.
+ *
+ * @param {object} options
+ * @param {string} options.action
+ * @param {string} options.buttonLabel
+ * @param {string[]} options.descriptionArray
+ * @param {string} options.descriptionLink
+ * @param {boolean} options.error
+ * @param {string} options.header
+ * @param {string} options.headerIconUrl
+ * @param {string} options.mainImageUrl
+ * @returns {TemplateResult}
+ */
+ messageCardTemplate({
+ action,
+ buttonLabel,
+ descriptionArray,
+ descriptionLink,
+ error,
+ header,
+ headerIconUrl,
+ mainImageUrl,
+ }) {
+ return html`
+ <fxview-empty-state
+ headerLabel=${header}
+ .descriptionLabels=${descriptionArray}
+ .descriptionLink=${ifDefined(descriptionLink)}
+ class="empty-state synced-tabs error"
+ isSelectedTab
+ mainImageUrl="${ifDefined(mainImageUrl)}"
+ ?errorGrayscale=${error}
+ headerIconUrl="${ifDefined(headerIconUrl)}"
+ id="empty-container"
+ >
+ <button
+ class="primary"
+ slot="primary-action"
+ ?hidden=${!buttonLabel}
+ data-l10n-id="${ifDefined(buttonLabel)}"
+ data-action="${action}"
+ @click=${e => this.controller.handleEvent(e)}
+ aria-details="empty-container"
+ ></button>
+ </fxview-empty-state>
+ `;
+ }
+
+ /**
+ * The template shown for a device that has tabs.
+ *
+ * @param {string} deviceName
+ * @param {string} deviceType
+ * @param {Array} tabItems
+ * @returns {TemplateResult}
+ */
+ deviceTemplate(deviceName, deviceType, tabItems) {
+ return html`<moz-card
+ type="accordion"
+ .heading=${deviceName}
+ icon
+ class=${deviceType}
+ >
+ <fxview-tab-list
+ compactRows
+ .tabItems=${ifDefined(tabItems)}
+ .updatesPaused=${false}
+ .searchQuery=${this.controller.searchQuery}
+ @fxview-tab-list-primary-action=${navigateToLink}
+ />
+ </moz-card>`;
+ }
+
+ /**
+ * The template shown for a device that has no tabs.
+ *
+ * @param {string} deviceName
+ * @param {string} deviceType
+ * @returns {TemplateResult}
+ */
+ noDeviceTabsTemplate(deviceName, deviceType) {
+ return html`<moz-card
+ .heading=${deviceName}
+ icon
+ class=${deviceType}
+ data-l10n-id="firefoxview-syncedtabs-device-notabs"
+ >
+ </moz-card>`;
+ }
+
+ /**
+ * The template shown for a device that has tabs, but no tabs that match the
+ * current search query.
+ *
+ * @param {string} deviceName
+ * @param {string} deviceType
+ * @returns {TemplateResult}
+ */
+ noSearchResultsTemplate(deviceName, deviceType) {
+ return html`<moz-card
+ .heading=${deviceName}
+ icon
+ class=${deviceType}
+ data-l10n-id="firefoxview-search-results-empty"
+ data-l10n-args=${JSON.stringify({
+ query: escapeHtmlEntities(this.controller.searchQuery),
+ })}
+ >
+ </moz-card>`;
+ }
+
+ /**
+ * The template shown for the list of synced devices.
+ *
+ * @returns {TemplateResult[]}
+ */
+ deviceListTemplate() {
+ return Object.values(this.controller.getRenderInfo()).map(
+ ({ name: deviceName, deviceType, tabItems, tabs }) => {
+ if (tabItems.length) {
+ return this.deviceTemplate(deviceName, deviceType, tabItems);
+ } else if (tabs.length) {
+ return this.noSearchResultsTemplate(deviceName, deviceType);
+ }
+ return this.noDeviceTabsTemplate(deviceName, deviceType);
+ }
+ );
+ }
+
+ render() {
+ const messageCard = this.controller.getMessageCard();
+ if (messageCard) {
+ return [this.stylesheet(), this.messageCardTemplate(messageCard)];
+ }
+ return html`
+ ${this.stylesheet()}
+ <fxview-search-textbox
+ data-l10n-id="firefoxview-search-text-box-syncedtabs"
+ data-l10n-attrs="placeholder"
+ @fxview-search-textbox-query=${this.onSearchQuery}
+ size="15"
+ ></fxview-search-textbox>
+ ${this.deviceListTemplate()}
+ `;
+ }
+
+ onSearchQuery(e) {
+ this.controller.searchQuery = e.detail.query;
+ this.requestUpdate();
+ }
+}
+
+customElements.define("sidebar-syncedtabs", SyncedTabsInSidebar);
diff --git a/browser/components/sidebar/sidebar.css b/browser/components/sidebar/sidebar.css
new file mode 100644
index 0000000000..34d43aa850
--- /dev/null
+++ b/browser/components/sidebar/sidebar.css
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url("chrome://global/skin/global.css");
+
+:root {
+ background-color: var(--lwt-sidebar-background-color);
+ color: var(--lwt-sidebar-text-color);
+}
+
+moz-card {
+ margin-block-start: var(--space-medium);
+
+ &.phone::part(icon),
+ &.mobile::part(icon) {
+ background-image: url('chrome://browser/skin/device-phone.svg');
+ }
+
+ &.desktop::part(icon) {
+ background-image: url('chrome://browser/skin/device-desktop.svg');
+ }
+
+ &.tablet::part(icon) {
+ background-image: url('chrome://browser/skin/device-tablet.svg');
+ }
+}
diff --git a/browser/components/sidebar/sidebar.ftl b/browser/components/sidebar/sidebar.ftl
new file mode 100644
index 0000000000..2a5ef75d83
--- /dev/null
+++ b/browser/components/sidebar/sidebar.ftl
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+sidebar-launcher-insights =
+ .title = Insights
+
+## Variables:
+## $date (string) - Date to be formatted based on locale
+
+sidebar-history-date-today =
+ .heading = Today — { DATETIME($date, dateStyle: "full") }
+sidebar-history-date-yesterday =
+ .heading = Yesterday — { DATETIME($date, dateStyle: "full") }
+sidebar-history-date-this-month =
+ .heading = { DATETIME($date, dateStyle: "full") }
+sidebar-history-date-prev-month =
+ .heading = { DATETIME($date, month: "long", year: "numeric") }
+
+##
+
+# "Search" is a noun (as in "Results of the search for")
+# Variables:
+# $query (String) - The search query used for searching through browser history.
+sidebar-search-results-header =
+ .heading = Search results for “{ $query }”
diff --git a/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs
index 29b57812bd..51f1f49fa5 100644
--- a/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs
+++ b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs
@@ -10,7 +10,7 @@ import { PseudoLocalizationButton } from "../PseudoLocalizationButton.jsx";
import { FluentPanel } from "../FluentPanel.jsx";
// Register the addon.
-addons.register(ADDON_ID, api => {
+addons.register(ADDON_ID, () => {
// Register the tool.
addons.add(TOOL_ID, {
type: types.TOOL,
diff --git a/browser/components/storybook/.storybook/main.js b/browser/components/storybook/.storybook/main.js
index 5791d1e492..0a5a55b851 100644
--- a/browser/components/storybook/.storybook/main.js
+++ b/browser/components/storybook/.storybook/main.js
@@ -13,17 +13,24 @@ const projectRoot = path.resolve(__dirname, "../../../../");
module.exports = {
// The ordering for this stories array affects the order that they are displayed in Storybook
stories: [
+ // Show the Storybook document first in the list
+ // so that navigating to firefoxux.github.io/firefox-desktop-components/
+ // lands on the Storybook.stories.md file
+ "../**/README.storybook.stories.md",
// Docs section
"../**/README.*.stories.md",
// UI Widgets section
`${projectRoot}/toolkit/content/widgets/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`,
// about:logins components stories
`${projectRoot}/browser/components/aboutlogins/content/components/**/*.stories.mjs`,
+ // Reader View components stories
+ `${projectRoot}/toolkit/components/reader/**/*.stories.mjs`,
// Everything else
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx|md)",
// Design system files
`${projectRoot}/toolkit/themes/shared/design-system/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`,
],
+ staticDirs: [`${projectRoot}/toolkit/themes/shared/design-system/docs/`],
addons: [
"@storybook/addon-links",
{
@@ -50,7 +57,7 @@ module.exports = {
};
return [...existingIndexers, customIndexer];
},
- webpackFinal: async (config, { configType }) => {
+ webpackFinal: async config => {
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// You can change the configuration based on that.
// 'PRODUCTION' is used when building the static version of storybook.
diff --git a/browser/components/storybook/.storybook/manager-head.html b/browser/components/storybook/.storybook/manager-head.html
new file mode 100644
index 0000000000..380828d40b
--- /dev/null
+++ b/browser/components/storybook/.storybook/manager-head.html
@@ -0,0 +1,22 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<link
+ rel="stylesheet"
+ href="chrome://global/skin/design-system/tokens-brand.css"
+/>
+
+<style>
+ /* light-dark doesn't work here, using media queries */
+ @media (prefers-color-scheme: light) {
+ iframe {
+ background-color: var(--color-white) !important;
+ }
+ }
+ @media (prefers-color-scheme: dark) {
+ iframe {
+ background-color: var(--color-gray-90) !important;
+ }
+ }
+</style>
diff --git a/browser/components/storybook/.storybook/markdown-story-utils.js b/browser/components/storybook/.storybook/markdown-story-utils.js
index 1cc78164ad..5926c5593c 100644
--- a/browser/components/storybook/.storybook/markdown-story-utils.js
+++ b/browser/components/storybook/.storybook/markdown-story-utils.js
@@ -121,19 +121,21 @@ function getStoryTitle(resourcePath) {
* @returns Path used to import a component into a story.
*/
function getImportPath(resourcePath) {
+ // We need to normalize the path for this logic to work cross-platform.
+ let normalizedPath = resourcePath.split(path.sep).join("/");
// Limiting this to toolkit widgets for now since we don't have any
// interactive examples in other docs stories.
- if (!resourcePath.includes("toolkit/content/widgets")) {
+ if (!normalizedPath.includes("toolkit/content/widgets")) {
return "";
}
- let componentName = getComponentName(resourcePath);
+ let componentName = getComponentName(normalizedPath);
let fileExtension = "";
if (componentName) {
- let mjsPath = resourcePath.replace(
+ let mjsPath = normalizedPath.replace(
"README.stories.md",
`${componentName}.mjs`
);
- let jsPath = resourcePath.replace(
+ let jsPath = normalizedPath.replace(
"README.stories.md",
`${componentName}.js`
);
diff --git a/browser/components/storybook/.storybook/preview-head.html b/browser/components/storybook/.storybook/preview-head.html
index 206972e714..ae9d8fdf5a 100644
--- a/browser/components/storybook/.storybook/preview-head.html
+++ b/browser/components/storybook/.storybook/preview-head.html
@@ -37,8 +37,12 @@
}
/* Ensure WithCommonStyles can grow to fit the page */
- #root-inner {
- height: 100vh;
+ html,
+ body,
+ #root,
+ #root-inner,
+ #storybook-root {
+ height: 100%;
}
/* Docs stories are being given unnecessary height, possibly because we
diff --git a/browser/components/storybook/.storybook/preview.mjs b/browser/components/storybook/.storybook/preview.mjs
index 4e0f3f407d..ec7fd42151 100644
--- a/browser/components/storybook/.storybook/preview.mjs
+++ b/browser/components/storybook/.storybook/preview.mjs
@@ -54,7 +54,7 @@ class WithCommonStyles extends MozLitElement {
font: message-box;
font-size: var(--font-size-root);
appearance: none;
- background-color: var(--color-canvas);
+ background-color: var(--background-color-canvas);
color: var(--text-color);
-moz-box-layout: flex;
}
@@ -113,6 +113,7 @@ export default {
title: "On this page",
},
},
+ options: { showPanel: true },
},
};
diff --git a/browser/components/storybook/docs/README.storybook.stories.md b/browser/components/storybook/docs/README.storybook.stories.md
index bb0fcdd1a2..5e94be7761 100644
--- a/browser/components/storybook/docs/README.storybook.stories.md
+++ b/browser/components/storybook/docs/README.storybook.stories.md
@@ -4,7 +4,7 @@
playground for UI components. We use Storybook to document our design system,
reusable components, and any specific components you might want to test with
dummy data. [Take a look at our Storybook
-instance!](https://firefoxux.github.io/firefox-desktop-components/?path=/story/docs-reusable-widgets--page)
+instance!](https://firefoxux.github.io/firefox-desktop-components/)
## Background
diff --git a/browser/components/storybook/stories/fxview-tab-list.stories.mjs b/browser/components/storybook/stories/fxview-tab-list.stories.mjs
index c8e2328c44..b18ad16e3a 100644
--- a/browser/components/storybook/stories/fxview-tab-list.stories.mjs
+++ b/browser/components/storybook/stories/fxview-tab-list.stories.mjs
@@ -84,7 +84,7 @@ let secondaryAction = e => {
e.target.querySelector("panel-list").toggle(e.detail.originalEvent);
};
-let primaryAction = e => {
+let primaryAction = () => {
// Open in new tab
};
diff --git a/browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs b/browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs
index 47571f789d..5fe9c8c3e5 100644
--- a/browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs
+++ b/browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs
@@ -96,7 +96,7 @@ SyncedTabsDeckComponent.prototype = {
this._deckView.destroy();
},
- observe(subject, topic, data) {
+ observe(subject, topic) {
switch (topic) {
case this._SyncedTabs.TOPIC_TABS_CHANGED:
this._syncedTabsListStore.getData();
diff --git a/browser/components/syncedtabs/TabListView.sys.mjs b/browser/components/syncedtabs/TabListView.sys.mjs
index 041d7300d9..70c98c4175 100644
--- a/browser/components/syncedtabs/TabListView.sys.mjs
+++ b/browser/components/syncedtabs/TabListView.sys.mjs
@@ -127,7 +127,7 @@ TabListView.prototype = {
},
// Client rows are hidden when the list is filtered
- _renderFilteredClient(client, filter) {
+ _renderFilteredClient(client) {
client.tabs.forEach((tab, index) => {
let node = this._renderTab(client, tab, index);
this.list.appendChild(node);
diff --git a/browser/components/tabpreview/jar.mn b/browser/components/tabpreview/jar.mn
index 8ff09ebb17..589bc71430 100644
--- a/browser/components/tabpreview/jar.mn
+++ b/browser/components/tabpreview/jar.mn
@@ -3,5 +3,5 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
- content/browser/tabpreview/tabpreview.mjs (tabpreview.mjs)
+ content/browser/tabpreview/tab-preview-panel.mjs (tab-preview-panel.mjs)
content/browser/tabpreview/tabpreview.css (tabpreview.css)
diff --git a/browser/components/tabpreview/tab-preview-panel.mjs b/browser/components/tabpreview/tab-preview-panel.mjs
new file mode 100644
index 0000000000..683b2c17ec
--- /dev/null
+++ b/browser/components/tabpreview/tab-preview-panel.mjs
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const POPUP_OPTIONS = {
+ position: "bottomleft topleft",
+ x: 0,
+ y: -2,
+};
+
+/**
+ * Detailed preview card that displays when hovering a tab
+ */
+export default class TabPreviewPanel {
+ constructor(panel) {
+ this._panel = panel;
+ this._win = panel.ownerGlobal;
+ this._tab = null;
+ this._thumbnailElement = null;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_prefDisableAutohide",
+ "ui.popup.disable_autohide",
+ false
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_prefPreviewDelay",
+ "ui.tooltip.delay_ms"
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_prefDisplayThumbnail",
+ "browser.tabs.cardPreview.showThumbnails",
+ false
+ );
+ this._timer = null;
+ }
+
+ getPrettyURI(uri) {
+ try {
+ const url = new URL(uri);
+ return `${url.hostname}`.replace(/^w{3}\./, "");
+ } catch {
+ return uri;
+ }
+ }
+
+ _needsThumbnailFor(tab) {
+ return !tab.selected;
+ }
+
+ _maybeRequestThumbnail() {
+ if (!this._prefDisplayThumbnail) {
+ return;
+ }
+ if (!this._needsThumbnailFor(this._tab)) {
+ return;
+ }
+ let tab = this._tab;
+ this._win.tabPreviews.get(tab).then(el => {
+ if (this._tab == tab && this._needsThumbnailFor(tab)) {
+ this._thumbnailElement = el;
+ this._updatePreview();
+ }
+ });
+ }
+
+ activate(tab) {
+ this._tab = tab;
+ this._thumbnailElement = null;
+ this._maybeRequestThumbnail();
+ if (this._panel.state == "open") {
+ this._updatePreview();
+ }
+ if (this._timer) {
+ return;
+ }
+ this._timer = this._win.setTimeout(() => {
+ this._timer = null;
+ this._panel.openPopup(this._tab, POPUP_OPTIONS);
+ }, this._prefPreviewDelay);
+ this._win.addEventListener("TabSelect", this);
+ this._panel.addEventListener("popupshowing", this);
+ }
+
+ deactivate(leavingTab = null) {
+ if (leavingTab) {
+ if (this._tab != leavingTab) {
+ return;
+ }
+ this._win.requestAnimationFrame(() => {
+ if (this._tab == leavingTab) {
+ this.deactivate();
+ }
+ });
+ return;
+ }
+ this._tab = null;
+ this._thumbnailElement = null;
+ this._panel.removeEventListener("popupshowing", this);
+ this._win.removeEventListener("TabSelect", this);
+ if (!this._prefDisableAutohide) {
+ this._panel.hidePopup();
+ }
+ if (this._timer) {
+ this._win.clearTimeout(this._timer);
+ this._timer = null;
+ }
+ }
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "popupshowing":
+ this._updatePreview();
+ break;
+ case "TabSelect":
+ if (this._thumbnailElement && !this._needsThumbnailFor(this._tab)) {
+ this._thumbnailElement.remove();
+ this._thumbnailElement = null;
+ }
+ break;
+ }
+ }
+
+ _updatePreview() {
+ this._panel.querySelector(".tab-preview-title").textContent =
+ this._displayTitle;
+ this._panel.querySelector(".tab-preview-uri").textContent =
+ this._displayURI;
+ let thumbnailContainer = this._panel.querySelector(
+ ".tab-preview-thumbnail-container"
+ );
+ if (thumbnailContainer.firstChild != this._thumbnailElement) {
+ thumbnailContainer.replaceChildren();
+ if (this._thumbnailElement) {
+ thumbnailContainer.appendChild(this._thumbnailElement);
+ }
+ this._panel.dispatchEvent(
+ new CustomEvent("previewThumbnailUpdated", {
+ detail: {
+ thumbnail: this._thumbnailElement,
+ },
+ })
+ );
+ }
+ if (this._tab && this._panel.state == "open") {
+ this._panel.moveToAnchor(
+ this._tab,
+ POPUP_OPTIONS.position,
+ POPUP_OPTIONS.x,
+ POPUP_OPTIONS.y
+ );
+ }
+ }
+
+ get _displayTitle() {
+ if (!this._tab) {
+ return "";
+ }
+ return this._tab.textLabel.textContent;
+ }
+
+ get _displayURI() {
+ if (!this._tab) {
+ return "";
+ }
+ return this.getPrettyURI(this._tab.linkedBrowser.currentURI.spec);
+ }
+}
diff --git a/browser/components/tabpreview/tabpreview.css b/browser/components/tabpreview/tabpreview.css
index 776f520c7d..e978266e5d 100644
--- a/browser/components/tabpreview/tabpreview.css
+++ b/browser/components/tabpreview/tabpreview.css
@@ -2,32 +2,21 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-.tab-preview-container {
- --tab-preview-background-color: light-dark(#fff, #42414d);
- --tab-preview-text-color: light-dark(#15141a, #fbfbfe);
- --tab-preview-border-color: light-dark(#cfcfd8, #8f8f9d);
-
- @media (prefers-contrast) {
- --tab-preview-background-color: Canvas;
- --tab-preview-text-color: CanvasText;
- }
+#tab-preview-panel {
+ --panel-width: 280px;
+ --panel-padding: 0;
+ --panel-background: var(--tab-selected-bgcolor);
+ --panel-color: var(--tab-selected-textcolor);
}
-.tab-preview-container {
- background-color: var(--tab-preview-background-color);
- color: var(--tab-preview-text-color);
- border-radius: 9px;
- display: inline-block;
- width: 280px;
- overflow: hidden;
- line-height: 1.5;
- border: 1px solid var(--tab-preview-border-color);
+.tab-preview-text-container {
+ padding: var(--space-small);
}
.tab-preview-title {
max-height: 3em;
overflow: hidden;
- font-weight: 600;
+ font-weight: var(--font-weight-bold);
}
.tab-preview-uri {
@@ -38,22 +27,18 @@
text-overflow: ellipsis;
}
-.tab-preview-text-container {
- padding: 8px;
-}
-
.tab-preview-thumbnail-container {
- border-top: 1px solid var(--tab-preview-border-color);
-}
-
-.tab-preview-thumbnail-container img,
-.tab-preview-thumbnail-container canvas {
- display: block;
- width: 100%;
-}
-
-@media (max-width: 640px) {
- .tab-preview-thumbnail-container {
+ border-top: 1px solid var(--panel-border-color);
+ &:empty {
display: none;
}
+ @media (width < 640px) {
+ display: none;
+ }
+
+ > img,
+ > canvas {
+ display: block;
+ width: 100%;
+ }
}
diff --git a/browser/components/tabpreview/tabpreview.mjs b/browser/components/tabpreview/tabpreview.mjs
deleted file mode 100644
index 2409c3fa7a..0000000000
--- a/browser/components/tabpreview/tabpreview.mjs
+++ /dev/null
@@ -1,237 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-import { html } from "chrome://global/content/vendor/lit.all.mjs";
-import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
-
-var { XPCOMUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/XPCOMUtils.sys.mjs"
-);
-
-const TAB_PREVIEW_USE_THUMBNAILS_PREF =
- "browser.tabs.cardPreview.showThumbnails";
-
-/**
- * Detailed preview card that displays when hovering a tab
- *
- * @property {MozTabbrowserTab} tab - the tab to preview
- * @fires TabPreview#previewhidden
- * @fires TabPreview#previewshown
- * @fires TabPreview#previewThumbnailUpdated
- */
-export default class TabPreview extends MozLitElement {
- static properties = {
- tab: { type: Object },
-
- _previewIsActive: { type: Boolean, state: true },
- _previewDelayTimeout: { type: Number, state: true },
- _displayTitle: { type: String, state: true },
- _displayURI: { type: String, state: true },
- _displayImg: { type: Object, state: true },
- };
-
- constructor() {
- super();
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "_prefPreviewDelay",
- "ui.tooltip.delay_ms"
- );
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "_prefDisplayThumbnail",
- TAB_PREVIEW_USE_THUMBNAILS_PREF,
- false
- );
- }
-
- // render this inside a <panel>
- createRenderRoot() {
- if (!document.createXULElement) {
- console.error(
- "Unable to create panel: document.createXULElement is not available"
- );
- return super.createRenderRoot();
- }
- this.attachShadow({ mode: "open" });
- this.panel = document.createXULElement("panel");
- this.panel.setAttribute("id", "tabPreviewPanel");
- this.panel.setAttribute("noautofocus", true);
- this.panel.setAttribute("norolluponanchor", true);
- this.panel.setAttribute("consumeoutsideclicks", "never");
- this.panel.setAttribute("rolluponmousewheel", "true");
- this.panel.setAttribute("level", "parent");
- this.shadowRoot.append(this.panel);
- return this.panel;
- }
-
- get previewCanShow() {
- return this._previewIsActive && this.tab;
- }
-
- get thumbnailCanShow() {
- return (
- this.previewCanShow &&
- this._prefDisplayThumbnail &&
- !this.tab.selected &&
- this._displayImg
- );
- }
-
- getPrettyURI(uri) {
- try {
- const url = new URL(uri);
- return `${url.hostname}`.replace(/^w{3}\./, "");
- } catch {
- return uri;
- }
- }
-
- handleEvent(e) {
- switch (e.type) {
- case "TabSelect": {
- this.requestUpdate();
- break;
- }
- case "popuphidden": {
- this.previewHidden();
- break;
- }
- }
- }
-
- showPreview() {
- this.panel.openPopup(this.tab, {
- position: "bottomleft topleft",
- y: -2,
- isContextMenu: false,
- });
- window.addEventListener("TabSelect", this);
- this.panel.addEventListener("popuphidden", this);
- }
-
- hidePreview() {
- this.panel.hidePopup();
- }
-
- previewHidden() {
- window.removeEventListener("TabSelect", this);
- this.panel.removeEventListener("popuphidden", this);
-
- /**
- * @event TabPreview#previewhidden
- * @type {CustomEvent}
- */
- this.dispatchEvent(new CustomEvent("previewhidden"));
- }
-
- // compute values derived from tab element
- willUpdate(changedProperties) {
- if (!changedProperties.has("tab")) {
- return;
- }
- if (!this.tab) {
- this._displayTitle = "";
- this._displayURI = "";
- this._displayImg = null;
- return;
- }
- this._displayTitle = this.tab.textLabel.textContent;
- this._displayURI = this.getPrettyURI(
- this.tab.linkedBrowser.currentURI.spec
- );
- this._displayImg = null;
- let { tab } = this;
- window.tabPreviews.get(this.tab).then(el => {
- if (this.tab == tab) {
- this._displayImg = el;
- }
- });
- }
-
- updated(changedProperties) {
- if (changedProperties.has("tab")) {
- // handle preview delay
- if (!this.tab) {
- clearTimeout(this._previewDelayTimeout);
- this._previewIsActive = false;
- } else {
- let lastTabVal = changedProperties.get("tab");
- if (!lastTabVal) {
- // tab was set from an empty state,
- // so wait for the delay duration before showing
- this._previewDelayTimeout = setTimeout(() => {
- this._previewIsActive = true;
- }, this._prefPreviewDelay);
- }
- }
- }
- if (changedProperties.has("_previewIsActive")) {
- if (!this._previewIsActive) {
- this.hidePreview();
- }
- }
- if (
- (changedProperties.has("tab") ||
- changedProperties.has("_previewIsActive")) &&
- this.previewCanShow
- ) {
- this.updateComplete.then(() => {
- if (this.panel.state == "open" || this.panel.state == "showing") {
- this.panel.moveToAnchor(this.tab, "bottomleft topleft", 0, -2);
- } else {
- this.showPreview();
- }
-
- this.dispatchEvent(
- /**
- * @event TabPreview#previewshown
- * @type {CustomEvent}
- * @property {object} detail
- * @property {MozTabbrowserTab} detail.tab - the tab being previewed
- */
- new CustomEvent("previewshown", {
- detail: { tab: this.tab },
- })
- );
- });
- }
- if (changedProperties.has("_displayImg")) {
- this.updateComplete.then(() => {
- /**
- * fires when the thumbnail for a preview is loaded
- * and added to the document.
- *
- * @event TabPreview#previewThumbnailUpdated
- * @type {CustomEvent}
- */
- this.dispatchEvent(new CustomEvent("previewThumbnailUpdated"));
- });
- }
- }
-
- render() {
- return html`
- <link
- rel="stylesheet"
- type="text/css"
- href="chrome://browser/content/tabpreview/tabpreview.css"
- />
- <div class="tab-preview-container">
- <div class="tab-preview-text-container">
- <div class="tab-preview-title">${this._displayTitle}</div>
- <div class="tab-preview-uri">${this._displayURI}</div>
- </div>
- ${this.thumbnailCanShow
- ? html`
- <div class="tab-preview-thumbnail-container">
- ${this._displayImg}
- </div>
- `
- : ""}
- </div>
- `;
- }
-}
-customElements.define("tab-preview", TabPreview);
diff --git a/browser/components/tests/browser/browser_contentpermissionprompt.js b/browser/components/tests/browser/browser_contentpermissionprompt.js
index 3e2eb24f62..11b18a6653 100644
--- a/browser/components/tests/browser/browser_contentpermissionprompt.js
+++ b/browser/components/tests/browser/browser_contentpermissionprompt.js
@@ -151,7 +151,7 @@ add_task(async function test_working_request() {
},
};
- let integration = base => ({
+ let integration = () => ({
createPermissionPrompt(type, request) {
Assert.equal(type, "test-permission-type");
Assert.ok(
diff --git a/browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js b/browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js
index d061d84b23..7bb8b22a98 100644
--- a/browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js
+++ b/browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js
@@ -11,12 +11,43 @@ add_setup(function add_setup() {
Services.prefs.setBoolPref("browser.mailto.dualPrompt", true);
});
+/* helper function to delete site specific settings needed to clean up
+ * the testing setup after some of these tests.
+ *
+ * @see: nsIContentPrefService2.idl
+ */
+function _deleteSiteSpecificSetting(domain, setting, context = null) {
+ const contentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+
+ return contentPrefs.removeByDomainAndName(domain, setting, context, {
+ handleResult(_) {},
+ handleCompletion() {
+ Assert.ok(true, "setting successfully deleted.");
+ },
+ handleError(_) {
+ Assert.ok(false, "could not delete site specific setting.");
+ },
+ });
+}
+
// site specific settings
const protocol = "mailto";
-const sss_domain = "test.example.com";
+const subdomain = (Math.random() + 1).toString(36).substring(7);
+const sss_domain = subdomain + ".example.com";
const ss_setting = "system.under.test";
add_task(async function check_null_value() {
+ Assert.ok(
+ /[a-z0-9].+\.[a-z]+\.[a-z]+/.test(sss_domain),
+ "test the validity of this random domain name before using it for tests: '" +
+ sss_domain +
+ "'"
+ );
+});
+
+add_task(async function check_null_value() {
Assert.equal(
null,
await WebProtocolHandlerRegistrar._getSiteSpecificSetting(
@@ -46,13 +77,31 @@ add_task(async function check_save_value() {
ss_setting,
ss_setting
);
+
+ let fetchedSiteSpecificSetting;
+ try {
+ fetchedSiteSpecificSetting =
+ await WebProtocolHandlerRegistrar._getSiteSpecificSetting(
+ sss_domain,
+ ss_setting
+ );
+ } finally {
+ // make sure the cleanup happens, no matter what
+ _deleteSiteSpecificSetting(sss_domain, ss_setting);
+ }
Assert.equal(
ss_setting,
+ fetchedSiteSpecificSetting,
+ "site specific setting save and retrieve test."
+ );
+
+ Assert.equal(
+ null,
await WebProtocolHandlerRegistrar._getSiteSpecificSetting(
sss_domain,
ss_setting
),
- "site specific setting save and retrieve test."
+ "site specific setting should not exist after delete."
);
});
diff --git a/browser/components/tests/browser/browser_quit_disabled.js b/browser/components/tests/browser/browser_quit_disabled.js
index 3b7e99a1bf..a2151e8953 100644
--- a/browser/components/tests/browser/browser_quit_disabled.js
+++ b/browser/components/tests/browser/browser_quit_disabled.js
@@ -27,7 +27,7 @@ add_task(async function test_quit_shortcut_disabled() {
let quitRequested = false;
let observer = {
- observe(subject, topic, data) {
+ observe(subject, topic) {
is(topic, "quit-application-requested", "Right observer topic");
ok(shouldQuit, "Quit shortcut should NOT have worked");
diff --git a/browser/components/tests/browser/head.js b/browser/components/tests/browser/head.js
index 89c8df8613..28b14aef0b 100644
--- a/browser/components/tests/browser/head.js
+++ b/browser/components/tests/browser/head.js
@@ -42,7 +42,7 @@ function mockShell(overrides = {}) {
isDefault: false,
isPinned: false,
- async checkPinCurrentAppToTaskbarAsync(privateBrowsing = false) {
+ async checkPinCurrentAppToTaskbarAsync() {
if (!this.canPin) {
throw Error;
}
@@ -50,7 +50,7 @@ function mockShell(overrides = {}) {
get isAppInDock() {
return this.isPinned;
},
- isCurrentAppPinnedToTaskbarAsync(privateBrowsing = false) {
+ isCurrentAppPinnedToTaskbarAsync() {
return Promise.resolve(this.isPinned);
},
isDefaultBrowser() {
diff --git a/browser/components/touchbar/MacTouchBar.sys.mjs b/browser/components/touchbar/MacTouchBar.sys.mjs
index 5b598efc00..6588920f5e 100644
--- a/browser/components/touchbar/MacTouchBar.sys.mjs
+++ b/browser/components/touchbar/MacTouchBar.sys.mjs
@@ -107,7 +107,7 @@ var gBuiltInInputs = {
type: kInputTypes.BUTTON,
callback: () => {
let win = lazy.BrowserWindowTracker.getTopWindow();
- win.BrowserHome();
+ win.BrowserCommands.home();
},
},
Fullscreen: {
diff --git a/browser/components/touchbar/tests/browser/browser_touchbar_tests.js b/browser/components/touchbar/tests/browser/browser_touchbar_tests.js
index c6326b4509..0da72143d1 100644
--- a/browser/components/touchbar/tests/browser/browser_touchbar_tests.js
+++ b/browser/components/touchbar/tests/browser/browser_touchbar_tests.js
@@ -139,7 +139,7 @@ function waitForFullScreenState(browser, state) {
return new Promise(resolve => {
let eventReceived = false;
- let observe = (subject, topic, data) => {
+ let observe = () => {
if (!eventReceived) {
return;
}
diff --git a/browser/components/translations/content/TranslationsPanelShared.sys.mjs b/browser/components/translations/content/TranslationsPanelShared.sys.mjs
index 570528df3f..f5045f57e0 100644
--- a/browser/components/translations/content/TranslationsPanelShared.sys.mjs
+++ b/browser/components/translations/content/TranslationsPanelShared.sys.mjs
@@ -11,9 +11,53 @@ ChromeUtils.defineESModuleGetters(lazy, {
/**
* A class containing static functionality that is shared by both
* the FullPageTranslationsPanel and SelectTranslationsPanel classes.
+ *
+ * It is recommended to read the documentation above the TranslationsParent class
+ * definition to understand the scope of the Translations architecture throughout
+ * Firefox.
+ *
+ * @see TranslationsParent
+ *
+ * The static instance of this class is a singleton in the parent process, and is
+ * available throughout all windows and tabs, just like the static instance of
+ * the TranslationsParent class.
+ *
+ * Unlike the TranslationsParent, this class is never instantiated as an actor
+ * outside of the static-context functionality defined below.
*/
export class TranslationsPanelShared {
- static #langListsInitState = new Map();
+ /**
+ * A map from Translations Panel instances to their initialized states.
+ * There is one instance of each panel per top ChromeWindow in Firefox.
+ *
+ * See the documentation above the TranslationsParent class for a detailed
+ * explanation of the translations architecture throughout Firefox.
+ *
+ * @see TranslationsParent
+ *
+ * @type {Map<FullPageTranslationsPanel | SelectTranslationsPanel, string>}
+ */
+ static #langListsInitState = new WeakMap();
+
+ /**
+ * True if the next language-list initialization to fail for testing.
+ *
+ * @see TranslationsPanelShared.ensureLangListsBuilt
+ *
+ * @type {boolean}
+ */
+ static #simulateLangListError = false;
+
+ /**
+ * Clears cached data regarding the initialization state of the
+ * FullPageTranslationsPanel or the SelectTranslationsPanel.
+ *
+ * This is only needed for test runners to ensure that each test
+ * starts from a clean slate.
+ */
+ static clearCache() {
+ this.#langListsInitState = new WeakMap();
+ }
/**
* Defines lazy getters for accessing elements in the document based on provided entries.
@@ -46,13 +90,25 @@ export class TranslationsPanelShared {
}
/**
+ * Ensures that the next call to ensureLangListBuilt wil fail
+ * for the purpose of testing the error state.
+ *
+ * @see TranslationsPanelShared.ensureLangListsBuilt
+ *
+ * @type {boolean}
+ */
+ static simulateLangListError() {
+ this.#simulateLangListError = true;
+ }
+
+ /**
* Retrieves the initialization state of language lists for the specified panel.
*
* @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
* - The panel for which to look up the state.
*/
static getLangListsInitState(panel) {
- return TranslationsPanelShared.#langListsInitState.get(panel.id);
+ return TranslationsPanelShared.#langListsInitState.get(panel);
}
/**
@@ -64,17 +120,17 @@ export class TranslationsPanelShared {
* @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
* - The panel for which to ensure language lists are built.
*/
- static async ensureLangListsBuilt(document, panel, innerWindowId) {
- const { id } = panel;
- switch (
- TranslationsPanelShared.#langListsInitState.get(`${id}-${innerWindowId}`)
- ) {
+ static async ensureLangListsBuilt(document, panel) {
+ const { panel: panelElement } = panel.elements;
+ switch (TranslationsPanelShared.#langListsInitState.get(panel)) {
case "initialized":
// This has already been initialized.
return;
case "error":
case undefined:
- // attempt to initialize
+ // Set the error state in case there is an early exit at any point.
+ // This will be set to "initialized" if everything succeeds.
+ TranslationsPanelShared.#langListsInitState.set(panel, "error");
break;
default:
throw new Error(
@@ -88,18 +144,28 @@ export class TranslationsPanelShared {
await lazy.TranslationsParent.getSupportedLanguages();
// Verify that we are in a proper state.
- if (languagePairs.length === 0) {
+ if (languagePairs.length === 0 || this.#simulateLangListError) {
+ this.#simulateLangListError = false;
throw new Error("No translation languages were retrieved.");
}
- const fromPopups = panel.querySelectorAll(
+ const fromPopups = panelElement.querySelectorAll(
".translations-panel-language-menupopup-from"
);
- const toPopups = panel.querySelectorAll(
+ const toPopups = panelElement.querySelectorAll(
".translations-panel-language-menupopup-to"
);
for (const popup of fromPopups) {
+ // For the moment, the FullPageTranslationsPanel includes its own
+ // menu item for "Choose another language" as the first item in the list
+ // with an empty-string for its value. The SelectTranslationsPanel has
+ // only languages in its list with BCP-47 tags for values. As such,
+ // this loop works for both panels, to remove all of the languages
+ // from the list, but ensuring that any empty-string items are retained.
+ while (popup.lastChild?.value) {
+ popup.lastChild.remove();
+ }
for (const { langTag, displayName } of fromLanguages) {
const fromMenuItem = document.createXULElement("menuitem");
fromMenuItem.setAttribute("value", langTag);
@@ -109,6 +175,9 @@ export class TranslationsPanelShared {
}
for (const popup of toPopups) {
+ while (popup.lastChild?.value) {
+ popup.lastChild.remove();
+ }
for (const { langTag, displayName } of toLanguages) {
const toMenuItem = document.createXULElement("menuitem");
toMenuItem.setAttribute("value", langTag);
@@ -117,6 +186,6 @@ export class TranslationsPanelShared {
}
}
- TranslationsPanelShared.#langListsInitState.set(id, "initialized");
+ TranslationsPanelShared.#langListsInitState.set(panel, "initialized");
}
}
diff --git a/browser/components/translations/content/fullPageTranslationsPanel.js b/browser/components/translations/content/fullPageTranslationsPanel.js
index 2e35440160..eddd3566f1 100644
--- a/browser/components/translations/content/fullPageTranslationsPanel.js
+++ b/browser/components/translations/content/fullPageTranslationsPanel.js
@@ -188,12 +188,19 @@ class CheckboxPageAction {
}
/**
- * This singleton class controls the Translations popup panel.
+ * This singleton class controls the FullPageTranslations panel.
*
* This component is a `/browser` component, and the actor is a `/toolkit` actor, so care
* must be taken to keep the presentation (this component) from the state management
* (the Translations actor). This class reacts to state changes coming from the
* Translations actor.
+ *
+ * A global instance of this class is created once per top ChromeWindow and is initialized
+ * when the new window is created.
+ *
+ * See the comment above TranslationsParent for more details.
+ *
+ * @see TranslationsParent
*/
var FullPageTranslationsPanel = new (class {
/** @type {Console?} */
@@ -374,21 +381,6 @@ var FullPageTranslationsPanel = new (class {
}
/**
- * @returns {TranslationsParent}
- */
- #getTranslationsActor() {
- const actor =
- gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
- "Translations"
- );
-
- if (!actor) {
- throw new Error("Unable to get the TranslationsParent");
- }
- return actor;
- }
-
- /**
* Fetches the language tags for the document and the user and caches the results
* Use `#getCachedDetectedLanguages` when the lang tags do not need to be re-fetched.
* This requires a bit of work to do, so prefer the cached version when possible.
@@ -396,8 +388,9 @@ var FullPageTranslationsPanel = new (class {
* @returns {Promise<LangTags>}
*/
async #fetchDetectedLanguages() {
- this.detectedLanguages =
- await this.#getTranslationsActor().getDetectedLanguages();
+ this.detectedLanguages = await TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ ).getDetectedLanguages();
return this.detectedLanguages;
}
@@ -421,11 +414,7 @@ var FullPageTranslationsPanel = new (class {
*/
async #ensureLangListsBuilt() {
try {
- await TranslationsPanelShared.ensureLangListsBuilt(
- document,
- this.elements.panel,
- gBrowser.selectedBrowser.innerWindowID
- );
+ await TranslationsPanelShared.ensureLangListsBuilt(document, this);
} catch (error) {
this.console?.error(error);
}
@@ -438,7 +427,9 @@ var FullPageTranslationsPanel = new (class {
* @param {TranslationsLanguageState} languageState
*/
#updateViewFromTranslationStatus(
- languageState = this.#getTranslationsActor().languageState
+ languageState = TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ ).languageState
) {
const { translateButton, toMenuList, fromMenuList, header, cancelButton } =
this.elements;
@@ -553,7 +544,7 @@ var FullPageTranslationsPanel = new (class {
// Unconditionally hide the intro text in case the panel is re-shown.
intro.hidden = true;
- if (TranslationsPanelShared.getLangListsInitState(panel) === "error") {
+ if (TranslationsPanelShared.getLangListsInitState(this) === "error") {
// There was an error, display it in the view rather than the language
// dropdowns.
const { cancelButton, errorHintAction } = this.elements;
@@ -722,8 +713,9 @@ var FullPageTranslationsPanel = new (class {
const neverTranslateSiteMenuItems = panel.ownerDocument.querySelectorAll(
".never-translate-site-menuitem"
);
- const neverTranslateSite =
- await this.#getTranslationsActor().shouldNeverTranslateSite();
+ const neverTranslateSite = await TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ ).shouldNeverTranslateSite();
for (const menuitem of neverTranslateSiteMenuItems) {
menuitem.setAttribute("checked", neverTranslateSite ? "true" : "false");
@@ -801,7 +793,9 @@ var FullPageTranslationsPanel = new (class {
async #showRevisitView({ fromLanguage, toLanguage }) {
const { fromMenuList, toMenuList, intro } = this.elements;
if (!this.#isShowingDefaultView()) {
- await this.#showDefaultView(this.#getTranslationsActor());
+ await this.#showDefaultView(
+ TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser)
+ );
}
intro.hidden = true;
fromMenuList.value = fromLanguage;
@@ -897,7 +891,7 @@ var FullPageTranslationsPanel = new (class {
PanelMultiView.hidePopup(panel);
await this.#showDefaultView(
- this.#getTranslationsActor(),
+ TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser),
true /* force this view to be shown */
);
@@ -1119,8 +1113,10 @@ var FullPageTranslationsPanel = new (class {
const { button } = this.buttonElements;
- const { requestedTranslationPair, locationChangeId } =
- this.#getTranslationsActor().languageState;
+ const { requestedTranslationPair } =
+ TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ ).languageState;
// Store this value because it gets modified when #showDefaultView is called below.
const isFirstUserInteraction = !this._hasShownPanel;
@@ -1132,7 +1128,9 @@ var FullPageTranslationsPanel = new (class {
this.console?.error(error);
});
} else {
- await this.#showDefaultView(this.#getTranslationsActor()).catch(error => {
+ await this.#showDefaultView(
+ TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser)
+ ).catch(error => {
this.console?.error(error);
});
}
@@ -1145,16 +1143,6 @@ var FullPageTranslationsPanel = new (class {
? button
: this.elements.appMenuButton;
- if (!TranslationsParent.isActiveLocation(locationChangeId)) {
- this.console?.log(`A translation panel open request was stale.`, {
- locationChangeId,
- newlocationChangeId:
- this.#getTranslationsActor().languageState.locationChangeId,
- currentURISpec: gBrowser.currentURI.spec,
- });
- return;
- }
-
this.console?.log(`Showing a translation panel`, gBrowser.currentURI.spec);
await this.#openPanelPopup(targetButton, {
@@ -1173,7 +1161,9 @@ var FullPageTranslationsPanel = new (class {
*/
#isTranslationsActive() {
const { requestedTranslationPair } =
- this.#getTranslationsActor().languageState;
+ TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ ).languageState;
return requestedTranslationPair !== null;
}
@@ -1183,7 +1173,9 @@ var FullPageTranslationsPanel = new (class {
async onTranslate() {
PanelMultiView.hidePopup(this.elements.panel);
- const actor = this.#getTranslationsActor();
+ const actor = TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ );
actor.translate(
this.elements.fromMenuList.value,
this.elements.toMenuList.value,
@@ -1205,7 +1197,7 @@ var FullPageTranslationsPanel = new (class {
this.#updateSettingsMenuLanguageCheckboxStates();
this.#updateSettingsMenuSiteCheckboxStates();
const popup = button.ownerDocument.getElementById(
- "translations-panel-settings-menupopup"
+ "full-page-translations-panel-settings-menupopup"
);
popup.openPopup(button, "after_end");
}
@@ -1331,8 +1323,9 @@ var FullPageTranslationsPanel = new (class {
*/
async onNeverTranslateSite() {
const pageAction = this.getCheckboxPageActionFor().neverTranslateSite();
- const toggledOn =
- await this.#getTranslationsActor().toggleNeverTranslateSitePermissions();
+ const toggledOn = await TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ ).toggleNeverTranslateSitePermissions();
TranslationsParent.telemetry().panel().onNeverTranslateSite(toggledOn);
this.#updateSettingsMenuSiteCheckboxStates();
await this.#doPageAction(pageAction);
@@ -1349,7 +1342,9 @@ var FullPageTranslationsPanel = new (class {
throw new Error("Expected to have a document language tag.");
}
- this.#getTranslationsActor().restorePage(docLangTag);
+ TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ ).restorePage(docLangTag);
}
/**
diff --git a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml
index 72e2bd7095..8c643ea3f6 100644
--- a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml
+++ b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml
@@ -4,99 +4,99 @@
<html:template id="template-select-translations-panel">
<panel id="select-translations-panel"
- class="panel-no-padding translations-panel"
+ class="panel-no-padding translations-panel translations-panel-view"
type="arrow"
role="alertdialog"
noautofocus="true"
aria-labelledby="translations-panel-header"
- orient="vertical">
- <panelmultiview id="select-translations-panel-multiview" mainViewId="select-translations-panel-view-default">
- <panelview id="select-translations-panel-view-default"
- class="PanelUI-subView translations-panel-view"
- role="document"
- mainview-with-header="true"
- has-custom-header="true">
- <hbox class="panel-header select-translations-panel-header">
- <html:h1 class="translations-panel-header-wrapper">
- <html:span id="select-translations-panel-header" data-l10n-id="select-translations-panel-header">
- </html:span>
- </html:h1>
- <hbox class="translations-panel-beta">
- <image id="select-translations-panel-beta-icon"
- class="translations-panel-beta-icon">
- </image>
- </hbox>
- <toolbarbutton id="select-translations-panel-settings"
- class="panel-info-button translations-panel-settings-gear-icon"
- data-l10n-id="translations-panel-settings-button"
- closemenu="none" />
- </hbox>
- <vbox class="select-translations-panel-content">
- <hbox id="select-translations-panel-lang-selection">
- <vbox flex="1">
- <label id="select-translations-panel-from-label"
- class="select-translations-panel-label"
- data-l10n-id="select-translations-panel-from-label">
- </label>
- <menulist id="select-translations-panel-from"
- flex="1"
- value="detect"
- size="large"
- data-l10n-id="translations-panel-choose-language"
- aria-labelledby="translations-panel-from-label">
- <menupopup id="select-translations-panel-from-menupopup"
- class="translations-panel-language-menupopup-from">
- <!-- The list of <menuitem> will be dynamically inserted. -->
- </menupopup>
- </menulist>
- </vbox>
- <vbox flex="1">
- <label id="select-translations-panel-to-label"
- class="select-translations-panel-label"
- data-l10n-id="select-translations-panel-to-label">
- </label>
- <menulist id="select-translations-panel-to"
- flex="1"
- value="detect"
- size="large"
- data-l10n-id="translations-panel-choose-language"
- aria-labelledby="translations-panel-to-label">
- <menupopup id="select-translations-panel-to-menupopup"
- class="translations-panel-language-menupopup-to">
- <!-- The list of <menuitem> will be dynamically inserted. -->
- </menupopup>
- </menulist>
- </vbox>
- </hbox>
+ orient="vertical"
+ onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)"
+ onpopuphidden="SelectTranslationsPanel.handlePanelPopupHiddenEvent(event)">
+ <hbox class="panel-header select-translations-panel-header">
+ <html:h1 class="translations-panel-header-wrapper">
+ <html:span id="select-translations-panel-header" data-l10n-id="select-translations-panel-header">
+ </html:span>
+ </html:h1>
+ <hbox class="translations-panel-beta">
+ <image id="select-translations-panel-beta-icon"
+ class="translations-panel-beta-icon">
+ </image>
+ </hbox>
+ <toolbarbutton id="select-translations-panel-settings"
+ class="panel-info-button translations-panel-settings-gear-icon"
+ data-l10n-id="translations-panel-settings-button"
+ closemenu="none" />
+ </hbox>
+ <vbox class="select-translations-panel-content">
+ <hbox id="select-translations-panel-lang-selection">
+ <vbox flex="1">
+ <label id="select-translations-panel-from-label"
+ class="select-translations-panel-label"
+ data-l10n-id="select-translations-panel-from-label">
+ </label>
+ <menulist id="select-translations-panel-from"
+ flex="1"
+ value=""
+ size="large"
+ data-l10n-id="translations-panel-choose-language"
+ aria-labelledby="select-translations-panel-from-label"
+ noinitialselection="true"
+ oncommand="SelectTranslationsPanel.onChangeFromLanguage(event)">
+ <menupopup id="select-translations-panel-from-menupopup"
+ onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)"
+ class="translations-panel-language-menupopup-from">
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ </menupopup>
+ </menulist>
</vbox>
- <vbox class="select-translations-panel-content">
- <html:textarea id="select-translations-panel-translation-area"
- data-l10n-id="select-translations-panel-placeholder-text"
- readonly="true"
- tabindex="0">
- </html:textarea>
+ <vbox flex="1">
+ <label id="select-translations-panel-to-label"
+ class="select-translations-panel-label"
+ data-l10n-id="select-translations-panel-to-label">
+ </label>
+ <menulist id="select-translations-panel-to"
+ flex="1"
+ value=""
+ size="large"
+ data-l10n-id="translations-panel-choose-language"
+ aria-labelledby="select-translations-panel-to-label"
+ noinitialselection="true"
+ oncommand="SelectTranslationsPanel.onChangeToLanguage(event)">
+ <menupopup id="select-translations-panel-to-menupopup"
+ onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)"
+ class="translations-panel-language-menupopup-to">
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ </menupopup>
+ </menulist>
</vbox>
+ </hbox>
+ </vbox>
+ <vbox class="select-translations-panel-content">
+ <html:textarea id="select-translations-panel-text-area"
+ class="select-translations-panel-text-area"
+ readonly="true"
+ tabindex="0">
+ </html:textarea>
+ </vbox>
- <hbox class="select-translations-panel-content">
- <button id="select-translations-panel-copy-button"
- class="footer-button select-translations-panel-button select-translations-panel-copy-button"
- data-l10n-id="select-translations-panel-copy-button">
- </button>
- </hbox>
+ <hbox class="select-translations-panel-content">
+ <button id="select-translations-panel-copy-button"
+ class="footer-button select-translations-panel-button select-translations-panel-copy-button"
+ data-l10n-id="select-translations-panel-copy-button">
+ </button>
+ </hbox>
- <html:moz-button-group class="panel-footer translations-panel-footer">
- <button id="select-translations-panel-translate-full-page-button"
- class="footer-button select-translations-panel-button"
- data-l10n-id="select-translations-panel-translate-full-page-button">
- </button>
- <button id="select-translations-panel-done-button"
- class="footer-button select-translations-panel-button"
- data-l10n-id="select-translations-panel-done-button"
- default="true"
- oncommand = "SelectTranslationsPanel.close()">
- </button>
- </html:moz-button-group>
- </panelview>
- </panelmultiview>
+ <html:moz-button-group class="panel-footer translations-panel-footer">
+ <button id="select-translations-panel-translate-full-page-button"
+ class="footer-button select-translations-panel-button"
+ data-l10n-id="select-translations-panel-translate-full-page-button">
+ </button>
+ <button id="select-translations-panel-done-button"
+ class="footer-button select-translations-panel-button"
+ data-l10n-id="select-translations-panel-done-button"
+ default="true"
+ oncommand = "SelectTranslationsPanel.close()">
+ </button>
+ </html:moz-button-group>
</panel>
</html:template>
diff --git a/browser/components/translations/content/selectTranslationsPanel.js b/browser/components/translations/content/selectTranslationsPanel.js
index b4fe3e9735..bb825eaefa 100644
--- a/browser/components/translations/content/selectTranslationsPanel.js
+++ b/browser/components/translations/content/selectTranslationsPanel.js
@@ -4,15 +4,27 @@
/* eslint-env mozilla/browser-window */
+/**
+ * @typedef {import("../../../../toolkit/components/translations/translations").SelectTranslationsPanelState} SelectTranslationsPanelState
+ */
+
ChromeUtils.defineESModuleGetters(this, {
LanguageDetector:
"resource://gre/modules/translation/LanguageDetector.sys.mjs",
TranslationsPanelShared:
"chrome://browser/content/translations/TranslationsPanelShared.sys.mjs",
+ Translator: "chrome://global/content/translations/Translator.mjs",
});
/**
- * This singleton class controls the Translations popup panel.
+ * This singleton class controls the SelectTranslations panel.
+ *
+ * A global instance of this class is created once per top ChromeWindow and is initialized
+ * when the context menu is opened in that window.
+ *
+ * See the comment above TranslationsParent for more details.
+ *
+ * @see TranslationsParent
*/
var SelectTranslationsPanel = new (class {
/** @type {Console?} */
@@ -40,6 +52,69 @@ var SelectTranslationsPanel = new (class {
}
/**
+ * The textarea height for shorter text.
+ *
+ * @type {string}
+ */
+ #shortTextHeight = "8em";
+
+ /**
+ * Retrieves the read-only textarea height for shorter text.
+ *
+ * @see #shortTextHeight
+ */
+ get shortTextHeight() {
+ return this.#shortTextHeight;
+ }
+
+ /**
+ * The textarea height for shorter text.
+ *
+ * @type {string}
+ */
+ #longTextHeight = "16em";
+
+ /**
+ * Retrieves the read-only textarea height for longer text.
+ *
+ * @see #longTextHeight
+ */
+ get longTextHeight() {
+ return this.#longTextHeight;
+ }
+
+ /**
+ * The threshold used to determine when the panel should
+ * use the short text-height vs. the long-text height.
+ *
+ * @type {number}
+ */
+ #textLengthThreshold = 800;
+
+ /**
+ * Retrieves the read-only text-length threshold.
+ *
+ * @see #textLengthThreshold
+ */
+ get textLengthThreshold() {
+ return this.#textLengthThreshold;
+ }
+
+ /**
+ * The localized placeholder text to display when idle.
+ *
+ * @type {string}
+ */
+ #idlePlaceholderText;
+
+ /**
+ * The localized placeholder text to display when translating.
+ *
+ * @type {string}
+ */
+ #translatingPlaceholderText;
+
+ /**
* Where the lazy elements are stored.
*
* @type {Record<string, Element>?}
@@ -47,6 +122,29 @@ var SelectTranslationsPanel = new (class {
#lazyElements;
/**
+ * The internal state of the SelectTranslationsPanel.
+ *
+ * @type {SelectTranslationsPanelState}
+ */
+ #translationState = { phase: "closed" };
+
+ /**
+ * The Translator for the current language pair.
+ *
+ * @type {Translator}
+ */
+ #translator;
+
+ /**
+ * An Id that increments with each translation, used to help keep track
+ * of whether an active translation request continue its progression or
+ * stop due to the existence of a newer translation request.
+ *
+ * @type {number}
+ */
+ #translationId = 0;
+
+ /**
* Lazily creates the dom elements, and lazily selects them.
*
* @returns {Record<string, Element>}
@@ -77,11 +175,12 @@ var SelectTranslationsPanel = new (class {
doneButton: "select-translations-panel-done-button",
fromLabel: "select-translations-panel-from-label",
fromMenuList: "select-translations-panel-from",
+ fromMenuPopup: "select-translations-panel-from-menupopup",
header: "select-translations-panel-header",
- multiview: "select-translations-panel-multiview",
- textArea: "select-translations-panel-translation-area",
+ textArea: "select-translations-panel-text-area",
toLabel: "select-translations-panel-to-label",
toMenuList: "select-translations-panel-to",
+ toMenuPopup: "select-translations-panel-to-menupopup",
translateFullPageButton:
"select-translations-panel-translate-full-page-button",
});
@@ -91,6 +190,43 @@ var SelectTranslationsPanel = new (class {
}
/**
+ * Attempts to determine the best language tag to use as the source language for translation.
+ * If the detected language is not supported, attempts to fallback to the document's language tag.
+ *
+ * @param {string} textToTranslate - The text for which the language detection and target language retrieval are performed.
+ *
+ * @returns {Promise<string>} - The code of a supported language, a supported document language, or the top detected language.
+ */
+ async getTopSupportedDetectedLanguage(textToTranslate) {
+ // First see if any of the detected languages are supported and return it if so.
+ const { language, languages } = await LanguageDetector.detectLanguage(
+ textToTranslate
+ );
+ for (const { languageCode } of languages) {
+ const isSupported = await TranslationsParent.isSupportedAsFromLang(
+ languageCode
+ );
+ if (isSupported) {
+ return languageCode;
+ }
+ }
+
+ // Since none of the detected languages were supported, check to see if the
+ // document has a specified language tag that is supported.
+ const actor = TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ );
+ const detectedLanguages = actor.languageState.detectedLanguages;
+ if (detectedLanguages?.isDocLangTagSupported) {
+ return detectedLanguages.docLangTag;
+ }
+
+ // No supported language was found, so return the top detected language
+ // to inform the panel's unsupported language state.
+ return language;
+ }
+
+ /**
* Detects the language of the provided text and retrieves a language pair for translation
* based on user settings.
*
@@ -101,9 +237,7 @@ var SelectTranslationsPanel = new (class {
*/
async getLangPairPromise(textToTranslate) {
const [fromLang, toLang] = await Promise.all([
- LanguageDetector.detectLanguage(textToTranslate).then(
- ({ language }) => language
- ),
+ SelectTranslationsPanel.getTopSupportedDetectedLanguage(textToTranslate),
TranslationsParent.getTopPreferredSupportedToLang(),
]);
@@ -122,99 +256,740 @@ var SelectTranslationsPanel = new (class {
}
/**
- * Builds the <menulist> of languages for both the "from" and "to". This can be
- * called every time the popup is shown, as it will retry when there is an error
- * (such as a network error) or be a noop if it's already initialized.
+ * Ensures that the from-language and to-language dropdowns are built.
+ *
+ * This can be called every time the popup is shown, since it will retry
+ * when there is an error (such as a network error) or be a no-op if the
+ * dropdowns have already been initialized.
*/
async #ensureLangListsBuilt() {
- try {
- await TranslationsPanelShared.ensureLangListsBuilt(
- document,
- this.elements.panel
- );
- } catch (error) {
- this.console?.error(error);
- }
+ await TranslationsPanelShared.ensureLangListsBuilt(document, this);
}
/**
- * Updates the language dropdown based on the provided language tag.
+ * Initializes the selected value of the given language dropdown based on the language tag.
*
* @param {string} langTag - A BCP-47 language tag.
- * @param {Element} menuList - The dropdown menu element that will be updated based on language support.
+ * @param {Element} menuList - The menu list element to update.
+ *
* @returns {Promise<void>}
*/
- async #updateLanguageDropdown(langTag, menuList) {
- const langTagIsSupported =
+ async #initializeLanguageMenuList(langTag, menuList) {
+ const isLangTagSupported =
menuList.id === this.elements.fromMenuList.id
? await TranslationsParent.isSupportedAsFromLang(langTag)
: await TranslationsParent.isSupportedAsToLang(langTag);
- if (langTagIsSupported) {
+ if (isLangTagSupported) {
// Remove the data-l10n-id because the menulist label will
// be populated from the supported language's display name.
- menuList.value = langTag;
menuList.removeAttribute("data-l10n-id");
+ menuList.value = langTag;
} else {
- // Set the data-l10n-id placeholder because no valid
- // language will be selected when the panel opens.
- menuList.value = undefined;
- document.l10n.setAttributes(
- menuList,
- "translations-panel-choose-language"
- );
- await document.l10n.translateElements([menuList]);
+ await this.#deselectLanguage(menuList);
}
}
/**
- * Updates the language selection dropdowns based on the given langPairPromise.
+ * Initializes the selected values of the from-language and to-language menu
+ * lists based on the result of the given language pair promise.
*
* @param {Promise<{fromLang?: string, toLang?: string}>} langPairPromise
+ *
* @returns {Promise<void>}
*/
- async #updateLanguageDropdowns(langPairPromise) {
+ async #initializeLanguageMenuLists(langPairPromise) {
const { fromLang, toLang } = await langPairPromise;
-
- this.console?.debug(`fromLang(${fromLang})`);
- this.console?.debug(`toLang(${toLang})`);
-
const { fromMenuList, toMenuList } = this.elements;
-
await Promise.all([
- this.#updateLanguageDropdown(fromLang, fromMenuList),
- this.#updateLanguageDropdown(toLang, toMenuList),
+ this.#initializeLanguageMenuList(fromLang, fromMenuList),
+ this.#initializeLanguageMenuList(toLang, toMenuList),
]);
}
/**
- * Opens the panel and populates the currently selected fromLang and toLang based
- * on the result of the langPairPromise.
+ * Opens the panel, ensuring the panel's UI and state are initialized correctly.
*
* @param {Event} event - The triggering event for opening the panel.
+ * @param {number} screenX - The x-axis location of the screen at which to open the popup.
+ * @param {number} screenY - The y-axis location of the screen at which to open the popup.
+ * @param {string} sourceText - The text to translate.
* @param {Promise} langPairPromise - Promise resolving to language pair data for initializing dropdowns.
+ *
* @returns {Promise<void>}
*/
- async open(event, langPairPromise) {
- this.console?.log("Showing a translation panel.");
+ async open(event, screenX, screenY, sourceText, langPairPromise) {
+ if (this.#isOpen()) {
+ return;
+ }
+ this.#registerSourceText(sourceText);
await this.#ensureLangListsBuilt();
- await this.#updateLanguageDropdowns(langPairPromise);
-
- // TODO(Bug 1878721) Rework the logic of where to open the panel.
- //
- // For the moment, the Select Translations panel opens at the
- // AppMenu Button, but it will eventually need to open near
- // to the selected content.
- const appMenuButton = document.getElementById("PanelUI-menu-button");
- const { panel, textArea } = this.elements;
-
- panel.addEventListener("popupshown", () => textArea.focus(), {
- once: true,
+
+ await Promise.all([
+ this.#cachePlaceholderText(),
+ this.#initializeLanguageMenuLists(langPairPromise),
+ ]);
+
+ this.#displayIdlePlaceholder();
+ this.#maybeRequestTranslation();
+ await this.#openPopup(event, screenX, screenY);
+ }
+
+ /**
+ * Opens a the panel popup at a location on the screen.
+ *
+ * @param {Event} event - The event that triggers the popup opening.
+ * @param {number} screenX - The x-axis location of the screen at which to open the popup.
+ * @param {number} screenY - The y-axis location of the screen at which to open the popup.
+ */
+ async #openPopup(event, screenX, screenY) {
+ await window.ensureCustomElements("moz-button-group");
+
+ this.console?.log("Showing SelectTranslationsPanel");
+ const { panel } = this.elements;
+ panel.openPopupAtScreen(screenX, screenY, /* isContextMenu */ false, event);
+ }
+
+ /**
+ * Adds the source text to the translation state and adapts the size of the text area based
+ * on the length of the text.
+ *
+ * @param {string} sourceText - The text to translate.
+ *
+ * @returns {Promise<void>}
+ */
+ #registerSourceText(sourceText) {
+ const { textArea } = this.elements;
+ this.#changeStateTo("idle", /* retainEntries */ false, {
+ sourceText,
});
- await PanelMultiView.openPopup(panel, appMenuButton, {
- position: "bottomright topright",
- triggerEvent: event,
- }).catch(error => this.console?.error(error));
+
+ if (sourceText.length < SelectTranslationsPanel.textLengthThreshold) {
+ textArea.style.height = SelectTranslationsPanel.shortTextHeight;
+ } else {
+ textArea.style.height = SelectTranslationsPanel.longTextHeight;
+ }
+ }
+
+ /**
+ * Caches the localized text to use as placeholders.
+ */
+ async #cachePlaceholderText() {
+ const [idleText, translatingText] = await document.l10n.formatValues([
+ { id: "select-translations-panel-idle-placeholder-text" },
+ { id: "select-translations-panel-translating-placeholder-text" },
+ ]);
+ this.#idlePlaceholderText = idleText;
+ this.#translatingPlaceholderText = translatingText;
+ }
+
+ /**
+ * Handles events when a popup is shown within the panel, including showing
+ * the panel itself.
+ *
+ * @param {Event} event - The event that triggered the popup to show.
+ */
+ handlePanelPopupShownEvent(event) {
+ const { panel, fromMenuPopup, toMenuPopup } = this.elements;
+ switch (event.target.id) {
+ case panel.id: {
+ this.#updatePanelUIFromState();
+ break;
+ }
+ case fromMenuPopup.id: {
+ this.#maybeTranslateOnEvents(["popuphidden"], fromMenuPopup);
+ break;
+ }
+ case toMenuPopup.id: {
+ this.#maybeTranslateOnEvents(["popuphidden"], toMenuPopup);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles events when a popup is closed within the panel, including closing
+ * the panel itself.
+ *
+ * @param {Event} event - The event that triggered the popup to close.
+ */
+ handlePanelPopupHiddenEvent(event) {
+ const { panel } = this.elements;
+ switch (event.target.id) {
+ case panel.id: {
+ this.#changeStateToClosed();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles events when the panels select from-language is changed.
+ */
+ onChangeFromLanguage() {
+ const { fromMenuList, toMenuList } = this.elements;
+ this.#maybeTranslateOnEvents(["blur", "keypress"], fromMenuList);
+ this.#maybeStealLanguageFrom(toMenuList);
+ }
+
+ /**
+ * Handles events when the panels select to-language is changed.
+ */
+ onChangeToLanguage() {
+ const { toMenuList, fromMenuList } = this.elements;
+ this.#maybeTranslateOnEvents(["blur", "keypress"], toMenuList);
+ this.#maybeStealLanguageFrom(fromMenuList);
+ }
+
+ /**
+ * Clears the selected language and ensures that the menu list displays
+ * the proper placeholder text.
+ *
+ * @param {Element} menuList - The target menu list element to update.
+ */
+ async #deselectLanguage(menuList) {
+ menuList.value = "";
+ document.l10n.setAttributes(menuList, "translations-panel-choose-language");
+ await document.l10n.translateElements([menuList]);
+ }
+
+ /**
+ * Deselects the language from the target menu list if both menu lists
+ * have the same language selected, simulating the effect of one menu
+ * list stealing the selected language value from the other.
+ *
+ * @param {Element} menuList - The target menu list element to update.
+ */
+ async #maybeStealLanguageFrom(menuList) {
+ const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair();
+ if (fromLanguage === toLanguage) {
+ await this.#deselectLanguage(menuList);
+ this.#maybeFocusMenuList(menuList);
+ }
+ }
+
+ /**
+ * Focuses on the given menu list if provided and empty, or defaults to focusing one
+ * of the from-menu or to-menu lists if either is empty.
+ *
+ * @param {Element} [menuList] - The menu list to focus if specified.
+ */
+ #maybeFocusMenuList(menuList) {
+ if (menuList && !menuList.value) {
+ menuList.focus({ focusVisible: true });
+ return;
+ }
+
+ const { fromMenuList, toMenuList } = this.elements;
+ if (!fromMenuList.value) {
+ fromMenuList.focus({ focusVisible: true });
+ } else if (!toMenuList.value) {
+ toMenuList.focus({ focusVisible: true });
+ }
+ }
+
+ /**
+ * Focuses the translated-text area and sets its overflow to auto post-animation.
+ */
+ #indicateTranslatedTextArea({ overflow }) {
+ const { textArea } = this.elements;
+ textArea.focus({ focusVisible: true });
+ requestAnimationFrame(() => {
+ // We want to set overflow to auto as the final animation, because if it is
+ // set before the translated text is displayed, then the scrollTop will
+ // move to the bottom as the text is populated.
+ //
+ // Setting scrollTop = 0 on its own works, but it sometimes causes an animation
+ // of the text jumping from the bottom to the top. It looks a lot cleaner to
+ // disable overflow before rendering the text, then re-enable it after it renders.
+ requestAnimationFrame(() => {
+ textArea.style.overflow = overflow;
+ textArea.scrollTop = 0;
+ });
+ });
+ }
+
+ /**
+ * Checks if the given language pair matches the panel's currently selected language pair.
+ *
+ * @param {string} fromLanguage - The from-language to compare.
+ * @param {string} toLanguage - The to-language to compare.
+ *
+ * @returns {boolean} - True if the given language pair matches the selected languages in the panel UI, otherwise false.
+ */
+ #isSelectedLangPair(fromLanguage, toLanguage) {
+ const { fromLanguage: selectedFromLang, toLanguage: selectedToLang } =
+ this.#getSelectedLanguagePair();
+ return fromLanguage === selectedFromLang && toLanguage === selectedToLang;
+ }
+
+ /**
+ * Checks if the translator's language configuration matches the given language pair.
+ *
+ * @param {string} fromLanguage - The from-language to compare.
+ * @param {string} toLanguage - The to-language to compare.
+ *
+ * @returns {boolean} - True if the translator's languages match the given pair, otherwise false.
+ */
+ #translatorMatchesLangPair(fromLanguage, toLanguage) {
+ return (
+ this.#translator?.fromLanguage === fromLanguage &&
+ this.#translator?.toLanguage === toLanguage
+ );
+ }
+
+ /**
+ * Retrieves the currently selected language pair from the menu lists.
+ *
+ * @returns {{fromLanguage: string, toLanguage: string}} An object containing the selected languages.
+ */
+ #getSelectedLanguagePair() {
+ const { fromMenuList, toMenuList } = this.elements;
+ return {
+ fromLanguage: fromMenuList.value,
+ toLanguage: toMenuList.value,
+ };
+ }
+
+ /**
+ * Retrieves the source text from the translation state.
+ * This value is not available when the panel is closed.
+ *
+ * @returns {string | undefined} The source text.
+ */
+ getSourceText() {
+ return this.#translationState?.sourceText;
+ }
+
+ /**
+ * Retrieves the source text from the translation state.
+ * This value is only available in the translated phase.
+ *
+ * @returns {string | undefined} The translated text.
+ */
+ getTranslatedText() {
+ return this.#translationState?.translatedText;
+ }
+
+ /**
+ * Retrieves the current phase of the translation state.
+ *
+ * @returns {SelectTranslationsPanelState}
+ */
+ #phase() {
+ return this.#translationState.phase;
+ }
+
+ /**
+ * @returns {boolean} True if the panel is open, otherwise false.
+ */
+ #isOpen() {
+ return this.#phase() !== "closed";
+ }
+
+ /**
+ * @returns {boolean} True if the panel is closed, otherwise false.
+ */
+ #isClosed() {
+ return this.#phase() === "closed";
+ }
+
+ /**
+ * Changes the translation state to a new phase with options to retain or overwrite existing entries.
+ *
+ * @param {SelectTranslationsPanelState} phase - The new phase to transition to.
+ * @param {boolean} [retainEntries] - Whether to retain existing state entries that are not overwritten.
+ * @param {object | null} [data=null] - Additional data to merge into the state.
+ * @throws {Error} If an invalid phase is specified.
+ */
+ #changeStateTo(phase, retainEntries, data = null) {
+ const { textArea } = this.elements;
+ switch (phase) {
+ case "translating": {
+ textArea.classList.add("translating");
+ break;
+ }
+ case "closed":
+ case "idle":
+ case "translatable":
+ case "translated": {
+ textArea.classList.remove("translating");
+ break;
+ }
+ default: {
+ throw new Error(`Invalid state change to '${phase}'`);
+ }
+ }
+
+ const previousPhase = this.#phase();
+ if (data && retainEntries) {
+ // Change the phase and apply new entries from data, but retain non-overwritten entries from previous state.
+ this.#translationState = { ...this.#translationState, phase, ...data };
+ } else if (data) {
+ // Change the phase and apply new entries from data, but drop any entries that are not overwritten by data.
+ this.#translationState = { phase, ...data };
+ } else if (retainEntries) {
+ // Change only the phase and retain all entries from previous data.
+ this.#translationState.phase = phase;
+ } else {
+ // Change the phase and delete all entries from previous data.
+ this.#translationState = { phase };
+ }
+
+ if (previousPhase === this.#phase()) {
+ // Do not continue on to update the UI because the phase didn't change.
+ return;
+ }
+
+ const { fromLanguage, toLanguage } = this.#translationState;
+ this.console?.debug(
+ `SelectTranslationsPanel (${fromLanguage ? fromLanguage : "??"}-${
+ toLanguage ? toLanguage : "??"
+ }) state change (${previousPhase} => ${phase})`
+ );
+
+ this.#updatePanelUIFromState();
+ }
+
+ /**
+ * Changes the phase to closed, discarding any entries in the translation state.
+ */
+ #changeStateToClosed() {
+ this.#changeStateTo("closed", /* retainEntries */ false);
+ }
+
+ /**
+ * Changes the phase from "translatable" to "translating".
+ *
+ * @throws {Error} If the current state is not "translatable".
+ */
+ #changeStateToTranslating() {
+ const phase = this.#phase();
+ if (phase !== "translatable") {
+ throw new Error(`Invalid state change (${phase} => translating)`);
+ }
+ this.#changeStateTo("translating", /* retainEntries */ true);
+ }
+
+ /**
+ * Changes the phase from "translating" to "translated".
+ *
+ * @throws {Error} If the current state is not "translating".
+ */
+ #changeStateToTranslated(translatedText) {
+ const phase = this.#phase();
+ if (phase !== "translating") {
+ throw new Error(`Invalid state change (${phase} => translated)`);
+ }
+ this.#changeStateTo("translated", /* retainEntries */ true, {
+ translatedText,
+ });
+ }
+
+ /**
+ * Transitions the phase of the state based on the given language pair.
+ *
+ * @param {string} fromLanguage - The BCP-47 from-language tag.
+ * @param {string} toLanguage - The BCP-47 to-language tag.
+ *
+ * @returns {SelectTranslationsPanelState} The new phase of the translation state.
+ */
+ #changeStateByLanguagePair(fromLanguage, toLanguage) {
+ const {
+ phase: previousPhase,
+ fromLanguage: previousFromLanguage,
+ toLanguage: previousToLanguage,
+ } = this.#translationState;
+
+ let nextPhase = "translatable";
+
+ if (
+ // No from-language is selected, so we cannot translate.
+ !fromLanguage ||
+ // No to-language is selected, so we cannot translate.
+ !toLanguage ||
+ // The same language has been selected, so we cannot translate.
+ fromLanguage === toLanguage
+ ) {
+ nextPhase = "idle";
+ } else if (
+ // The languages have not changed, so there is nothing to do.
+ previousFromLanguage === fromLanguage &&
+ previousToLanguage === toLanguage
+ ) {
+ nextPhase = previousPhase;
+ }
+
+ this.#changeStateTo(nextPhase, /* retainEntries */ true, {
+ fromLanguage,
+ toLanguage,
+ });
+
+ return nextPhase;
+ }
+
+ /**
+ * Determines whether translation should continue based on panel state and language pair.
+ *
+ * @param {number} translationId - The id of the translation request to match.
+ * @param {string} fromLanguage - The from-language to analyze.
+ * @param {string} toLanguage - The to-language to analyze.
+ *
+ * @returns {boolean} True if translation should continue with the given pair, otherwise false.
+ */
+ #shouldContinueTranslation(translationId, fromLanguage, toLanguage) {
+ return (
+ // Continue only if the panel is still open.
+ this.#isOpen() &&
+ // Continue only if the current translationId matches.
+ translationId === this.#translationId &&
+ // Continue only if the given language pair is still the actively selected pair.
+ this.#isSelectedLangPair(fromLanguage, toLanguage) &&
+ // Continue only if the given language pair matches the current translator.
+ this.#translatorMatchesLangPair(fromLanguage, toLanguage)
+ );
+ }
+
+ /**
+ * Displays the placeholder text for the translation state's "idle" phase.
+ */
+ #displayIdlePlaceholder() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ textArea.value = this.#idlePlaceholderText;
+ this.#updateTextDirection();
+ this.#updateConditionalUIEnabledState();
+ this.#maybeFocusMenuList();
+ }
+
+ /**
+ * Displays the placeholder text for the translation state's "translating" phase.
+ */
+ #displayTranslatingPlaceholder() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ textArea.value = this.#translatingPlaceholderText;
+ this.#updateTextDirection();
+ this.#updateConditionalUIEnabledState();
+ this.#indicateTranslatedTextArea({ overflow: "hidden" });
+ }
+
+ /**
+ * Displays the translated text for the translation state's "translated" phase.
+ */
+ #displayTranslatedText() {
+ const { toLanguage } = this.#getSelectedLanguagePair();
+ const { textArea } = SelectTranslationsPanel.elements;
+ textArea.value = this.getTranslatedText();
+ this.#updateTextDirection(toLanguage);
+ this.#updateConditionalUIEnabledState();
+ this.#indicateTranslatedTextArea({ overflow: "auto" });
+ }
+
+ /**
+ * Enables or disables UI components that are conditional on a valid language pair being selected.
+ */
+ #updateConditionalUIEnabledState() {
+ const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair();
+ const { copyButton, translateFullPageButton, textArea } = this.elements;
+
+ const invalidLangPairSelected = !fromLanguage || !toLanguage;
+ const isTranslating = this.#phase() === "translating";
+
+ textArea.disabled = invalidLangPairSelected;
+ translateFullPageButton.disabled = invalidLangPairSelected;
+ copyButton.disabled = invalidLangPairSelected || isTranslating;
+ }
+
+ /**
+ * Updates the panel UI based on the current phase of the translation state.
+ */
+ #updatePanelUIFromState() {
+ switch (this.#phase()) {
+ case "idle": {
+ this.#displayIdlePlaceholder();
+ break;
+ }
+ case "translating": {
+ this.#displayTranslatingPlaceholder();
+ break;
+ }
+ case "translated": {
+ this.#displayTranslatedText();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Sets the text direction attribute in the text areas based on the specified language.
+ * Uses the given language tag if provided, otherwise uses the current app locale.
+ *
+ * @param {string} [langTag] - The language tag to determine text direction.
+ */
+ #updateTextDirection(langTag) {
+ const { textArea } = this.elements;
+ if (langTag) {
+ const scriptDirection = Services.intl.getScriptDirection(langTag);
+ textArea.setAttribute("dir", scriptDirection);
+ } else {
+ textArea.removeAttribute("dir");
+ }
+ }
+
+ /**
+ * Requests a translations port for a given language pair.
+ *
+ * @param {string} fromLanguage - The from-language.
+ * @param {string} toLanguage - The to-language.
+ *
+ * @returns {Promise<MessagePort | undefined>} The message port promise.
+ */
+ async #requestTranslationsPort(fromLanguage, toLanguage) {
+ const innerWindowId =
+ gBrowser.selectedBrowser.browsingContext.top.embedderElement
+ .innerWindowID;
+ if (!innerWindowId) {
+ return undefined;
+ }
+ const port = await TranslationsParent.requestTranslationsPort(
+ innerWindowId,
+ fromLanguage,
+ toLanguage
+ );
+ return port;
+ }
+
+ /**
+ * Retrieves the existing translator for the specified language pair if it matches,
+ * otherwise creates a new translator.
+ *
+ * @param {string} fromLanguage - The source language code.
+ * @param {string} toLanguage - The target language code.
+ *
+ * @returns {Promise<Translator>} A promise that resolves to a `Translator` instance for the given language pair.
+ */
+ async #getOrCreateTranslator(fromLanguage, toLanguage) {
+ if (this.#translatorMatchesLangPair(fromLanguage, toLanguage)) {
+ return this.#translator;
+ }
+
+ this.console?.log(
+ `Creating new Translator (${fromLanguage}-${toLanguage})`
+ );
+ if (this.#translator) {
+ this.#translator.destroy();
+ this.#translator = null;
+ }
+
+ this.#translator = await Translator.create(
+ fromLanguage,
+ toLanguage,
+ this.#requestTranslationsPort
+ );
+ return this.#translator;
+ }
+
+ /**
+ * Initiates the translation process if the panel state and selected languages
+ * meet the conditions for translation.
+ */
+ #maybeRequestTranslation() {
+ if (this.#isClosed()) {
+ return;
+ }
+ const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair();
+ const nextState = this.#changeStateByLanguagePair(fromLanguage, toLanguage);
+ if (nextState !== "translatable") {
+ return;
+ }
+
+ const translationId = ++this.#translationId;
+ this.#getOrCreateTranslator(fromLanguage, toLanguage)
+ .then(translator => {
+ if (
+ this.#shouldContinueTranslation(
+ translationId,
+ fromLanguage,
+ toLanguage
+ )
+ ) {
+ this.#changeStateToTranslating();
+ return translator.translate(this.getSourceText());
+ }
+ return null;
+ })
+ .then(translatedText => {
+ if (
+ translatedText &&
+ this.#shouldContinueTranslation(
+ translationId,
+ fromLanguage,
+ toLanguage
+ )
+ ) {
+ this.#changeStateToTranslated(translatedText);
+ } else if (this.#isOpen()) {
+ this.#changeStateTo("idle", /* retainEntires */ false, {
+ sourceText: this.getSourceText(),
+ });
+ }
+ })
+ .catch(error => this.console?.error(error));
+ }
+
+ /**
+ * Attaches event listeners to the target element for initiating translation on specified event types.
+ *
+ * @param {string[]} eventTypes - An array of event types to listen for.
+ * @param {object} target - The target element to attach event listeners to.
+ * @throws {Error} If an unrecognized event type is provided.
+ */
+ #maybeTranslateOnEvents(eventTypes, target) {
+ if (!target.translationListenerCallbacks) {
+ target.translationListenerCallbacks = [];
+ }
+ if (target.translationListenerCallbacks.length === 0) {
+ for (const eventType of eventTypes) {
+ let callback;
+ switch (eventType) {
+ case "blur":
+ case "popuphidden": {
+ callback = () => {
+ this.#maybeRequestTranslation();
+ this.#removeTranslationListeners(target);
+ };
+ break;
+ }
+ case "keypress": {
+ callback = event => {
+ if (event.key === "Enter") {
+ this.#maybeRequestTranslation();
+ }
+ this.#removeTranslationListeners(target);
+ };
+ break;
+ }
+ default: {
+ throw new Error(
+ `Invalid translation event type given: '${eventType}`
+ );
+ }
+ }
+ target.addEventListener(eventType, callback, { once: true });
+ target.translationListenerCallbacks.push({ eventType, callback });
+ }
+ }
+ }
+
+ /**
+ * Removes all translation event listeners from the target element.
+ *
+ * @param {Element} target - The element from which event listeners are to be removed.
+ */
+ #removeTranslationListeners(target) {
+ for (const { eventType, callback } of target.translationListenerCallbacks) {
+ target.removeEventListener(eventType, callback);
+ }
+ target.translationListenerCallbacks = [];
}
})();
diff --git a/browser/components/translations/moz.build b/browser/components/translations/moz.build
index 212b93e509..49f3afc632 100644
--- a/browser/components/translations/moz.build
+++ b/browser/components/translations/moz.build
@@ -3,7 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
with Files("**"):
- BUG_COMPONENT = ("Firefox", "Translation")
+ BUG_COMPONENT = ("Firefox", "Translations")
BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
diff --git a/browser/components/translations/tests/browser/browser.toml b/browser/components/translations/tests/browser/browser.toml
index a9d36363da..472ae28866 100644
--- a/browser/components/translations/tests/browser/browser.toml
+++ b/browser/components/translations/tests/browser/browser.toml
@@ -15,6 +15,10 @@ support-files = [
["browser_translations_about_preferences_settings_ui.js"]
+["browser_translations_full_page_move_tab_to_new_window.js"]
+
+["browser_translations_full_page_multiple_windows.js"]
+
["browser_translations_full_page_panel_a11y_focus.js"]
["browser_translations_full_page_panel_always_translate_language_bad_data.js"]
@@ -51,8 +55,6 @@ support-files = [
["browser_translations_full_page_panel_engine_unsupported.js"]
-["browser_translations_full_page_panel_engine_unsupported_lang.js"]
-
["browser_translations_full_page_panel_firstrun.js"]
["browser_translations_full_page_panel_firstrun_revisit.js"]
@@ -62,6 +64,8 @@ skip-if = ["true"]
["browser_translations_full_page_panel_gear.js"]
+["browser_translations_full_page_panel_init_failure.js"]
+
["browser_translations_full_page_panel_never_translate_language.js"]
["browser_translations_full_page_panel_never_translate_site_auto.js"]
@@ -77,6 +81,8 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227
["browser_translations_full_page_panel_switch_languages.js"]
+["browser_translations_full_page_panel_unsupported_lang.js"]
+
["browser_translations_full_page_reader_mode.js"]
["browser_translations_full_page_telemetry_firstrun_auto_translate.js"]
@@ -109,6 +115,30 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227
["browser_translations_select_context_menu_with_text_selected.js"]
-["browser_translations_select_panel_language_selectors.js"]
+["browser_translations_select_panel_engine_cache.js"]
+
+["browser_translations_select_panel_fallback_to_doc_language.js"]
+
+["browser_translations_select_panel_open_to_idle_state.js"]
+
+["browser_translations_select_panel_retranslate_on_change_language_directly.js"]
+
+["browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js"]
+
+["browser_translations_select_panel_select_current_language_directly.js"]
+
+["browser_translations_select_panel_select_current_language_from_dropdown_menu.js"]
+
+["browser_translations_select_panel_select_same_from_and_to_languages_directly.js"]
+
+["browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js"]
+
+["browser_translations_select_panel_translate_on_change_language_directly.js"]
+
+["browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js"]
+
+["browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js"]
+
+["browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js"]
-["browser_translations_select_panel_mainview_ui.js"]
+["browser_translations_select_panel_translate_on_open.js"]
diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js
index ee81b84a36..f618b27814 100644
--- a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js
+++ b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js
@@ -22,8 +22,8 @@ add_task(async function test_translations_settings_pane_elements() {
translationsSettingsDescription,
translateAlwaysHeader,
translateNeverHeader,
- translateAlwaysAddButton,
- translateNeverAddButton,
+ translateAlwaysMenuList,
+ translateNeverMenuList,
translateNeverSiteHeader,
translateNeverSiteDesc,
translateDownloadLanguagesHeader,
@@ -41,8 +41,8 @@ add_task(async function test_translations_settings_pane_elements() {
translationsSettingsDescription,
translateAlwaysHeader,
translateNeverHeader,
- translateAlwaysAddButton,
- translateNeverAddButton,
+ translateAlwaysMenuList,
+ translateNeverMenuList,
translateNeverSiteHeader,
translateNeverSiteDesc,
translateDownloadLanguagesHeader,
@@ -74,14 +74,203 @@ add_task(async function test_translations_settings_pane_elements() {
translationsSettingsDescription,
translateAlwaysHeader,
translateNeverHeader,
- translateAlwaysAddButton,
- translateNeverAddButton,
+ translateAlwaysMenuList,
+ translateNeverMenuList,
translateNeverSiteHeader,
translateNeverSiteDesc,
translateDownloadLanguagesHeader,
translateDownloadLanguagesLearnMore,
},
});
+ await cleanup();
+});
+
+add_task(async function test_translations_settings_always_translate() {
+ const {
+ cleanup,
+ elements: { settingsButton },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", true]],
+ });
+
+ const document = gBrowser.selectedBrowser.contentDocument;
+
+ assertVisibility({
+ message: "Expect paneGeneral elements to be visible.",
+ visible: { settingsButton },
+ });
+
+ const { translateAlwaysMenuList } =
+ await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
+ settingsButton
+ );
+ let alwaysTranslateSection = document.getElementById(
+ "translations-settings-always-translate-section"
+ );
+ await testLanguageList(alwaysTranslateSection, translateAlwaysMenuList);
+
+ await cleanup();
+});
+
+async function testLanguageList(translateSection, menuList) {
+ const sectionName =
+ translateSection.id === "translations-settings-always-translate-section"
+ ? "Always"
+ : "Never";
+
+ is(
+ translateSection.querySelector(".translations-settings-languages-card"),
+ null,
+ `Language list not present in ${sectionName} Translate list`
+ );
+
+ for (let i = 0; i < menuList.children[0].children.length; i++) {
+ menuList.value = menuList.children[0].children[i].value;
+
+ let clickMenu = BrowserTestUtils.waitForEvent(menuList, "command");
+ menuList.dispatchEvent(new Event("command"));
+ await clickMenu;
+
+ /** Languages are always added on the top, so check the firstChild
+ * for newly added languages.
+ * the firstChild.lastChild.innerText is the language display name
+ * which is compared with the menulist display name that is selected
+ */
+ is(
+ translateSection.querySelector(".translations-settings-language-list")
+ .firstChild.lastChild.innerText,
+ getIntlDisplayName(menuList.children[0].children[i].value),
+ `Language list has element ${getIntlDisplayName(
+ menuList.children[0].children[i].value
+ )}`
+ );
+ }
+ /** The test cases has 4 languages, so check if 4 languages are added to the list */
+ let langNum = translateSection.querySelector(
+ ".translations-settings-language-list"
+ ).childElementCount;
+ is(langNum, 4, "Number of languages added is 4");
+
+ const languagelist = translateSection.querySelector(
+ ".translations-settings-language-list"
+ );
+
+ for (let i = 0; i < langNum; i++) {
+ // Delete the first language in the list
+ let langName = languagelist.children[0].lastChild.innerText;
+ let langButton = languagelist.children[0].querySelector("moz-button");
+
+ let clickButton = BrowserTestUtils.waitForEvent(langButton, "click");
+ langButton.dispatchEvent(new Event("click"));
+ await clickButton;
+
+ if (i < langNum - 1) {
+ is(
+ languagelist.childElementCount,
+ langNum - i - 1,
+ `${langName} removed from ${sectionName} Translate`
+ );
+ } else {
+ /** Check if the language list card is removed after removing the last language */
+ is(
+ translateSection.querySelector(".translations-settings-languages-card"),
+ null,
+ `${langName} removed from ${sectionName} Translate`
+ );
+ }
+ }
+}
+
+add_task(async function test_translations_settings_never_translate() {
+ const {
+ cleanup,
+ elements: { settingsButton },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", true]],
+ });
+
+ const document = gBrowser.selectedBrowser.contentDocument;
+
+ assertVisibility({
+ message: "Expect paneGeneral elements to be visible.",
+ visible: { settingsButton },
+ });
+
+ const { translateNeverMenuList } =
+ await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
+ settingsButton
+ );
+ let neverTranslateSection = document.getElementById(
+ "translations-settings-never-translate-section"
+ );
+ await testLanguageList(neverTranslateSection, translateNeverMenuList);
+ await cleanup();
+});
+
+add_task(async function test_translations_settings_download_languages() {
+ const {
+ cleanup,
+ elements: { settingsButton },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", true]],
+ });
+ assertVisibility({
+ message: "Expect paneGeneral elements to be visible.",
+ visible: { settingsButton },
+ });
+
+ const { translateDownloadLanguagesList } =
+ await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
+ settingsButton
+ );
+
+ let langList = translateDownloadLanguagesList.querySelector(
+ ".translations-settings-language-list"
+ );
+
+ for (let i = 0; i < langList.children.length; i++) {
+ is(
+ langList.children[i]
+ .querySelector("moz-button")
+ .classList.contains("translations-settings-download-icon"),
+ true,
+ "Download icon is visible"
+ );
+
+ let clickButton = BrowserTestUtils.waitForEvent(
+ langList.children[i].querySelector("moz-button"),
+ "click"
+ );
+ langList.children[i]
+ .querySelector("moz-button")
+ .dispatchEvent(new Event("click"));
+ await clickButton;
+
+ is(
+ langList.children[i]
+ .querySelector("moz-button")
+ .classList.contains("translations-settings-delete-icon"),
+ true,
+ "Delete icon is visible"
+ );
+
+ clickButton = BrowserTestUtils.waitForEvent(
+ langList.children[i].querySelector("moz-button"),
+ "click"
+ );
+ langList.children[i]
+ .querySelector("moz-button")
+ .dispatchEvent(new Event("click"));
+ await clickButton;
+
+ is(
+ langList.children[i]
+ .querySelector("moz-button")
+ .classList.contains("translations-settings-download-icon"),
+ true,
+ "Download icon is visible"
+ );
+ }
await cleanup();
});
diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_move_tab_to_new_window.js b/browser/components/translations/tests/browser/browser_translations_full_page_move_tab_to_new_window.js
new file mode 100644
index 0000000000..f384fc59c8
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_full_page_move_tab_to_new_window.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests a specific situation described in Bug 1893776
+ * where the Translations panels were not initializing correctly after
+ * dragging a tab to become its own new window after opening the panel
+ * in the previous window.
+ */
+add_task(async function test_browser_translations_full_page_multiple_windows() {
+ const window1 = window;
+ const testPage = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible.",
+ window1
+ );
+
+ info("Opening FullPageTranslationsPanel in window1");
+ await FullPageTranslationsTestUtils.openPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ info("Moving the tab to a new window of its own");
+ const window2 = await window1.gBrowser.replaceTabWithWindow(testPage.tab);
+ const swapDocShellPromise = BrowserTestUtils.waitForEvent(
+ testPage.tab.linkedBrowser,
+ "SwapDocShells"
+ );
+ await swapDocShellPromise;
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible.",
+ window2
+ );
+
+ info("Opening FullPageTranslationsPanel in window2");
+ await FullPageTranslationsTestUtils.openPanel({
+ win: window2,
+ });
+
+ info("Translating the same page in window2");
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ win: window2,
+ downloadHandler: testPage.resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertLangTagIsShownOnTranslationsButton(
+ "es",
+ "en",
+ window2
+ );
+
+ await testPage.cleanup();
+ await BrowserTestUtils.closeWindow(window2);
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_multiple_windows.js b/browser/components/translations/tests/browser/browser_translations_full_page_multiple_windows.js
new file mode 100644
index 0000000000..9bdee2c406
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_full_page_multiple_windows.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * @param {Window} win
+ */
+function focusWindow(win) {
+ const promise = BrowserTestUtils.waitForEvent(win, "focus");
+ win.focus();
+ return promise;
+}
+
+/**
+ * Test that the full page translation panel works when multiple windows are used.
+ */
+add_task(async function test_browser_translations_full_page_multiple_windows() {
+ const window1 = window;
+ const testPage1 = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ const window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ const testPage2 = await loadTestPage({
+ win: window2,
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ // Focus back to the original window first. This ensures coverage for invalid caching
+ // logic involving multiple windows.
+ await focusWindow(window1);
+
+ info("Testing window 1");
+ await FullPageTranslationsTestUtils.openPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: testPage1.resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ testPage1.runInPage,
+ "Window 1 gets translated",
+ window1
+ );
+
+ await focusWindow(window2);
+
+ info("Testing window 2");
+ await FullPageTranslationsTestUtils.openPanel({ win: window2 });
+ await FullPageTranslationsTestUtils.clickTranslateButton({ win: window2 });
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ testPage2.runInPage,
+ "Window 2 gets translated",
+ window2
+ );
+
+ await testPage2.cleanup();
+ await BrowserTestUtils.closeWindow(window2);
+ await testPage1.cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_init_failure.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_init_failure.js
new file mode 100644
index 0000000000..34986726b8
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_init_failure.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies that the proper error message is displayed in
+ * the FullPageTranslationsPanel if the panel tries to open, but the language
+ * dropdown menus fail to initialize.
+ */
+add_task(async function test_full_page_translations_panel_init_failure() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ TranslationsPanelShared.simulateLangListError();
+ await FullPageTranslationsTestUtils.openPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewInitFailure,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js
index 74d92381b9..01af5cbd8d 100644
--- a/browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js
+++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js
@@ -37,7 +37,7 @@ add_task(async function test_translations_panel_retry() {
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
});
- FullPageTranslationsTestUtils.switchSelectedToLanguage("fr");
+ FullPageTranslationsTestUtils.changeSelectedToLanguage("fr");
await FullPageTranslationsTestUtils.clickTranslateButton({
downloadHandler: resolveDownloads,
diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js
index 0c5db67b20..6ab70e634f 100644
--- a/browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js
+++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js
@@ -30,29 +30,29 @@ add_task(async function test_translations_panel_switch_language() {
FullPageTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "es" });
FullPageTranslationsTestUtils.assertSelectedToLanguage({ langTag: "en" });
- FullPageTranslationsTestUtils.switchSelectedFromLanguage("en");
+ FullPageTranslationsTestUtils.changeSelectedFromLanguage("en");
ok(
translateButton.disabled,
"The translate button is disabled when the languages are the same"
);
- FullPageTranslationsTestUtils.switchSelectedFromLanguage("es");
+ FullPageTranslationsTestUtils.changeSelectedFromLanguage("es");
ok(
!translateButton.disabled,
"When the languages are different it can be translated"
);
- FullPageTranslationsTestUtils.switchSelectedFromLanguage("");
+ FullPageTranslationsTestUtils.changeSelectedFromLanguage("");
ok(
translateButton.disabled,
"The translate button is disabled nothing is selected."
);
- FullPageTranslationsTestUtils.switchSelectedFromLanguage("en");
- FullPageTranslationsTestUtils.switchSelectedToLanguage("fr");
+ FullPageTranslationsTestUtils.changeSelectedFromLanguage("en");
+ FullPageTranslationsTestUtils.changeSelectedToLanguage("fr");
ok(!translateButton.disabled, "The translate button can now be used");
diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_engine_unsupported_lang.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_unsupported_lang.js
index 21f7e8fdb7..59be1e329b 100644
--- a/browser/components/translations/tests/browser/browser_translations_full_page_panel_engine_unsupported_lang.js
+++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_unsupported_lang.js
@@ -23,6 +23,9 @@ add_task(async function test_unsupported_lang() {
});
await FullPageTranslationsTestUtils.clickChangeSourceLanguageButton();
+ FullPageTranslationsTestUtils.assertPanelViewDefault();
+ FullPageTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "" });
+ FullPageTranslationsTestUtils.assertSelectedToLanguage({ langTag: "en" });
await cleanup();
});
diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js b/browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js
index ef13940b3f..41183cc9cf 100644
--- a/browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js
+++ b/browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js
@@ -24,7 +24,7 @@ add_task(async function test_translations_telemetry_switch_from_language() {
});
FullPageTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "es" });
- FullPageTranslationsTestUtils.switchSelectedFromLanguage("en");
+ FullPageTranslationsTestUtils.changeSelectedFromLanguage("en");
await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
expectedEventCount: 1,
@@ -45,7 +45,7 @@ add_task(async function test_translations_telemetry_switch_from_language() {
}
);
- FullPageTranslationsTestUtils.switchSelectedFromLanguage("es");
+ FullPageTranslationsTestUtils.changeSelectedFromLanguage("es");
await TestTranslationsTelemetry.assertEvent(
Glean.translationsPanel.changeFromLanguage,
@@ -56,7 +56,7 @@ add_task(async function test_translations_telemetry_switch_from_language() {
}
);
- FullPageTranslationsTestUtils.switchSelectedFromLanguage("");
+ FullPageTranslationsTestUtils.changeSelectedFromLanguage("");
await TestTranslationsTelemetry.assertEvent(
Glean.translationsPanel.changeFromLanguage,
@@ -65,7 +65,7 @@ add_task(async function test_translations_telemetry_switch_from_language() {
}
);
- FullPageTranslationsTestUtils.switchSelectedFromLanguage("en");
+ FullPageTranslationsTestUtils.changeSelectedFromLanguage("en");
await TestTranslationsTelemetry.assertEvent(
Glean.translationsPanel.changeFromLanguage,
@@ -100,7 +100,7 @@ add_task(async function test_translations_telemetry_switch_to_language() {
});
FullPageTranslationsTestUtils.assertSelectedToLanguage({ langTag: "en" });
- FullPageTranslationsTestUtils.switchSelectedToLanguage("fr");
+ FullPageTranslationsTestUtils.changeSelectedToLanguage("fr");
await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
expectedEventCount: 1,
@@ -121,7 +121,7 @@ add_task(async function test_translations_telemetry_switch_to_language() {
}
);
- FullPageTranslationsTestUtils.switchSelectedToLanguage("en");
+ FullPageTranslationsTestUtils.changeSelectedToLanguage("en");
await TestTranslationsTelemetry.assertEvent(
Glean.translationsPanel.changeToLanguage,
@@ -132,7 +132,7 @@ add_task(async function test_translations_telemetry_switch_to_language() {
}
);
- FullPageTranslationsTestUtils.switchSelectedToLanguage("");
+ FullPageTranslationsTestUtils.changeSelectedToLanguage("");
await TestTranslationsTelemetry.assertEvent(
Glean.translationsPanel.changeToLanguage,
@@ -141,7 +141,7 @@ add_task(async function test_translations_telemetry_switch_to_language() {
}
);
- FullPageTranslationsTestUtils.switchSelectedToLanguage("en");
+ FullPageTranslationsTestUtils.changeSelectedToLanguage("en");
await TestTranslationsTelemetry.assertEvent(
Glean.translationsPanel.changeToLanguage,
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js
index a6b3f71924..c3ed228ecc 100644
--- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js
@@ -19,7 +19,7 @@
add_task(
async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_no_text_selected() {
const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", false]],
});
@@ -34,8 +34,8 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
- openAtSpanishParagraph: true,
+ selectSpanishSentence: false,
+ openAtSpanishSentence: true,
expectMenuItemVisible: false,
},
"The translate-selection context menu item should be unavailable when the feature is disabled."
@@ -54,7 +54,7 @@ add_task(
add_task(
async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_text_selected() {
const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", false]],
});
@@ -69,8 +69,8 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: true,
- openAtSpanishParagraph: true,
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
expectMenuItemVisible: false,
},
"The translate-selection context menu item should be unavailable when the feature is disabled."
@@ -89,7 +89,7 @@ add_task(
add_task(
async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_clicking_a_hyperlink() {
const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", false]],
});
@@ -102,7 +102,7 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
+ selectSpanishSentence: false,
openAtSpanishHyperlink: true,
expectMenuItemVisible: false,
},
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js
index 99cff2b4ec..788ca7de63 100644
--- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js
@@ -12,7 +12,7 @@
add_task(
async function test_translate_selection_menuitem_with_text_selected_and_full_page_translations_active() {
const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
@@ -27,8 +27,8 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: true,
- openAtSpanishParagraph: true,
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
expectMenuItemVisible: true,
expectedTargetLanguage: "en",
},
@@ -52,8 +52,8 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: true,
- openAtSpanishParagraph: true,
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
expectMenuItemVisible: false,
},
"The translate-selection context menu item should be unavailable while full-page translations is active."
@@ -70,8 +70,8 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: true,
- openAtSpanishParagraph: true,
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
expectMenuItemVisible: true,
expectedTargetLanguage: "en",
},
@@ -91,7 +91,7 @@ add_task(
add_task(
async function test_translate_selection_menuitem_with_link_clicked_and_full_page_translations_active() {
const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
@@ -106,7 +106,7 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
+ selectSpanishSentence: false,
openAtSpanishHyperlink: true,
expectMenuItemVisible: true,
expectedTargetLanguage: "en",
@@ -131,7 +131,7 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
+ selectSpanishSentence: false,
openAtSpanishHyperlink: true,
expectMenuItemVisible: false,
},
@@ -149,7 +149,7 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
+ selectSpanishSentence: false,
openAtSpanishHyperlink: true,
expectMenuItemVisible: true,
expectedTargetLanguage: "en",
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js
index cefd83f046..83e836489f 100644
--- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js
@@ -12,7 +12,7 @@
add_task(
async function test_translate_selection_menuitem_translate_link_text_to_target_language() {
const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
@@ -25,7 +25,7 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
+ selectSpanishSentence: false,
openAtSpanishHyperlink: true,
expectMenuItemVisible: true,
expectedTargetLanguage: "en",
@@ -47,7 +47,7 @@ add_task(
add_task(
async function test_translate_selection_menuitem_translate_link_text_in_preferred_language() {
const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
@@ -60,7 +60,7 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
+ selectSpanishSentence: false,
openAtEnglishHyperlink: true,
expectMenuItemVisible: true,
expectedTargetLanguage: null,
@@ -82,7 +82,7 @@ add_task(
add_task(
async function test_translate_selection_menuitem_selected_text_takes_precedence_over_link_text() {
const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
@@ -95,7 +95,7 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: true,
+ selectSpanishSentence: true,
openAtEnglishHyperlink: true,
expectMenuItemVisible: true,
expectedTargetLanguage: "en",
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js
index 82e5d3ba63..5e7d482441 100644
--- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js
@@ -10,7 +10,7 @@
add_task(
async function test_translate_selection_menuitem_is_unavailable_when_no_text_is_selected() {
const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
@@ -25,8 +25,8 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
- openAtSpanishParagraph: true,
+ selectSpanishSentence: false,
+ openAtSpanishSentence: true,
expectMenuItemVisible: false,
},
"The translate-selection context menu item should be unavailable when no text is selected."
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js
index deb5911a37..6b44f2ca1f 100644
--- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js
@@ -12,7 +12,7 @@
add_task(
async function test_translate_selection_menuitem_when_selected_text_is_not_preferred_language() {
const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
@@ -27,8 +27,8 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: true,
- openAtSpanishParagraph: true,
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
expectMenuItemVisible: true,
expectedTargetLanguage: "en",
},
@@ -49,21 +49,21 @@ add_task(
add_task(
async function test_translate_selection_menuitem_when_selected_text_is_preferred_language() {
const { cleanup, runInPage } = await loadTestPage({
- page: ENGLISH_PAGE_URL,
+ page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
await FullPageTranslationsTestUtils.assertTranslationsButton(
- { button: false },
+ { button: true, circleArrows: false, locale: false, icon: true },
"The button is available."
);
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectFirstParagraph: true,
- openAtFirstParagraph: true,
+ selectEnglishSentence: true,
+ openAtEnglishSentence: true,
expectMenuItemVisible: true,
expectedTargetLanguage: null,
},
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_engine_cache.js b/browser/components/translations/tests/browser/browser_translations_select_panel_engine_cache.js
new file mode 100644
index 0000000000..a0ef58c694
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_engine_cache.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests that the SelectTranslationsPanel successfully
+ * caches the engine within the Translator for the given language pair,
+ * and if that engine is destroyed, the Translator will correctly reinitialize
+ * the engine, even for the same language pair.
+ */
+add_task(
+ async function test_select_translations_panel_translate_sentence_on_open() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSection: true,
+ openAtFrenchSection: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ expectedDownloads: 1,
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSentence: true,
+ openAtFrenchSentence: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ // No downloads because the engine is cached for this language pair.
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ info("Explicitly destroying the Translations Engine.");
+ await destroyTranslationsEngine();
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ openAtFrenchHyperlink: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ // Expect downloads again since the engine was destroyed.
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_fallback_to_doc_language.js b/browser/components/translations/tests/browser/browser_translations_select_panel_fallback_to_doc_language.js
new file mode 100644
index 0000000000..d2c6f42486
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_fallback_to_doc_language.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests the case of opening the SelectTranslationsPanel when the
+ * detected language is unsupported, but the page language is known to be a supported
+ * language. The panel should automatically fall back to the page language in an
+ * effort to combat falsely identified selections.
+ */
+add_task(
+ async function test_select_translations_panel_translate_sentence_on_open() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ // Do not include French.
+ { fromLang: "es", toLang: "en" },
+ { fromLang: "en", toLang: "es" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSentence: true,
+ openAtFrenchSentence: true,
+ // French is not supported, but the page is in Spanish, so expect Spanish.
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_language_selectors.js b/browser/components/translations/tests/browser/browser_translations_select_panel_language_selectors.js
deleted file mode 100644
index 1dcc76450f..0000000000
--- a/browser/components/translations/tests/browser/browser_translations_select_panel_language_selectors.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-add_task(
- async function test_select_translations_panel_open_spanish_language_selectors() {
- const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
- languagePairs: LANGUAGE_PAIRS,
- prefs: [["browser.translations.select.enable", true]],
- });
-
- await SelectTranslationsTestUtils.openPanel(runInPage, {
- selectSpanishParagraph: true,
- openAtSpanishParagraph: true,
- expectedTargetLanguage: "en",
- onOpenPanel: SelectTranslationsTestUtils.assertPanelViewDefault,
- });
-
- SelectTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "es" });
- SelectTranslationsTestUtils.assertSelectedToLanguage({ langTag: "en" });
-
- await SelectTranslationsTestUtils.clickDoneButton();
-
- await cleanup();
- }
-);
-
-add_task(
- async function test_select_translations_panel_open_english_language_selectors() {
- const { cleanup, runInPage } = await loadTestPage({
- page: ENGLISH_PAGE_URL,
- languagePairs: LANGUAGE_PAIRS,
- prefs: [["browser.translations.select.enable", true]],
- });
-
- await SelectTranslationsTestUtils.openPanel(runInPage, {
- selectFirstParagraph: true,
- openAtFirstParagraph: true,
- expectedTargetLanguage: "en",
- onOpenPanel: SelectTranslationsTestUtils.assertPanelViewDefault,
- });
-
- SelectTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "en" });
- SelectTranslationsTestUtils.assertSelectedToLanguage({
- l10nId: "translations-panel-choose-language",
- });
-
- await SelectTranslationsTestUtils.clickDoneButton();
-
- await cleanup();
- }
-);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_mainview_ui.js b/browser/components/translations/tests/browser/browser_translations_select_panel_mainview_ui.js
deleted file mode 100644
index 79d21e57d0..0000000000
--- a/browser/components/translations/tests/browser/browser_translations_select_panel_mainview_ui.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-/**
- * This test case verifies the visibility and initial state of UI elements within the
- * Select Translations Panel's main-view UI.
- */
-add_task(
- async function test_select_translations_panel_mainview_ui_element_visibility() {
- const { cleanup, runInPage } = await loadTestPage({
- page: SPANISH_PAGE_URL,
- languagePairs: LANGUAGE_PAIRS,
- prefs: [["browser.translations.select.enable", true]],
- });
-
- await FullPageTranslationsTestUtils.assertTranslationsButton(
- { button: true, circleArrows: false, locale: false, icon: true },
- "The button is available."
- );
-
- await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
-
- await SelectTranslationsTestUtils.openPanel(runInPage, {
- selectSpanishParagraph: true,
- openAtSpanishParagraph: true,
- expectedTargetLanguage: "es",
- onOpenPanel: SelectTranslationsTestUtils.assertPanelViewDefault,
- });
-
- await SelectTranslationsTestUtils.clickDoneButton();
-
- await cleanup();
- }
-);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js b/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js
new file mode 100644
index 0000000000..d5a1096e70
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the select translations panel's functionality when opened with an unsupported
+ * from-language, ensuring it opens with the correct view with no from-language selected.
+ */
+add_task(
+ async function test_select_translations_panel_open_no_selected_from_lang() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
+ expectedFromLanguage: null,
+ expectedToLanguage: "en",
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the select translations panel's functionality when opened with an undetermined
+ * to-language, ensuring it opens with the correct view with no to-language selected.
+ */
+add_task(
+ async function test_select_translations_panel_open_no_selected_to_lang() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectEnglishSentence: true,
+ openAtEnglishSentence: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: null,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_directly.js
new file mode 100644
index 0000000000..fff0326f75
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_directly.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of triggering a translation by directly switching
+ * the from-language when the panel is already in the "translated" state from a previous
+ * language pair.
+ */
+add_task(
+ async function test_select_translations_panel_retranslate_on_change_from_language_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSentence: true,
+ openAtFrenchSentence: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
+ openDropdownMenu: false,
+ pivotTranslation: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of triggering a translation by directly switching
+ * the to-language when the panel is already in the "translated" state from a previous
+ * language pair.
+ */
+add_task(
+ async function test_select_translations_panel_retranslate_on_change_to_language_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSentence: true,
+ openAtFrenchSentence: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], {
+ openDropdownMenu: false,
+ pivotTranslation: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js
new file mode 100644
index 0000000000..16f2cb39f7
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of triggering a translation by switching the
+ * from-language by opening the language dropdown menu when the panel is already in
+ * the "translated" state from a previous language pair.
+ */
+add_task(
+ async function test_select_translations_panel_retranslate_on_change_from_language_via_popup() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ openAtSpanishHyperlink: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], {
+ openDropdownMenu: true,
+ pivotTranslation: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of triggering a translation by switching the
+ * to-language by opening the language dropdown menu when the panel is already in
+ * the "translated" state from a previous language pair.
+ */
+add_task(
+ async function test_select_translations_panel_retranslate_on_change_to_language_via_popup() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ openAtSpanishHyperlink: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], {
+ openDropdownMenu: true,
+ pivotTranslation: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_directly.js
new file mode 100644
index 0000000000..f45326800f
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_directly.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of directly switching the from-language to the same
+ * from-language that is already selected, ensuring no change occurs to the translation state,
+ * and that no re-translation is triggered.
+ */
+add_task(
+ async function test_select_translations_panel_select_current_from_language_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
+ openDropdownMenu: false,
+ // No downloads are resolved, because no re-translation is triggered.
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of directly switching the to-language to the same
+ * to-language that is already selected, ensuring no change occurs to the translation state,
+ * and that no re-translation is triggered.
+ */
+add_task(
+ async function test_select_translations_panel_select_current_from_language_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ openAtFrenchHyperlink: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], {
+ openDropdownMenu: false,
+ // No downloads are resolved, because no re-translation is triggered.
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_from_dropdown_menu.js
new file mode 100644
index 0000000000..04aa731cf2
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_from_dropdown_menu.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of directly switching the from-language to the same
+ * from-language that is already selected by opening the language dropdown menu,
+ * ensuring no change occurs to the translation state, and that no re-translation is triggered.
+ */
+add_task(
+ async function test_select_translations_panel_select_current_from_language_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
+ openDropdownMenu: true,
+ // No downloads are resolved, because no re-translation is triggered.
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of directly switching the to-language to the same
+ * to-language that is already selected by opening the language dropdown menu,
+ * ensuring no change occurs to the translation state, and that no re-translation is triggered.
+ */
+add_task(
+ async function test_select_translations_panel_select_current_from_language_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ openAtFrenchHyperlink: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], {
+ openDropdownMenu: true,
+ // No downloads are resolved, because no re-translation is triggered.
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js
new file mode 100644
index 0000000000..95feac6708
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of switching the from-language to the same value
+ * that is currently selected in the to-language, effectively stealing the to-language's
+ * value, leaving it unselected and focused.
+ */
+add_task(
+ async function test_select_translations_panel_select_same_from_language_directly() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: null,
+ expectedToLanguage: "en",
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["en"], {
+ openDropdownMenu: false,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewNoFromToSelected,
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of switching the to-language to the same value
+ * that is currently selected in the from-language, effectively stealing the from-language's
+ * value, leaving it unselected and focused.
+ */
+add_task(
+ async function test_select_translations_panel_select_same_to_language_directly() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectEnglishSection: true,
+ openAtEnglishSection: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: null,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], {
+ openDropdownMenu: false,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js
new file mode 100644
index 0000000000..5c27be411f
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of switching the from-language to the same value
+ * that is currently selected in the to-language by opening the language dropdown menu,
+ * effectively stealing the to-language's value, leaving it unselected and focused.
+ */
+add_task(
+ async function test_select_translations_panel_select_same_from_language_via_popup() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: null,
+ expectedToLanguage: "en",
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["en"], {
+ openDropdownMenu: true,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewNoFromToSelected,
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of switching the to-language to the same value
+ * that is currently selected in the from-language by opening the language dropdown menu,
+ * effectively stealing the from-language's value, leaving it unselected and focused.
+ */
+add_task(
+ async function test_select_translations_panel_select_same_to_language_via_popup() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectEnglishSection: true,
+ openAtEnglishSection: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: null,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], {
+ openDropdownMenu: true,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js
new file mode 100644
index 0000000000..64d067d1f4
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of triggering a translation by directly switching
+ * the from-language to a valid selection when the panel is in the "idle" state without
+ * valid language pair.
+ */
+add_task(
+ async function test_select_translations_panel_translate_on_change_from_language_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: null,
+ expectedToLanguage: "en",
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], {
+ openDropdownMenu: false,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of triggering a translation by directly switching
+ * the to-language to a valid selection when the panel is in the "idle" state without
+ * valid language pair.
+ */
+add_task(
+ async function test_select_translations_panel_translate_on_change_to_language_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectEnglishSection: true,
+ openAtEnglishSection: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: null,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], {
+ openDropdownMenu: false,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js
new file mode 100644
index 0000000000..0cd205d721
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of triggering a translation by switching the
+ * from-language to a valid selection by opening the language dropdown when the panel
+ * is in the "idle" state without valid language pair.
+ */
+add_task(
+ async function test_select_translations_panel_translate_on_change_from_language() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: null,
+ expectedToLanguage: "en",
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of triggering a translation by switching the
+ * to-language to a valid selection by opening the language dropdown when the panel
+ * is in the "idle" state without valid language pair.
+ */
+add_task(
+ async function test_select_translations_panel_translate_on_change_to_language() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectEnglishSection: true,
+ openAtEnglishSection: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: null,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js
new file mode 100644
index 0000000000..b3c02a96f6
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of directly changing the from-language in rapid succession,
+ * ensuring that any triggered translations are resolved/dropped in order, and that the final translated
+ * state matches the final selected language.
+ */
+add_task(
+ async function test_select_translations_panel_translate_on_change_from_language_multiple_times_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fa", toLang: "en" },
+ { fromLang: "en", toLang: "fa" },
+ { fromLang: "fi", toLang: "en" },
+ { fromLang: "en", toLang: "fi" },
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ { fromLang: "sl", toLang: "en" },
+ { fromLang: "en", toLang: "sl" },
+ { fromLang: "uk", toLang: "en" },
+ { fromLang: "en", toLang: "uk" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
+ expectedFromLanguage: null,
+ expectedToLanguage: "en",
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(
+ ["fa", "fi", "fr", "sl", "uk"],
+ {
+ openDropdownMenu: false,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ }
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of directly changing the to-language in rapid succession,
+ * ensuring that any triggered translations are resolved/dropped in order, and that the final translated
+ * state matches the final selected language.
+ */
+add_task(
+ async function test_select_translations_panel_translate_on_change_to_language_multiple_times_directly() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ { fromLang: "es", toLang: "en" },
+ { fromLang: "en", toLang: "es" },
+ { fromLang: "fa", toLang: "en" },
+ { fromLang: "en", toLang: "fa" },
+ { fromLang: "fi", toLang: "en" },
+ { fromLang: "en", toLang: "fi" },
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ { fromLang: "sl", toLang: "en" },
+ { fromLang: "en", toLang: "sl" },
+ { fromLang: "uk", toLang: "en" },
+ { fromLang: "en", toLang: "uk" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ openAtEnglishHyperlink: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: null,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(
+ ["es", "fa", "fi", "fr", "sl", "uk", "fa"],
+ {
+ openDropdownMenu: false,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ }
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js
new file mode 100644
index 0000000000..50c877cfbc
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of directly changing the from-language in rapid succession
+ * by opening the language dropdown menu, ensuring that any triggered translations are resolved/dropped
+ * in order, and that the final translated state matches the final selected language.
+ */
+add_task(
+ async function test_select_translations_panel_translate_on_change_from_language_multiple_times_via_popup() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fa", toLang: "en" },
+ { fromLang: "en", toLang: "fa" },
+ { fromLang: "fi", toLang: "en" },
+ { fromLang: "en", toLang: "fi" },
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ { fromLang: "sl", toLang: "en" },
+ { fromLang: "en", toLang: "sl" },
+ { fromLang: "uk", toLang: "en" },
+ { fromLang: "en", toLang: "uk" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
+ expectedFromLanguage: null,
+ expectedToLanguage: "en",
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(
+ ["fa", "fi", "fr", "sl", "uk"],
+ {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ }
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of directly changing the to-language in rapid succession
+ * by opening the language dropdown menu, ensuring that any triggered translations are resolved/dropped
+ * in order, and that the final translated state matches the final selected language.
+ */
+add_task(
+ async function test_select_translations_panel_translate_on_change_to_language_multiple_times_via_popup() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: [
+ { fromLang: "es", toLang: "en" },
+ { fromLang: "en", toLang: "es" },
+ { fromLang: "fa", toLang: "en" },
+ { fromLang: "en", toLang: "fa" },
+ { fromLang: "fi", toLang: "en" },
+ { fromLang: "en", toLang: "fi" },
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ { fromLang: "sl", toLang: "en" },
+ { fromLang: "en", toLang: "sl" },
+ { fromLang: "uk", toLang: "en" },
+ { fromLang: "en", toLang: "uk" },
+ ],
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectEnglishSentence: true,
+ openAtEnglishSentence: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: null,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(
+ ["es", "fa", "fi", "fr", "sl", "uk"],
+ {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ }
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js
new file mode 100644
index 0000000000..7c7d6d88c9
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests the case of opening the SelectTranslationsPanel to a valid
+ * language pair from a short selection of text, which should trigger a translation
+ * on panel open.
+ */
+add_task(
+ async function test_select_translations_panel_translate_sentence_on_open() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSentence: true,
+ openAtFrenchSentence: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case tests the case of opening the SelectTranslationsPanel to a valid
+ * language pair from hyperlink text, which should trigger a translation on panel open.
+ */
+add_task(
+ async function test_select_translations_panel_translate_link_text_on_open() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ openAtSpanishHyperlink: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case tests the case of opening the SelectTranslationsPanel to a valid
+ * language pair from a long selection of text, which should trigger a translation
+ * on panel open.
+ */
+add_task(
+ async function test_select_translations_panel_translate_long_text_on_open() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSection: true,
+ openAtFrenchSection: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/head.js b/browser/components/translations/tests/browser/head.js
index 200ed08719..454de9146b 100644
--- a/browser/components/translations/tests/browser/head.js
+++ b/browser/components/translations/tests/browser/head.js
@@ -18,7 +18,7 @@ async function addTab(url) {
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
url,
- true // Wait for laod
+ true // Wait for load
);
return {
tab,
@@ -65,7 +65,6 @@ function click(element, message) {
*/
function getAllByL10nId(l10nId, doc = document) {
const elements = doc.querySelectorAll(`[data-l10n-id="${l10nId}"]`);
- console.log(doc);
if (elements.length === 0) {
throw new Error("Could not find the element by l10n id: " + l10nId);
}
@@ -285,6 +284,19 @@ async function toggleReaderMode() {
*/
class SharedTranslationsTestUtils {
/**
+ * Asserts that the specified element currently has focus.
+ *
+ * @param {Element} element - The element to check for focus.
+ */
+ static _assertHasFocus(element) {
+ is(
+ document.activeElement,
+ element,
+ `The element '${element.id}' should have focus.`
+ );
+ }
+
+ /**
* Asserts that the mainViewId of the panel matches the given string.
*
* @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
@@ -300,53 +312,30 @@ class SharedTranslationsTestUtils {
}
/**
- * Asserts that the selected from-language matches the provided arguments.
+ * Asserts that the selected language in the menu matches the langTag or l10nId.
*
- * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
- * - The UI component or panel whose selected from-language is being asserted.
- * @param {object} options - An object containing assertion parameters.
- * @param {string} [options.langTag] - A BCP-47 language tag.
- * @param {string} [options.l10nId] - A localization identifier.
+ * @param {Element} menuList - The menu list element to check.
+ * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against.
+ * @param {string} [options.langTag] - The BCP-47 language tag to match.
+ * @param {string} [options.l10nId] - The localization Id to match.
*/
- static _assertSelectedFromLanguage(panel, { langTag, l10nId }) {
- const { fromMenuList } = panel.elements;
- is(
- fromMenuList.value,
- langTag,
- "Expected selected from-language to match the given language tag"
+ static _assertSelectedLanguage(menuList, { langTag, l10nId }) {
+ ok(
+ menuList.label,
+ `The label for the menulist ${menuList.id} should not be empty.`
);
- if (l10nId) {
- is(
- fromMenuList.getAttribute("data-l10n-id"),
- l10nId,
- "Expected selected from-language to match the given l10n id"
- );
- }
- }
-
- /**
- * Asserts that the selected to-language matches the provided arguments.
- *
- * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
- * - The UI component or panel whose selected from-language is being asserted.
- * @param {object} options - An object containing assertion parameters.
- * @param {string} [options.langTag] - A BCP-47 language tag.
- * @param {string} [options.l10nId] - A localization identifier.
- */
- static _assertSelectedToLanguage(panel, { langTag, l10nId }) {
- const { toMenuList } = panel.elements;
if (langTag) {
is(
- toMenuList.value,
+ menuList.value,
langTag,
- "Expected selected to-language to match the given language tag"
+ `Expected ${menuList.id} selection to match '${langTag}'`
);
}
if (l10nId) {
is(
- toMenuList.getAttribute("data-l10n-id"),
+ menuList.getAttribute("data-l10n-id"),
l10nId,
- "Expected selected to-language to match the given l10n id"
+ `Expected ${menuList.id} l10nId to match '${l10nId}'`
);
}
}
@@ -391,6 +380,7 @@ class SharedTranslationsTestUtils {
* This is often used to trigger the event on the expected element.
* @param {Function|null} [postEventAssertion=null] - An optional callback function to execute after
* the event has occurred.
+ * @param {ChromeWindow} [win]
* @throws Throws if the element with the specified `elementId` does not exist.
* @returns {Promise<void>}
*/
@@ -398,18 +388,21 @@ class SharedTranslationsTestUtils {
elementId,
eventName,
callback,
- postEventAssertion = null
+ postEventAssertion = null,
+ win = window
) {
- const element = document.getElementById(elementId);
+ const element = win.document.getElementById(elementId);
if (!element) {
- throw new Error("Unable to find the translations panel element.");
+ throw new Error(
+ `Unable to find the ${elementId} element in the document.`
+ );
}
const promise = BrowserTestUtils.waitForEvent(element, eventName);
await callback();
- info("Waiting for the translations panel popup to be shown");
+ info(`Waiting for the ${elementId} ${eventName} event`);
await promise;
if (postEventAssertion) {
- postEventAssertion();
+ await postEventAssertion();
}
// Wait a single tick on the event loop.
await new Promise(resolve => setTimeout(resolve, 0));
@@ -568,10 +561,12 @@ class FullPageTranslationsTestUtils {
*
* @param {string} fromLanguage - The BCP-47 language tag being translated from.
* @param {string} toLanguage - The BCP-47 language tag being translated into.
+ * @param {ChromeWindow} win
*/
- static async #assertLangTagIsShownOnTranslationsButton(
+ static async assertLangTagIsShownOnTranslationsButton(
fromLanguage,
- toLanguage
+ toLanguage,
+ win = window
) {
info(
`Ensuring that the translations button displays the language tag "${toLanguage}"`
@@ -579,7 +574,8 @@ class FullPageTranslationsTestUtils {
const { button, locale } =
await FullPageTranslationsTestUtils.assertTranslationsButton(
{ button: true, circleArrows: false, locale: true, icon: true },
- "The icon presents the locale."
+ "The icon presents the locale.",
+ win
);
is(
locale.innerText,
@@ -605,12 +601,14 @@ class FullPageTranslationsTestUtils {
* @param {string} toLanguage - The BCP-47 language tag being translated into.
* @param {Function} runInPage - Allows running a closure in the content page.
* @param {string} message - An optional message to log to info.
+ * @param {ChromeWindow} [win]
*/
static async assertPageIsTranslated(
fromLanguage,
toLanguage,
runInPage,
- message = null
+ message = null,
+ win = window
) {
if (message) {
info(message);
@@ -625,9 +623,10 @@ class FullPageTranslationsTestUtils {
);
};
await runInPage(callback, { fromLang: fromLanguage, toLang: toLanguage });
- await FullPageTranslationsTestUtils.#assertLangTagIsShownOnTranslationsButton(
+ await FullPageTranslationsTestUtils.assertLangTagIsShownOnTranslationsButton(
fromLanguage,
- toLanguage
+ toLanguage,
+ win
);
}
@@ -668,6 +667,9 @@ class FullPageTranslationsTestUtils {
changeSourceLanguageButton: false,
dismissErrorButton: false,
error: false,
+ errorMessage: false,
+ errorMessageHint: false,
+ errorHintAction: false,
fromMenuList: false,
fromLabel: false,
header: false,
@@ -744,6 +746,34 @@ class FullPageTranslationsTestUtils {
}
/**
+ * Asserts that panel element visibility matches the initialization-failure view.
+ */
+ static assertPanelViewInitFailure() {
+ info("Checking that the panel shows the default view");
+ const { translateButton } = FullPageTranslationsPanel.elements;
+ FullPageTranslationsTestUtils.#assertPanelMainViewId(
+ "full-page-translations-panel-view-default"
+ );
+ FullPageTranslationsTestUtils.#assertPanelElementVisibility({
+ cancelButton: true,
+ error: true,
+ errorMessage: true,
+ errorMessageHint: true,
+ errorHintAction: true,
+ header: true,
+ translateButton: true,
+ });
+ is(
+ translateButton.disabled,
+ true,
+ "The translate button should be disabled."
+ );
+ FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
+ "translations-panel-header"
+ );
+ }
+
+ /**
* Asserts that panel element visibility matches the panel error view.
*/
static assertPanelViewError() {
@@ -753,6 +783,7 @@ class FullPageTranslationsTestUtils {
);
FullPageTranslationsTestUtils.#assertPanelElementVisibility({
error: true,
+ errorMessage: true,
...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
});
FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
@@ -854,25 +885,31 @@ class FullPageTranslationsTestUtils {
/**
* Asserts that the selected from-language matches the provided language tag.
*
- * @param {string} langTag - A BCP-47 language tag.
+ * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against.
+ * @param {string} [options.langTag] - The BCP-47 language tag to match.
+ * @param {string} [options.l10nId] - The localization Id to match.
*/
static assertSelectedFromLanguage({ langTag, l10nId }) {
- SharedTranslationsTestUtils._assertSelectedFromLanguage(
- FullPageTranslationsPanel,
- { langTag, l10nId }
- );
+ const { fromMenuList } = FullPageTranslationsPanel.elements;
+ SharedTranslationsTestUtils._assertSelectedLanguage(fromMenuList, {
+ langTag,
+ l10nId,
+ });
}
/**
* Asserts that the selected to-language matches the provided language tag.
*
- * @param {string} langTag - A BCP-47 language tag.
+ * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against.
+ * @param {string} [options.langTag] - The BCP-47 language tag to match.
+ * @param {string} [options.l10nId] - The localization Id to match.
*/
static assertSelectedToLanguage({ langTag, l10nId }) {
- SharedTranslationsTestUtils._assertSelectedToLanguage(
- FullPageTranslationsPanel,
- { langTag, l10nId }
- );
+ const { toMenuList } = FullPageTranslationsPanel.elements;
+ SharedTranslationsTestUtils._assertSelectedLanguage(toMenuList, {
+ langTag,
+ l10nId,
+ });
}
/**
@@ -880,16 +917,21 @@ class FullPageTranslationsTestUtils {
*
* @param {Record<string, boolean>} visibleAssertions
* @param {string} message The message for the assertion.
+ * @param {ChromeWindow} [win]
* @returns {HTMLElement}
*/
- static async assertTranslationsButton(visibleAssertions, message) {
+ static async assertTranslationsButton(
+ visibleAssertions,
+ message,
+ win = window
+ ) {
const elements = {
- button: document.getElementById("translations-button"),
- icon: document.getElementById("translations-button-icon"),
- circleArrows: document.getElementById(
+ button: win.document.getElementById("translations-button"),
+ icon: win.document.getElementById("translations-button-icon"),
+ circleArrows: win.document.getElementById(
"translations-button-circle-arrows"
),
- locale: document.getElementById("translations-button-locale"),
+ locale: win.document.getElementById("translations-button-locale"),
};
for (const [name, element] of Object.entries(elements)) {
@@ -1083,25 +1125,31 @@ class FullPageTranslationsTestUtils {
* @param {boolean} config.pivotTranslation
* - True if the expected translation is a pivot translation, otherwise false.
* Affects the number of expected downloads.
+ * @param {ChromeWindow} [config.win]
+ * - An optional ChromeWindow, for multi-window tests.
*/
static async clickTranslateButton({
downloadHandler = null,
pivotTranslation = false,
+ win = window,
} = {}) {
logAction();
- const { translateButton } = FullPageTranslationsPanel.elements;
+ const { translateButton } = win.FullPageTranslationsPanel.elements;
assertVisibility({ visible: { translateButton } });
await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
"popuphidden",
() => {
click(translateButton);
- }
+ },
+ null /* postEventAssertion */,
+ win
);
if (downloadHandler) {
await FullPageTranslationsTestUtils.assertTranslationsButton(
{ button: true, circleArrows: true, locale: false, icon: true },
- "The icon presents the loading indicator."
+ "The icon presents the loading indicator.",
+ win
);
await downloadHandler(pivotTranslation ? 2 : 1);
}
@@ -1117,21 +1165,26 @@ class FullPageTranslationsTestUtils {
* - Open the panel from the app menu. If false, uses the translations button.
* @param {boolean} config.openWithKeyboard
* - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
+ * @param {ChromeWindow} [config.win]
+ * - An optional window for multi-window tests.
*/
static async openPanel({
onOpenPanel = null,
openFromAppMenu = false,
openWithKeyboard = false,
+ win = window,
}) {
logAction();
- await closeAllOpenPanelsAndMenus();
+ await closeAllOpenPanelsAndMenus(win);
if (openFromAppMenu) {
await FullPageTranslationsTestUtils.#openPanelViaAppMenu({
+ win,
onOpenPanel,
openWithKeyboard,
});
} else {
await FullPageTranslationsTestUtils.#openPanelViaTranslationsButton({
+ win,
onOpenPanel,
openWithKeyboard,
});
@@ -1146,21 +1199,26 @@ class FullPageTranslationsTestUtils {
* - A function to run as soon as the panel opens.
* @param {boolean} config.openWithKeyboard
* - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
+ * @param {ChromeWindow} [config.win]
*/
static async #openPanelViaAppMenu({
onOpenPanel = null,
openWithKeyboard = false,
+ win = window,
}) {
logAction();
- const appMenuButton = getById("PanelUI-menu-button");
+ const appMenuButton = getById("PanelUI-menu-button", win.document);
if (openWithKeyboard) {
hitEnterKey(appMenuButton, "Opening the app-menu button with keyboard");
} else {
click(appMenuButton, "Opening the app-menu button");
}
- await BrowserTestUtils.waitForEvent(window.PanelUI.mainView, "ViewShown");
+ await BrowserTestUtils.waitForEvent(win.PanelUI.mainView, "ViewShown");
- const translateSiteButton = getById("appMenu-translate-button");
+ const translateSiteButton = getById(
+ "appMenu-translate-button",
+ win.document
+ );
is(
translateSiteButton.disabled,
@@ -1189,16 +1247,19 @@ class FullPageTranslationsTestUtils {
* - A function to run as soon as the panel opens.
* @param {boolean} config.openWithKeyboard
* - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
+ * @param {ChromeWindow} [config.win]
*/
static async #openPanelViaTranslationsButton({
onOpenPanel = null,
openWithKeyboard = false,
+ win = window,
}) {
logAction();
const { button } =
await FullPageTranslationsTestUtils.assertTranslationsButton(
{ button: true },
- "The translations button is visible."
+ "The translations button is visible.",
+ win
);
await FullPageTranslationsTestUtils.waitForPanelPopupEvent(
"popupshown",
@@ -1209,7 +1270,8 @@ class FullPageTranslationsTestUtils {
click(button, "Opening the popup");
}
},
- onOpenPanel
+ onOpenPanel,
+ win
);
}
@@ -1242,7 +1304,7 @@ class FullPageTranslationsTestUtils {
*
* @param {string} langTag - A BCP-47 language tag.
*/
- static switchSelectedFromLanguage(langTag) {
+ static changeSelectedFromLanguage(langTag) {
logAction(langTag);
const { fromMenuList } = FullPageTranslationsPanel.elements;
fromMenuList.value = langTag;
@@ -1254,7 +1316,7 @@ class FullPageTranslationsTestUtils {
*
* @param {string} langTag - A BCP-47 language tag.
*/
- static switchSelectedToLanguage(langTag) {
+ static changeSelectedToLanguage(langTag) {
logAction(langTag);
const { toMenuList } = FullPageTranslationsPanel.elements;
toMenuList.value = langTag;
@@ -1270,20 +1332,23 @@ class FullPageTranslationsTestUtils {
* @param {Function} callback
* @param {Function} postEventAssertion
* An optional assertion to be made immediately after the event occurs.
+ * @param {ChromeWindow} [win]
* @returns {Promise<void>}
*/
static async waitForPanelPopupEvent(
eventName,
callback,
- postEventAssertion = null
+ postEventAssertion = null,
+ win = window
) {
// De-lazify the panel elements.
- FullPageTranslationsPanel.elements;
+ win.FullPageTranslationsPanel.elements;
await SharedTranslationsTestUtils._waitForPopupEvent(
"full-page-translations-panel",
eventName,
callback,
- postEventAssertion
+ postEventAssertion,
+ win
);
}
}
@@ -1297,31 +1362,47 @@ class SelectTranslationsTestUtils {
*
* @param {Function} runInPage - A content-exposed function to run within the context of the page.
* @param {object} options - Options for how to open the context menu and what properties to assert about the translate-selection item.
- * @param {boolean} options.selectFirstParagraph - Selects the first paragraph before opening the context menu.
- * @param {boolean} options.selectSpanishParagraph - Selects the Spanish paragraph before opening the context menu.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {boolean} options.expectMenuItemIsVisible - Whether the translate-selection item is expected to be visible.
- * Does not assert visibility if left undefined.
- * @param {string} options.expectedTargetLanguage - The target language for translation.
- * @param {boolean} options.openAtFirstParagraph - Opens the context menu at the first paragraph in the test page.
- * @param {boolean} options.openAtSpanishParagraph - Opens the context menu at the Spanish paragraph in the test page.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at the English hyperlink in the test page.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at the Spanish hyperlink in the test page.
- * This is only available in SPANISH_TEST_PAGE.
+ *
+ * The following options will only work when testing SELECT_TEST_PAGE_URL.
+ *
+ * @param {boolean} options.expectMenuItemVisible - Whether the select-translations menu item should be present in the context menu.
+ * @param {boolean} options.expectedTargetLanguage - The expected target language to be shown in the context menu.
+ * @param {boolean} options.selectFrenchSection - Selects the section of French text.
+ * @param {boolean} options.selectEnglishSection - Selects the section of English text.
+ * @param {boolean} options.selectSpanishSection - Selects the section of Spanish text.
+ * @param {boolean} options.selectFrenchSentence - Selects a French sentence.
+ * @param {boolean} options.selectEnglishSentence - Selects an English sentence.
+ * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence.
+ * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text.
+ * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text.
+ * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text.
+ * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence.
+ * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence.
+ * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence.
+ * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text.
+ * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text.
+ * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text.
* @param {string} [message] - A message to log to info.
* @throws Throws an error if the properties of the translate-selection item do not match the expected options.
*/
static async assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectFirstParagraph,
- selectSpanishParagraph,
- expectMenuItemIsVisible,
+ expectMenuItemVisible,
expectedTargetLanguage,
- openAtFirstParagraph,
- openAtSpanishParagraph,
+ selectFrenchSection,
+ selectEnglishSection,
+ selectSpanishSection,
+ selectFrenchSentence,
+ selectEnglishSentence,
+ selectSpanishSentence,
+ openAtFrenchSection,
+ openAtEnglishSection,
+ openAtSpanishSection,
+ openAtFrenchSentence,
+ openAtEnglishSentence,
+ openAtSpanishSentence,
+ openAtFrenchHyperlink,
openAtEnglishHyperlink,
openAtSpanishHyperlink,
},
@@ -1336,10 +1417,21 @@ class SelectTranslationsTestUtils {
await closeAllOpenPanelsAndMenus();
await SelectTranslationsTestUtils.openContextMenu(runInPage, {
- selectFirstParagraph,
- selectSpanishParagraph,
- openAtFirstParagraph,
- openAtSpanishParagraph,
+ expectMenuItemVisible,
+ expectedTargetLanguage,
+ selectFrenchSection,
+ selectEnglishSection,
+ selectSpanishSection,
+ selectFrenchSentence,
+ selectEnglishSentence,
+ selectSpanishSentence,
+ openAtFrenchSection,
+ openAtEnglishSection,
+ openAtSpanishSection,
+ openAtFrenchSentence,
+ openAtEnglishSentence,
+ openAtSpanishSentence,
+ openAtFrenchHyperlink,
openAtEnglishHyperlink,
openAtSpanishHyperlink,
});
@@ -1349,16 +1441,21 @@ class SelectTranslationsTestUtils {
/* ensureIsVisible */ false
);
- if (expectMenuItemIsVisible !== undefined) {
- const visibility = expectMenuItemIsVisible ? "visible" : "hidden";
- assertVisibility({ [visibility]: menuItem });
+ if (expectMenuItemVisible !== undefined) {
+ const visibility = expectMenuItemVisible ? "visible" : "hidden";
+ assertVisibility({ [visibility]: { menuItem } });
}
- if (expectMenuItemIsVisible === true) {
+ if (expectMenuItemVisible === true) {
if (expectedTargetLanguage) {
// Target language expected, check for the data-l10n-id with a `{$language}` argument.
const expectedL10nId =
- selectFirstParagraph === true || selectSpanishParagraph === true
+ selectFrenchSection ||
+ selectEnglishSection ||
+ selectSpanishSection ||
+ selectFrenchSentence ||
+ selectEnglishSentence ||
+ selectSpanishSentence
? "main-context-menu-translate-selection-to-language"
: "main-context-menu-translate-link-text-to-language";
await waitForCondition(
@@ -1381,7 +1478,12 @@ class SelectTranslationsTestUtils {
} else {
// No target language expected, check for the data-l10n-id that has no `{$language}` argument.
const expectedL10nId =
- selectFirstParagraph === true || selectSpanishParagraph === true
+ selectFrenchSection ||
+ selectEnglishSection ||
+ selectSpanishSection ||
+ selectFrenchSentence ||
+ selectEnglishSentence ||
+ selectSpanishSentence
? "main-context-menu-translate-selection"
: "main-context-menu-translate-link-text";
await waitForCondition(
@@ -1399,6 +1501,21 @@ class SelectTranslationsTestUtils {
}
/**
+ * Elements that should always be visible in the SelectTranslationsPanel.
+ */
+ static #alwaysPresentElements = {
+ betaIcon: true,
+ copyButton: true,
+ doneButton: true,
+ fromLabel: true,
+ fromMenuList: true,
+ header: true,
+ toLabel: true,
+ toMenuList: true,
+ textArea: true,
+ };
+
+ /**
* Asserts that for each provided expectation, the visible state of the corresponding
* element in FullPageTranslationsPanel.elements both exists and matches the visibility expectation.
*
@@ -1409,16 +1526,7 @@ class SelectTranslationsTestUtils {
SharedTranslationsTestUtils._assertPanelElementVisibility(
SelectTranslationsPanel.elements,
{
- betaIcon: false,
- copyButton: false,
- doneButton: false,
- fromLabel: false,
- fromMenuList: false,
- header: false,
- textArea: false,
- toLabel: false,
- toMenuList: false,
- translateFullPageButton: false,
+ ...SelectTranslationsTestUtils.#alwaysPresentElements,
// Overwrite any of the above defaults with the passed in expectations.
...expectations,
}
@@ -1426,49 +1534,255 @@ class SelectTranslationsTestUtils {
}
/**
- * Asserts that the mainViewId of the panel matches the given string.
- *
- * @param {string} expectedId
+ * Asserts that the SelectTranslationsPanel UI matches the expected
+ * state when the panel has completed its translation.
*/
- static #assertPanelMainViewId(expectedId) {
- SharedTranslationsTestUtils._assertPanelMainViewId(
- SelectTranslationsPanel,
- expectedId
+ static assertPanelViewTranslated() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ ok(
+ !textArea.classList.contains("translating"),
+ "The textarea should not have the translating class."
+ );
+ SelectTranslationsTestUtils.#assertPanelElementVisibility({
+ ...SelectTranslationsTestUtils.#alwaysPresentElements,
+ });
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
+ textArea: true,
+ copyButton: true,
+ translateFullPageButton: true,
+ });
+ SelectTranslationsTestUtils.#assertPanelHasTranslatedText();
+ SelectTranslationsTestUtils.#assertPanelTextAreaHeight();
+ SelectTranslationsTestUtils.#assertPanelTextAreaOverflow();
+ }
+
+ static #assertPanelTextAreaDirection(langTag = null) {
+ const expectedTextDirection = langTag
+ ? Services.intl.getScriptDirection(langTag)
+ : null;
+ const { textArea } = SelectTranslationsPanel.elements;
+ const actualTextDirection = textArea.getAttribute("dir");
+
+ is(
+ actualTextDirection,
+ expectedTextDirection,
+ `The text direction should be ${expectedTextDirection}`
);
}
/**
- * Asserts that panel element visibility matches the default panel view.
+ * Asserts that the SelectTranslationsPanel translated text area is
+ * both scrollable and scrolled to the top.
*/
- static assertPanelViewDefault() {
- info("Checking that the select-translations panel shows the default view");
- SelectTranslationsTestUtils.#assertPanelMainViewId(
- "select-translations-panel-view-default"
+ static #assertPanelTextAreaOverflow() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ is(
+ textArea.style.overflow,
+ "auto",
+ "The translated-text area should be scrollable."
+ );
+ if (textArea.scrollHeight > textArea.clientHeight) {
+ is(
+ textArea.scrollTop,
+ 0,
+ "The translated-text area should be scrolled to the top."
+ );
+ }
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel translated text area is
+ * the correct height for the length of the translated text.
+ */
+ static #assertPanelTextAreaHeight() {
+ const { textArea } = SelectTranslationsPanel.elements;
+
+ if (
+ SelectTranslationsPanel.getSourceText().length <
+ SelectTranslationsPanel.textLengthThreshold
+ ) {
+ is(
+ textArea.style.height,
+ SelectTranslationsPanel.shortTextHeight,
+ "The panel text area should have the short-text height"
+ );
+ } else {
+ is(
+ textArea.style.height,
+ SelectTranslationsPanel.longTextHeight,
+ "The panel text area should have the long-text height"
+ );
+ }
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI matches the expected
+ * state when the panel is actively translating text.
+ */
+ static assertPanelViewActivelyTranslating() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ ok(
+ textArea.classList.contains("translating"),
+ "The textarea should have the translating class."
);
SelectTranslationsTestUtils.#assertPanelElementVisibility({
- betaIcon: true,
- fromLabel: true,
- fromMenuList: true,
- header: true,
+ ...SelectTranslationsTestUtils.#alwaysPresentElements,
+ });
+ SelectTranslationsTestUtils.#assertPanelHasTranslatingPlaceholder();
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI matches the expected
+ * state when no from-language is selected in the panel.
+ */
+ static async assertPanelViewNoFromLangSelected() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ ok(
+ !textArea.classList.contains("translating"),
+ "The textarea should not have the translating class."
+ );
+ SelectTranslationsTestUtils.#assertPanelElementVisibility({
+ ...SelectTranslationsTestUtils.#alwaysPresentElements,
+ });
+ await SelectTranslationsTestUtils.#assertPanelHasIdlePlaceholder();
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
+ textArea: false,
+ copyButton: false,
+ translateFullPageButton: false,
+ });
+ SelectTranslationsTestUtils.assertSelectedFromLanguage(null);
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI matches the expected
+ * state when no to-language is selected in the panel.
+ */
+ static async assertPanelViewNoToLangSelected() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ ok(
+ !textArea.classList.contains("translating"),
+ "The textarea should not have the translating class."
+ );
+ SelectTranslationsTestUtils.#assertPanelElementVisibility({
+ ...SelectTranslationsTestUtils.#alwaysPresentElements,
+ });
+ SelectTranslationsTestUtils.assertSelectedToLanguage(null);
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
+ textArea: false,
+ copyButton: false,
+ translateFullPageButton: false,
+ });
+ await SelectTranslationsTestUtils.#assertPanelHasIdlePlaceholder();
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI contains the
+ * idle placeholder text.
+ */
+ static async #assertPanelHasIdlePlaceholder() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ const expected = await document.l10n.formatValue(
+ "select-translations-panel-idle-placeholder-text"
+ );
+ is(
+ textArea.value,
+ expected,
+ "Translated text area should be the idle placeholder."
+ );
+ SelectTranslationsTestUtils.#assertPanelTextAreaDirection();
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI contains the
+ * translating placeholder text.
+ */
+ static async #assertPanelHasTranslatingPlaceholder() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ const expected = await document.l10n.formatValue(
+ "select-translations-panel-translating-placeholder-text"
+ );
+ is(
+ textArea.value,
+ expected,
+ "Active translation text area should have the translating placeholder."
+ );
+ SelectTranslationsTestUtils.#assertPanelTextAreaDirection();
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
+ textArea: true,
+ copyButton: false,
+ translateFullPageButton: true,
+ });
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI contains the
+ * translated text.
+ */
+ static #assertPanelHasTranslatedText() {
+ const { textArea, fromMenuList, toMenuList } =
+ SelectTranslationsPanel.elements;
+ const fromLanguage = fromMenuList.value;
+ const toLanguage = toMenuList.value;
+ const translatedSuffix = ` [${fromLanguage} to ${toLanguage}]`;
+ ok(
+ textArea.value.endsWith(translatedSuffix),
+ `Translated text should match ${fromLanguage} to ${toLanguage}`
+ );
+ is(
+ SelectTranslationsPanel.getSourceText().length,
+ SelectTranslationsPanel.getTranslatedText().length -
+ translatedSuffix.length,
+ "Expected translated text length to correspond to the source text length."
+ );
+ SelectTranslationsTestUtils.#assertPanelTextAreaDirection(toLanguage);
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
textArea: true,
- toLabel: true,
- toMenuList: true,
copyButton: true,
- doneButton: true,
translateFullPageButton: true,
});
}
/**
+ * Asserts the enabled state of action buttons in the SelectTranslationsPanel.
+ *
+ * @param {boolean} expectEnabled - Whether the buttons should be enabled (true) or not (false).
+ */
+ static #assertConditionalUIEnabled({
+ copyButton: copyButtonEnabled,
+ translateFullPageButton: translateFullPageButtonEnabled,
+ textArea: textAreaEnabled,
+ }) {
+ const { copyButton, translateFullPageButton, textArea } =
+ SelectTranslationsPanel.elements;
+ is(
+ copyButton.disabled,
+ !copyButtonEnabled,
+ `The copy button should be ${copyButtonEnabled ? "enabled" : "disabled"}.`
+ );
+ is(
+ translateFullPageButton.disabled,
+ !translateFullPageButtonEnabled,
+ `The translate-full-page button should be ${
+ translateFullPageButtonEnabled ? "enabled" : "disabled"
+ }.`
+ );
+ is(
+ textArea.disabled,
+ !textAreaEnabled,
+ `The translated-text area should be ${
+ textAreaEnabled ? "enabled" : "disabled"
+ }.`
+ );
+ }
+
+ /**
* Asserts that the selected from-language matches the provided language tag.
*
* @param {string} langTag - A BCP-47 language tag.
*/
- static assertSelectedFromLanguage({ langTag, l10nId }) {
- SharedTranslationsTestUtils._assertSelectedFromLanguage(
- SelectTranslationsPanel,
- { langTag, l10nId }
- );
+ static assertSelectedFromLanguage(langTag = null) {
+ const { fromMenuList } = SelectTranslationsPanel.elements;
+ SelectTranslationsTestUtils.#assertSelectedLanguage(fromMenuList, langTag);
}
/**
@@ -1476,11 +1790,29 @@ class SelectTranslationsTestUtils {
*
* @param {string} langTag - A BCP-47 language tag.
*/
- static assertSelectedToLanguage({ langTag, l10nId }) {
- SharedTranslationsTestUtils._assertSelectedToLanguage(
- SelectTranslationsPanel,
- { langTag, l10nId }
- );
+ static assertSelectedToLanguage(langTag = null) {
+ const { toMenuList } = SelectTranslationsPanel.elements;
+ SelectTranslationsTestUtils.#assertSelectedLanguage(toMenuList, langTag);
+ }
+
+ /**
+ * Asserts the selected language in the given menu list if a langTag is provided.
+ * If no langTag is given, asserts that the menulist displays the localized placeholder.
+ *
+ * @param {object} menuList - The menu list object to check.
+ * @param {string} [langTag] - The optional language tag to assert against.
+ */
+ static #assertSelectedLanguage(menuList, langTag) {
+ if (langTag) {
+ SharedTranslationsTestUtils._assertSelectedLanguage(menuList, {
+ langTag,
+ });
+ } else {
+ SharedTranslationsTestUtils._assertSelectedLanguage(menuList, {
+ l10nId: "translations-panel-choose-language",
+ });
+ SharedTranslationsTestUtils._assertHasFocus(menuList);
+ }
}
/**
@@ -1503,110 +1835,258 @@ class SelectTranslationsTestUtils {
*
* @param {Function} runInPage - A content-exposed function to run within the context of the page.
* @param {object} options - Options for opening the context menu.
- * @param {boolean} options.selectFirstParagraph - Selects the first paragraph before opening the context menu.
- * @param {boolean} options.selectSpanishParagraph - Selects the Spanish paragraph before opening the context menu.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {boolean} options.openAtFirstParagraph - Opens the context menu at the first paragraph in the test page.
- * @param {boolean} options.openAtSpanishParagraph - Opens the context menu at the Spanish paragraph in the test page.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at the English hyperlink in the test page.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at the Spanish hyperlink in the test page.
- * This is only available in SPANISH_TEST_PAGE.
+ *
+ * The following options will only work when testing SELECT_TEST_PAGE_URL.
+ *
+ * @param {boolean} options.selectFrenchSection - Selects the section of French text.
+ * @param {boolean} options.selectEnglishSection - Selects the section of English text.
+ * @param {boolean} options.selectSpanishSection - Selects the section of Spanish text.
+ * @param {boolean} options.selectFrenchSentence - Selects a French sentence.
+ * @param {boolean} options.selectEnglishSentence - Selects an English sentence.
+ * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence.
+ * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text.
+ * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text.
+ * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text.
+ * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence.
+ * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence.
+ * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence.
+ * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text.
+ * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text.
+ * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text.
* @throws Throws an error if no valid option was provided for opening the menu.
*/
- static async openContextMenu(
- runInPage,
- {
- selectFirstParagraph,
- selectSpanishParagraph,
- openAtFirstParagraph,
- openAtSpanishParagraph,
- openAtEnglishHyperlink,
- openAtSpanishHyperlink,
- }
- ) {
+ static async openContextMenu(runInPage, options) {
logAction();
- if (selectFirstParagraph === true) {
- await runInPage(async TranslationsTest => {
- const { getFirstParagraph } = TranslationsTest.getSelectors();
- const paragraph = getFirstParagraph();
- TranslationsTest.selectContentElement(paragraph);
- });
- }
+ const maybeSelectContentFrom = async keyword => {
+ const conditionVariableName = `select${keyword}`;
+ const selectorFunctionName = `get${keyword}`;
+
+ if (options[conditionVariableName]) {
+ await runInPage(
+ async (TranslationsTest, data) => {
+ const selectorFunction =
+ TranslationsTest.getSelectors()[data.selectorFunctionName];
+ if (typeof selectorFunction === "function") {
+ const paragraph = selectorFunction();
+ TranslationsTest.selectContentElement(paragraph);
+ }
+ },
+ { selectorFunctionName }
+ );
+ }
+ };
- if (selectSpanishParagraph === true) {
- await runInPage(async TranslationsTest => {
- const { getSpanishParagraph } = TranslationsTest.getSelectors();
- const paragraph = getSpanishParagraph();
- TranslationsTest.selectContentElement(paragraph);
- });
+ await maybeSelectContentFrom("FrenchSection");
+ await maybeSelectContentFrom("EnglishSection");
+ await maybeSelectContentFrom("SpanishSection");
+ await maybeSelectContentFrom("FrenchSentence");
+ await maybeSelectContentFrom("EnglishSentence");
+ await maybeSelectContentFrom("SpanishSentence");
+
+ const maybeOpenContextMenuAt = async keyword => {
+ const optionVariableName = `openAt${keyword}`;
+ const selectorFunctionName = `get${keyword}`;
+
+ if (options[optionVariableName]) {
+ await SharedTranslationsTestUtils._waitForPopupEvent(
+ "contentAreaContextMenu",
+ "popupshown",
+ async () => {
+ await runInPage(
+ async (TranslationsTest, data) => {
+ const selectorFunction =
+ TranslationsTest.getSelectors()[data.selectorFunctionName];
+ if (typeof selectorFunction === "function") {
+ const element = selectorFunction();
+ await TranslationsTest.rightClickContentElement(element);
+ }
+ },
+ { selectorFunctionName }
+ );
+ }
+ );
+ }
+ };
+
+ await maybeOpenContextMenuAt("FrenchSection");
+ await maybeOpenContextMenuAt("EnglishSection");
+ await maybeOpenContextMenuAt("SpanishSection");
+ await maybeOpenContextMenuAt("FrenchSentence");
+ await maybeOpenContextMenuAt("EnglishSentence");
+ await maybeOpenContextMenuAt("SpanishSentence");
+ await maybeOpenContextMenuAt("FrenchHyperlink");
+ await maybeOpenContextMenuAt("EnglishHyperlink");
+ await maybeOpenContextMenuAt("SpanishHyperlink");
+ }
+
+ /**
+ * Handles language-model downloads for the SelectTranslationsPanel, ensuring that expected
+ * UI states match based on the resolved download state.
+ *
+ * @param {object} options - Configuration options for downloads.
+ * @param {function(number): Promise<void>} options.downloadHandler - The function to resolve or reject the downloads.
+ * @param {boolean} [options.pivotTranslation] - Whether to expect a pivot translation.
+ *
+ * @returns {Promise<void>}
+ */
+ static async handleDownloads({ downloadHandler, pivotTranslation }) {
+ const { textArea } = SelectTranslationsPanel.elements;
+
+ if (downloadHandler) {
+ if (textArea.style.overflow !== "hidden") {
+ await BrowserTestUtils.waitForMutationCondition(
+ textArea,
+ { attributes: true, attributeFilter: ["style"] },
+ () => textArea.style.overflow === "hidden"
+ );
+ }
+
+ await SelectTranslationsTestUtils.assertPanelViewActivelyTranslating();
+ await downloadHandler(pivotTranslation ? 2 : 1);
}
- if (openAtFirstParagraph === true) {
- await SharedTranslationsTestUtils._waitForPopupEvent(
- "contentAreaContextMenu",
- "popupshown",
- async () => {
- await runInPage(async TranslationsTest => {
- const { getFirstParagraph } = TranslationsTest.getSelectors();
- const paragraph = getFirstParagraph();
- await TranslationsTest.rightClickContentElement(paragraph);
- });
- }
+ if (textArea.style.overflow === "hidden") {
+ await BrowserTestUtils.waitForMutationCondition(
+ textArea,
+ { attributes: true, attributeFilter: ["style"] },
+ () => textArea.style.overflow === "auto"
);
- return;
}
+ }
- if (openAtSpanishParagraph === true) {
- await SharedTranslationsTestUtils._waitForPopupEvent(
- "contentAreaContextMenu",
- "popupshown",
- async () => {
- await runInPage(async TranslationsTest => {
- const { getSpanishParagraph } = TranslationsTest.getSelectors();
- const paragraph = getSpanishParagraph();
- await TranslationsTest.rightClickContentElement(paragraph);
- });
- }
+ /**
+ * Switches the selected from-language to the provided language tags
+ *
+ * @param {string[]} langTags - An array of BCP-47 language tags.
+ * @param {object} options - Configuration options for the language change.
+ * @param {boolean} options.openDropdownMenu - Determines whether the language change should be made via a dropdown menu or directly.
+ *
+ * @returns {Promise<void>}
+ */
+ static async changeSelectedFromLanguage(langTags, options) {
+ const { fromMenuList, fromMenuPopup } = SelectTranslationsPanel.elements;
+ const { openDropdownMenu } = options;
+
+ const switchFn = openDropdownMenu
+ ? SelectTranslationsTestUtils.#changeSelectedLanguageViaDropdownMenu
+ : SelectTranslationsTestUtils.#changeSelectedLanguageDirectly;
+
+ await switchFn(
+ langTags,
+ { menuList: fromMenuList, menuPopup: fromMenuPopup },
+ options
+ );
+ }
+
+ /**
+ * Switches the selected to-language to the provided language tag.
+ *
+ * @param {string[]} langTags - An array of BCP-47 language tags.
+ * @param {object} options - Options for selecting paragraphs and opening the context menu.
+ * @param {boolean} options.openDropdownMenu - Determines whether the language change should be made via a dropdown menu or directly.
+ * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable.
+ * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change.
+ *
+ * @returns {Promise<void>}
+ */
+ static async changeSelectedToLanguage(langTags, options) {
+ const { toMenuList, toMenuPopup } = SelectTranslationsPanel.elements;
+ const { openDropdownMenu } = options;
+
+ const switchFn = openDropdownMenu
+ ? SelectTranslationsTestUtils.#changeSelectedLanguageViaDropdownMenu
+ : SelectTranslationsTestUtils.#changeSelectedLanguageDirectly;
+
+ await switchFn(
+ langTags,
+ { menuList: toMenuList, menuPopup: toMenuPopup },
+ options
+ );
+ }
+
+ /**
+ * Directly changes the selected language to each provided language tag without using a dropdown menu.
+ *
+ * @param {string[]} langTags - An array of BCP-47 language tags for direct selection.
+ * @param {object} elements - Elements required for changing the selected language.
+ * @param {Element} elements.menuList - The menu list element where languages are directly changed.
+ * @param {object} options - Configuration options for language change and additional actions.
+ * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable.
+ * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change.
+ *
+ * @returns {Promise<void>}
+ */
+ static async #changeSelectedLanguageDirectly(langTags, elements, options) {
+ const { menuList } = elements;
+ const { onChangeLanguage, downloadHandler } = options;
+
+ for (const langTag of langTags) {
+ const menuListUpdated = BrowserTestUtils.waitForMutationCondition(
+ menuList,
+ { attributes: true, attributeFilter: ["value"] },
+ () => menuList.value === langTag
);
- return;
+
+ menuList.value = langTag;
+ menuList.dispatchEvent(new Event("command"));
+ await menuListUpdated;
}
- if (openAtEnglishHyperlink === true) {
- await SharedTranslationsTestUtils._waitForPopupEvent(
- "contentAreaContextMenu",
- "popupshown",
- async () => {
- await runInPage(async TranslationsTest => {
- const { getEnglishHyperlink } = TranslationsTest.getSelectors();
- const hyperlink = getEnglishHyperlink();
- await TranslationsTest.rightClickContentElement(hyperlink);
- });
- }
- );
- return;
+ if (downloadHandler) {
+ menuList.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await SelectTranslationsTestUtils.handleDownloads(options);
}
- if (openAtSpanishHyperlink === true) {
- await SharedTranslationsTestUtils._waitForPopupEvent(
- "contentAreaContextMenu",
+ if (onChangeLanguage) {
+ await onChangeLanguage();
+ }
+ }
+
+ /**
+ * Changes the selected language by opening the dropdown menu for each provided language tag.
+ *
+ * @param {string[]} langTags - An array of BCP-47 language tags for selection via dropdown.
+ * @param {object} elements - Elements involved in the dropdown language selection process.
+ * @param {Element} elements.menuList - The element that triggers the dropdown menu.
+ * @param {Element} elements.menuPopup - The dropdown menu element containing selectable languages.
+ * @param {object} options - Configuration options for language change and additional actions.
+ * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable.
+ * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change.
+ *
+ * @returns {Promise<void>}
+ */
+ static async #changeSelectedLanguageViaDropdownMenu(
+ langTags,
+ elements,
+ options
+ ) {
+ const { menuList, menuPopup } = elements;
+ const { onChangeLanguage } = options;
+ for (const langTag of langTags) {
+ await SelectTranslationsTestUtils.waitForPanelPopupEvent(
"popupshown",
- async () => {
- await runInPage(async TranslationsTest => {
- const { getSpanishHyperlink } = TranslationsTest.getSelectors();
- const hyperlink = getSpanishHyperlink();
- await TranslationsTest.rightClickContentElement(hyperlink);
- });
+ () => click(menuList)
+ );
+
+ const menuItem = menuPopup.querySelector(`[value="${langTag}"]`);
+ await SelectTranslationsTestUtils.waitForPanelPopupEvent(
+ "popuphidden",
+ () => {
+ click(menuItem);
+ // Synthesizing a click on the menuitem isn't closing the popup
+ // as a click normally would, so this tab keypress is added to
+ // ensure the popup closes.
+ EventUtils.synthesizeKey("KEY_Tab");
}
);
- return;
- }
- throw new Error(
- "openContextMenu() was not provided a declaration for which element to open the menu at."
- );
+ await SelectTranslationsTestUtils.handleDownloads(options);
+ if (onChangeLanguage) {
+ await onChangeLanguage();
+ }
+ }
}
/**
@@ -1614,36 +2094,32 @@ class SelectTranslationsTestUtils {
*
* @param {Function} runInPage - A content-exposed function to run within the context of the page.
* @param {object} options - Options for selecting paragraphs and opening the context menu.
- * @param {boolean} options.selectFirstParagraph - Selects the first paragraph before opening the context menu.
- * @param {boolean} options.selectSpanishParagraph - Selects the Spanish paragraph before opening the context menu.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {string} options.expectedTargetLanguage - The target language for translation.
- * @param {boolean} options.openAtFirstParagraph - Opens the context menu at the first paragraph.
- * @param {boolean} options.openAtSpanishParagraph - Opens at the Spanish paragraph.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {boolean} options.openAtEnglishHyperlink - Opens at the English hyperlink.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {boolean} options.openAtSpanishHyperlink - Opens at the Spanish hyperlink.
- * This is only available in SPANISH_TEST_PAGE.
- * @param {Function|null} [options.onOpenPanel=null] - An optional callback function to execute after the panel opens.
- * @param {string|null} [message=null] - An optional message to log to info.
+ *
+ * The following options will only work when testing SELECT_TEST_PAGE_URL.
+ *
+ * @param {string} options.expectedFromLanguage - The expected from-language tag.
+ * @param {string} options.expectedToLanguage - The expected to-language tag.
+ * @param {boolean} options.selectFrenchSection - Selects the section of French text.
+ * @param {boolean} options.selectEnglishSection - Selects the section of English text.
+ * @param {boolean} options.selectSpanishSection - Selects the section of Spanish text.
+ * @param {boolean} options.selectFrenchSentence - Selects a French sentence.
+ * @param {boolean} options.selectEnglishSentence - Selects an English sentence.
+ * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence.
+ * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text.
+ * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text.
+ * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text.
+ * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence.
+ * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence.
+ * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence.
+ * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text.
+ * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text.
+ * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text.
+ * @param {Function} [options.onOpenPanel] - An optional callback function to execute after the panel opens.
+ * @param {string|null} [message] - An optional message to log to info.
* @throws Throws an error if the context menu could not be opened with the provided options.
* @returns {Promise<void>}
*/
- static async openPanel(
- runInPage,
- {
- selectFirstParagraph,
- selectSpanishParagraph,
- expectedTargetLanguage,
- openAtFirstParagraph,
- openAtSpanishParagraph,
- openAtEnglishHyperlink,
- openAtSpanishHyperlink,
- onOpenPanel,
- },
- message
- ) {
+ static async openPanel(runInPage, options, message) {
logAction();
if (message) {
@@ -1652,15 +2128,7 @@ class SelectTranslationsTestUtils {
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
- {
- selectFirstParagraph,
- selectSpanishParagraph,
- expectedTargetLanguage,
- openAtFirstParagraph,
- openAtSpanishParagraph,
- openAtEnglishHyperlink,
- openAtSpanishHyperlink,
- },
+ options,
message
);
@@ -1668,9 +2136,28 @@ class SelectTranslationsTestUtils {
await SelectTranslationsTestUtils.waitForPanelPopupEvent(
"popupshown",
- () => click(menuItem),
- onOpenPanel
+ async () => {
+ click(menuItem);
+ await closeContextMenuIfOpen();
+ },
+ async () => {
+ const { onOpenPanel } = options;
+ await SelectTranslationsTestUtils.handleDownloads(options);
+ if (onOpenPanel) {
+ await onOpenPanel();
+ }
+ }
);
+
+ const { expectedFromLanguage, expectedToLanguage } = options;
+ if (expectedFromLanguage !== undefined) {
+ SelectTranslationsTestUtils.assertSelectedFromLanguage(
+ expectedFromLanguage
+ );
+ }
+ if (expectedToLanguage !== undefined) {
+ SelectTranslationsTestUtils.assertSelectedToLanguage(expectedToLanguage);
+ }
}
/**
@@ -1732,10 +2219,10 @@ class TranslationsSettingsTestUtils {
translateNeverHeader: document.getElementById(
"translations-settings-never-translate"
),
- translateAlwaysAddButton: document.getElementById(
+ translateAlwaysMenuList: document.getElementById(
"translations-settings-always-translate-list"
),
- translateNeverAddButton: document.getElementById(
+ translateNeverMenuList: document.getElementById(
"translations-settings-never-translate-list"
),
translateNeverSiteHeader: document.getElementById(
@@ -1744,12 +2231,15 @@ class TranslationsSettingsTestUtils {
translateNeverSiteDesc: document.getElementById(
"translations-settings-never-sites"
),
- translateDownloadLanguagesHeader: document.getElementById(
- "translations-settings-download-languages"
- ),
+ translateDownloadLanguagesHeader: document
+ .getElementById("translations-settings-download-section")
+ .querySelector("h2"),
translateDownloadLanguagesLearnMore: document.getElementById(
"download-languages-learn-more"
),
+ translateDownloadLanguagesList: document.getElementById(
+ "translations-settings-download-section"
+ ),
};
return elements;
diff --git a/browser/components/uitour/UITour-lib.js b/browser/components/uitour/UITour-lib.js
index 0df3059425..a83ec95200 100644
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -9,7 +9,7 @@ if (typeof Mozilla == "undefined") {
var Mozilla = {};
}
-(function ($) {
+(function () {
"use strict";
// create namespace
diff --git a/browser/components/uitour/UITour.sys.mjs b/browser/components/uitour/UITour.sys.mjs
index fef68a5a95..920815fec5 100644
--- a/browser/components/uitour/UITour.sys.mjs
+++ b/browser/components/uitour/UITour.sys.mjs
@@ -339,7 +339,7 @@ export var UITour = {
let callback = buttonData.callbackID;
let button = {
label: buttonData.label,
- callback: event => {
+ callback: () => {
this.sendPageCallback(browser, callback);
},
};
@@ -694,7 +694,7 @@ export var UITour = {
}
},
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
lazy.log.debug("observe: aTopic =", aTopic);
switch (aTopic) {
// The browser message manager is disconnected when the <browser> is
@@ -918,7 +918,7 @@ export var UITour = {
);
},
- getTarget(aWindow, aTargetName, aSticky = false) {
+ getTarget(aWindow, aTargetName) {
lazy.log.debug("getTarget:", aTargetName);
if (typeof aTargetName != "string" || !aTargetName) {
lazy.log.warn("getTarget: Invalid target name specified");
@@ -1280,7 +1280,7 @@ export var UITour = {
tooltipButtons.hidden = !aButtons.length;
let tooltipClose = document.getElementById("UITourTooltipClose");
- let closeButtonCallback = event => {
+ let closeButtonCallback = () => {
this.hideInfo(document.defaultView);
if (aOptions && aOptions.closeButtonCallback) {
aOptions.closeButtonCallback();
@@ -1301,7 +1301,7 @@ export var UITour = {
tooltip.addEventListener(
"popuphiding",
- function (event) {
+ function () {
tooltipClose.removeEventListener("command", closeButtonCallback);
if (aOptions.targetCallback && aAnchor.removeTargetListener) {
aAnchor.removeTargetListener(document, targetCallback);
diff --git a/browser/components/uitour/test/browser_UITour.js b/browser/components/uitour/test/browser_UITour.js
index d9e517af1f..c961ab3c0d 100644
--- a/browser/components/uitour/test/browser_UITour.js
+++ b/browser/components/uitour/test/browser_UITour.js
@@ -323,7 +323,7 @@ var tests = [
() => {
highlight.addEventListener(
"animationstart",
- function (aEvent) {
+ function () {
ok(
true,
"Animation occurred again even though the effect was the same"
diff --git a/browser/components/uitour/test/browser_UITour3.js b/browser/components/uitour/test/browser_UITour3.js
index 526994f420..2820fbd020 100644
--- a/browser/components/uitour/test/browser_UITour3.js
+++ b/browser/components/uitour/test/browser_UITour3.js
@@ -81,7 +81,7 @@ add_UITour_task(async function test_info_buttons_1() {
);
is(
buttons.children[0].getAttribute("image"),
- "",
+ null,
"Text should have no image"
);
is(buttons.children[0].className, "", "Text should have no class");
@@ -94,7 +94,7 @@ add_UITour_task(async function test_info_buttons_1() {
);
is(
buttons.children[1].getAttribute("image"),
- "",
+ null,
"Link should have no image"
);
is(buttons.children[1].className, "button-link", "Check link class");
@@ -107,7 +107,7 @@ add_UITour_task(async function test_info_buttons_1() {
);
is(
buttons.children[2].getAttribute("image"),
- "",
+ null,
"First button should have no image"
);
is(buttons.children[2].className, "", "Button 1 should have no class");
@@ -173,7 +173,7 @@ add_UITour_task(async function test_info_buttons_2() {
);
is(
buttons.children[1].getAttribute("image"),
- "",
+ null,
"Link should have no image"
);
ok(
@@ -188,7 +188,7 @@ add_UITour_task(async function test_info_buttons_2() {
);
is(
buttons.children[2].getAttribute("image"),
- "",
+ null,
"First button should have no image"
);
diff --git a/browser/components/uitour/test/browser_UITour_defaultBrowser.js b/browser/components/uitour/test/browser_UITour_defaultBrowser.js
index 721ab2f8c0..a8572e49ab 100644
--- a/browser/components/uitour/test/browser_UITour_defaultBrowser.js
+++ b/browser/components/uitour/test/browser_UITour_defaultBrowser.js
@@ -12,10 +12,10 @@ Services.scriptloader.loadSubScript(
function MockShellService() {}
MockShellService.prototype = {
QueryInterface: ChromeUtils.generateQI(["nsIShellService"]),
- isDefaultBrowser(aStartupCheck, aForAllTypes) {
+ isDefaultBrowser() {
return false;
},
- setDefaultBrowser(aForAllUsers) {
+ setDefaultBrowser() {
setDefaultBrowserCalled = true;
},
shouldCheckDefaultBrowser: false,
@@ -26,7 +26,7 @@ MockShellService.prototype = {
BACKGROUND_FILL: 4,
BACKGROUND_FIT: 5,
BACKGROUND_SPAN: 6,
- setDesktopBackground(aElement, aPosition) {},
+ setDesktopBackground() {},
desktopBackgroundColor: 0,
};
diff --git a/browser/components/uitour/test/browser_UITour_modalDialog.js b/browser/components/uitour/test/browser_UITour_modalDialog.js
index a711ee2f2e..5d1e0a5303 100644
--- a/browser/components/uitour/test/browser_UITour_modalDialog.js
+++ b/browser/components/uitour/test/browser_UITour_modalDialog.js
@@ -39,7 +39,7 @@ var observer = SpecialPowers.wrapCallbackObject({
return this;
},
- observe(subject, topic, data) {
+ observe() {
var doc = getDialogDoc();
if (doc) {
handleDialog(doc);
diff --git a/browser/components/uitour/test/head.js b/browser/components/uitour/test/head.js
index 07b941ba1c..6c7ca00d6e 100644
--- a/browser/components/uitour/test/head.js
+++ b/browser/components/uitour/test/head.js
@@ -194,14 +194,7 @@ function hideInfoPromise(...args) {
* function name to call to generate the buttons/options instead of the
* buttons/options themselves. This makes the signature differ from the content one.
*/
-function showInfoPromise(
- target,
- title,
- text,
- icon,
- buttonsFunctionName,
- optionsFunctionName
-) {
+function showInfoPromise() {
let popup = document.getElementById("UITourTooltip");
let shownPromise = promisePanelElementShown(window, popup);
return SpecialPowers.spawn(gTestTab.linkedBrowser, [[...arguments]], args => {
@@ -271,7 +264,7 @@ function promisePanelElementEvent(win, aPanel, aEvent) {
reject(aEvent + " event did not happen within 5 seconds.");
}, 5000);
- function onPanelEvent(e) {
+ function onPanelEvent() {
aPanel.removeEventListener(aEvent, onPanelEvent);
win.clearTimeout(timeoutId);
// Wait one tick to let UITour.sys.mjs process the event as well.
@@ -321,7 +314,7 @@ async function loadUITourTestPage(callback, host = "https://example.org/") {
// return a function which calls the method of the same name on
// contentWin.Mozilla.UITour in a ContentTask.
let UITourHandler = {
- get(target, prop, receiver) {
+ get(target, prop) {
return (...args) => {
let browser = gTestTab.linkedBrowser;
// We need to proxy any callback functions using messages:
diff --git a/browser/components/urlbar/.eslintrc.js b/browser/components/urlbar/.eslintrc.js
index 8ead689bcc..aac2436d20 100644
--- a/browser/components/urlbar/.eslintrc.js
+++ b/browser/components/urlbar/.eslintrc.js
@@ -5,8 +5,6 @@
"use strict";
module.exports = {
- extends: ["plugin:mozilla/require-jsdoc"],
-
rules: {
"mozilla/var-only-at-top-level": "error",
"no-unused-expressions": "error",
diff --git a/browser/components/urlbar/UrlbarController.sys.mjs b/browser/components/urlbar/UrlbarController.sys.mjs
index 9bfc3a645d..7e4d0ff1c5 100644
--- a/browser/components/urlbar/UrlbarController.sys.mjs
+++ b/browser/components/urlbar/UrlbarController.sys.mjs
@@ -133,6 +133,7 @@ export class UrlbarController {
// notifications related to the previous query.
this.notify(NOTIFICATIONS.QUERY_STARTED, queryContext);
await this.manager.startQuery(queryContext, this);
+
// If the query has been cancelled, onQueryFinished was notified already.
// Note this._lastQueryContextWrapper may have changed in the meanwhile.
if (
@@ -144,6 +145,16 @@ export class UrlbarController {
this.manager.cancelQuery(queryContext);
this.notify(NOTIFICATIONS.QUERY_FINISHED, queryContext);
}
+
+ // Record a potential exposure if the current search string matches one of
+ // the registered keywords.
+ if (!queryContext.isPrivate) {
+ let searchStr = queryContext.trimmedLowerCaseSearchString;
+ if (lazy.UrlbarPrefs.get("potentialExposureKeywords").has(searchStr)) {
+ this.engagementEvent.addPotentialExposure(searchStr);
+ }
+ }
+
return queryContext;
}
@@ -335,7 +346,7 @@ export class UrlbarController {
}
event.preventDefault();
break;
- case KeyEvent.DOM_VK_TAB:
+ case KeyEvent.DOM_VK_TAB: {
// It's always possible to tab through results when the urlbar was
// focused with the mouse or has a search string, or when the view
// already has a selection.
@@ -368,6 +379,7 @@ export class UrlbarController {
event.preventDefault();
}
break;
+ }
case KeyEvent.DOM_VK_PAGE_DOWN:
case KeyEvent.DOM_VK_PAGE_UP:
if (event.ctrlKey) {
@@ -592,8 +604,8 @@ export class UrlbarController {
/**
* Triggers a "dismiss" engagement for the selected result if one is selected
* and it's not the heuristic. Providers that can respond to dismissals of
- * their results should implement `onEngagement()`, handle the dismissal, and
- * call `controller.removeResult()`.
+ * their results should implement `onLegacyEngagement()`, handle the
+ * dismissal, and call `controller.removeResult()`.
*
* @param {Event} event
* The event that triggered dismissal.
@@ -783,13 +795,6 @@ class TelemetryEvent {
interactionType: this._getStartInteractionType(event, searchString),
searchString,
};
-
- this._controller.manager.notifyEngagementChange(
- "start",
- queryContext,
- {},
- this._controller
- );
}
/**
@@ -821,17 +826,31 @@ class TelemetryEvent {
* @param {DOMElement} [details.element] The picked view element.
*/
record(event, details) {
+ // Prevent re-entering `record()`. This can happen because
+ // `#internalRecord()` will notify an engagement to the provider, that may
+ // execute an action blurring the input field. Then both an engagement
+ // and an abandonment would be recorded for the same session.
+ // Nulling out `_startEventInfo` doesn't save us in this case, because it
+ // happens after `#internalRecord()`, and `isSessionOngoing` must be
+ // calculated inside it.
+ if (this.#handlingRecord) {
+ return;
+ }
+
// This should never throw, or it may break the urlbar.
try {
- this._internalRecord(event, details);
+ this.#handlingRecord = true;
+ this.#internalRecord(event, details);
} catch (ex) {
console.error("Could not record event: ", ex);
} finally {
+ this.#handlingRecord = false;
+
// Reset the start event info except for engagements that do not end the
// search session. In that case, the view stays open and further
// engagements are possible and should be recorded when they occur.
// (`details.isSessionOngoing` is not a param; rather, it's set by
- // `_internalRecord()`.)
+ // `#internalRecord()`.)
if (!details.isSessionOngoing) {
this._startEventInfo = null;
this._discarded = false;
@@ -839,19 +858,10 @@ class TelemetryEvent {
}
}
- _internalRecord(event, details) {
+ #internalRecord(event, details) {
const startEventInfo = this._startEventInfo;
if (!this._category || !startEventInfo) {
- if (this._discarded && this._category && details?.selType !== "dismiss") {
- let { queryContext } = this._controller._lastQueryContextWrapper || {};
- this._controller.manager.notifyEngagementChange(
- "discard",
- queryContext,
- {},
- this._controller
- );
- }
return;
}
if (
@@ -938,6 +948,10 @@ class TelemetryEvent {
}
);
+ if (!details.isSessionOngoing) {
+ this.#recordEndOfSessionTelemetry(details.searchString);
+ }
+
if (skipLegacyTelemetry) {
this._controller.manager.notifyEngagementChange(
method,
@@ -1091,23 +1105,6 @@ class TelemetryEvent {
return;
}
- // First check to see if we can record an exposure event
- if (method === "abandonment" || method === "engagement") {
- if (this.#exposureResultTypes.size) {
- let exposure = {
- results: [...this.#exposureResultTypes].sort().join(","),
- };
- this._controller.logger.debug(
- `exposure event: ${JSON.stringify(exposure)}`
- );
- Glean.urlbar.exposure.record(exposure);
- }
-
- // reset the provider list on the controller
- this.#exposureResultTypes.clear();
- this.#tentativeExposureResultTypes.clear();
- }
-
this._controller.logger.info(
`${method} event: ${JSON.stringify(eventInfo)}`
);
@@ -1115,6 +1112,38 @@ class TelemetryEvent {
Glean.urlbar[method].record(eventInfo);
}
+ #recordEndOfSessionTelemetry(searchString) {
+ // exposures
+ if (this.#exposureResultTypes.size) {
+ let exposure = {
+ results: [...this.#exposureResultTypes].sort().join(","),
+ };
+ this._controller.logger.debug(
+ `exposure event: ${JSON.stringify(exposure)}`
+ );
+ Glean.urlbar.exposure.record(exposure);
+ this.#exposureResultTypes.clear();
+ }
+ this.#tentativeExposureResultTypes.clear();
+
+ // potential exposures
+ if (this.#potentialExposureKeywords.size) {
+ let normalizedSearchString = searchString.trim().toLowerCase();
+ for (let keyword of this.#potentialExposureKeywords) {
+ let data = {
+ keyword,
+ terminal: keyword == normalizedSearchString,
+ };
+ this._controller.logger.debug(
+ `potential_exposure event: ${JSON.stringify(data)}`
+ );
+ Glean.urlbar.potentialExposure.record(data);
+ }
+ GleanPings.urlbarPotentialExposure.submit();
+ this.#potentialExposureKeywords.clear();
+ }
+ }
+
/**
* Registers an exposure for a result in the current urlbar session. All
* exposures that are added during a session are recorded in an exposure event
@@ -1164,6 +1193,16 @@ class TelemetryEvent {
this.#tentativeExposureResultTypes.clear();
}
+ /**
+ * Registers a potential exposure in the current urlbar session.
+ *
+ * @param {string} keyword
+ * The keyword that was matched.
+ */
+ addPotentialExposure(keyword) {
+ this.#potentialExposureKeywords.add(keyword);
+ }
+
#getInteractionType(
method,
startEventInfo,
@@ -1350,8 +1389,12 @@ class TelemetryEvent {
}
}
+ // Used to avoid re-entering `record()`.
+ #handlingRecord = false;
+
#previousSearchWordsSet = null;
#exposureResultTypes = new Set();
#tentativeExposureResultTypes = new Set();
+ #potentialExposureKeywords = new Set();
}
diff --git a/browser/components/urlbar/UrlbarInput.sys.mjs b/browser/components/urlbar/UrlbarInput.sys.mjs
index 96fc7b9301..a96e862cff 100644
--- a/browser/components/urlbar/UrlbarInput.sys.mjs
+++ b/browser/components/urlbar/UrlbarInput.sys.mjs
@@ -1364,7 +1364,7 @@ export class UrlbarInput {
// The value setter clobbers the actiontype attribute, so we need this
// helper to restore it afterwards.
const setValueAndRestoreActionType = (value, allowTrim) => {
- this._setValue(value, allowTrim);
+ this._setValue(value, { allowTrim });
switch (result.type) {
case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
@@ -1555,7 +1555,7 @@ export class UrlbarInput {
!this.value.endsWith(" ")
) {
this._autofillPlaceholder = null;
- this._setValue(this.window.gBrowser.userTypedValue, false);
+ this._setValue(this.window.gBrowser.userTypedValue);
}
return false;
@@ -1940,7 +1940,7 @@ export class UrlbarInput {
}
set value(val) {
- this._setValue(val, true);
+ this._setValue(val, { allowTrim: true });
}
get lastSearchString() {
@@ -2107,7 +2107,7 @@ export class UrlbarInput {
this.searchMode = searchMode;
let value = result.payload.query?.trimStart() || "";
- this._setValue(value, false);
+ this._setValue(value);
if (startQuery) {
this.startQuery({ allowAutofill: false });
@@ -2253,10 +2253,6 @@ export class UrlbarInput {
"--urlbar-height",
px(getBoundsWithoutFlushing(this.textbox).height)
);
- this.textbox.style.setProperty(
- "--urlbar-toolbar-height",
- px(getBoundsWithoutFlushing(this._toolbar).height)
- );
this.setAttribute("breakout", "true");
this.textbox.parentNode.setAttribute("breakout", "true");
@@ -2266,7 +2262,16 @@ export class UrlbarInput {
});
}
- _setValue(val, allowTrim) {
+ /**
+ * Sets the input field value.
+ *
+ * @param {string} val The new value to set.
+ * @param {object} [options] Options for setting.
+ * @param {boolean} [options.allowTrim] Whether the value can be trimmed.
+ *
+ * @returns {string} The set value.
+ */
+ _setValue(val, { allowTrim = false } = {}) {
// Don't expose internal about:reader URLs to the user.
let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val);
if (originalUrl) {
@@ -2730,7 +2735,7 @@ export class UrlbarInput {
}) {
// The autofilled value may be a URL that includes a scheme at the
// beginning. Do not allow it to be trimmed.
- this._setValue(value, false);
+ this._setValue(value);
this.inputField.setSelectionRange(selectionStart, selectionEnd);
this._autofillPlaceholder = {
value,
@@ -3152,8 +3157,8 @@ export class UrlbarInput {
this.select();
this.window.goDoCommand("cmd_paste");
this.setResultForCurrentValue(null);
- this.controller.clearLastQueryContextCache();
this.handleCommand();
+ this.controller.clearLastQueryContextCache();
this._suppressStartQuery = false;
});
@@ -3504,7 +3509,7 @@ export class UrlbarInput {
}
if (untrim) {
this._focusUntrimmedValue = this._untrimmedValue;
- this._setValue(this._focusUntrimmedValue, false);
+ this._setValue(this._focusUntrimmedValue);
}
}
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
index 022d0b1c7c..cd8a6b0f4c 100644
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
@@ -65,7 +65,7 @@ const PREF_URLBAR_DEFAULTS = new Map([
["autoFill.stddevMultiplier", [0.0, "float"]],
// Feature gate pref for clipboard suggestions in the urlbar.
- ["clipboard.featureGate", true],
+ ["clipboard.featureGate", false],
// Whether to show a link for using the search functionality provided by the
// active view if the the view utilizes OpenSearch.
@@ -1507,12 +1507,28 @@ class Preferences {
return this.shouldHandOffToSearchModePrefs.some(
prefName => !this.get(prefName)
);
- case "autoFillAdaptiveHistoryUseCountThreshold":
+ case "autoFillAdaptiveHistoryUseCountThreshold": {
const nimbusValue =
this._nimbus.autoFillAdaptiveHistoryUseCountThreshold;
return nimbusValue === undefined
? this.get("autoFill.adaptiveHistory.useCountThreshold")
: parseFloat(nimbusValue);
+ }
+ case "potentialExposureKeywords": {
+ // Get the keywords array from Nimbus or prefs and convert it to a Set.
+ // If the value comes from Nimbus, it will already be an array. If it
+ // comes from prefs, it should be a stringified array.
+ let value = this._readPref(pref);
+ if (typeof value == "string") {
+ try {
+ value = JSON.parse(value);
+ } catch (e) {}
+ }
+ if (!Array.isArray(value)) {
+ value = null;
+ }
+ return new Set(value);
+ }
}
return this._readPref(pref);
}
diff --git a/browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs b/browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs
index 62f85b2348..be607a80d5 100644
--- a/browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs
@@ -49,7 +49,7 @@ class ProviderAboutPages extends UrlbarProvider {
* @returns {boolean} Whether this provider should be invoked for the search.
*/
isActive(queryContext) {
- return queryContext.trimmedSearchString.toLowerCase().startsWith("about:");
+ return queryContext.trimmedLowerCaseSearchString.startsWith("about:");
}
/**
@@ -61,7 +61,7 @@ class ProviderAboutPages extends UrlbarProvider {
* result. A UrlbarResult should be passed to it.
*/
startQuery(queryContext, addCallback) {
- let searchString = queryContext.trimmedSearchString.toLowerCase();
+ let searchString = queryContext.trimmedLowerCaseSearchString;
for (const aboutUrl of lazy.AboutPagesUtils.visibleAboutUrls) {
if (aboutUrl.startsWith(searchString)) {
let result = new lazy.UrlbarResult(
diff --git a/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs b/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs
index 32e605206e..7470df0fea 100644
--- a/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs
@@ -653,7 +653,7 @@ class ProviderAutofill extends UrlbarProvider {
queryType: QUERYTYPE.AUTOFILL_ADAPTIVE,
// `fullSearchString` is the value the user typed including a prefix if
// they typed one. `searchString` has been stripped of the prefix.
- fullSearchString: queryContext.searchString.toLowerCase(),
+ fullSearchString: queryContext.lowerCaseSearchString,
searchString: this._searchString,
strippedPrefix: this._strippedPrefix,
useCountThreshold: lazy.UrlbarPrefs.get(
diff --git a/browser/components/urlbar/UrlbarProviderCalculator.sys.mjs b/browser/components/urlbar/UrlbarProviderCalculator.sys.mjs
index a55531167c..3f0ffed299 100644
--- a/browser/components/urlbar/UrlbarProviderCalculator.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderCalculator.sys.mjs
@@ -157,7 +157,7 @@ class ProviderCalculator extends UrlbarProvider {
return viewUpdate;
}
- onEngagement(state, queryContext, details) {
+ onLegacyEngagement(state, queryContext, details) {
let { result } = details;
if (result?.providerName == this.name) {
lazy.ClipboardHelper.copyString(result.payload.value);
diff --git a/browser/components/urlbar/UrlbarProviderClipboard.sys.mjs b/browser/components/urlbar/UrlbarProviderClipboard.sys.mjs
index 5337e610cc..1dc5bb9b86 100644
--- a/browser/components/urlbar/UrlbarProviderClipboard.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderClipboard.sys.mjs
@@ -143,10 +143,7 @@ class ProviderClipboard extends UrlbarProvider {
addCallback(this, result);
}
- onEngagement(state, queryContext, details, controller) {
- if (!["engagement", "abandonment"].includes(state)) {
- return;
- }
+ onLegacyEngagement(state, queryContext, details, controller) {
const visibleResults = controller.view?.visibleResults ?? [];
for (const result of visibleResults) {
if (
diff --git a/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs b/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs
index 63c94ee8f3..5714f11e72 100644
--- a/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs
@@ -246,7 +246,7 @@ class ProviderContextualSearch extends UrlbarProvider {
};
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
let { result } = details;
if (result?.providerName == this.name) {
this.#pickResult(result, controller.browserWindow);
diff --git a/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs b/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs
index f929a1c003..17b6a4c9b0 100644
--- a/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs
@@ -200,7 +200,7 @@ class ProviderInputHistory extends UrlbarProvider {
}
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
let { result } = details;
if (result?.providerName != this.name) {
return;
@@ -236,7 +236,7 @@ class ProviderInputHistory extends UrlbarProvider {
SQL_ADAPTIVE_QUERY,
{
parent: lazy.PlacesUtils.tagsFolderId,
- search_string: queryContext.searchString.toLowerCase(),
+ search_string: queryContext.lowerCaseSearchString,
matchBehavior: Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE,
searchBehavior: lazy.UrlbarPrefs.get("defaultBehavior"),
userContextId: lazy.UrlbarPrefs.get("switchTabs.searchAllContainers")
diff --git a/browser/components/urlbar/UrlbarProviderInterventions.sys.mjs b/browser/components/urlbar/UrlbarProviderInterventions.sys.mjs
index 08b4ea36b7..68b9c1665d 100644
--- a/browser/components/urlbar/UrlbarProviderInterventions.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderInterventions.sys.mjs
@@ -703,7 +703,7 @@ class ProviderInterventions extends UrlbarProvider {
}
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
let { result } = details;
// `selType` is "tip" when the tip's main button is picked. Ignore clicks on
@@ -714,10 +714,8 @@ class ProviderInterventions extends UrlbarProvider {
this.#pickResult(result, controller.browserWindow);
}
- if (["engagement", "abandonment"].includes(state)) {
- for (let tip of this.tipsShownInCurrentEngagement) {
- Services.telemetry.keyedScalarAdd("urlbar.tips", `${tip}-shown`, 1);
- }
+ for (let tip of this.tipsShownInCurrentEngagement) {
+ Services.telemetry.keyedScalarAdd("urlbar.tips", `${tip}-shown`, 1);
}
this.tipsShownInCurrentEngagement.clear();
}
diff --git a/browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs b/browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs
index 351e8ff60b..362f683027 100644
--- a/browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs
@@ -178,7 +178,7 @@ class ProviderOmnibox extends UrlbarProvider {
);
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
let { result } = details;
if (result?.providerName != this.name) {
return;
diff --git a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
index 650acd1730..c94ebee80a 100644
--- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
@@ -1517,7 +1517,7 @@ class ProviderPlaces extends UrlbarProvider {
search.notifyResult(false);
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
let { result } = details;
if (result?.providerName != this.name) {
return;
diff --git a/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs b/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs
index f199b6b892..29370cbaaf 100644
--- a/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs
@@ -95,7 +95,7 @@ class ProviderQuickActions extends UrlbarProvider {
*/
async startQuery(queryContext, addCallback) {
await lazy.QuickActionsLoaderDefault.ensureLoaded();
- let input = queryContext.trimmedSearchString.toLowerCase();
+ let input = queryContext.trimmedLowerCaseSearchString;
if (
!queryContext.searchMode &&
@@ -241,7 +241,7 @@ class ProviderQuickActions extends UrlbarProvider {
}
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
// Ignore engagements on other results that didn't end the session.
if (details.result?.providerName != this.name && details.isSessionOngoing) {
return;
diff --git a/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs b/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs
index 78e254616e..fbc8cc8c3f 100644
--- a/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs
@@ -229,7 +229,7 @@ class ProviderQuickSuggest extends UrlbarProvider {
}
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
// Ignore engagements on other results that didn't end the session.
if (details.result?.providerName != this.name && details.isSessionOngoing) {
return;
@@ -237,7 +237,7 @@ class ProviderQuickSuggest extends UrlbarProvider {
// Reset the Merino session ID when a session ends. By design for the user's
// privacy, we don't keep it around between engagements.
- if (state != "start" && !details.isSessionOngoing) {
+ if (!details.isSessionOngoing) {
this.#merino?.resetSession();
}
@@ -486,8 +486,8 @@ class ProviderQuickSuggest extends UrlbarProvider {
* end of the engagement or that was dismissed. Null if no quick suggest
* result was present.
* @param {object} details
- * The `details` object that was passed to `onEngagement()`. It must look
- * like this: `{ selType, selIndex }`
+ * The `details` object that was passed to `onLegacyEngagement()`. It must
+ * look like this: `{ selType, selIndex }`
*/
#recordEngagement(queryContext, result, details) {
let resultSelType = "";
@@ -781,8 +781,8 @@ class ProviderQuickSuggest extends UrlbarProvider {
* True if the main part of the result's row was clicked; false if a button
* like help or dismiss was clicked or if no part of the row was clicked.
* @param {object} options.details
- * The `details` object that was passed to `onEngagement()`. It must look
- * like this: `{ selType, selIndex }`
+ * The `details` object that was passed to `onLegacyEngagement()`. It must
+ * look like this: `{ selType, selIndex }`
*/
#recordNavSuggestionTelemetry({
queryContext,
diff --git a/browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs b/browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs
index 48006d09c0..b3c322ffa1 100644
--- a/browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs
@@ -188,7 +188,7 @@ class ProviderQuickSuggestContextualOptIn extends UrlbarProvider {
row.ownerGlobal.A11yUtils.announce({ raw: alertText });
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
let { result } = details;
if (result?.providerName != this.name) {
return;
diff --git a/browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs b/browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs
index ceeba729d4..1565013440 100644
--- a/browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs
@@ -63,7 +63,7 @@ class ProviderRecentSearches extends UrlbarProvider {
return 1;
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
let { result } = details;
if (result?.providerName != this.name) {
return;
diff --git a/browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs b/browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs
index 8cb3532d94..e3d13feb56 100644
--- a/browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs
@@ -352,7 +352,7 @@ class ProviderSearchSuggestions extends UrlbarProvider {
return undefined;
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
let { result } = details;
if (result?.providerName != this.name) {
return;
diff --git a/browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs b/browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs
index b19528619c..a7a23a3228 100644
--- a/browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs
@@ -273,7 +273,7 @@ class ProviderSearchTips extends UrlbarProvider {
lazy.UrlbarPrefs.set(`tipShownCount.${tip}`, MAX_SHOWN_COUNT);
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
// Ignore engagements on other results that didn't end the session.
let { result } = details;
if (result?.providerName != this.name && details.isSessionOngoing) {
diff --git a/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs b/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs
index 9aabef3d19..0cce6481b1 100644
--- a/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs
@@ -194,7 +194,7 @@ class ProviderTabToSearch extends UrlbarProvider {
* Called when a result from the provider is selected. "Selected" refers to
* the user highlighing the result with the arrow keys/Tab, before it is
* picked. onSelection is also called when a user clicks a result. In the
- * event of a click, onSelection is called just before onEngagement.
+ * event of a click, onSelection is called just before onLegacyEngagement.
*
* @param {UrlbarResult} result
* The result that was selected.
@@ -226,7 +226,7 @@ class ProviderTabToSearch extends UrlbarProvider {
}
}
- onEngagement(state, queryContext, details) {
+ onLegacyEngagement(state, queryContext, details) {
let { result, element } = details;
if (
result?.providerName == this.name &&
diff --git a/browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs b/browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs
index b3a91bcbe4..db9e8df382 100644
--- a/browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs
@@ -173,7 +173,7 @@ class ProviderTokenAliasEngines extends UrlbarProvider {
}
async _getAutofillResult(queryContext) {
- let lowerCaseSearchString = queryContext.searchString.toLowerCase();
+ let { lowerCaseSearchString } = queryContext;
// The user is typing a specific engine. We should show a heuristic result.
for (let { engine, tokenAliases } of this._engines) {
diff --git a/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs b/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs
index e9d968f20f..a046de37d4 100644
--- a/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs
@@ -193,7 +193,7 @@ class ProviderTopSites extends UrlbarProvider {
return site;
});
- // Store Sponsored Top Sites so we can use it in `onEngagement`
+ // Store Sponsored Top Sites so we can use it in `onLegacyEngagement`
if (sponsoredSites.length) {
this.sponsoredSites = sponsoredSites;
}
@@ -333,12 +333,8 @@ class ProviderTopSites extends UrlbarProvider {
}
}
- onEngagement(state, queryContext) {
- if (
- !queryContext.isPrivate &&
- this.sponsoredSites &&
- ["engagement", "abandonment"].includes(state)
- ) {
+ onLegacyEngagement(state, queryContext) {
+ if (!queryContext.isPrivate && this.sponsoredSites) {
for (let site of this.sponsoredSites) {
Services.telemetry.keyedScalarAdd(
SCALAR_CATEGORY_TOPSITES,
diff --git a/browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs b/browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs
index 98c4d025e4..a5ad28d2aa 100644
--- a/browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs
@@ -169,7 +169,7 @@ class ProviderUnitConversion extends UrlbarProvider {
addCallback(this, result);
}
- onEngagement(state, queryContext, details) {
+ onLegacyEngagement(state, queryContext, details) {
let { result, element } = details;
if (result?.providerName == this.name) {
const { textContent } = element.querySelector(
diff --git a/browser/components/urlbar/UrlbarProviderWeather.sys.mjs b/browser/components/urlbar/UrlbarProviderWeather.sys.mjs
index 24342fecab..8e9b6b8f3e 100644
--- a/browser/components/urlbar/UrlbarProviderWeather.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderWeather.sys.mjs
@@ -115,7 +115,7 @@ class ProviderWeather extends UrlbarProvider {
return false;
}
- return keywords.has(queryContext.searchString.trim().toLocaleLowerCase());
+ return keywords.has(queryContext.trimmedLowerCaseSearchString);
}
/**
@@ -163,7 +163,7 @@ class ProviderWeather extends UrlbarProvider {
return lazy.QuickSuggest.weather.getViewUpdate(result);
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
// Ignore engagements on other results that didn't end the session.
if (details.result?.providerName != this.name && details.isSessionOngoing) {
return;
@@ -243,7 +243,7 @@ class ProviderWeather extends UrlbarProvider {
* A non-empty string means the user picked the weather row or some part of
* it, and both impression and click telemetry will be recorded. The
* non-empty-string values come from the `details.selType` passed in to
- * `onEngagement()`; see `TelemetryEvent.typeFromElement()`.
+ * `onLegacyEngagement()`; see `TelemetryEvent.typeFromElement()`.
*/
#recordEngagementTelemetry(result, isPrivate, selType) {
// Indexes recorded in quick suggest telemetry are 1-based, so add 1 to the
diff --git a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
index 609b0735e1..ac70e03e1b 100644
--- a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
+++ b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
@@ -334,11 +334,11 @@ class ProvidersManager {
/**
* Notifies all providers when the user starts and ends an engagement with the
- * urlbar. For details on parameters, see UrlbarProvider.onEngagement().
+ * urlbar. For details on parameters, see
+ * UrlbarProvider.onLegacyEngagement().
*
* @param {string} state
- * The state of the engagement, one of: start, engagement, abandonment,
- * discard
+ * The state of the engagement, one of: engagement, abandonment
* @param {UrlbarQueryContext} queryContext
* The engagement's query context, if available.
* @param {object} details
@@ -349,7 +349,7 @@ class ProvidersManager {
notifyEngagementChange(state, queryContext, details = {}, controller) {
for (let provider of this.providers) {
provider.tryMethod(
- "onEngagement",
+ "onLegacyEngagement",
state,
queryContext,
details,
diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs
index 2bbb5d1ab0..9fca8426a3 100644
--- a/browser/components/urlbar/UrlbarUtils.sys.mjs
+++ b/browser/components/urlbar/UrlbarUtils.sys.mjs
@@ -854,7 +854,7 @@ export var UrlbarUtils = {
* @returns {string} The modified paste data.
*/
stripUnsafeProtocolOnPaste(pasteData) {
- while (true) {
+ for (;;) {
let scheme = "";
try {
scheme = Services.io.extractScheme(pasteData);
@@ -1831,6 +1831,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
isBlockable: {
type: "boolean",
},
+ isManageable: {
+ type: "boolean",
+ },
isPinned: {
type: "boolean",
},
@@ -2175,6 +2178,8 @@ export class UrlbarQueryContext {
this.pendingHeuristicProviders = new Set();
this.deferUserSelectionProviders = new Set();
this.trimmedSearchString = this.searchString.trim();
+ this.lowerCaseSearchString = this.searchString.toLowerCase();
+ this.trimmedLowerCaseSearchString = this.trimmedSearchString.toLowerCase();
this.userContextId =
lazy.UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
options.userContextId,
@@ -2431,21 +2436,12 @@ export class UrlbarProvider {
* @param {string} _state
* The state of the engagement, one of the following strings:
*
- * start
- * A new query has started in the urlbar.
* engagement
* The user picked a result in the urlbar or used paste-and-go.
* abandonment
* The urlbar was blurred (i.e., lost focus).
- * discard
- * This doesn't correspond to a user action, but it means that the
- * urlbar has discarded the engagement for some reason, and the
- * `onEngagement` implementation should ignore it.
- *
* @param {UrlbarQueryContext} _queryContext
- * The engagement's query context. This is *not* guaranteed to be defined
- * when `state` is "start". It will always be defined for "engagement" and
- * "abandonment".
+ * The engagement's query context.
* @param {object} _details
* This object is non-empty only when `state` is "engagement" or
* "abandonment", and it describes the search string and engaged result.
@@ -2479,7 +2475,7 @@ export class UrlbarProvider {
* @param {UrlbarController} _controller
* The associated controller.
*/
- onEngagement(_state, _queryContext, _details, _controller) {}
+ onLegacyEngagement(_state, _queryContext, _details, _controller) {}
/**
* Called before a result from the provider is selected. See `onSelection`
@@ -2497,8 +2493,8 @@ export class UrlbarProvider {
* Called when a result from the provider is selected. "Selected" refers to
* the user highlighing the result with the arrow keys/Tab, before it is
* picked. onSelection is also called when a user clicks a result. In the
- * event of a click, onSelection is called just before onEngagement. Note that
- * this is called when heuristic results are pre-selected.
+ * event of a click, onSelection is called just before onLegacyEngagement.
+ * Note that this is called when heuristic results are pre-selected.
*
* @param {UrlbarResult} _result
* The result that was selected.
@@ -2581,8 +2577,8 @@ export class UrlbarProvider {
/**
* Gets the list of commands that should be shown in the result menu for a
* given result from the provider. All commands returned by this method should
- * be handled by implementing `onEngagement()` with the possible exception of
- * commands automatically handled by the urlbar, like "help".
+ * be handled by implementing `onLegacyEngagement()` with the possible
+ * exception of commands automatically handled by the urlbar, like "help".
*
* @param {UrlbarResult} _result
* The menu will be shown for this result.
@@ -2594,8 +2590,8 @@ export class UrlbarProvider {
* {string} name
* The name of the command. Must be specified unless `children` is
* present. When a command is picked, its name will be passed as
- * `details.selType` to `onEngagement()`. The special name "separator"
- * will create a menu separator.
+ * `details.selType` to `onLegacyEngagement()`. The special name
+ * "separator" will create a menu separator.
* {object} l10n
* An l10n object for the command's label: `{ id, args }`
* Must be specified unless `name` is "separator".
diff --git a/browser/components/urlbar/UrlbarView.sys.mjs b/browser/components/urlbar/UrlbarView.sys.mjs
index b5fe1e1955..3d6ea46781 100644
--- a/browser/components/urlbar/UrlbarView.sys.mjs
+++ b/browser/components/urlbar/UrlbarView.sys.mjs
@@ -48,6 +48,7 @@ const ZERO_PREFIX_SCALAR_EXPOSURE = "urlbar.zeroprefix.exposure";
const RESULT_MENU_COMMANDS = {
DISMISS: "dismiss",
HELP: "help",
+ MANAGE: "manage",
};
const getBoundsWithoutFlushing = element =>
@@ -3139,6 +3140,15 @@ export class UrlbarView {
},
});
}
+ if (result.payload.isManageable) {
+ commands.push({
+ name: RESULT_MENU_COMMANDS.MANAGE,
+ l10n: {
+ id: "urlbar-result-menu-manage-firefox-suggest",
+ },
+ });
+ }
+
let rv = commands.length ? commands : null;
this.#resultMenuCommands.set(result, rv);
return rv;
diff --git a/browser/components/urlbar/docs/dynamic-result-types.rst b/browser/components/urlbar/docs/dynamic-result-types.rst
index f72c5e4a13..2c81c1656f 100644
--- a/browser/components/urlbar/docs/dynamic-result-types.rst
+++ b/browser/components/urlbar/docs/dynamic-result-types.rst
@@ -152,8 +152,8 @@ aren't relevant to dynamic result types, and you should choose values
appropriate to your use case.
If any elements created in the view for your results can be picked with the
-keyboard or mouse, then be sure to implement your provider's ``onEngagement``
-method.
+keyboard or mouse, then be sure to implement your provider's
+``onLegacyEngagement`` method.
For help on implementing providers in general, see the address bar's
`Architecture Overview`__.
@@ -616,7 +616,7 @@ URL Navigation
If a result's payload includes a string ``url`` property and a boolean
``shouldNavigate: true`` property, then picking the result will navigate to the
-URL. The ``onEngagement`` method of the result's provider will still be called
+URL. The ``onLegacyEngagement`` method of the result's provider will still be called
before navigation.
Text Highlighting
diff --git a/browser/components/urlbar/metrics.yaml b/browser/components/urlbar/metrics.yaml
index 95337d84eb..173ee08a10 100644
--- a/browser/components/urlbar/metrics.yaml
+++ b/browser/components/urlbar/metrics.yaml
@@ -110,7 +110,6 @@ urlbar:
`intervention_unknown`,
`intervention_update`,
`keyword`,
- `merino_adm_nonsponsored`,
`merino_adm_sponsored`,
`merino_amo`,
`merino_top_picks`,
@@ -159,6 +158,7 @@ urlbar:
notification_emails:
- fx-search-telemetry@mozilla.com
expires: never
+
engagement:
type: event
description: Recorded when the user executes an action on a result.
@@ -242,7 +242,6 @@ urlbar:
`intervention_unknown`,
`intervention_update`,
`keyword`,
- `merino_adm_nonsponsored`,
`merino_adm_sponsored`,
`merino_amo`,
`merino_top_picks`,
@@ -363,7 +362,6 @@ urlbar:
`intervention_unknown`,
`intervention_update`,
`keyword`,
- `merino_adm_nonsponsored`,
`merino_adm_sponsored`,
`merino_amo`,
`merino_top_picks`,
@@ -432,6 +430,40 @@ urlbar:
- fx-search-telemetry@mozilla.com
expires: never
+ potential_exposure:
+ type: event
+ description: >
+ This event is recorded in the `urlbar-potential-exposure` ping, which is
+ submitted at the end of urlbar sessions during which the user typed a
+ keyword defined by the Nimbus variable `potentialExposureKeywords`. A
+ "session" begins when the user focuses the urlbar and ends with an
+ engagement or abandonment. The ping will contain one event per unique
+ keyword that is typed during the session. This ping is not submitted for
+ sessions in private windows.
+ extra_keys:
+ keyword:
+ type: string
+ description: >
+ The matched keyword.
+ terminal:
+ type: boolean
+ description: >
+ Whether the matched keyword was present at the end of the urlbar
+ session. If true, the session ended with the keyword. If false, the
+ keyword was typed at some point during the session but the session
+ did not end with it.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1881875
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1881875
+ data_sensitivity:
+ - stored_content
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ expires: never
+ send_in_pings:
+ - urlbar-potential-exposure
+
quick_suggest_contextual_opt_in:
type: event
description: >
diff --git a/browser/components/urlbar/pings.yaml b/browser/components/urlbar/pings.yaml
index 8153b62863..4c46f16909 100644
--- a/browser/components/urlbar/pings.yaml
+++ b/browser/components/urlbar/pings.yaml
@@ -19,3 +19,19 @@ quick-suggest:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1854755
notification_emails:
- najiang@mozilla.com
+
+urlbar-potential-exposure:
+ description: |
+ This ping is submitted at the end of urlbar sessions during which the user
+ typed a keyword defined by the Nimbus variable `potentialExposureKeywords`.
+ A "session" begins when the user focuses the urlbar and ends with an
+ engagement or abandonment. The ping will contain one
+ `urlbar.potential_exposure` event per unique keyword that is typed during
+ the session. This ping is not submitted for sessions in private windows.
+ include_client_id: false
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1881875
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1881875
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
diff --git a/browser/components/urlbar/private/AddonSuggestions.sys.mjs b/browser/components/urlbar/private/AddonSuggestions.sys.mjs
index 23311cec1c..ace82e41d3 100644
--- a/browser/components/urlbar/private/AddonSuggestions.sys.mjs
+++ b/browser/components/urlbar/private/AddonSuggestions.sys.mjs
@@ -21,7 +21,7 @@ const UTM_PARAMS = {
};
const RESULT_MENU_COMMAND = {
- HELP: "help",
+ MANAGE: "manage",
NOT_INTERESTED: "not_interested",
NOT_RELEVANT: "not_relevant",
SHOW_LESS_FREQUENTLY: "show_less_frequently",
@@ -212,9 +212,9 @@ export class AddonSuggestions extends BaseFeature {
},
{ name: "separator" },
{
- name: RESULT_MENU_COMMAND.HELP,
+ name: RESULT_MENU_COMMAND.MANAGE,
l10n: {
- id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ id: "urlbar-result-menu-manage-firefox-suggest",
},
}
);
@@ -224,8 +224,8 @@ export class AddonSuggestions extends BaseFeature {
handleCommand(view, result, selType) {
switch (selType) {
- case RESULT_MENU_COMMAND.HELP:
- // "help" is handled by UrlbarInput, no need to do anything here.
+ case RESULT_MENU_COMMAND.MANAGE:
+ // "manage" is handled by UrlbarInput, no need to do anything here.
break;
// selType == "dismiss" when the user presses the dismiss key shortcut.
case "dismiss":
diff --git a/browser/components/urlbar/private/AdmWikipedia.sys.mjs b/browser/components/urlbar/private/AdmWikipedia.sys.mjs
index 3ab5bad09f..596e15df4c 100644
--- a/browser/components/urlbar/private/AdmWikipedia.sys.mjs
+++ b/browser/components/urlbar/private/AdmWikipedia.sys.mjs
@@ -190,14 +190,11 @@ export class AdmWikipedia extends BaseFeature {
sponsoredBlockId: suggestion.block_id,
sponsoredAdvertiser: suggestion.advertiser,
sponsoredIabCategory: suggestion.iab_category,
- helpUrl: lazy.QuickSuggest.HELP_URL,
- helpL10n: {
- id: "urlbar-result-menu-learn-more-about-firefox-suggest",
- },
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
+ isManageable: true,
};
let result = new lazy.UrlbarResult(
diff --git a/browser/components/urlbar/private/MDNSuggestions.sys.mjs b/browser/components/urlbar/private/MDNSuggestions.sys.mjs
index c9e7da18af..3efedbd12a 100644
--- a/browser/components/urlbar/private/MDNSuggestions.sys.mjs
+++ b/browser/components/urlbar/private/MDNSuggestions.sys.mjs
@@ -15,7 +15,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
});
const RESULT_MENU_COMMAND = {
- HELP: "help",
+ MANAGE: "manage",
NOT_INTERESTED: "not_interested",
NOT_RELEVANT: "not_relevant",
};
@@ -157,9 +157,9 @@ export class MDNSuggestions extends BaseFeature {
},
{ name: "separator" },
{
- name: RESULT_MENU_COMMAND.HELP,
+ name: RESULT_MENU_COMMAND.MANAGE,
l10n: {
- id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ id: "urlbar-result-menu-manage-firefox-suggest",
},
},
];
@@ -167,8 +167,8 @@ export class MDNSuggestions extends BaseFeature {
handleCommand(view, result, selType) {
switch (selType) {
- case RESULT_MENU_COMMAND.HELP:
- // "help" is handled by UrlbarInput, no need to do anything here.
+ case RESULT_MENU_COMMAND.MANAGE:
+ // "manage" is handled by UrlbarInput, no need to do anything here.
break;
// selType == "dismiss" when the user presses the dismiss key shortcut.
case "dismiss":
diff --git a/browser/components/urlbar/private/SuggestBackendRust.sys.mjs b/browser/components/urlbar/private/SuggestBackendRust.sys.mjs
index 2d96e7540f..3993149757 100644
--- a/browser/components/urlbar/private/SuggestBackendRust.sys.mjs
+++ b/browser/components/urlbar/private/SuggestBackendRust.sys.mjs
@@ -136,11 +136,12 @@ export class SuggestBackendRust extends BaseFeature {
suggestion.provider = type;
suggestion.is_sponsored = type == "Amp" || type == "Yelp";
if (Array.isArray(suggestion.icon)) {
- suggestion.icon_blob = new Blob(
- [new Uint8Array(suggestion.icon)],
- type == "Yelp" ? { type: "image/svg+xml" } : null
- );
+ suggestion.icon_blob = new Blob([new Uint8Array(suggestion.icon)], {
+ type: suggestion.iconMimetype ?? "",
+ });
+
delete suggestion.icon;
+ delete suggestion.iconMimetype;
}
}
diff --git a/browser/components/urlbar/private/YelpSuggestions.sys.mjs b/browser/components/urlbar/private/YelpSuggestions.sys.mjs
index 4cf454c71d..e2a2803bd7 100644
--- a/browser/components/urlbar/private/YelpSuggestions.sys.mjs
+++ b/browser/components/urlbar/private/YelpSuggestions.sys.mjs
@@ -15,8 +15,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
});
const RESULT_MENU_COMMAND = {
- HELP: "help",
INACCURATE_LOCATION: "inaccurate_location",
+ MANAGE: "manage",
NOT_INTERESTED: "not_interested",
NOT_RELEVANT: "not_relevant",
SHOW_LESS_FREQUENTLY: "show_less_frequently",
@@ -168,9 +168,9 @@ export class YelpSuggestions extends BaseFeature {
},
{ name: "separator" },
{
- name: RESULT_MENU_COMMAND.HELP,
+ name: RESULT_MENU_COMMAND.MANAGE,
l10n: {
- id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ id: "urlbar-result-menu-manage-firefox-suggest",
},
}
);
@@ -180,8 +180,8 @@ export class YelpSuggestions extends BaseFeature {
handleCommand(view, result, selType, searchString) {
switch (selType) {
- case RESULT_MENU_COMMAND.HELP:
- // "help" is handled by UrlbarInput, no need to do anything here.
+ case RESULT_MENU_COMMAND.MANAGE:
+ // "manage" is handled by UrlbarInput, no need to do anything here.
break;
case RESULT_MENU_COMMAND.INACCURATE_LOCATION:
// Currently the only way we record this feedback is in the Glean
diff --git a/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs
index cfc9ecb3d8..f576f4ca19 100644
--- a/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs
+++ b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs
@@ -158,7 +158,7 @@ export var UrlbarTestUtils = {
lazy.UrlbarPrefs.get("trimURLs") &&
value != lazy.BrowserUIUtils.trimURL(value)
) {
- window.gURLBar._setValue(value, false);
+ window.gURLBar._setValue(value);
fireInputEvent = true;
} else {
window.gURLBar.value = value;
@@ -1315,10 +1315,7 @@ export var UrlbarTestUtils = {
// Set most of the string directly instead of going through sendString,
// so that we don't make life unnecessarily hard for consumers by
// possibly starting multiple searches.
- win.gURLBar._setValue(
- text.substr(0, text.length - 1),
- false /* allowTrim = */
- );
+ win.gURLBar._setValue(text.substr(0, text.length - 1));
}
this.EventUtils.sendString(text.substr(-1, 1), win);
},
@@ -1490,7 +1487,7 @@ class TestProvider extends UrlbarProvider {
* @param {Function} [options.onSelection]
* If given, a function that will be called when
* {@link UrlbarView.#selectElement} method is called.
- * @param {Function} [options.onEngagement]
+ * @param {Function} [options.onLegacyEngagement]
* If given, a function that will be called when engagement.
* @param {Function} [options.delayResultsPromise]
* If given, we'll await on this before returning results.
@@ -1503,7 +1500,7 @@ class TestProvider extends UrlbarProvider {
addTimeout = 0,
onCancel = null,
onSelection = null,
- onEngagement = null,
+ onLegacyEngagement = null,
delayResultsPromise = null,
} = {}) {
if (delayResultsPromise && addTimeout) {
@@ -1520,7 +1517,7 @@ class TestProvider extends UrlbarProvider {
this._type = type;
this._onCancel = onCancel;
this._onSelection = onSelection;
- this._onEngagement = onEngagement;
+ this._onLegacyEngagement = onLegacyEngagement;
// As this has been a common source of mistakes, auto-upgrade the provider
// type to heuristic if any result is heuristic.
@@ -1574,8 +1571,8 @@ class TestProvider extends UrlbarProvider {
this._onSelection?.(result, element);
}
- onEngagement(state, queryContext, details, controller) {
- this._onEngagement?.(state, queryContext, details, controller);
+ onLegacyEngagement(state, queryContext, details, controller) {
+ this._onLegacyEngagement?.(state, queryContext, details, controller);
}
}
diff --git a/browser/components/urlbar/tests/browser-tips/browser_picks.js b/browser/components/urlbar/tests/browser-tips/browser_picks.js
index ba0ff69357..c9d725dfb5 100644
--- a/browser/components/urlbar/tests/browser-tips/browser_picks.js
+++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js
@@ -117,8 +117,8 @@ async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) {
});
UrlbarProvidersManager.registerProvider(provider);
- let onEngagementPromise = new Promise(
- resolve => (provider.onEngagement = resolve)
+ let onLegacyEngagementPromise = new Promise(
+ resolve => (provider.onLegacyEngagement = resolve)
);
// Do a search to show our tip result.
@@ -142,8 +142,8 @@ async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) {
);
}
- // Now pick the target and wait for provider.onEngagement to be called and
- // the URL to load if necessary.
+ // Now pick the target and wait for provider.onLegacyEngagement to be called
+ // and the URL to load if necessary.
let loadPromise;
if (buttonUrl || helpUrl) {
loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
@@ -160,7 +160,7 @@ async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) {
EventUtils.synthesizeKey("KEY_Enter");
}
});
- await onEngagementPromise;
+ await onLegacyEngagementPromise;
await loadPromise;
// Check telemetry.
diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js
index a82a2d658b..8c98e27993 100644
--- a/browser/components/urlbar/tests/browser-tips/browser_searchTips.js
+++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js
@@ -18,7 +18,7 @@ ChromeUtils.defineESModuleGetters(this, {
"resource:///modules/UrlbarProviderSearchTips.sys.mjs",
});
-// These should match the same consts in UrlbarProviderSearchTips.jsm.
+// These should match the same consts in UrlbarProviderSearchTips.sys.mjs.
const MAX_SHOWN_COUNT = 4;
const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js
index 72d05cf632..6c0550a2df 100644
--- a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js
+++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js
@@ -25,7 +25,7 @@ XPCOMUtils.defineLazyServiceGetter(
"nsIClipboardHelper"
);
-// These should match the same consts in UrlbarProviderSearchTips.jsm.
+// These should match the same consts in UrlbarProviderSearchTips.sys.mjs.
const MAX_SHOWN_COUNT = 4;
const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
diff --git a/browser/components/urlbar/tests/browser/browser.toml b/browser/components/urlbar/tests/browser/browser.toml
index b9934aa838..44b964e5ca 100644
--- a/browser/components/urlbar/tests/browser/browser.toml
+++ b/browser/components/urlbar/tests/browser/browser.toml
@@ -4,7 +4,11 @@ support-files = [
"head.js",
"head-common.js",
]
-
+skip-if = [
+ "os == 'linux' && os_version == '18.04' && asan", # long running manifest
+ "os == 'linux' && os_version == '18.04' && tsan", # long running manifest
+ "win11_2009 && asan", # long running manifest
+]
prefs = [
"browser.bookmarks.testing.skipDefaultBookmarksImport=true",
"browser.urlbar.trending.featureGate=false",
@@ -280,6 +284,8 @@ support-files = [
["browser_keyword_select_and_type.js"]
+["browser_less_common_selection_manipulations.js"]
+
["browser_loadRace.js"]
["browser_locationBarCommand.js"]
@@ -398,9 +404,6 @@ https_first_disabled = true
["browser_revert.js"]
-["browser_search_continuation.js"]
-support-files = ["search-engines", "../../../search/test/browser/trendingSuggestionEngine.sjs"]
-
["browser_searchFunction.js"]
["browser_searchHistoryLimit.js"]
@@ -490,6 +493,9 @@ support-files = [
["browser_search_bookmarks_from_bookmarks_menu.js"]
+["browser_search_continuation.js"]
+support-files = ["search-engines", "../../../search/test/browser/trendingSuggestionEngine.sjs"]
+
["browser_search_history_from_history_panel.js"]
["browser_selectStaleResults.js"]
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
index f191cae321..d01734959a 100644
--- a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
@@ -2,7 +2,7 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
-async function testVal(aExpected, overflowSide = "") {
+async function testVal(aExpected, overflowSide = null) {
info(`Testing ${aExpected}`);
try {
gURLBar.setURI(makeURI(aExpected));
diff --git a/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js
index 427a7419c8..bb710c7065 100644
--- a/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js
+++ b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js
@@ -98,7 +98,7 @@ add_task(async function clearURLBarAfterManuallyLoadingAboutHome() {
() => {}
);
// This opens about:newtab:
- BrowserOpenTab();
+ BrowserCommands.openTab();
let tab = await promiseTabOpenedAndSwitchedTo;
is(gURLBar.value, "", "URL bar should be empty");
is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
@@ -132,7 +132,7 @@ add_task(async function dontTemporarilyShowAboutHome() {
let win = OpenBrowserWindow();
await windowOpenedPromise;
let promiseTabSwitch = BrowserTestUtils.switchTab(win.gBrowser, () => {});
- win.BrowserOpenTab();
+ win.BrowserCommands.openTab();
await promiseTabSwitch;
currentBrowser = win.gBrowser.selectedBrowser;
is(win.gBrowser.visibleTabs.length, 2, "2 tabs opened");
diff --git a/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js
index 8c4b05501e..54f40a85ee 100644
--- a/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js
+++ b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js
@@ -389,11 +389,11 @@ class TestProvider extends UrlbarTestUtils.TestProvider {
];
}
- onEngagement(state, queryContext, details, controller) {
+ onLegacyEngagement(state, queryContext, details, controller) {
if (details.result?.providerName == this.name) {
let { selType } = details;
- info(`onEngagement called, selType=` + selType);
+ info(`onLegacyEngagement called, selType=` + selType);
if (!this.commandCount.hasOwnProperty(selType)) {
this.commandCount[selType] = 0;
diff --git a/browser/components/urlbar/tests/browser/browser_copy_during_load.js b/browser/components/urlbar/tests/browser/browser_copy_during_load.js
index 3eaa53bcda..e1d352a171 100644
--- a/browser/components/urlbar/tests/browser/browser_copy_during_load.js
+++ b/browser/components/urlbar/tests/browser/browser_copy_during_load.js
@@ -45,7 +45,7 @@ add_task(async function () {
null,
true
);
- BrowserStop();
+ BrowserCommands.stop();
await browserStoppedPromise;
});
});
diff --git a/browser/components/urlbar/tests/browser/browser_dynamicResults.js b/browser/components/urlbar/tests/browser/browser_dynamicResults.js
index aad15e0145..2ba1b7ab5f 100644
--- a/browser/components/urlbar/tests/browser/browser_dynamicResults.js
+++ b/browser/components/urlbar/tests/browser/browser_dynamicResults.js
@@ -511,7 +511,7 @@ add_task(async function shouldNavigate() {
await UrlbarTestUtils.promisePopupClose(window, () =>
EventUtils.synthesizeKey("KEY_Enter")
);
- // Verify that onEngagement was still called.
+ // Verify that onLegacyEngagement was still called.
let [result, pickedElement] = await pickPromise;
Assert.equal(result, row.result, "Picked result");
Assert.equal(pickedElement, element, "Picked element");
@@ -904,7 +904,7 @@ class TestProvider extends UrlbarTestUtils.TestProvider {
};
}
- onEngagement(state, queryContext, details, _controller) {
+ onLegacyEngagement(state, queryContext, details, _controller) {
if (this._pickPromiseResolve) {
let { result, element } = details;
this._pickPromiseResolve([result, element]);
diff --git a/browser/components/urlbar/tests/browser/browser_engagement.js b/browser/components/urlbar/tests/browser/browser_engagement.js
index b1998b6f55..fbc321e322 100644
--- a/browser/components/urlbar/tests/browser/browser_engagement.js
+++ b/browser/components/urlbar/tests/browser/browser_engagement.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-// Tests the UrlbarProvider.onEngagement() method.
+// Tests the UrlbarProvider.onLegacyEngagement() method.
"use strict";
@@ -110,32 +110,21 @@ async function doTest({
let provider = new TestProvider();
UrlbarProvidersManager.registerProvider(provider);
- let startPromise = provider.promiseEngagement();
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window: win,
value: "test",
fireInputEvent: true,
});
- let [state, queryContext, details, controller] = await startPromise;
- Assert.equal(
- controller.input.isPrivate,
- expectedIsPrivate,
- "Start isPrivate"
- );
- Assert.equal(state, "start", "Start state");
-
- // `queryContext` isn't always defined for `start`, and `onEngagement`
- // shouldn't rely on it being defined on start, but there's no good reason to
- // assert that it's not defined here.
-
- // Similarly, `details` is never defined for `start`, but there's no good
- // reason to assert that it's not defined.
-
let endPromise = provider.promiseEngagement();
let { result, element } = (await endEngagement()) ?? {};
- [state, queryContext, details, controller] = await endPromise;
+ let [state, queryContext, details, controller] = await endPromise;
+
+ Assert.ok(
+ ["engagement", "abandonment"].includes(state),
+ "State should be either 'engagement' or 'abandonment'"
+ );
Assert.equal(controller.input.isPrivate, expectedIsPrivate, "End isPrivate");
Assert.equal(state, expectedEndState, "End state");
Assert.ok(queryContext, "End queryContext");
@@ -179,7 +168,7 @@ async function doTest({
}
/**
- * Test provider that resolves promises when onEngagement is called.
+ * Test provider that resolves promises when onLegacyEngagement is called.
*/
class TestProvider extends UrlbarTestUtils.TestProvider {
_resolves = [];
@@ -197,7 +186,7 @@ class TestProvider extends UrlbarTestUtils.TestProvider {
});
}
- onEngagement(...args) {
+ onLegacyEngagement(...args) {
let resolve = this._resolves.shift();
if (resolve) {
resolve(args);
diff --git a/browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js b/browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js
new file mode 100644
index 0000000000..2ad6ee0e07
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js
@@ -0,0 +1,288 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests less common mouse/keyboard manipulations of the address bar input
+ * field selection, for example:
+ * - Home/Del
+ * - Shift+Right/Left
+ * - Drag selection
+ * - Double-click on word
+ *
+ * All the tests set up some initial conditions, and check it. Then optionally
+ * they can manipulate the selection further, and check the results again.
+ * We want to ensure the final selection is the expected one, even if in the
+ * future we change our trimming strategy for the input field value.
+ */
+
+const tests = [
+ {
+ description: "Test HOME starting from full selection",
+ openPanel() {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ },
+ get selection() {
+ return [0, gURLBar.value.length];
+ },
+ manipulate() {
+ // Cursor must move to the first visible character, regardless of any
+ // "untrimming" we could be doing.
+ this._visibleValue = gURLBar.value;
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_Home");
+ }
+ },
+ get modifiedSelection() {
+ let start = gURLBar.value.indexOf(this._visibleValue);
+ return [start, start];
+ },
+ },
+ {
+ description: "Test END starting from full selection",
+ openPanel() {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ },
+ get selection() {
+ return [0, gURLBar.value.length];
+ },
+ manipulate() {
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowRight", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_End", {});
+ }
+ },
+ get modifiedSelection() {
+ return [gURLBar.value.length, gURLBar.value.length];
+ },
+ },
+ {
+ description: "Test SHIFT+LEFT starting from full selection",
+ openPanel() {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ },
+ get selection() {
+ return [0, gURLBar.value.length];
+ },
+ manipulate() {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ },
+ get modifiedSelection() {
+ return [0, gURLBar.value.length - 1];
+ },
+ },
+ {
+ description: "Test SHIFT+RIGHT starting from full selection",
+ openPanel() {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ },
+ get selection() {
+ return [0, gURLBar.value.length];
+ },
+ manipulate() {
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ },
+ get modifiedSelection() {
+ return [0, gURLBar.value.length];
+ },
+ },
+ {
+ description: "Test Drag Selection from the first character",
+ async openPanel() {
+ this._expectedSelectedText = gURLBar.value.substring(0, 5);
+ await selectWithMouseDrag(
+ getTextWidth(gURLBar.value[0]) / 2 - 1,
+ getTextWidth(gURLBar.value.substring(0, 5))
+ );
+ },
+ get selection() {
+ return [
+ 0,
+ gURLBar.value.indexOf(this._expectedSelectedText) +
+ this._expectedSelectedText.length,
+ ];
+ },
+ },
+ {
+ description: "Test Drag Selection from the last character",
+ async openPanel() {
+ this._expectedSelectedText = gURLBar.value.substring(-5);
+ await selectWithMouseDrag(
+ getTextWidth(gURLBar.value) + 1,
+ getTextWidth(this._expectedSelectedText)
+ );
+ },
+ get selection() {
+ return [
+ gURLBar.value.indexOf(this._expectedSelectedText),
+ gURLBar.value.length,
+ ];
+ },
+ },
+ {
+ description: "Test Drag Selection in the middle of the string",
+ async openPanel() {
+ this._expectedSelectedText = gURLBar.value.substring(5, 10);
+ await selectWithMouseDrag(
+ getTextWidth(gURLBar.value.substring(0, 5)),
+ getTextWidth(gURLBar.value.substring(0, 10))
+ );
+ },
+ get selection() {
+ let start = gURLBar.value.indexOf(this._expectedSelectedText);
+ return [start, start + this._expectedSelectedText.length];
+ },
+ },
+ {
+ description: "Test Double-click on word",
+ async openPanel() {
+ let wordBoundaryIndex = gURLBar.value.search(/\btest/);
+ this._expectedSelectedText = "test";
+ await selectWithDoubleClick(
+ getTextWidth(gURLBar.value.substring(0, wordBoundaryIndex))
+ );
+ },
+ get selection() {
+ let start = gURLBar.value.indexOf(this._expectedSelectedText);
+ return [start, start + this._expectedSelectedText.length];
+ },
+ },
+ {
+ description: "Click at the right of the text",
+ openPanel() {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ },
+ get selection() {
+ return [0, gURLBar.value.length];
+ },
+ manipulate() {
+ let rect = gURLBar.inputField.getBoundingClientRect();
+ EventUtils.synthesizeMouse(
+ gURLBar.inputField,
+ getTextWidth(gURLBar.value) + 10,
+ rect.height / 2,
+ {}
+ );
+ },
+ get modifiedSelection() {
+ return [gURLBar.value.length, gURLBar.value.length];
+ },
+ },
+];
+
+add_setup(async function () {
+ gURLBar.inputField.style.font = "14px monospace";
+ registerCleanupFunction(() => {
+ gURLBar.inputField.style.font = null;
+ });
+});
+
+add_task(async function https() {
+ await doTest("https://example.com/test/some/page.htm");
+});
+
+add_task(async function http() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await doTest("http://example.com/test/other/page.htm");
+});
+
+async function doTest(url) {
+ await BrowserTestUtils.withNewTab(url, async () => {
+ for (let test of tests) {
+ gURLBar.blur();
+ info(test.description);
+ await UrlbarTestUtils.promisePopupOpen(window, async () => {
+ await test.openPanel();
+ });
+ info(
+ `Selected text is <${gURLBar.value.substring(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd
+ )}>`
+ );
+ Assert.deepEqual(
+ test.selection,
+ [gURLBar.selectionStart, gURLBar.selectionEnd],
+ "Check selection"
+ );
+
+ if (test.manipulate) {
+ await test.manipulate();
+ info(
+ `Selected text is <${gURLBar.value.substring(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd
+ )}>`
+ );
+ Assert.deepEqual(
+ test.modifiedSelection,
+ [gURLBar.selectionStart, gURLBar.selectionEnd],
+ "Check selection after manipulation"
+ );
+ }
+ }
+ });
+}
+
+function getTextWidth(inputText) {
+ const canvas =
+ getTextWidth.canvas ||
+ (getTextWidth.canvas = document.createElement("canvas"));
+ let context = canvas.getContext("2d");
+ context.font = window
+ .getComputedStyle(gURLBar.inputField)
+ .getPropertyValue("font");
+ return context.measureText(inputText).width;
+}
+
+function selectWithMouseDrag(fromX, toX) {
+ let target = gURLBar.inputField;
+ let rect = target.getBoundingClientRect();
+ let promise = BrowserTestUtils.waitForEvent(target, "mouseup");
+ EventUtils.synthesizeMouse(
+ target,
+ fromX,
+ rect.height / 2,
+ { type: "mousemove" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ fromX,
+ rect.height / 2,
+ { type: "mousedown" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ toX,
+ rect.height / 2,
+ { type: "mousemove" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ toX,
+ rect.height / 2,
+ { type: "mouseup" },
+ target.ownerGlobal
+ );
+ return promise;
+}
+
+function selectWithDoubleClick(offsetX) {
+ let target = gURLBar.inputField;
+ let rect = target.getBoundingClientRect();
+ let promise = BrowserTestUtils.waitForEvent(target, "dblclick");
+ EventUtils.synthesizeMouse(target, offsetX, rect.height / 2, {
+ clickCount: 1,
+ });
+ EventUtils.synthesizeMouse(target, offsetX, rect.height / 2, {
+ clickCount: 2,
+ });
+ return promise;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
index 84c45e586a..92409f979f 100644
--- a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
+++ b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
@@ -276,7 +276,7 @@ async function typeAndCommand(eventType, details = {}) {
async function triggerCommand(eventType, details = {}) {
Assert.equal(
await UrlbarTestUtils.promiseUserContextId(window),
- gBrowser.selectedTab.getAttribute("usercontextid"),
+ gBrowser.selectedTab.getAttribute("usercontextid") || "",
"userContextId must be the same as the originating tab"
);
diff --git a/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js
index 2f8e871bfe..66a8ed3a41 100644
--- a/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js
+++ b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js
@@ -42,7 +42,7 @@ add_task(async function () {
// tab button.
let userInput = window.windowUtils.setHandlingUserInput(true);
try {
- BrowserOpenTab();
+ BrowserCommands.openTab();
} finally {
userInput.destruct();
}
diff --git a/browser/components/urlbar/tests/browser/browser_raceWithTabs.js b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js
index 17560ea101..821aa0f0ee 100644
--- a/browser/components/urlbar/tests/browser/browser_raceWithTabs.js
+++ b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js
@@ -41,7 +41,7 @@ add_task(async function hitEnterLoadInRightTab() {
gBrowser.tabContainer,
"TabOpen"
);
- BrowserOpenTab();
+ BrowserCommands.openTab();
let oldTab = (await oldTabOpenPromise).target;
let oldTabLoadedPromise = BrowserTestUtils.browserLoaded(
oldTab.linkedBrowser,
@@ -60,7 +60,7 @@ add_task(async function hitEnterLoadInRightTab() {
EventUtils.sendKey("return");
info("Immediately open a second tab");
- BrowserOpenTab();
+ BrowserCommands.openTab();
let newTab = (await tabOpenPromise).target;
info("Created new tab; waiting for tabs to load");
diff --git a/browser/components/urlbar/tests/browser/browser_result_menu.js b/browser/components/urlbar/tests/browser/browser_result_menu.js
index ccbe247598..f00b92fa63 100644
--- a/browser/components/urlbar/tests/browser/browser_result_menu.js
+++ b/browser/components/urlbar/tests/browser/browser_result_menu.js
@@ -201,10 +201,10 @@ add_task(async function firefoxSuggest() {
],
});
- // Implement the provider's `onEngagement()` so it removes the result.
- let onEngagementCallCount = 0;
- provider.onEngagement = (state, queryContext, details, controller) => {
- onEngagementCallCount++;
+ // Implement the provider's `onLegacyEngagement()` so it removes the result.
+ let onLegacyEngagementCallCount = 0;
+ provider.onLegacyEngagement = (state, queryContext, details, controller) => {
+ onLegacyEngagementCallCount++;
controller.removeResult(details.result);
};
@@ -245,9 +245,9 @@ add_task(async function firefoxSuggest() {
});
Assert.greater(
- onEngagementCallCount,
+ onLegacyEngagementCallCount,
0,
- "onEngagement() should have been called"
+ "onLegacyEngagement() should have been called"
);
Assert.equal(
UrlbarTestUtils.getResultCount(window),
diff --git a/browser/components/urlbar/tests/browser/browser_stop_pending.js b/browser/components/urlbar/tests/browser/browser_stop_pending.js
index 50f5dfdeec..938a57dc28 100644
--- a/browser/components/urlbar/tests/browser/browser_stop_pending.js
+++ b/browser/components/urlbar/tests/browser/browser_stop_pending.js
@@ -125,7 +125,7 @@ add_task(async function () {
null,
true
);
- BrowserStop();
+ BrowserCommands.stop();
await browserStoppedPromise;
is(
@@ -207,7 +207,7 @@ add_task(async function () {
null,
true
);
- BrowserStop();
+ BrowserCommands.stop();
await browserStoppedPromise;
is(
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js
index 318b29ad19..b2591a0c14 100644
--- a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js
@@ -344,8 +344,8 @@ async function impressions_test(isOnboarding) {
5
);
- // See javadoc for UrlbarProviderTabToSearch.onEngagement for discussion
- // about retained results.
+ // See javadoc for UrlbarProviderTabToSearch.onLegacyEngagement for
+ // discussion about retained results.
info("Reopen the result set with retained results. Record impression.");
await UrlbarTestUtils.promisePopupOpen(window, () => {
EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml
index cf6bc80318..a72f2d9b8d 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml
@@ -26,8 +26,6 @@ prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"]
["browser_glean_telemetry_abandonment_n_chars_n_words.js"]
-["browser_glean_telemetry_abandonment_type.js"]
-
["browser_glean_telemetry_abandonment_sap.js"]
["browser_glean_telemetry_abandonment_search_engine_default_id.js"]
@@ -36,6 +34,8 @@ prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"]
["browser_glean_telemetry_abandonment_tips.js"]
+["browser_glean_telemetry_abandonment_type.js"]
+
["browser_glean_telemetry_engagement_edge_cases.js"]
["browser_glean_telemetry_engagement_groups.js"]
@@ -66,4 +66,8 @@ skip-if = ["verify"] # Bug 1852375 - MerinoTestUtils.initWeather() doesn't play
["browser_glean_telemetry_exposure_edge_cases.js"]
+["browser_glean_telemetry_potential_exposure.js"]
+
["browser_glean_telemetry_record_preferences.js"]
+
+["browser_glean_telemetry_reenter.js"]
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js
index 99145d7cc3..b8a16bd10c 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js
@@ -19,7 +19,7 @@ function checkUrlbarFocus(win, focusState) {
// URL bar records the correct abandonment telemetry with abandonment type
// "tab_swtich".
add_task(async function tabSwitchFocusedToFocused() {
- await doTest(async browser => {
+ await doTest(async () => {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "test search",
@@ -45,7 +45,7 @@ add_task(async function tabSwitchFocusedToFocused() {
// URL bar loses focus logs abandonment telemetry with abandonment type
// "blur".
add_task(async function tabSwitchFocusedToUnfocused() {
- await doTest(async browser => {
+ await doTest(async () => {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "test search",
@@ -65,7 +65,7 @@ add_task(async function tabSwitchFocusedToUnfocused() {
// the URL bar gains focus does not record any abandonment telemetry, reflecting
// no change in focus state relevant to abandonment.
add_task(async function tabSwitchUnFocusedToFocused() {
- await doTest(async browser => {
+ await doTest(async () => {
checkUrlbarFocus(window, false);
let promiseTabOpened = BrowserTestUtils.waitForEvent(
@@ -91,7 +91,7 @@ add_task(async function tabSwitchUnFocusedToFocused() {
// Checks that switching between two tabs, both with unfocused URL bars, does
// not trigger any abandonment telmetry.
add_task(async function tabSwitchUnFocusedToUnFocused() {
- await doTest(async browser => {
+ await doTest(async () => {
checkUrlbarFocus(window, false);
let tab2 = await BrowserTestUtils.openNewForegroundTab(window.gBrowser);
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js
index ff31bdc52a..053d307088 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js
@@ -63,7 +63,7 @@ add_task(async function selected_result_tip() {
),
],
priority: 1,
- onEngagement: () => {
+ onLegacyEngagement: () => {
deferred.resolve();
},
});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js
index 6b1dedbce2..59c4460e52 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js
@@ -101,11 +101,32 @@ add_task(async function engagement_type_dismiss() {
});
add_task(async function engagement_type_help() {
- const cleanupQuickSuggest = await ensureQuickSuggestInit();
+ const url = "https://example.com/";
+ const helpUrl = "https://example.com/help";
+ let provider = new UrlbarTestUtils.TestProvider({
+ priority: Infinity,
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url,
+ isBlockable: true,
+ blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest" },
+ helpUrl,
+ helpL10n: {
+ id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ },
+ }
+ ),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(provider);
await doTest(async () => {
- await openPopup("sponsored");
- await selectRowByURL("https://example.com/sponsored");
+ await openPopup("test");
+ await selectRowByURL(url);
+
const onTabOpened = BrowserTestUtils.waitForNewTab(gBrowser);
UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L");
const tab = await onTabOpened;
@@ -114,5 +135,26 @@ add_task(async function engagement_type_help() {
assertEngagementTelemetry([{ engagement_type: "help" }]);
});
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function engagement_type_manage() {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit();
+
+ await doTest(async () => {
+ await openPopup("sponsored");
+ await selectRowByURL("https://example.com/sponsored");
+
+ const onManagePageLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "about:preferences#search"
+ );
+ UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "M");
+ await onManagePageLoaded;
+
+ assertEngagementTelemetry([{ engagement_type: "manage" }]);
+ });
+
await cleanupQuickSuggest();
});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js
index 07e8b9b360..ef2ec623bc 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js
@@ -11,7 +11,7 @@ add_setup(async function () {
await initExposureTest();
});
-add_task(async function exposureSponsoredOnEngagement() {
+add_task(async function exposureSponsoredOnLegacyEngagement() {
await doExposureTest({
prefs: [
["browser.urlbar.exposureResults", suggestResultType("adm_sponsored")],
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_potential_exposure.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_potential_exposure.js
new file mode 100644
index 0000000000..275e3968eb
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_potential_exposure.js
@@ -0,0 +1,438 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests the `urlbar-potential-exposure` ping.
+
+const WAIT_FOR_PING_TIMEOUT_MS = 1000;
+
+// Avoid timeouts in verify mode, especially on Mac.
+requestLongerTimeout(3);
+
+add_setup(async function test_setup() {
+ Services.fog.testResetFOG();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ registerCleanupFunction(() => {
+ Services.fog.testResetFOG();
+ });
+});
+
+add_task(async function oneKeyword_noMatch_1() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["exam"],
+ expectedEvents: [],
+ });
+});
+
+add_task(async function oneKeyword_noMatch_2() {
+ await doTest({
+ keywords: ["exam"],
+ searchStrings: ["example"],
+ expectedEvents: [],
+ });
+});
+
+add_task(async function oneKeyword_oneMatch_terminal_1() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["example"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: true } }],
+ });
+});
+
+add_task(async function oneKeyword_oneMatch_terminal_2() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["exam", "example"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: true } }],
+ });
+});
+
+add_task(async function oneKeyword_oneMatch_nonterminal_1() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["example", "exam"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: false } }],
+ });
+});
+
+add_task(async function oneKeyword_oneMatch_nonterminal_2() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["ex", "example", "exam"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: false } }],
+ });
+});
+
+add_task(async function oneKeyword_dupeMatches_terminal_1() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["example", "example"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: true } }],
+ });
+});
+
+add_task(async function oneKeyword_dupeMatches_terminal_2() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["example", "exampl", "example"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: true } }],
+ });
+});
+
+add_task(async function oneKeyword_dupeMatches_terminal_3() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["exam", "example", "example"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: true } }],
+ });
+});
+
+add_task(async function oneKeyword_dupeMatches_terminal_4() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["exam", "example", "exampl", "example"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: true } }],
+ });
+});
+
+add_task(async function oneKeyword_dupeMatches_nonterminal_1() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["example", "example", "exampl"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: false } }],
+ });
+});
+
+add_task(async function oneKeyword_dupeMatches_nonterminal_2() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["exam", "example", "example", "exampl"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: false } }],
+ });
+});
+
+add_task(async function oneKeyword_dupeMatches_nonterminal_3() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["example", "exam", "example", "exampl"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: false } }],
+ });
+});
+
+add_task(async function oneKeyword_dupeMatches_nonterminal_4() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["exam", "example", "exampl", "example", "exampl"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: false } }],
+ });
+});
+
+add_task(async function manyKeywords_noMatch() {
+ await doTest({
+ keywords: ["foo", "bar", "baz"],
+ searchStrings: ["example"],
+ expectedEvents: [],
+ });
+});
+
+add_task(async function manyKeywords_oneMatch_terminal_1() {
+ await doTest({
+ keywords: ["foo", "bar", "baz"],
+ searchStrings: ["bar"],
+ expectedEvents: [{ extra: { keyword: "bar", terminal: true } }],
+ });
+});
+
+add_task(async function manyKeywords_oneMatch_terminal_2() {
+ await doTest({
+ keywords: ["foo", "bar", "baz"],
+ searchStrings: ["example", "bar"],
+ expectedEvents: [{ extra: { keyword: "bar", terminal: true } }],
+ });
+});
+
+add_task(async function manyKeywords_oneMatch_nonterminal_1() {
+ await doTest({
+ keywords: ["foo", "bar", "baz"],
+ searchStrings: ["bar", "example"],
+ expectedEvents: [{ extra: { keyword: "bar", terminal: false } }],
+ });
+});
+
+add_task(async function manyKeywords_oneMatch_nonterminal_2() {
+ await doTest({
+ keywords: ["foo", "bar", "baz"],
+ searchStrings: ["exam", "bar", "example"],
+ expectedEvents: [{ extra: { keyword: "bar", terminal: false } }],
+ });
+});
+
+add_task(async function manyKeywords_manyMatches_terminal_1() {
+ let keywords = ["foo", "bar", "baz"];
+ await doTest({
+ keywords,
+ searchStrings: keywords,
+ expectedEvents: keywords.map((keyword, i) => ({
+ extra: { keyword, terminal: i == keywords.length - 1 },
+ })),
+ });
+});
+
+add_task(async function manyKeywords_manyMatches_terminal_2() {
+ let keywords = ["foo", "bar", "baz"];
+ await doTest({
+ keywords,
+ searchStrings: ["exam", "foo", "exampl", "bar", "example", "baz"],
+ expectedEvents: keywords.map((keyword, i) => ({
+ extra: { keyword, terminal: i == keywords.length - 1 },
+ })),
+ });
+});
+
+add_task(async function manyKeywords_manyMatches_nonterminal_1() {
+ let keywords = ["foo", "bar", "baz"];
+ await doTest({
+ keywords,
+ searchStrings: ["foo", "bar", "baz", "example"],
+ expectedEvents: keywords.map(keyword => ({
+ extra: { keyword, terminal: false },
+ })),
+ });
+});
+
+add_task(async function manyKeywords_manyMatches_nonterminal_2() {
+ let keywords = ["foo", "bar", "baz"];
+ await doTest({
+ keywords,
+ searchStrings: ["exam", "foo", "exampl", "bar", "example", "baz", "exam"],
+ expectedEvents: keywords.map(keyword => ({
+ extra: { keyword, terminal: false },
+ })),
+ });
+});
+
+add_task(async function manyKeywords_dupeMatches_terminal() {
+ let keywords = ["foo", "bar", "baz"];
+ let searchStrings = [...keywords, ...keywords];
+ await doTest({
+ keywords,
+ searchStrings,
+ expectedEvents: keywords.map((keyword, i) => ({
+ extra: { keyword, terminal: i == keywords.length - 1 },
+ })),
+ });
+});
+
+add_task(async function manyKeywords_dupeMatches_nonterminal() {
+ let keywords = ["foo", "bar", "baz"];
+ let searchStrings = [...keywords, ...keywords, "example"];
+ await doTest({
+ keywords,
+ searchStrings,
+ expectedEvents: keywords.map(keyword => ({
+ extra: { keyword, terminal: false },
+ })),
+ });
+});
+
+add_task(async function searchStringNormalization_terminal() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: [" ExaMPLe "],
+ expectedEvents: [{ extra: { keyword: "example", terminal: true } }],
+ });
+});
+
+add_task(async function searchStringNormalization_nonterminal() {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: [" ExaMPLe ", "foo"],
+ expectedEvents: [{ extra: { keyword: "example", terminal: false } }],
+ });
+});
+
+add_task(async function multiWordKeyword() {
+ await doTest({
+ keywords: ["this has multiple words"],
+ searchStrings: ["this has multiple words"],
+ expectedEvents: [
+ { extra: { keyword: "this has multiple words", terminal: true } },
+ ],
+ });
+});
+
+// Smoke test that ends a session with an engagement instead of an abandonment
+// as other tasks in this file do.
+add_task(async function engagement() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await doTest({
+ keywords: ["example"],
+ searchStrings: ["example"],
+ endSession: () =>
+ // Hit the Enter key on the heuristic search result.
+ UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ ),
+ expectedEvents: [{ extra: { keyword: "example", terminal: true } }],
+ });
+ });
+});
+
+// Smoke test that uses Nimbus to set keywords instead of a pref as other tasks
+// in this file do.
+add_task(async function nimbus() {
+ let keywords = ["foo", "bar", "baz"];
+ await doTest({
+ useNimbus: true,
+ keywords,
+ searchStrings: keywords,
+ expectedEvents: keywords.map((keyword, i) => ({
+ extra: { keyword, terminal: i == keywords.length - 1 },
+ })),
+ });
+});
+
+// The ping should not be submitted for sessions in private windows.
+add_task(async function privateWindow() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await doTest({
+ win: privateWin,
+ keywords: ["example"],
+ searchStrings: ["example"],
+ expectedEvents: [],
+ });
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function invalidPotentialExposureKeywords_pref() {
+ await doTest({
+ keywords: "not an array of keywords",
+ searchStrings: ["example", "not an array of keywords"],
+ expectedEvents: [],
+ });
+});
+
+add_task(async function invalidPotentialExposureKeywords_nimbus() {
+ await doTest({
+ useNimbus: true,
+ keywords: "not an array of keywords",
+ searchStrings: ["example", "not an array of keywords"],
+ expectedEvents: [],
+ });
+});
+
+async function doTest({
+ keywords,
+ searchStrings,
+ expectedEvents,
+ endSession = null,
+ useNimbus = false,
+ win = window,
+}) {
+ endSession ||= () =>
+ UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur());
+
+ let nimbusCleanup;
+ let keywordsJson = JSON.stringify(keywords);
+ if (useNimbus) {
+ nimbusCleanup = await UrlbarTestUtils.initNimbusFeature({
+ potentialExposureKeywords: keywordsJson,
+ });
+ } else {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.potentialExposureKeywords", keywordsJson]],
+ });
+ }
+
+ let pingPromise = waitForPing();
+
+ for (let value of searchStrings) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value,
+ window: win,
+ });
+ }
+ await endSession();
+
+ // Wait `WAIT_FOR_PING_TIMEOUT_MS` for the ping to be submitted before
+ // reporting a timeout. Note that some tasks do not expect a ping to be
+ // submitted, and they rely on this timeout behavior.
+ info("Awaiting ping promise");
+ let events = null;
+ events = await Promise.race([
+ pingPromise,
+ new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ if (!events) {
+ info("Timed out waiting for ping");
+ }
+ resolve([]);
+ }, WAIT_FOR_PING_TIMEOUT_MS)
+ ),
+ ]);
+
+ assertEvents(events, expectedEvents);
+
+ if (nimbusCleanup) {
+ await nimbusCleanup();
+ } else {
+ await SpecialPowers.popPrefEnv();
+ }
+ Services.fog.testResetFOG();
+}
+
+function waitForPing() {
+ return new Promise(resolve => {
+ GleanPings.urlbarPotentialExposure.testBeforeNextSubmit(() => {
+ let events = Glean.urlbar.potentialExposure.testGetValue();
+ info("testBeforeNextSubmit got events: " + JSON.stringify(events));
+ resolve(events);
+ });
+ });
+}
+
+function assertEvents(actual, expected) {
+ info("Comparing events: " + JSON.stringify({ actual, expected }));
+
+ // Add some expected boilerplate properties to the expected events so that
+ // callers don't have to but so that we still check them.
+ expected = expected.map(e => ({
+ category: "urlbar",
+ name: "potential_exposure",
+ // `testGetValue()` stringifies booleans for some reason. Let callers
+ // specify booleans since booleans are correct, and stringify them here.
+ ...stringifyBooleans(e),
+ }));
+
+ // Filter out properties from the actual events that aren't defined in the
+ // expected events. Ignore unimportant properties like timestamps.
+ actual = actual.map((a, i) =>
+ Object.fromEntries(
+ Object.entries(a).filter(([key]) => expected[i]?.hasOwnProperty(key))
+ )
+ );
+
+ Assert.deepEqual(actual, expected, "Checking expected Glean events");
+}
+
+function stringifyBooleans(obj) {
+ let newObj = {};
+ for (let [key, value] of Object.entries(obj)) {
+ if (value && typeof value == "object") {
+ newObj[key] = stringifyBooleans(value);
+ } else if (typeof value == "boolean") {
+ newObj[key] = String(value);
+ } else {
+ newObj[key] = value;
+ }
+ }
+ return newObj;
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_reenter.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_reenter.js
new file mode 100644
index 0000000000..51bdc84870
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_reenter.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test we don't re-enter record() (and record both an engagement and an
+// abandonment) when handling an engagement blurs the input field.
+
+const TEST_URL = "https://example.com/";
+
+add_task(async function () {
+ await setup();
+ let deferred = Promise.withResolvers();
+ const provider = new UrlbarTestUtils.TestProvider({
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url: TEST_URL,
+ helpUrl: "https://example.com/help",
+ helpL10n: {
+ id: "urlbar-result-menu-tip-get-help",
+ },
+ }
+ ),
+ ],
+ priority: 999,
+ onLegacyEngagement: () => {
+ info("Blur the address bar during the onLegacyEngagement notification");
+ gURLBar.blur();
+ // Run at the next tick to be sure spurious events would have happened.
+ TestUtils.waitForTick().then(() => {
+ deferred.resolve();
+ });
+ },
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ // This should cover at least engagement and abandonment.
+ let engagementSpy = sinon.spy(provider, "onLegacyEngagement");
+
+ let beforeRecordCall = false,
+ recordReentered = false;
+ let recordStub = sinon
+ .stub(gURLBar.controller.engagementEvent, "record")
+ .callsFake((...args) => {
+ recordReentered = beforeRecordCall;
+ beforeRecordCall = true;
+ recordStub.wrappedMethod.apply(gURLBar.controller.engagementEvent, args);
+ beforeRecordCall = false;
+ });
+
+ registerCleanupFunction(() => {
+ sinon.restore();
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+
+ await doTest(async () => {
+ await openPopup("example");
+ await selectRowByURL(TEST_URL);
+ EventUtils.synthesizeKey("VK_RETURN");
+ await deferred.promise;
+
+ assertEngagementTelemetry([{ engagement_type: "enter" }]);
+ assertAbandonmentTelemetry([]);
+
+ Assert.ok(recordReentered, "`record()` was re-entered");
+ Assert.equal(
+ engagementSpy.callCount,
+ 1,
+ "`onLegacyEngagement` was invoked twice"
+ );
+ Assert.equal(
+ engagementSpy.args[0][0],
+ "engagement",
+ "`engagement` notified"
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js
index 4317a50930..1373cc7e27 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/head.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js
@@ -10,6 +10,7 @@ Services.scriptloader.loadSubScript(
ChromeUtils.defineESModuleGetters(this, {
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
});
const lazy = {};
@@ -210,12 +211,7 @@ async function doTest(testFn) {
await QuickSuggest.blockedSuggestions.clear();
await QuickSuggest.blockedSuggestions._test_readyPromise;
await updateTopSites(() => true);
-
- try {
- await BrowserTestUtils.withNewTab(gBrowser, testFn);
- } catch (e) {
- console.error(e);
- }
+ await BrowserTestUtils.withNewTab(gBrowser, testFn);
}
async function initGroupTest() {
diff --git a/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs
index 2ba9dce8be..1002b4e231 100644
--- a/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs
+++ b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs
@@ -490,6 +490,8 @@ class _QuickSuggestTestUtils {
* Whether the result is expected to be sponsored.
* @param {boolean} [options.isBestMatch]
* Whether the result is expected to be a best match.
+ * @param {boolean} [options.isManageable]
+ * Whether the result is expected to show Manage result menu item.
* @returns {result}
* The quick suggest result.
*/
@@ -500,6 +502,7 @@ class _QuickSuggestTestUtils {
index = -1,
isSponsored = true,
isBestMatch = false,
+ isManageable = true,
} = {}) {
this.Assert.ok(
url || originalUrl,
@@ -574,11 +577,19 @@ class _QuickSuggestTestUtils {
}
this.Assert.equal(
- result.payload.helpUrl,
- lazy.QuickSuggest.HELP_URL,
- "Result helpURL"
+ result.payload.isManageable,
+ isManageable,
+ "Result isManageable"
);
+ if (!isManageable) {
+ this.Assert.equal(
+ result.payload.helpUrl,
+ lazy.QuickSuggest.HELP_URL,
+ "Result helpURL"
+ );
+ }
+
this.Assert.ok(
row._buttons.get("menu"),
"The menu button should be present"
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
index 130afe8c53..98f6ba6117 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
@@ -164,3 +164,40 @@ add_tasks_with_rust(
await cleanUpNimbus();
}
);
+
+// Tests the "Manage" result menu for sponsored suggestion.
+add_tasks_with_rust(async function resultMenu_manage_sponsored() {
+ await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "fra",
+ });
+
+ const managePage = "about:preferences#search";
+ let onManagePageLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ managePage
+ );
+ // Click the command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, "manage", {
+ resultIndex: 1,
+ });
+ await onManagePageLoaded;
+ Assert.equal(
+ browser.currentURI.spec,
+ managePage,
+ "The manage page is loaded"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Tests the "Manage" result menu for non-sponsored suggestion.
+add_tasks_with_rust(async function resultMenu_manage_nonSponsored() {
+ await doManageTest({
+ input: "nonspon",
+ index: 1,
+ });
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js
index b09345aa54..f34b479134 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js
@@ -245,10 +245,15 @@ add_task(async function resultMenu_notInterested() {
});
// Tests the "Not relevant" result menu dismissal command.
-add_task(async function notRelevant() {
+add_task(async function resultMenu_notRelevant() {
await doDismissTest("not_relevant", false);
});
+// Tests the "Manage" result menu.
+add_task(async function resultMenu_manage() {
+ await doManageTest({ input: "only match the Merino suggestion", index: 1 });
+});
+
// Tests the row/group label.
add_task(async function rowLabel() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
index c400cf72f6..3fa91e5a32 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
@@ -5,11 +5,6 @@
"use strict";
-ChromeUtils.defineESModuleGetters(this, {
- CONTEXTUAL_SERVICES_PING_TYPES:
- "resource:///modules/PartnerLinkAttribution.sys.mjs",
-});
-
const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
const { TIMESTAMP_TEMPLATE } = QuickSuggest;
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js
index b7da7533c4..33bd37703d 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js
@@ -21,6 +21,9 @@ const REMOTE_SETTINGS_DATA = [
},
];
+// Avoid timeouts in verify mode. They're especially common on Mac.
+requestLongerTimeout(5);
+
add_setup(async function () {
await QuickSuggestTestUtils.ensureQuickSuggestInit({
remoteSettingsRecords: REMOTE_SETTINGS_DATA,
@@ -28,35 +31,37 @@ add_setup(async function () {
});
add_tasks_with_rust(async function basic() {
- const suggestion = REMOTE_SETTINGS_DATA[0].attachment[0];
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: suggestion.keywords[0],
- });
- Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
-
- const { element, result } = await UrlbarTestUtils.getDetailsOfResultAt(
- window,
- 1
- );
- Assert.equal(
- result.providerName,
- UrlbarProviderQuickSuggest.name,
- "The result should be from the expected provider"
- );
- Assert.equal(
- result.payload.provider,
- UrlbarPrefs.get("quickSuggestRustEnabled") ? "Mdn" : "MDNSuggestions"
- );
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ const suggestion = REMOTE_SETTINGS_DATA[0].attachment[0];
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: suggestion.keywords[0],
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element, result } = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 1
+ );
+ Assert.equal(
+ result.providerName,
+ UrlbarProviderQuickSuggest.name,
+ "The result should be from the expected provider"
+ );
+ Assert.equal(
+ result.payload.provider,
+ UrlbarPrefs.get("quickSuggestRustEnabled") ? "Mdn" : "MDNSuggestions"
+ );
- const onLoad = BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- false,
- result.payload.url
- );
- EventUtils.synthesizeMouseAtCenter(element.row, {});
- await onLoad;
- Assert.ok(true, "Expected page is loaded");
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ result.payload.url
+ );
+ EventUtils.synthesizeMouseAtCenter(element.row, {});
+ await onLoad;
+ Assert.ok(true, "Expected page is loaded");
+ });
await PlacesUtils.history.clear();
});
@@ -111,7 +116,7 @@ add_tasks_with_rust(async function resultMenu_notInterested() {
});
// Tests the "Not relevant" result menu dismissal command.
-add_tasks_with_rust(async function notRelevant() {
+add_tasks_with_rust(async function resultMenu_notRelevant() {
await doDismissTest("not_relevant");
Assert.equal(UrlbarPrefs.get("suggest.mdn"), true);
@@ -123,6 +128,11 @@ add_tasks_with_rust(async function notRelevant() {
await QuickSuggest.blockedSuggestions.clear();
});
+// Tests the "Manage" result menu.
+add_tasks_with_rust(async function resultMenu_manage() {
+ await doManageTest({ input: "array", index: 1 });
+});
+
async function doDismissTest(command) {
const keyword = REMOTE_SETTINGS_DATA[0].attachment[0].keywords[0];
// Do a search.
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js
index 0064b6a297..a40a35893b 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js
@@ -4,12 +4,6 @@
"use strict";
// Browser tests for Pocket suggestions.
-//
-// TODO: Make this work with Rust enabled. Right now, running this test with
-// Rust hits the following error on ingest, which prevents ingest from finishing
-// successfully:
-//
-// 0:03.17 INFO Console message: [JavaScript Error: "1698289045697 urlbar ERROR QuickSuggest.SuggestBackendRust :: Ingest error: Error executing SQL: FOREIGN KEY constraint failed" {file: "resource://gre/modules/Log.sys.mjs" line: 722}]
// The expected index of the Pocket suggestion.
const EXPECTED_RESULT_INDEX = 1;
@@ -30,6 +24,8 @@ const REMOTE_SETTINGS_DATA = [
},
];
+requestLongerTimeout(5);
+
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
@@ -47,7 +43,7 @@ add_setup(async function () {
});
});
-add_task(async function basic() {
+add_tasks_with_rust(async function basic() {
await BrowserTestUtils.withNewTab("about:blank", async () => {
// Do a search.
await UrlbarTestUtils.promiseAutocompleteResultPopup({
@@ -96,7 +92,7 @@ add_task(async function basic() {
});
// Tests the "Show less frequently" command.
-add_task(async function resultMenu_showLessFrequently() {
+add_tasks_with_rust(async function resultMenu_showLessFrequently() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.urlbar.pocket.featureGate", true],
@@ -235,7 +231,7 @@ async function doShowLessFrequently({ input, expected, keepViewOpen = false }) {
}
// Tests the "Not interested" result menu dismissal command.
-add_task(async function resultMenu_notInterested() {
+add_tasks_with_rust(async function resultMenu_notInterested() {
await doDismissTest("not_interested");
// Re-enable suggestions and wait until PocketSuggestions syncs them from
@@ -245,7 +241,7 @@ add_task(async function resultMenu_notInterested() {
});
// Tests the "Not relevant" result menu dismissal command.
-add_task(async function notRelevant() {
+add_tasks_with_rust(async function notRelevant() {
await doDismissTest("not_relevant");
});
@@ -361,7 +357,7 @@ async function doDismissTest(command) {
}
// Tests row labels.
-add_task(async function rowLabel() {
+add_tasks_with_rust(async function rowLabel() {
const testCases = [
// high confidence keyword best match
{
@@ -389,7 +385,7 @@ add_task(async function rowLabel() {
});
// Tests visibility of "Show less frequently" menu.
-add_task(async function showLessFrequentlyMenuVisibility() {
+add_tasks_with_rust(async function showLessFrequentlyMenuVisibility() {
const testCases = [
// high confidence keyword best match
{
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js
index b7c2bdc25c..7197946171 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js
@@ -401,6 +401,11 @@ async function doDismiss({ menu, assert }) {
await UrlbarTestUtils.promisePopupClose(window);
}
+// Tests the "Manage" result menu.
+add_task(async function resultMenu_manage() {
+ await doManageTest({ input: "ramen", index: 1 });
+});
+
// Tests the row/group label.
add_task(async function rowLabel() {
let tests = [
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js
index 001c54458c..71c289e0ef 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js
@@ -7,11 +7,6 @@
"use strict";
-ChromeUtils.defineESModuleGetters(this, {
- CONTEXTUAL_SERVICES_PING_TYPES:
- "resource:///modules/PartnerLinkAttribution.sys.mjs",
-});
-
const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
const MERINO_SUGGESTION = {
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js
index 00cbe6c4e1..2c75b63a71 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js
@@ -7,11 +7,6 @@
"use strict";
-ChromeUtils.defineESModuleGetters(this, {
- CONTEXTUAL_SERVICES_PING_TYPES:
- "resource:///modules/PartnerLinkAttribution.sys.mjs",
-});
-
const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
const MERINO_RESULT = {
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js
index 821c5cf470..eab48faaaf 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js
@@ -8,8 +8,6 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
- CONTEXTUAL_SERVICES_PING_TYPES:
- "resource:///modules/PartnerLinkAttribution.sys.mjs",
UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
});
@@ -376,8 +374,11 @@ async function doEngagementWithoutAddingResultToView(
let getPriorityStub = sandbox.stub(UrlbarProviderQuickSuggest, "getPriority");
getPriorityStub.returns(Infinity);
- // Spy on `UrlbarProviderQuickSuggest.onEngagement()`.
- let onEngagementSpy = sandbox.spy(UrlbarProviderQuickSuggest, "onEngagement");
+ // Spy on `UrlbarProviderQuickSuggest.onLegacyEngagement()`.
+ let onLegacyEngagementSpy = sandbox.spy(
+ UrlbarProviderQuickSuggest,
+ "onLegacyEngagement"
+ );
let sandboxCleanup = () => {
getPriorityStub?.restore();
@@ -454,7 +455,7 @@ async function doEngagementWithoutAddingResultToView(
});
await loadPromise;
- let engagementCalls = onEngagementSpy.getCalls().filter(call => {
+ let engagementCalls = onLegacyEngagementSpy.getCalls().filter(call => {
let state = call.args[0];
return state == "engagement";
});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js
index 9a1aa06c02..f541801bae 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js
@@ -7,11 +7,6 @@
"use strict";
-ChromeUtils.defineESModuleGetters(this, {
- CONTEXTUAL_SERVICES_PING_TYPES:
- "resource:///modules/PartnerLinkAttribution.sys.mjs",
-});
-
const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
const REMOTE_SETTINGS_RESULT = {
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js
index 7c477e8af7..b11a491c92 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js
@@ -7,11 +7,6 @@
"use strict";
-ChromeUtils.defineESModuleGetters(this, {
- CONTEXTUAL_SERVICES_PING_TYPES:
- "resource:///modules/PartnerLinkAttribution.sys.mjs",
-});
-
const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
const REMOTE_SETTINGS_RESULT = {
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/head.js b/browser/components/urlbar/tests/quicksuggest/browser/head.js
index cc5f449e94..a1bf0feabe 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/head.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/head.js
@@ -12,7 +12,7 @@ Services.scriptloader.loadSubScript(
ChromeUtils.defineESModuleGetters(this, {
CONTEXTUAL_SERVICES_PING_TYPES:
- "resource:///modules/PartnerLinkAttribution.jsm",
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
UrlbarProviderQuickSuggest:
@@ -522,6 +522,45 @@ async function doCommandTest({
info("Finished command test: " + JSON.stringify({ commandOrArray }));
}
+/*
+ * Do test the "Manage" result menu item.
+ *
+ * @param {object} options
+ * Options
+ * @param {number} options.index
+ * The index of the suggestion that will be checked in the results list.
+ * @param {number} options.input
+ * The input value on the urlbar.
+ */
+async function doManageTest({ index, input }) {
+ await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input,
+ });
+
+ const managePage = "about:preferences#search";
+ let onManagePageLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ managePage
+ );
+ // Click the command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, "manage", {
+ resultIndex: index,
+ });
+ await onManagePageLoaded;
+
+ Assert.equal(
+ browser.currentURI.spec,
+ managePage,
+ "The manage page is loaded"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+}
+
/**
* Gets a row in the view, which is assumed to be open, and asserts that it's a
* particular quick suggest row. If it is, the row is returned. If it's not,
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/head.js b/browser/components/urlbar/tests/quicksuggest/unit/head.js
index 73bedf468e..5808e06bdf 100644
--- a/browser/components/urlbar/tests/quicksuggest/unit/head.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/head.js
@@ -182,14 +182,11 @@ function makeWikipediaResult({
qsSuggestion: keyword,
sponsoredAdvertiser: "Wikipedia",
sponsoredIabCategory: "5 - Education",
- helpUrl: QuickSuggest.HELP_URL,
- helpL10n: {
- id: "urlbar-result-menu-learn-more-about-firefox-suggest",
- },
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
+ isManageable: true,
telemetryType: "adm_nonsponsored",
},
};
@@ -256,14 +253,11 @@ function makeAmpResult({
sponsoredBlockId: blockId,
sponsoredAdvertiser: advertiser,
sponsoredIabCategory: iabCategory,
- helpUrl: QuickSuggest.HELP_URL,
- helpL10n: {
- id: "urlbar-result-menu-learn-more-about-firefox-suggest",
- },
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
+ isManageable: true,
telemetryType: "adm_sponsored",
descriptionL10n: { id: "urlbar-result-action-sponsored" },
},
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js
index 1c00cb5320..ecb7c3dd09 100644
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js
@@ -3884,7 +3884,7 @@ async function checkSearch({ name, searchString, expectedResults }) {
removeResult() {},
},
});
- UrlbarProviderQuickSuggest.onEngagement(
+ UrlbarProviderQuickSuggest.onLegacyEngagement(
"engagement",
context,
{
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js
index 61b1b9186f..c98fc5b6b4 100644
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js
@@ -149,7 +149,7 @@ add_task(async function canceledQueries() {
});
function endEngagement({ controller, context = null, state = "engagement" }) {
- UrlbarProviderQuickSuggest.onEngagement(
+ UrlbarProviderQuickSuggest.onLegacyEngagement(
state,
context ||
createContext("endEngagement", {
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js
index cd794f435b..8479b97210 100644
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js
@@ -723,7 +723,7 @@ add_tasks_with_rust(async function block() {
let result = context.results[0];
let provider = UrlbarProvidersManager.getProvider(result.providerName);
Assert.ok(provider, "Sanity check: Result provider found");
- provider.onEngagement(
+ provider.onLegacyEngagement(
"engagement",
context,
{
diff --git a/browser/components/urlbar/tests/unit/test_exposure.js b/browser/components/urlbar/tests/unit/test_exposure.js
index e3ce0b8479..3e63e668d7 100644
--- a/browser/components/urlbar/tests/unit/test_exposure.js
+++ b/browser/components/urlbar/tests/unit/test_exposure.js
@@ -3,7 +3,6 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
ChromeUtils.defineESModuleGetters(this, {
- QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
UrlbarProviderQuickSuggest:
"resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
});
@@ -177,14 +176,11 @@ function makeAmpResult({
sponsoredBlockId: blockId,
sponsoredAdvertiser: advertiser,
sponsoredIabCategory: iabCategory,
- helpUrl: QuickSuggest.HELP_URL,
- helpL10n: {
- id: "urlbar-result-menu-learn-more-about-firefox-suggest",
- },
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
+ isManageable: true,
telemetryType: "adm_sponsored",
descriptionL10n: { id: "urlbar-result-action-sponsored" },
},
@@ -240,14 +236,11 @@ function makeWikipediaResult({
qsSuggestion: keyword,
sponsoredAdvertiser: "Wikipedia",
sponsoredIabCategory: "5 - Education",
- helpUrl: QuickSuggest.HELP_URL,
- helpL10n: {
- id: "urlbar-result-menu-learn-more-about-firefox-suggest",
- },
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
+ isManageable: true,
telemetryType: "adm_nonsponsored",
},
};
diff --git a/browser/components/urlbar/tests/unit/test_l10nCache.js b/browser/components/urlbar/tests/unit/test_l10nCache.js
index e92c75fa01..bd93cc50d6 100644
--- a/browser/components/urlbar/tests/unit/test_l10nCache.js
+++ b/browser/components/urlbar/tests/unit/test_l10nCache.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-// Tests L10nCache in UrlbarUtils.jsm.
+// Tests L10nCache in UrlbarUtils.sys.mjs.
"use strict";