From def92d1b8e9d373e2f6f27c366d578d97d8960c6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:34:50 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- browser/components/BrowserContentHandler.sys.mjs | 51 +- browser/components/BrowserGlue.sys.mjs | 102 +- .../tests/browser/browser_sessionRestore.js | 2 +- .../components/aboutlogins/tests/browser/head.js | 2 +- browser/components/aboutwelcome/.eslintrc.js | 2 - .../aboutwelcome/actors/AboutWelcomeParent.sys.mjs | 2 +- .../aboutwelcome/content-src/aboutwelcome.scss | 139 ++- .../components/MultiStageAboutWelcome.jsx | 16 +- .../components/MultiStageProtonScreen.jsx | 55 +- .../aboutwelcome/content-src/components/Themes.jsx | 9 +- .../aboutwelcome/content/aboutwelcome.bundle.js | 36 +- .../aboutwelcome/content/aboutwelcome.css | 106 +- .../browser_aboutwelcome_configurable_ui.js | 2 +- .../browser/browser_aboutwelcome_toolbar_button.js | 2 +- .../tests/unit/MultiStageAWProton.test.jsx | 23 + .../tests/unit/MultiStageAboutWelcome.test.jsx | 66 ++ .../aboutwelcome/tests/unit/unit-entry.js | 14 +- browser/components/asrouter/.eslintrc.js | 2 - .../asrouter/actors/ASRouterChild.sys.mjs | 6 +- browser/components/asrouter/bin/import-rollouts.js | 4 - .../asrouter/content-src/asrouter-utils.mjs | 6 +- .../components/ASRouterAdmin/ASRouterAdmin.jsx | 16 +- .../BackgroundTaskMessagingExperiment.schema.json | 69 +- .../schemas/MessagingExperiment.schema.json | 415 ++++---- .../asrouter/content-src/schemas/make-schemas.py | 3 - .../OnboardingMessage/WhatsNewMessage.schema.json | 73 -- .../asrouter/content/asrouter-admin.bundle.js | 52 +- .../asrouter/docs/targeting-attributes.md | 11 - .../components/asrouter/modules/ASRouter.sys.mjs | 47 +- .../ASRouterParentProcessMessageHandler.sys.mjs | 9 +- .../asrouter/modules/ASRouterTargeting.sys.mjs | 9 - .../components/asrouter/modules/ActorConstants.mjs | 46 + .../asrouter/modules/ActorConstants.sys.mjs | 49 - .../asrouter/modules/MomentsPageHub.sys.mjs | 2 +- .../modules/OnboardingMessageProvider.sys.mjs | 100 ++ .../asrouter/modules/PanelTestProvider.sys.mjs | 128 --- .../asrouter/modules/ToolbarBadgeHub.sys.mjs | 36 - .../asrouter/modules/ToolbarPanelHub.sys.mjs | 544 ---------- browser/components/asrouter/moz.build | 4 +- browser/components/asrouter/package.json | 1 + .../tests/InflightAssetsMessageProvider.sys.mjs | 112 +-- .../tests/NimbusRolloutMessageProvider.sys.mjs | 421 ++++++++ .../components/asrouter/tests/browser/browser.toml | 5 + .../tests/browser/browser_asrouter_bug1761522.js | 2 +- .../asrouter/tests/browser/browser_asrouter_cfr.js | 117 --- .../tests/browser/browser_asrouter_keyboard_cfr.js | 162 +++ .../browser_asrouter_milestone_message_cfr.js | 78 ++ .../browser/browser_asrouter_momentspagehub.js | 4 +- .../asrouter/tests/unit/ASRouter.test.js | 107 -- .../asrouter/tests/unit/ASRouterChild.test.js | 3 +- .../asrouter/tests/unit/ASRouterParent.test.js | 2 +- .../ASRouterParentProcessMessageHandler.test.js | 27 +- .../asrouter/tests/unit/ToolbarBadgeHub.test.js | 166 ---- .../asrouter/tests/unit/ToolbarPanelHub.test.js | 760 -------------- .../content-src/components/ASRouterAdmin.test.jsx | 44 +- .../components/asrouter/tests/unit/unit-entry.js | 2 +- browser/components/asrouter/tests/xpcshell/head.js | 4 - .../tests/xpcshell/test_PanelTestProvider.js | 1 - browser/components/asrouter/yamscripts.yml | 1 + browser/components/backup/.eslintrc.js | 9 - browser/components/backup/BackupResources.sys.mjs | 24 +- browser/components/backup/BackupService.sys.mjs | 129 ++- browser/components/backup/content/debug.html | 46 + browser/components/backup/content/debug.js | 59 ++ .../components/backup/docs/backup-resources.rst | 18 + browser/components/backup/docs/index.rst | 1 + browser/components/backup/jar.mn | 9 + browser/components/backup/metrics.yaml | 276 ++++++ browser/components/backup/moz.build | 10 + .../backup/resources/AddonsBackupResource.sys.mjs | 100 ++ .../backup/resources/BackupResource.sys.mjs | 83 +- .../backup/resources/CookiesBackupResource.sys.mjs | 25 + .../CredentialsAndSecurityBackupResource.sys.mjs | 53 + .../resources/FormHistoryBackupResource.sys.mjs | 25 + .../resources/MiscDataBackupResource.sys.mjs | 101 ++ .../backup/resources/PlacesBackupResource.sys.mjs | 91 ++ .../resources/PreferencesBackupResource.sys.mjs | 98 ++ .../resources/SessionStoreBackupResource.sys.mjs | 53 + browser/components/backup/tests/xpcshell/head.js | 167 ++++ .../backup/tests/xpcshell/test_BackupResource.js | 77 ++ .../backup/tests/xpcshell/test_BrowserResource.js | 63 -- .../tests/xpcshell/test_MiscDataBackupResource.js | 113 +++ .../tests/xpcshell/test_PlacesBackupResource.js | 226 +++++ .../xpcshell/test_PreferencesBackupResource.js | 132 +++ .../backup/tests/xpcshell/test_createBackup.js | 74 ++ .../backup/tests/xpcshell/test_measurements.js | 547 ++++++++++- .../components/backup/tests/xpcshell/xpcshell.toml | 14 +- .../content/ContentAnalysis.sys.mjs | 99 +- .../contextualidentity/test/browser/browser_eme.js | 2 +- .../test/browser/browser_favicon.js | 2 +- .../browser_forgetAPI_EME_forgetThisSite.js | 2 +- .../test/browser/browser_forgetaboutsite.js | 6 +- .../test/browser/browser_guessusercontext.js | 2 +- .../test/browser/browser_middleClick.js | 2 +- .../test/browser/browser_serviceworkers.js | 2 +- .../test/browser/browser_windowName.js | 4 +- .../test/browser/file_set_storages.html | 2 +- .../content/protectionsPanel.inc.xhtml | 4 +- .../customizableui/CustomizableUI.sys.mjs | 4 +- .../customizableui/CustomizableWidgets.sys.mjs | 4 +- .../customizableui/CustomizeMode.sys.mjs | 20 +- .../components/customizableui/content/panelUI.js | 43 +- .../test/browser_1087303_button_fullscreen.js | 2 +- .../test/browser_1087303_button_preferences.js | 2 +- ...84275_PanelMultiView_toggle_with_other_popup.js | 2 +- ...wser_885052_customize_mode_observers_disabed.js | 4 +- .../browser_940307_panel_click_closure_handling.js | 7 +- .../test/browser_947914_button_newPrivateWindow.js | 2 +- .../test/browser_947914_button_newWindow.js | 2 +- .../test/browser_947914_button_zoomReset.js | 2 +- .../browser_972267_customizationchange_events.js | 2 +- .../test/browser_customization_context_menus.js | 10 +- .../test/browser_editcontrols_update.js | 2 +- .../test/browser_open_in_lazy_tab.js | 2 +- .../test/browser_panelUINotifications.js | 12 +- ...UINotifications_fullscreen_noAutoHideToolbar.js | 2 +- .../test/browser_panelUINotifications_modals.js | 4 - .../browser_panelUINotifications_multiWindow.js | 6 +- .../test/browser_switch_to_customize_mode.js | 2 +- .../test/browser_synced_tabs_menu.js | 6 +- browser/components/customizableui/test/head.js | 12 +- browser/components/doh/DoHConfig.sys.mjs | 2 +- .../browser/browser_remoteSettings_newProfile.js | 2 +- .../components/enterprisepolicies/Policies.sys.mjs | 91 +- .../helpers/ProxyPolicies.sys.mjs | 24 + .../helpers/WebsiteFilter.sys.mjs | 4 +- .../schemas/policies-schema.json | 16 + .../enterprisepolicies/tests/browser/browser.toml | 2 + .../browser/browser_policy_pageinfo_permissions.js | 4 +- .../browser/browser_policy_translateenabled.js | 53 + .../browser_policy_disable_app_update.js | 2 +- .../browser_policy_disable_developer_tools.js | 2 +- .../enterprisepolicies/tests/browser/head.js | 2 +- .../enterprisepolicies/tests/xpcshell/head.js | 2 +- .../tests/xpcshell/test_extensionsettings.js | 115 ++- .../tests/xpcshell/test_requestedlocales.js | 6 +- .../tests/xpcshell/test_simple_pref_policies.js | 54 + .../tests/xpcshell/test_sorted_alphabetically.js | 2 +- .../tests/xpcshell/xpcshell.toml | 5 +- .../components/extensions/parent/ext-browser.js | 8 +- .../parent/ext-chrome-settings-overrides.js | 2 +- .../components/extensions/parent/ext-commands.js | 7 +- .../extensions/parent/ext-devtools-panels.js | 42 +- browser/components/extensions/parent/ext-tabs.js | 8 +- .../components/extensions/schemas/commands.json | 6 + browser/components/extensions/schemas/tabs.json | 4 +- .../extensions/test/browser/browser.toml | 10 +- .../browser/browser_ext_browserAction_context.js | 6 +- .../browser_ext_browserAction_contextMenu.js | 6 - ...er_ext_browserAction_popup_preload_smoketest.js | 2 +- .../browser_ext_chrome_settings_overrides_home.js | 10 +- .../test/browser/browser_ext_commands_onCommand.js | 14 +- .../browser_ext_contentscript_nontab_connect.js | 6 + .../browser_ext_contentscript_sender_url.js | 21 +- .../test/browser/browser_ext_contextMenus_icons.js | 6 +- .../test/browser/browser_ext_tabs_onCreated.js | 2 +- .../browser/browser_ext_url_overrides_newtab.js | 2 +- .../test/browser/browser_unified_extensions.js | 4 + .../components/firefoxview/HistoryController.mjs | 188 ++++ browser/components/firefoxview/OpenTabs.sys.mjs | 55 +- .../firefoxview/SyncedTabsController.sys.mjs | 333 +++++++ browser/components/firefoxview/card-container.css | 6 +- browser/components/firefoxview/card-container.mjs | 2 +- .../firefox-view-tabs-setup-manager.sys.mjs | 17 +- browser/components/firefoxview/firefoxview.css | 19 +- browser/components/firefoxview/firefoxview.html | 12 +- browser/components/firefoxview/firefoxview.mjs | 16 + .../components/firefoxview/fxview-empty-state.css | 2 +- browser/components/firefoxview/fxview-tab-list.css | 22 +- browser/components/firefoxview/fxview-tab-list.mjs | 689 +++++-------- browser/components/firefoxview/fxview-tab-row.css | 178 +--- browser/components/firefoxview/helpers.mjs | 17 + browser/components/firefoxview/history.css | 13 +- browser/components/firefoxview/history.mjs | 224 ++--- browser/components/firefoxview/jar.mn | 4 + .../components/firefoxview/opentabs-tab-list.css | 32 + .../components/firefoxview/opentabs-tab-list.mjs | 593 +++++++++++ .../components/firefoxview/opentabs-tab-row.css | 119 +++ browser/components/firefoxview/opentabs.mjs | 42 +- browser/components/firefoxview/recentlyclosed.mjs | 16 +- browser/components/firefoxview/syncedtabs.mjs | 387 ++------ .../firefoxview/tests/browser/browser.toml | 8 +- .../browser_firefoxview_dragDrop_pinned_tab.js | 102 ++ .../tests/browser/browser_firefoxview_paused.js | 99 -- .../browser_firefoxview_search_telemetry.js | 5 +- .../tests/browser/browser_firefoxview_tab.js | 6 +- .../tests/browser/browser_history_firefoxview.js | 15 +- .../tests/browser/browser_opentabs_cards.js | 10 +- .../tests/browser/browser_opentabs_firefoxview.js | 4 +- .../tests/browser/browser_opentabs_recency.js | 350 ++++--- .../browser/browser_opentabs_tab_indicators.js | 10 +- .../browser/browser_recentlyclosed_firefoxview.js | 12 + .../browser_syncedtabs_errors_firefoxview.js | 272 ++++- .../browser/browser_syncedtabs_firefoxview.js | 7 +- .../browser_tab_list_keyboard_navigation.js | 4 +- .../tests/browser/browser_tab_on_close_warning.js | 2 +- .../tests/chrome/test_fxview_tab_list.html | 5 - browser/components/ion/content/ion.js | 12 +- .../components/ion/test/browser/browser_ion_ui.js | 35 +- .../components/messagepreview/messagepreview.js | 14 +- browser/components/migration/.eslintrc.js | 5 +- browser/components/migration/FileMigrators.sys.mjs | 5 +- browser/components/migration/MigratorBase.sys.mjs | 10 +- .../migration/content/migration-wizard.mjs | 9 +- browser/components/newtab/.eslintrc.js | 8 +- browser/components/newtab/common/Actions.mjs | 463 +++++++++ browser/components/newtab/common/Actions.sys.mjs | 457 --------- browser/components/newtab/common/Reducers.sys.mjs | 15 +- .../newtab/content-src/activity-stream.jsx | 5 +- .../newtab/content-src/components/Base/Base.jsx | 150 ++- .../newtab/content-src/components/Base/_Base.scss | 38 +- .../newtab/content-src/components/Card/Card.jsx | 5 +- .../newtab/content-src/components/Card/types.js | 30 - .../newtab/content-src/components/Card/types.mjs | 30 + .../CollapsibleSection/CollapsibleSection.jsx | 2 +- .../ComponentPerfTimer/ComponentPerfTimer.jsx | 5 +- .../components/ConfirmDialog/ConfirmDialog.jsx | 2 +- .../components/ContextMenu/ContextMenu.jsx | 4 +- .../ContentSection/ContentSection.jsx | 15 +- .../components/CustomizeMenu/CustomizeMenu.jsx | 4 +- .../components/CustomizeMenu/_CustomizeMenu.scss | 4 + .../DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx | 11 +- .../DiscoveryStreamAdmin/SimpleHashRouter.jsx | 8 +- .../DiscoveryStreamBase/DiscoveryStreamBase.jsx | 6 +- .../CardGrid/CardGrid.jsx | 9 +- .../CollectionCardGrid/CollectionCardGrid.jsx | 2 +- .../DiscoveryStreamComponents/DSCard/DSCard.jsx | 11 +- .../DSContextFooter/DSContextFooter.jsx | 2 +- .../DSEmptyState/DSEmptyState.jsx | 5 +- .../DSLinkMenu/DSLinkMenu.jsx | 2 +- .../DSPrivacyModal/DSPrivacyModal.jsx | 5 +- .../DSSignup/DSSignup.jsx | 2 +- .../DSTextPromo/DSTextPromo.jsx | 2 +- .../FeatureHighlight/FeatureHighlight.jsx | 2 +- .../Navigation/Navigation.jsx | 2 +- .../SafeAnchor/SafeAnchor.jsx | 5 +- .../TopicsWidget/TopicsWidget.jsx | 2 +- .../ImpressionStats.jsx | 11 +- .../content-src/components/LinkMenu/LinkMenu.jsx | 2 +- .../components/ModalOverlay/ModalOverlay.jsx | 2 +- .../content-src/components/Search/Search.jsx | 5 +- .../content-src/components/Sections/Sections.jsx | 9 +- .../components/TopSites/SearchShortcutsForm.jsx | 5 +- .../content-src/components/TopSites/TopSite.jsx | 5 +- .../components/TopSites/TopSiteForm.jsx | 5 +- .../TopSites/TopSiteImpressionWrapper.jsx | 6 +- .../content-src/components/TopSites/TopSites.jsx | 7 +- .../components/TopSites/TopSitesConstants.js | 39 - .../components/TopSites/TopSitesConstants.mjs | 39 + .../WallpapersSection/WallpapersSection.jsx | 100 ++ .../WallpapersSection/_WallpapersSection.scss | 87 ++ .../components/newtab/content-src/lib/constants.js | 38 - .../newtab/content-src/lib/constants.mjs | 38 + .../content-src/lib/detect-user-session-start.js | 82 -- .../content-src/lib/detect-user-session-start.mjs | 82 ++ .../newtab/content-src/lib/init-store.js | 140 --- .../newtab/content-src/lib/init-store.mjs | 143 +++ .../newtab/content-src/lib/link-menu-options.js | 309 ------ .../newtab/content-src/lib/link-menu-options.mjs | 309 ++++++ .../newtab/content-src/lib/perf-service.js | 104 -- .../newtab/content-src/lib/perf-service.mjs | 102 ++ .../newtab/content-src/lib/screenshot-utils.js | 61 -- .../newtab/content-src/lib/screenshot-utils.mjs | 61 ++ .../newtab/content-src/lib/selectLayoutRender.js | 255 ----- .../newtab/content-src/lib/selectLayoutRender.mjs | 255 +++++ .../content-src/styles/_activity-stream.scss | 12 + .../newtab/css/activity-stream-linux.css | 150 ++- .../components/newtab/css/activity-stream-mac.css | 150 ++- .../newtab/css/activity-stream-windows.css | 150 ++- .../newtab/data/content/activity-stream.bundle.js | 942 ++++++++++++------ .../data/content/assets/wallpapers/dark-beach.avif | Bin 0 -> 4043 bytes .../data/content/assets/wallpapers/dark-color.avif | Bin 0 -> 2413 bytes .../content/assets/wallpapers/dark-landscape.avif | Bin 0 -> 9381 bytes .../content/assets/wallpapers/dark-mountain.avif | Bin 0 -> 11602 bytes .../data/content/assets/wallpapers/dark-panda.avif | Bin 0 -> 4606 bytes .../data/content/assets/wallpapers/dark-sky.avif | Bin 0 -> 2216 bytes .../content/assets/wallpapers/light-beach.avif | Bin 0 -> 3806 bytes .../content/assets/wallpapers/light-color.avif | Bin 0 -> 2267 bytes .../content/assets/wallpapers/light-landscape.avif | Bin 0 -> 2527 bytes .../content/assets/wallpapers/light-mountain.avif | Bin 0 -> 5915 bytes .../content/assets/wallpapers/light-panda.avif | Bin 0 -> 8667 bytes .../data/content/assets/wallpapers/light-sky.avif | Bin 0 -> 2540 bytes browser/components/newtab/karma.mc.config.js | 22 +- .../components/newtab/lib/AboutPreferences.sys.mjs | 2 +- .../components/newtab/lib/ActivityStream.sys.mjs | 30 +- .../lib/ActivityStreamMessageChannel.sys.mjs | 2 +- .../newtab/lib/DiscoveryStreamFeed.sys.mjs | 39 +- .../components/newtab/lib/DownloadsManager.sys.mjs | 2 +- browser/components/newtab/lib/FaviconFeed.sys.mjs | 2 +- .../components/newtab/lib/HighlightsFeed.sys.mjs | 2 +- browser/components/newtab/lib/NewTabInit.sys.mjs | 2 +- browser/components/newtab/lib/PlacesFeed.sys.mjs | 2 +- browser/components/newtab/lib/PrefsFeed.sys.mjs | 2 +- .../newtab/lib/RecommendationProvider.sys.mjs | 2 +- .../components/newtab/lib/SectionsManager.sys.mjs | 4 +- .../components/newtab/lib/SystemTickFeed.sys.mjs | 2 +- .../components/newtab/lib/TelemetryFeed.sys.mjs | 49 +- browser/components/newtab/lib/TopSitesFeed.sys.mjs | 2 +- .../components/newtab/lib/TopStoriesFeed.sys.mjs | 2 +- .../components/newtab/lib/WallpaperFeed.sys.mjs | 117 +++ browser/components/newtab/metrics.yaml | 31 +- .../test/browser/browser_as_load_location.js | 2 +- .../test/browser/browser_newtab_overrides.js | 4 +- browser/components/newtab/test/schemas/pings.js | 5 +- .../newtab/test/unit/common/Actions.test.js | 2 +- .../newtab/test/unit/common/Reducers.test.js | 2 +- .../test/unit/content-src/components/Base.test.jsx | 79 +- .../test/unit/content-src/components/Card.test.jsx | 5 +- .../components/ComponentPerfTimer.test.jsx | 5 +- .../content-src/components/ConfirmDialog.test.jsx | 5 +- .../content-src/components/CustomiseMenu.test.jsx | 2 +- .../components/DiscoveryStreamAdmin.test.jsx | 5 +- .../DiscoveryStreamComponents/CardGrid.test.jsx | 5 +- .../DiscoveryStreamComponents/DSCard.test.jsx | 22 +- .../DSContextFooter.test.jsx | 2 +- .../DSPrivacyModal.test.jsx | 2 +- .../ImpressionStats.test.jsx | 73 +- .../TopicsWidget.test.jsx | 5 +- .../unit/content-src/components/Sections.test.jsx | 2 +- .../unit/content-src/components/TopSites.test.jsx | 5 +- .../TopSites/TopSiteImpressionWrapper.test.jsx | 2 +- .../lib/detect-user-session-start.test.js | 5 +- .../test/unit/content-src/lib/init-store.test.js | 5 +- .../content-src/lib/selectLayoutRender.test.js | 2 +- .../newtab/test/unit/lib/AboutPreferences.test.js | 5 +- .../newtab/test/unit/lib/ActivityStream.test.js | 2 +- .../unit/lib/ActivityStreamMessageChannel.test.js | 7 +- .../test/unit/lib/DiscoveryStreamFeed.test.js | 30 +- .../newtab/test/unit/lib/DownloadsManager.test.js | 2 +- .../newtab/test/unit/lib/FaviconFeed.test.js | 2 +- .../newtab/test/unit/lib/NewTabInit.test.js | 5 +- .../newtab/test/unit/lib/PrefsFeed.test.js | 5 +- .../test/unit/lib/RecommendationProvider.test.js | 5 +- .../newtab/test/unit/lib/SectionsManager.test.js | 2 +- .../newtab/test/unit/lib/SystemTickFeed.test.js | 2 +- .../newtab/test/xpcshell/test_HighlightsFeed.js | 2 +- .../newtab/test/xpcshell/test_PlacesFeed.js | 2 +- .../newtab/test/xpcshell/test_TelemetryFeed.js | 56 +- .../newtab/test/xpcshell/test_TopSitesFeed.js | 2 +- .../newtab/test/xpcshell/test_WallpaperFeed.js | 115 +++ .../components/newtab/test/xpcshell/xpcshell.toml | 2 + .../newtab/webpack.system-addon.config.js | 2 +- .../originattributes/test/browser/browser.toml | 2 +- .../originattributes/test/browser/browser_cache.js | 8 +- .../test/browser/browser_favicon_firstParty.js | 6 +- .../test/browser/browser_favicon_userContextId.js | 4 +- .../browser/browser_firstPartyIsolation_saveAs.js | 4 +- .../test/browser/browser_httpauth.js | 6 +- .../test/browser/browser_imageCacheIsolation.js | 4 +- .../test/browser/browser_sanitize.js | 4 +- .../originattributes/test/browser/file_saveAs.sjs | 4 +- .../test/browser/file_thirdPartyChild.video.ogv | Bin 16049 -> 0 bytes .../test/browser/file_thirdPartyChild.video.webm | Bin 0 -> 17931 bytes .../originattributes/test/browser/head.js | 2 +- browser/components/pagedata/.eslintrc.js | 2 - .../components/pagedata/PageDataService.sys.mjs | 16 +- browser/components/places/.eslintrc.js | 9 - browser/components/places/content/places.js | 2 +- browser/components/places/content/places.xhtml | 4 + .../browser_bookmark_context_menu_contents.js | 4 +- .../tests/browser/browser_sidebarpanels_click.js | 8 +- browser/components/places/tests/browser/head.js | 2 +- .../components/pocket/content/SaveToPocket.sys.mjs | 2 +- .../content/panels/js/components/Home/Home.jsx | 2 +- .../content/panels/js/components/Saved/Saved.jsx | 2 +- .../pocket/content/panels/js/home/overlay.jsx | 2 +- .../pocket/content/panels/js/main.bundle.js | 14 +- .../components/pocket/content/panels/js/main.mjs | 2 +- .../pocket/content/panels/js/saved/overlay.jsx | 2 +- .../pocket/content/panels/js/signup/overlay.jsx | 2 +- .../content/panels/js/style-guide/overlay.jsx | 2 +- browser/components/pocket/content/pktApi.sys.mjs | 27 +- browser/components/pocket/content/pktUI.js | 28 +- .../test/browser_pocket_button_icon_state.js | 10 +- .../test/unit/browser_pocket_AboutPocketParent.js | 20 +- browser/components/preferences/preferences.js | 26 - browser/components/preferences/preferences.xhtml | 4 +- browser/components/preferences/privacy.inc.xhtml | 27 +- browser/components/preferences/privacy.js | 23 +- browser/components/preferences/sync.inc.xhtml | 39 +- browser/components/preferences/tests/browser.toml | 6 +- .../tests/browser_applications_selection.js | 20 +- .../preferences/tests/browser_contentblocking.js | 2 +- .../tests/browser_privacy_dnsoverhttps.js | 162 +++ .../preferences/tests/browser_subdialogs.js | 15 +- .../preferences/tests/siteData/browser.toml | 2 + .../tests/siteData/browser_clearSiteData.js | 41 +- .../tests/siteData/browser_clearSiteData_v2.js | 258 +++++ .../components/preferences/translations.inc.xhtml | 54 +- browser/components/preferences/translations.js | 270 +++++ .../browser/browser_oa_private_browsing_window.js | 2 +- ...browser_privatebrowsing_about_nimbus_dismiss.js | 2 +- ...ser_privatebrowsing_about_nimbus_impressions.js | 4 +- .../test/browser/browser_privatebrowsing_cache.js | 2 +- .../browser_privatebrowsing_certexceptionsui.js | 2 +- .../browser/browser_privatebrowsing_cleanup.js | 2 +- .../browser/browser_privatebrowsing_favicon.js | 6 +- .../browser_privatebrowsing_geoprompt_page.html | 2 +- ...rowsing_last_private_browsing_context_exited.js | 4 +- .../browser_privatebrowsing_lastpbcontextexited.js | 4 +- .../test/browser/browser_privatebrowsing_ui.js | 2 +- .../browser/browser_privatebrowsing_zoomrestore.js | 4 +- .../components/protections/content/protections.mjs | 2 +- .../test/browser/browser_protections_monitor.js | 2 +- .../WebProtocolHandlerRegistrar.sys.mjs | 4 +- .../reportbrokensite/ReportBrokenSite.sys.mjs | 2 +- .../test/browser/browser_back_buttons.js | 41 +- .../test/browser/browser_keyboard_navigation.js | 60 +- .../test/browser/browser_prefers_contrast.js | 2 +- .../test/browser/browser_reason_dropdown.js | 194 ++-- .../browser/browser_report_site_issue_fallback.js | 13 +- .../test/browser/browser_send_more_info.js | 29 +- .../test/browser/browser_tab_key_order.js | 13 +- .../reportbrokensite/test/browser/head.js | 2 +- .../browser/browser_dynamical_window_rounding.js | 2 +- .../test/browser/browser_navigator.js | 2 +- .../browser/browser_spoofing_keyboard_event.js | 2 +- .../test/browser/browser_timezone.js | 9 + .../test/browser/file_animationapi_iframee.html | 2 +- .../test/browser/file_animationapi_iframer.html | 2 +- .../file_hwconcurrency_aboutblank_popupmaker.html | 2 +- .../file_hwconcurrency_blob_popupmaker.html | 2 +- ...file_hwconcurrency_blobcrossorigin_iframer.html | 2 +- .../file_hwconcurrency_data_popupmaker.html | 2 +- .../test/browser/file_hwconcurrency_iframer.html | 2 +- ...wconcurrency_sandboxediframe_double_framee.html | 2 +- .../test/browser/file_navigator_iframee.html | 2 +- .../browser/file_reduceTimePrecision_iframer.html | 2 +- .../resistfingerprinting/test/browser/head.js | 6 +- .../test/mochitest/test_geolocation.html | 4 +- .../content/test/browser_whitelisted.js | 2 +- .../components/safebrowsing/content/test/head.js | 2 +- .../screenshots/ScreenshotsOverlayChild.sys.mjs | 151 ++- .../screenshots/ScreenshotsUtils.sys.mjs | 5 +- .../components/screenshots/content/screenshots.css | 21 +- .../screenshots/content/screenshots.html | 22 +- .../components/screenshots/content/screenshots.js | 132 ++- browser/components/screenshots/overlay/overlay.css | 41 +- .../components/screenshots/screenshots-buttons.css | 3 +- .../components/screenshots/screenshots-buttons.js | 31 +- .../screenshots/tests/browser/browser.toml | 4 + .../tests/browser/browser_iframe_test.js | 4 +- .../tests/browser/browser_keyboard_shortcuts.js | 128 +++ .../browser_screenshots_drag_scroll_test.js | 117 ++- .../tests/browser/browser_screenshots_drag_test.js | 66 +- .../browser_screenshots_test_toggle_pref.js | 2 +- .../tests/browser/browser_test_element_picker.js | 4 +- .../browser/browser_test_selection_size_text.js | 86 ++ .../components/screenshots/tests/browser/head.js | 50 +- browser/components/search/.eslintrc.js | 2 - .../search/DomainToCategoriesMap.worker.mjs | 101 -- .../components/search/SearchSERPTelemetry.sys.mjs | 860 ++++++++++++++-- browser/components/search/metrics.yaml | 104 ++ browser/components/search/moz.build | 6 +- .../search/schema/search-telemetry-schema.json | 444 --------- .../search/schema/search-telemetry-ui-schema.json | 23 - .../search/schema/search-telemetry-v2-schema.json | 444 +++++++++ .../schema/search-telemetry-v2-ui-schema.json | 25 + .../search/test/browser/telemetry/browser.toml | 23 +- ...ry_categorization_enabled_by_nimbus_variable.js | 15 +- ...ch_telemetry_domain_categorization_ad_values.js | 13 + ...lemetry_domain_categorization_download_timer.js | 35 +- ...h_telemetry_domain_categorization_extraction.js | 46 +- ...ry_domain_categorization_no_sponsored_values.js | 141 +++ ...emetry_domain_categorization_ping_submission.js | 302 ++++++ ...earch_telemetry_domain_categorization_region.js | 12 + ...ch_telemetry_domain_categorization_reporting.js | 60 ++ ...emetry_domain_categorization_reporting_timer.js | 21 +- ...domain_categorization_reporting_timer_wakeup.js | 19 +- .../search/test/browser/telemetry/head.js | 59 +- ...tryDomainCategorizationReportingWithoutAds.html | 18 + .../telemetry/searchTelemetryDomainExtraction.html | 31 + .../search/test/marionette/manifest.toml | 2 + .../search/test/marionette/telemetry/manifest.toml | 4 + .../marionette/telemetry/test_ping_submitted.py | 89 ++ .../components/search/test/unit/corruptDB.sqlite | Bin 0 -> 32772 bytes .../test/unit/test_domain_to_categories_store.js | 361 +++++++ .../test_search_telemetry_categorization_sync.js | 75 +- .../test_search_telemetry_config_validation.js | 2 +- .../search/test/unit/test_ui_schemas_valid.js | 31 + browser/components/search/test/unit/xpcshell.toml | 11 +- .../components/sessionstore/ContentRestore.sys.mjs | 435 -------- .../sessionstore/ContentSessionStore.sys.mjs | 685 ------------- .../RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs | 4 +- .../components/sessionstore/SessionFile.sys.mjs | 68 ++ .../components/sessionstore/SessionSaver.sys.mjs | 2 +- .../components/sessionstore/SessionStartup.sys.mjs | 29 +- .../components/sessionstore/SessionStore.sys.mjs | 415 +------- .../sessionstore/StartupPerformance.sys.mjs | 2 +- .../components/sessionstore/TabAttributes.sys.mjs | 43 +- .../sessionstore/TabStateFlusher.sys.mjs | 100 +- .../sessionstore/content/aboutSessionRestore.js | 20 +- .../sessionstore/content/content-sessionStore.js | 13 - browser/components/sessionstore/jar.mn | 1 - browser/components/sessionstore/moz.build | 4 +- .../test/SessionStoreTestUtils.sys.mjs | 4 +- browser/components/sessionstore/test/browser.toml | 268 +---- .../test/browser_354894_perwindowpb.js | 10 +- .../sessionstore/test/browser_394759_basic.js | 2 +- .../sessionstore/test/browser_394759_behavior.js | 16 +- .../sessionstore/test/browser_394759_purge.js | 2 +- .../components/sessionstore/test/browser_459906.js | 4 +- .../components/sessionstore/test/browser_461743.js | 2 +- .../components/sessionstore/test/browser_464199.js | 2 +- .../sessionstore/test/browser_464620_a.js | 2 +- .../sessionstore/test/browser_464620_b.js | 2 +- .../components/sessionstore/test/browser_526613.js | 2 +- .../components/sessionstore/test/browser_580512.js | 6 +- .../sessionstore/test/browser_586068-apptabs.js | 7 +- .../browser_586068-browser_state_interrupted.js | 9 +- .../test/browser_586068-multi_window.js | 9 +- .../test/browser_586068-window_state.js | 7 +- .../test/browser_586068-window_state_override.js | 7 +- .../components/sessionstore/test/browser_589246.js | 12 +- .../components/sessionstore/test/browser_590268.js | 2 +- ...ser_615394-SSWindowState_events_duplicateTab.js | 4 +- ..._615394-SSWindowState_events_setBrowserState.js | 2 +- ...wser_615394-SSWindowState_events_setTabState.js | 6 +- ...r_615394-SSWindowState_events_setWindowState.js | 6 +- ...ser_615394-SSWindowState_events_undoCloseTab.js | 4 +- ..._615394-SSWindowState_events_undoCloseWindow.js | 8 +- .../components/sessionstore/test/browser_618151.js | 2 +- .../components/sessionstore/test/browser_636279.js | 2 +- .../components/sessionstore/test/browser_645428.js | 2 +- .../sessionstore/test/browser_687710_2.js | 66 +- .../components/sessionstore/test/browser_705597.js | 10 +- .../components/sessionstore/test/browser_707862.js | 20 +- .../components/sessionstore/test/browser_739531.js | 2 +- .../sessionstore/test/browser_async_flushes.js | 52 - .../sessionstore/test/browser_async_remove_tab.js | 30 +- .../test/browser_async_window_flushing.js | 11 +- .../sessionstore/test/browser_attributes.js | 59 +- .../sessionstore/test/browser_bfcache_telemetry.js | 3 +- .../test/browser_closed_tabs_closed_windows.js | 10 - .../sessionstore/test/browser_cookies.js | 2 +- .../sessionstore/test/browser_crashedTabs.js | 2 +- .../test/browser_docshell_uuid_consistency.js | 94 +- .../sessionstore/test/browser_frame_history.js | 2 +- .../sessionstore/test/browser_frametree.js | 2 +- .../sessionstore/test/browser_history_persist.js | 138 +-- .../test/browser_newtab_userTypedValue.js | 4 +- .../sessionstore/test/browser_oldformat.toml | 301 ++++++ .../test/browser_parentProcessRestoreHash.js | 4 +- ...rowser_restoreLastClosedTabOrWindowOrSession.js | 2 +- .../test/browser_send_async_message_oom.js | 75 -- .../sessionstore/test/browser_sessionHistory.js | 9 +- .../test/browser_sessionStoreContainer.js | 2 +- .../test/browser_should_restore_tab.js | 4 +- .../test/browser_windowStateContainer.js | 2 +- browser/components/sessionstore/test/head.js | 49 +- browser/components/shell/HeadlessShell.sys.mjs | 2 +- browser/components/shell/ShellService.sys.mjs | 18 + .../shell/content/setDesktopBackground.js | 2 +- .../components/shell/nsIWindowsShellService.idl | 12 +- browser/components/shell/test/browser_1119088.js | 2 +- browser/components/shell/test/browser_420786.js | 2 +- .../test/browser_setDesktopBackgroundPreview.js | 2 +- browser/components/shell/test/head.js | 4 +- .../tests/browser/browser_exposure_telemetry.js | 2 +- .../tests/browser/browser_shopping_settings.js | 8 +- .../tests/browser/browser_shopping_urlbar.js | 10 +- .../shopping/tests/browser/browser_ui_telemetry.js | 2 +- browser/components/sidebar/browser-sidebar.js | 744 ++++++++++++++ browser/components/sidebar/jar.mn | 9 + browser/components/sidebar/sidebar-history.html | 34 + browser/components/sidebar/sidebar-history.mjs | 201 ++++ browser/components/sidebar/sidebar-launcher.css | 34 + browser/components/sidebar/sidebar-launcher.mjs | 169 ++++ browser/components/sidebar/sidebar-page.mjs | 45 + browser/components/sidebar/sidebar-syncedtabs.html | 45 + browser/components/sidebar/sidebar-syncedtabs.mjs | 191 ++++ browser/components/sidebar/sidebar.css | 27 + browser/components/sidebar/sidebar.ftl | 26 + .../.storybook/addon-fluent/preset/manager.mjs | 2 +- browser/components/storybook/.storybook/main.js | 9 +- .../storybook/.storybook/manager-head.html | 22 + .../storybook/.storybook/markdown-story-utils.js | 10 +- .../storybook/.storybook/preview-head.html | 8 +- .../components/storybook/.storybook/preview.mjs | 3 +- .../storybook/docs/README.storybook.stories.md | 2 +- .../storybook/stories/fxview-tab-list.stories.mjs | 2 +- .../syncedtabs/SyncedTabsDeckComponent.sys.mjs | 2 +- browser/components/syncedtabs/TabListView.sys.mjs | 2 +- browser/components/tabpreview/jar.mn | 2 +- .../components/tabpreview/tab-preview-panel.mjs | 174 ++++ browser/components/tabpreview/tabpreview.css | 53 +- browser/components/tabpreview/tabpreview.mjs | 237 ----- .../browser/browser_contentpermissionprompt.js | 2 +- .../browser_default_webprotocol_handler_mailto.js | 53 +- .../tests/browser/browser_quit_disabled.js | 2 +- browser/components/tests/browser/head.js | 4 +- browser/components/touchbar/MacTouchBar.sys.mjs | 2 +- .../tests/browser/browser_touchbar_tests.js | 2 +- .../content/TranslationsPanelShared.sys.mjs | 93 +- .../content/fullPageTranslationsPanel.js | 91 +- .../content/selectTranslationsPanel.inc.xhtml | 172 ++-- .../content/selectTranslationsPanel.js | 895 +++++++++++++++-- browser/components/translations/moz.build | 2 +- .../translations/tests/browser/browser.toml | 38 +- ...r_translations_about_preferences_settings_ui.js | 201 +++- ...ranslations_full_page_move_tab_to_new_window.js | 64 ++ ...wser_translations_full_page_multiple_windows.js | 68 ++ ...ions_full_page_panel_engine_unsupported_lang.js | 28 - ...er_translations_full_page_panel_init_failure.js | 25 + .../browser_translations_full_page_panel_retry.js | 2 +- ...ranslations_full_page_panel_switch_languages.js | 10 +- ...ranslations_full_page_panel_unsupported_lang.js | 31 + ...lations_full_page_telemetry_switch_languages.js | 16 +- ...lations_select_context_menu_feature_disabled.js | 16 +- ...text_menu_with_full_page_translations_active.js | 22 +- ...nslations_select_context_menu_with_hyperlink.js | 12 +- ...ns_select_context_menu_with_no_text_selected.js | 6 +- ...tions_select_context_menu_with_text_selected.js | 14 +- ...owser_translations_select_panel_engine_cache.js | 59 ++ ...ations_select_panel_fallback_to_doc_language.js | 38 + ...translations_select_panel_language_selectors.js | 54 - ...rowser_translations_select_panel_mainview_ui.js | 36 - ...translations_select_panel_open_to_idle_state.js | 61 ++ ...anel_retranslate_on_change_language_directly.js | 70 ++ ...nslate_on_change_language_from_dropdown_menu.js | 68 ++ ...elect_panel_select_current_language_directly.js | 71 ++ ...l_select_current_language_from_dropdown_menu.js | 71 ++ ...l_select_same_from_and_to_languages_directly.js | 71 ++ ...ame_from_and_to_languages_from_dropdown_menu.js | 71 ++ ..._panel_translate_on_change_language_directly.js | 71 ++ ...nslate_on_change_language_from_dropdown_menu.js | 71 ++ ...e_on_change_language_multiple_times_directly.js | 97 ++ ...e_language_multiple_times_from_dropdown_menu.js | 98 ++ ..._translations_select_panel_translate_on_open.js | 86 ++ .../components/translations/tests/browser/head.js | 1036 ++++++++++++++------ browser/components/uitour/UITour-lib.js | 2 +- browser/components/uitour/UITour.sys.mjs | 10 +- browser/components/uitour/test/browser_UITour.js | 2 +- browser/components/uitour/test/browser_UITour3.js | 10 +- .../uitour/test/browser_UITour_defaultBrowser.js | 6 +- .../uitour/test/browser_UITour_modalDialog.js | 2 +- browser/components/uitour/test/head.js | 13 +- browser/components/urlbar/.eslintrc.js | 2 - browser/components/urlbar/UrlbarController.sys.mjs | 121 ++- browser/components/urlbar/UrlbarInput.sys.mjs | 29 +- browser/components/urlbar/UrlbarPrefs.sys.mjs | 20 +- .../urlbar/UrlbarProviderAboutPages.sys.mjs | 4 +- .../urlbar/UrlbarProviderAutofill.sys.mjs | 2 +- .../urlbar/UrlbarProviderCalculator.sys.mjs | 2 +- .../urlbar/UrlbarProviderClipboard.sys.mjs | 5 +- .../urlbar/UrlbarProviderContextualSearch.sys.mjs | 2 +- .../urlbar/UrlbarProviderInputHistory.sys.mjs | 4 +- .../urlbar/UrlbarProviderInterventions.sys.mjs | 8 +- .../urlbar/UrlbarProviderOmnibox.sys.mjs | 2 +- .../components/urlbar/UrlbarProviderPlaces.sys.mjs | 2 +- .../urlbar/UrlbarProviderQuickActions.sys.mjs | 4 +- .../urlbar/UrlbarProviderQuickSuggest.sys.mjs | 12 +- ...lbarProviderQuickSuggestContextualOptIn.sys.mjs | 2 +- .../urlbar/UrlbarProviderRecentSearches.sys.mjs | 2 +- .../urlbar/UrlbarProviderSearchSuggestions.sys.mjs | 2 +- .../urlbar/UrlbarProviderSearchTips.sys.mjs | 2 +- .../urlbar/UrlbarProviderTabToSearch.sys.mjs | 4 +- .../urlbar/UrlbarProviderTokenAliasEngines.sys.mjs | 2 +- .../urlbar/UrlbarProviderTopSites.sys.mjs | 10 +- .../urlbar/UrlbarProviderUnitConversion.sys.mjs | 2 +- .../urlbar/UrlbarProviderWeather.sys.mjs | 6 +- .../urlbar/UrlbarProvidersManager.sys.mjs | 8 +- browser/components/urlbar/UrlbarUtils.sys.mjs | 32 +- browser/components/urlbar/UrlbarView.sys.mjs | 10 + .../urlbar/docs/dynamic-result-types.rst | 6 +- browser/components/urlbar/metrics.yaml | 38 +- browser/components/urlbar/pings.yaml | 16 + .../urlbar/private/AddonSuggestions.sys.mjs | 10 +- .../components/urlbar/private/AdmWikipedia.sys.mjs | 5 +- .../urlbar/private/MDNSuggestions.sys.mjs | 10 +- .../urlbar/private/SuggestBackendRust.sys.mjs | 9 +- .../urlbar/private/YelpSuggestions.sys.mjs | 10 +- .../urlbar/tests/UrlbarTestUtils.sys.mjs | 17 +- .../urlbar/tests/browser-tips/browser_picks.js | 10 +- .../tests/browser-tips/browser_searchTips.js | 2 +- .../browser-tips/browser_searchTips_interaction.js | 2 +- .../components/urlbar/tests/browser/browser.toml | 14 +- .../tests/browser/browser_UrlbarInput_overflow.js | 2 +- .../tests/browser/browser_aboutHomeLoading.js | 4 +- .../browser_acknowledgeFeedbackAndDismissal.js | 4 +- .../tests/browser/browser_copy_during_load.js | 2 +- .../urlbar/tests/browser/browser_dynamicResults.js | 4 +- .../urlbar/tests/browser/browser_engagement.js | 29 +- .../browser_less_common_selection_manipulations.js | 288 ++++++ .../tests/browser/browser_locationBarCommand.js | 2 +- .../browser_primary_selection_safe_on_new_tab.js | 2 +- .../urlbar/tests/browser/browser_raceWithTabs.js | 4 +- .../urlbar/tests/browser/browser_result_menu.js | 12 +- .../urlbar/tests/browser/browser_stop_pending.js | 4 +- .../browser_urlbar_telemetry_tabtosearch.js | 4 +- .../tests/engagementTelemetry/browser/browser.toml | 8 +- .../browser_glean_telemetry_abandonment_type.js | 8 +- .../browser_glean_telemetry_engagement_tips.js | 2 +- .../browser_glean_telemetry_engagement_type.js | 48 +- .../browser/browser_glean_telemetry_exposure.js | 2 +- .../browser_glean_telemetry_potential_exposure.js | 438 +++++++++ .../browser/browser_glean_telemetry_reenter.js | 79 ++ .../tests/engagementTelemetry/browser/head.js | 8 +- .../quicksuggest/QuickSuggestTestUtils.sys.mjs | 17 +- .../quicksuggest/browser/browser_quicksuggest.js | 37 + .../browser/browser_quicksuggest_addons.js | 7 +- .../browser/browser_quicksuggest_block.js | 5 - .../browser/browser_quicksuggest_mdn.js | 68 +- .../browser/browser_quicksuggest_pocket.js | 20 +- .../browser/browser_quicksuggest_yelp.js | 5 + .../browser/browser_telemetry_dynamicWikipedia.js | 5 - .../browser/browser_telemetry_gleanEmptyStrings.js | 5 - .../browser_telemetry_impressionEdgeCases.js | 11 +- .../browser/browser_telemetry_nonsponsored.js | 5 - .../browser/browser_telemetry_sponsored.js | 5 - .../urlbar/tests/quicksuggest/browser/head.js | 41 +- .../urlbar/tests/quicksuggest/unit/head.js | 10 +- .../unit/test_quicksuggest_impressionCaps.js | 2 +- .../unit/test_quicksuggest_merinoSessions.js | 2 +- .../urlbar/tests/quicksuggest/unit/test_weather.js | 2 +- .../components/urlbar/tests/unit/test_exposure.js | 11 +- .../components/urlbar/tests/unit/test_l10nCache.js | 2 +- 717 files changed, 22834 insertions(+), 11583 deletions(-) delete mode 100644 browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json create mode 100644 browser/components/asrouter/modules/ActorConstants.mjs delete mode 100644 browser/components/asrouter/modules/ActorConstants.sys.mjs delete mode 100644 browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_keyboard_cfr.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_milestone_message_cfr.js delete mode 100644 browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js delete mode 100644 browser/components/backup/.eslintrc.js create mode 100644 browser/components/backup/content/debug.html create mode 100644 browser/components/backup/content/debug.js create mode 100644 browser/components/backup/docs/backup-resources.rst create mode 100644 browser/components/backup/jar.mn create mode 100644 browser/components/backup/resources/AddonsBackupResource.sys.mjs create mode 100644 browser/components/backup/resources/CookiesBackupResource.sys.mjs create mode 100644 browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs create mode 100644 browser/components/backup/resources/FormHistoryBackupResource.sys.mjs create mode 100644 browser/components/backup/resources/MiscDataBackupResource.sys.mjs create mode 100644 browser/components/backup/resources/PlacesBackupResource.sys.mjs create mode 100644 browser/components/backup/resources/PreferencesBackupResource.sys.mjs create mode 100644 browser/components/backup/resources/SessionStoreBackupResource.sys.mjs create mode 100644 browser/components/backup/tests/xpcshell/head.js create mode 100644 browser/components/backup/tests/xpcshell/test_BackupResource.js delete mode 100644 browser/components/backup/tests/xpcshell/test_BrowserResource.js create mode 100644 browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js create mode 100644 browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js create mode 100644 browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js create mode 100644 browser/components/backup/tests/xpcshell/test_createBackup.js create mode 100644 browser/components/enterprisepolicies/tests/browser/browser_policy_translateenabled.js create mode 100644 browser/components/firefoxview/HistoryController.mjs create mode 100644 browser/components/firefoxview/SyncedTabsController.sys.mjs create mode 100644 browser/components/firefoxview/opentabs-tab-list.css create mode 100644 browser/components/firefoxview/opentabs-tab-list.mjs create mode 100644 browser/components/firefoxview/opentabs-tab-row.css create mode 100644 browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js create mode 100644 browser/components/newtab/common/Actions.mjs delete mode 100644 browser/components/newtab/common/Actions.sys.mjs delete mode 100644 browser/components/newtab/content-src/components/Card/types.js create mode 100644 browser/components/newtab/content-src/components/Card/types.mjs delete mode 100644 browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs create mode 100644 browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx create mode 100644 browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss delete mode 100644 browser/components/newtab/content-src/lib/constants.js create mode 100644 browser/components/newtab/content-src/lib/constants.mjs delete mode 100644 browser/components/newtab/content-src/lib/detect-user-session-start.js create mode 100644 browser/components/newtab/content-src/lib/detect-user-session-start.mjs delete mode 100644 browser/components/newtab/content-src/lib/init-store.js create mode 100644 browser/components/newtab/content-src/lib/init-store.mjs delete mode 100644 browser/components/newtab/content-src/lib/link-menu-options.js create mode 100644 browser/components/newtab/content-src/lib/link-menu-options.mjs delete mode 100644 browser/components/newtab/content-src/lib/perf-service.js create mode 100644 browser/components/newtab/content-src/lib/perf-service.mjs delete mode 100644 browser/components/newtab/content-src/lib/screenshot-utils.js create mode 100644 browser/components/newtab/content-src/lib/screenshot-utils.mjs delete mode 100644 browser/components/newtab/content-src/lib/selectLayoutRender.js create mode 100644 browser/components/newtab/content-src/lib/selectLayoutRender.mjs create mode 100644 browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/dark-color.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/light-beach.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/light-color.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/light-panda.avif create mode 100644 browser/components/newtab/data/content/assets/wallpapers/light-sky.avif create mode 100644 browser/components/newtab/lib/WallpaperFeed.sys.mjs create mode 100644 browser/components/newtab/test/xpcshell/test_WallpaperFeed.js delete mode 100644 browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv create mode 100644 browser/components/originattributes/test/browser/file_thirdPartyChild.video.webm delete mode 100644 browser/components/places/.eslintrc.js create mode 100644 browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js create mode 100644 browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js create mode 100644 browser/components/screenshots/tests/browser/browser_test_selection_size_text.js delete mode 100644 browser/components/search/DomainToCategoriesMap.worker.mjs delete mode 100644 browser/components/search/schema/search-telemetry-schema.json delete mode 100644 browser/components/search/schema/search-telemetry-ui-schema.json create mode 100644 browser/components/search/schema/search-telemetry-v2-schema.json create mode 100644 browser/components/search/schema/search-telemetry-v2-ui-schema.json create mode 100644 browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js create mode 100644 browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js create mode 100644 browser/components/search/test/browser/telemetry/searchTelemetryDomainCategorizationReportingWithoutAds.html create mode 100644 browser/components/search/test/marionette/telemetry/manifest.toml create mode 100644 browser/components/search/test/marionette/telemetry/test_ping_submitted.py create mode 100644 browser/components/search/test/unit/corruptDB.sqlite create mode 100644 browser/components/search/test/unit/test_domain_to_categories_store.js create mode 100644 browser/components/search/test/unit/test_ui_schemas_valid.js delete mode 100644 browser/components/sessionstore/ContentRestore.sys.mjs delete mode 100644 browser/components/sessionstore/ContentSessionStore.sys.mjs delete mode 100644 browser/components/sessionstore/content/content-sessionStore.js create mode 100644 browser/components/sessionstore/test/browser_oldformat.toml delete mode 100644 browser/components/sessionstore/test/browser_send_async_message_oom.js create mode 100644 browser/components/sidebar/browser-sidebar.js create mode 100644 browser/components/sidebar/sidebar-history.html create mode 100644 browser/components/sidebar/sidebar-history.mjs create mode 100644 browser/components/sidebar/sidebar-launcher.css create mode 100644 browser/components/sidebar/sidebar-launcher.mjs create mode 100644 browser/components/sidebar/sidebar-page.mjs create mode 100644 browser/components/sidebar/sidebar-syncedtabs.html create mode 100644 browser/components/sidebar/sidebar-syncedtabs.mjs create mode 100644 browser/components/sidebar/sidebar.css create mode 100644 browser/components/sidebar/sidebar.ftl create mode 100644 browser/components/storybook/.storybook/manager-head.html create mode 100644 browser/components/tabpreview/tab-preview-panel.mjs delete mode 100644 browser/components/tabpreview/tabpreview.mjs create mode 100644 browser/components/translations/tests/browser/browser_translations_full_page_move_tab_to_new_window.js create mode 100644 browser/components/translations/tests/browser/browser_translations_full_page_multiple_windows.js delete mode 100644 browser/components/translations/tests/browser/browser_translations_full_page_panel_engine_unsupported_lang.js create mode 100644 browser/components/translations/tests/browser/browser_translations_full_page_panel_init_failure.js create mode 100644 browser/components/translations/tests/browser/browser_translations_full_page_panel_unsupported_lang.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_engine_cache.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_fallback_to_doc_language.js delete mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_language_selectors.js delete mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_mainview_ui.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_directly.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_directly.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_from_dropdown_menu.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js create mode 100644 browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js create mode 100644 browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_potential_exposure.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_reenter.js (limited to 'browser/components') 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.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 { ) : null} -
-
- {content.title ? this.renderTitle(content) : null} +
+ {content.title || content.subtitle ? ( +
+ {content.title ? this.renderTitle(content) : null} - {content.subtitle ? ( - -

+

+ + ) : null} + {content.cta_paragraph ? ( + - - ) : null} - {content.cta_paragraph ? ( - - ) : null} -

+ ) : null} +
+ ) : null} {content.video_container ? ( { + const category = props.content.tiles?.category?.type; return (
-
+
{props.content.tiles.data.map( - ({ theme, label, tooltip, description }) => ( + ({ theme, label, tooltip, description, type }) => ( {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} -
- +
@@ -104,7 +108,11 @@ name="recentlyclosed" type="page" > - +
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` `; }; + stylesheets() { + return html``; + } + 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` - - ${when( - this.pinnedTabsGridView && this.pinnedTabs.length, - () => html` -
- ${this.pinnedTabs.map((tabItem, i) => - this.itemTemplate(tabItem, i) - )} -
- ` - )} + ${this.stylesheets()}
html` - ` - )} - ${when( - !lazy.virtualListEnabledPref, - () => html` - ${this.tabItems.map((tabItem, i) => this.itemTemplate(tabItem, i))} - ` + `, + () => + html`${this.tabItems.map((tabItem, i) => + this.itemTemplate(tabItem, i) + )}` )}
`; } - #emptySearchResultsTemplate() { + emptySearchResultsTemplate() { return html` `; } } -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`${string.substring(indexStart, indexEnd)}` + ); + prevIndexEnd = regex.lastIndex; } - this.tabElement.toggleMuteAudio(); + fragments.push(string.substring(prevIndexEnd)); + return fragments; + } + + stylesheets() { + return html``; } - #faviconTemplate() { + faviconTemplate() { return html``; + } + + titleTemplate() { + const title = this.title; + return html` - ${when( - this.pinnedTabsGridView && - this.indicators?.includes("pinned") && - (this.indicators?.includes("muted") || - this.indicators?.includes("soundplaying")), - () => html` - - ` + this.searchQuery, + () => this.highlightSearchMatches(this.searchQuery, title), + () => title )} `; } - #pinnedTabItemTemplate() { - return html` `; + ${when( + this.searchQuery, + () => + this.highlightSearchMatches( + this.searchQuery, + this.formatURIForDisplay(this.url) + ), + () => this.formatURIForDisplay(this.url) + )} + `; } - #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` + + ${relativeString} + `; + } + + timeTemplate() { const timeString = this.timeFluentId(this.dateTimeFormat); const time = this.time; const timeArgs = JSON.stringify({ time }); + return html` + `; + } - return html` html`` + )}`; + } + + tertiaryButtonTemplate() { + return html`${when( + this.tertiaryL10nId && this.tertiaryActionHandler, + () => html`` + )}`; + } +} + +export class FxviewTabRow extends FxviewTabRowBase { + render() { + return html` + ${this.stylesheets()} + - ${this.#faviconTemplate()} - - ${when( - this.searchQuery, - () => this.#highlightSearchMatches(this.searchQuery, title), - () => title - )} - - - - ${when( - this.searchQuery, - () => - this.#highlightSearchMatches( - this.searchQuery, - this.formatURIForDisplay(this.url) - ), - () => this.formatURIForDisplay(this.url) - )} - - - - ${relativeString} - - - + ${this.faviconTemplate()} ${this.titleTemplate()} + ${when( + !this.compact, + () => html`${this.urlTemplate()} ${this.dateTemplate()} + ${this.timeTemplate()}` + )} - ${when( - this.indicators?.includes("soundplaying") || - this.indicators?.includes("muted"), - () => html``, - () => html`` - )} - ${when( - this.secondaryL10nId && this.secondaryActionHandler, - () => html`` - )} - ${when( - this.tertiaryL10nId && this.tertiaryActionHandler, - () => html`` - )}`; - } - - render() { - return html` - ${when( - this.containerObj, - () => html` - - ` - )} - - - ${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`${string.substring(indexStart, indexEnd)}` - ); - prevIndexEnd = regex.lastIndex; - } - fragments.push(string.substring(prevIndexEnd)); - return fragments; - } } customElements.define("fxview-tab-row", FxviewTabRow); @@ -1040,10 +810,16 @@ export class VirtualList extends MozLitElement { this.isSubList = false; this.isVisible = false; this.intersectionObserver = new IntersectionObserver( - ([entry]) => (this.isVisible = entry.isIntersecting), + ([entry]) => { + this.isVisible = entry.isIntersecting; + }, { root: this.ownerDocument } ); - this.resizeObserver = new ResizeObserver(([entry]) => { + this.selfResizeObserver = new ResizeObserver(() => { + // Trigger the intersection observer once the tab rows have rendered + this.triggerIntersectionObserver(); + }); + this.childResizeObserver = new ResizeObserver(([entry]) => { if (entry.contentRect?.height > 0) { // Update properties on top-level virtual-list this.parentElement.itemHeightEstimate = entry.contentRect.height; @@ -1058,7 +834,8 @@ export class VirtualList extends MozLitElement { disconnectedCallback() { super.disconnectedCallback(); this.intersectionObserver.disconnect(); - this.resizeObserver.disconnect(); + this.childResizeObserver.disconnect(); + this.selfResizeObserver.disconnect(); } triggerIntersectionObserver() { @@ -1090,7 +867,6 @@ export class VirtualList extends MozLitElement { this.items.slice(i, i + this.maxRenderCountEstimate) ); } - this.triggerIntersectionObserver(); } } @@ -1103,13 +879,17 @@ export class VirtualList extends MozLitElement { firstUpdated() { this.intersectionObserver.observe(this); + this.selfResizeObserver.observe(this); if (this.isSubList && this.children[0]) { - this.resizeObserver.observe(this.children[0]); + this.childResizeObserver.observe(this.children[0]); } } updated(changedProperties) { this.updateListHeight(changedProperties); + if (changedProperties.has("items") && !this.isSubList) { + this.triggerIntersectionObserver(); + } } updateListHeight(changedProperties) { @@ -1157,5 +937,4 @@ export class VirtualList extends MozLitElement { return ""; } } - customElements.define("virtual-list", VirtualList); diff --git a/browser/components/firefoxview/fxview-tab-row.css b/browser/components/firefoxview/fxview-tab-row.css index 219d7e8aa2..c1c8f967a7 100644 --- a/browser/components/firefoxview/fxview-tab-row.css +++ b/browser/components/firefoxview/fxview-tab-row.css @@ -2,9 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@import url("chrome://global/skin/design-system/text-and-typography.css"); + :host { - --fxviewtabrow-element-background-hover: color-mix(in srgb, currentColor 14%, transparent); - --fxviewtabrow-element-background-active: color-mix(in srgb, currentColor 21%, transparent); + --fxviewtabrow-element-background-hover: var(--button-background-color-ghost-hover); + --fxviewtabrow-element-background-active: var(--button-background-color-ghost-active); display: grid; grid-template-columns: subgrid; grid-column: span 9; @@ -12,7 +14,7 @@ border-radius: 4px; } -@media (prefers-contrast) { +@media (forced-colors) { :host { --fxviewtabrow-element-background-hover: ButtonText; --fxviewtabrow-element-background-active: ButtonText; @@ -32,115 +34,42 @@ cursor: pointer; text-decoration: none; - :host(.pinned) & { - padding: var(--space-small); - min-width: unset; - margin: 0; + :host([compact]) & { + grid-template-columns: min-content auto; } } .fxview-tab-row-main, .fxview-tab-row-main:visited, -.fxview-tab-row-main:hover:active, -.fxview-tab-row-button { +.fxview-tab-row-main:hover:active { color: inherit; } -.fxview-tab-row-main:hover, -.fxview-tab-row-button.ghost-button.icon-button:enabled:hover { +.fxview-tab-row-main:hover { background-color: var(--fxviewtabrow-element-background-hover); color: var(--fxviewtabrow-text-color-hover); - - & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after { - stroke: var(--fxview-indicator-stroke-color-hover); - } } -.fxview-tab-row-main:hover:active, -.fxview-tab-row-button.ghost-button.icon-button:enabled:hover:active { +.fxview-tab-row-main:hover:active { background-color: var(--fxviewtabrow-element-background-active); } -@media (prefers-contrast) { - a.fxview-tab-row-main, - a.fxview-tab-row-main:hover, - a.fxview-tab-row-main:active { +@media (forced-colors) { + .fxview-tab-row-main, + .fxview-tab-row-main:hover, + .fxview-tab-row-main:active { background-color: transparent; border: 1px solid LinkText; color: LinkText; } - a.fxview-tab-row-main:visited, - a.fxview-tab-row-main:visited:hover { + .fxview-tab-row-main:visited, + .fxview-tab-row-main:visited:hover { border: 1px solid VisitedText; color: VisitedText; } } -.fxview-tab-row-favicon-wrapper { - height: 16px; - position: relative; - - .fxview-tab-row-favicon::after, - .fxview-tab-row-button::after, - &.pinned .fxview-tab-row-pinned-media-button { - display: block; - content: ""; - background-size: 12px; - background-position: center; - background-repeat: no-repeat; - position: relative; - height: 12px; - width: 12px; - -moz-context-properties: fill, stroke; - fill: currentColor; - stroke: var(--fxview-background-color-secondary); - } - - &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after { - inset-block-start: 9px; - inset-inline-end: -6px; - } - - &.pinnedOnNewTab .fxview-tab-row-favicon::after, - &.pinnedOnNewTab .fxview-tab-row-button::after { - background-image: url("chrome://browser/skin/pin-12.svg"); - } - - &.bookmark .fxview-tab-row-favicon::after, - &.bookmark .fxview-tab-row-button::after { - background-image: url("chrome://browser/skin/bookmark-12.svg"); - fill: var(--fxview-primary-action-background); - } - - &.attention .fxview-tab-row-favicon::after, - &.attention .fxview-tab-row-button::after { - background-image: radial-gradient(circle, light-dark(rgb(42, 195, 162), rgb(84, 255, 189)), light-dark(rgb(42, 195, 162), rgb(84, 255, 189)) 2px, transparent 2px); - height: 4px; - width: 100%; - inset-block-start: 20px; - } - - &.pinned .fxview-tab-row-pinned-media-button { - inset-block-start: -10px; - inset-inline-end: -10px; - border-radius: 100%; - background-color: var(--fxview-background-color-secondary); - padding: 6px; - min-width: 0; - min-height: 0; - position: absolute; - - &[muted="true"] { - background-image: url("chrome://global/skin/media/audio-muted.svg"); - } - - &[soundplaying="true"] { - background-image: url("chrome://global/skin/media/audio.svg"); - } - } -} - .fxview-tab-row-favicon { background-size: cover; -moz-context-properties: fill; @@ -155,15 +84,6 @@ text-align: match-parent; } -.fxview-tab-row-container-indicator { - height: 16px; - width: 16px; - background-image: var(--identity-icon); - background-size: cover; - -moz-context-properties: fill; - fill: var(--identity-icon-color); -} - .fxview-tab-row-url { color: var(--text-color-deemphasized); text-decoration-line: underline; @@ -182,62 +102,22 @@ font-weight: 400; } -.fxview-tab-row-button { - margin: 0; - cursor: pointer; - min-width: 0; - background-color: transparent; - - &[muted="true"], - &[soundplaying="true"] { - background-size: 16px; - background-repeat: no-repeat; - background-position: center; - -moz-context-properties: fill; - fill: currentColor; - } - - &[muted="true"] { - background-image: url("chrome://global/skin/media/audio-muted.svg"); - } - - &[soundplaying="true"] { - background-image: url("chrome://global/skin/media/audio.svg"); - } - - &.dismiss-button { - background-image: url("chrome://global/skin/icons/close.svg"); - } - - &.options-button { - background-image: url("chrome://global/skin/icons/more.svg"); - } +.fxview-tab-row-button::part(button) { + color: var(--fxview-text-primary-color) } -@media (prefers-contrast) { - .fxview-tab-row-button, - button.fxview-tab-row-main { - border: 1px solid ButtonText; - color: ButtonText; - } +.fxview-tab-row-button[muted="true"]::part(button) { + background-image: url("chrome://global/skin/media/audio-muted.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled:hover, - button.fxview-tab-row-main:enabled:hover { - border: 1px solid SelectedItem; - color: SelectedItem; - } +.fxview-tab-row-button[soundplaying="true"]::part(button) { + background-image: url("chrome://global/skin/media/audio.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled:active, - button.fxview-tab-row-main:enabled:active { - color: SelectedItem; - } +.fxview-tab-row-button.dismiss-button::part(button) { + background-image: url("chrome://global/skin/icons/close.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled, - .fxview-tab-row-button.ghost-button.icon-button:enabled:hover, - .fxview-tab-row-button.ghost-button.icon-button:enabled:active - button.fxview-tab-row-main:enabled, - button.fxview-tab-row-main:enabled:hover, - button.fxview-tab-row-main:enabled:active { - background-color: ButtonFace; - } +.fxview-tab-row-button.options-button::part(button) { + background-image: url("chrome://global/skin/icons/more.svg"); } diff --git a/browser/components/firefoxview/helpers.mjs b/browser/components/firefoxview/helpers.mjs index 3cb308a587..b206deef18 100644 --- a/browser/components/firefoxview/helpers.mjs +++ b/browser/components/firefoxview/helpers.mjs @@ -173,3 +173,20 @@ export function escapeHtmlEntities(text) { .replace(/"/g, """) .replace(/'/g, "'"); } + +export function navigateToLink(e) { + let currentWindow = + e.target.ownerGlobal.browsingContext.embedderWindowGlobal.browsingContext + .window; + if (currentWindow.openTrustedLinkIn) { + let where = lazy.BrowserUtils.whereToOpenLink( + e.detail.originalEvent, + false, + true + ); + if (where == "current") { + where = "tab"; + } + currentWindow.openTrustedLinkIn(e.originalTarget.url, where); + } +} diff --git a/browser/components/firefoxview/history.css b/browser/components/firefoxview/history.css index dd2786a8c7..a10291ddb5 100644 --- a/browser/components/firefoxview/history.css +++ b/browser/components/firefoxview/history.css @@ -51,19 +51,8 @@ cursor: pointer; } -.import-history-banner .close { +moz-button.close::part(button) { background-image: url("chrome://global/skin/icons/close-12.svg"); - background-repeat: no-repeat; - background-position: center center; - -moz-context-properties: fill; - fill: currentColor; - min-width: auto; - min-height: auto; - width: 24px; - height: 24px; - margin: 0; - padding: 0; - flex-shrink: 0; } dialog { diff --git a/browser/components/firefoxview/history.mjs b/browser/components/firefoxview/history.mjs index 1fe028449b..478422d49b 100644 --- a/browser/components/firefoxview/history.mjs +++ b/browser/components/firefoxview/history.mjs @@ -7,18 +7,21 @@ import { ifDefined, when, } from "chrome://global/content/vendor/lit.all.mjs"; -import { escapeHtmlEntities, isSearchEnabled } from "./helpers.mjs"; +import { + escapeHtmlEntities, + isSearchEnabled, + navigateToLink, +} from "./helpers.mjs"; import { ViewPage } from "./viewpage.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/migration/migration-wizard.mjs"; +import { HistoryController } from "./HistoryController.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", - FirefoxViewPlacesQuery: - "resource:///modules/firefox-view-places-query.sys.mjs", - PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", }); @@ -26,13 +29,6 @@ let XPCOMUtils = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ).XPCOMUtils; -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "maxRowsPref", - "browser.firefox-view.max-history-rows", - -1 -); - const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history"; const IMPORT_HISTORY_DISMISSED_PREF = @@ -44,35 +40,30 @@ class HistoryInView extends ViewPage { constructor() { super(); this._started = false; - this.allHistoryItems = new Map(); - this.historyMapByDate = []; - this.historyMapBySite = []; // Setting maxTabsLength to -1 for no max this.maxTabsLength = -1; - this.placesQuery = new lazy.FirefoxViewPlacesQuery(); - this.searchQuery = ""; - this.searchResults = null; - this.sortOption = "date"; this.profileAge = 8; this.fullyUpdated = false; this.cumulativeSearches = 0; } + controller = new HistoryController(this, { + searchResultsLimit: SEARCH_RESULTS_LIMIT, + }); + start() { if (this._started) { return; } this._started = true; - this.#updateAllHistoryItems(); - this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data)); + this.controller.updateAllHistoryItems(); this.toggleVisibilityInCardContainer(); } async connectedCallback() { super.connectedCallback(); - await this.updateHistoryData(); XPCOMUtils.defineLazyPreferenceGetter( this, "importHistoryDismissedPref", @@ -91,6 +82,7 @@ class HistoryInView extends ViewPage { this.requestUpdate(); } ); + if (!this.importHistoryDismissedPref && !this.hasImportedHistoryPrefs) { let profileAccessor = await lazy.ProfileAge(); let profileCreateTime = await profileAccessor.created; @@ -106,7 +98,6 @@ class HistoryInView extends ViewPage { return; } this._started = false; - this.placesQuery.close(); this.toggleVisibilityInCardContainer(); } @@ -120,32 +111,6 @@ class HistoryInView extends ViewPage { ); } - async #updateAllHistoryItems(allHistoryItems) { - if (allHistoryItems) { - this.allHistoryItems = allHistoryItems; - } else { - await this.updateHistoryData(); - } - this.resetHistoryMaps(); - this.lists.forEach(list => list.requestUpdate()); - await this.#updateSearchResults(); - } - - async #updateSearchResults() { - if (this.searchQuery) { - try { - this.searchResults = await this.placesQuery.searchHistory( - this.searchQuery, - SEARCH_RESULTS_LIMIT - ); - } catch (e) { - // Connection interrupted, ignore. - } - } else { - this.searchResults = null; - } - } - viewVisibleCallback() { this.start(); } @@ -166,14 +131,8 @@ class HistoryInView extends ViewPage { }; static properties = { - ...ViewPage.properties, - allHistoryItems: { type: Map }, - historyMapByDate: { type: Array }, - historyMapBySite: { type: Array }, // Making profileAge a reactive property for testing profileAge: { type: Number }, - searchResults: { type: Array }, - sortOption: { type: String }, }; async getUpdateComplete() { @@ -181,70 +140,8 @@ class HistoryInView extends ViewPage { await Promise.all(Array.from(this.cards).map(card => card.updateComplete)); } - async updateHistoryData() { - this.allHistoryItems = await this.placesQuery.getHistory({ - daysOld: 60, - limit: lazy.maxRowsPref, - sortBy: this.sortOption, - }); - } - - resetHistoryMaps() { - this.historyMapByDate = []; - this.historyMapBySite = []; - } - - createHistoryMaps() { - if (this.sortOption === "date" && !this.historyMapByDate.length) { - const { - visitsFromToday, - visitsFromYesterday, - visitsByDay, - visitsByMonth, - } = this.placesQuery; - - // Add visits from today and yesterday. - if (visitsFromToday.length) { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-today", - items: visitsFromToday, - }); - } - if (visitsFromYesterday.length) { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-yesterday", - items: visitsFromYesterday, - }); - } - - // Add visits from this month, grouped by day. - visitsByDay.forEach(visits => { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-this-month", - items: visits, - }); - }); - - // Add visits from previous months, grouped by month. - visitsByMonth.forEach(visits => { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-prev-month", - items: visits, - }); - }); - } else if (this.sortOption === "site" && !this.historyMapBySite.length) { - this.historyMapBySite = Array.from( - this.allHistoryItems.entries(), - ([domain, items]) => ({ - domain, - items, - l10nId: domain ? null : "firefoxview-history-site-localhost", - }) - ).sort((a, b) => a.domain.localeCompare(b.domain)); - } - } - onPrimaryAction(e) { + navigateToLink(e); // Record telemetry Services.telemetry.recordEvent( "firefoxview_next", @@ -254,26 +151,13 @@ class HistoryInView extends ViewPage { {} ); - if (this.searchQuery) { + if (this.controller.searchQuery) { const searchesHistogram = Services.telemetry.getKeyedHistogramById( "FIREFOX_VIEW_CUMULATIVE_SEARCHES" ); searchesHistogram.add("history", this.cumulativeSearches); this.cumulativeSearches = 0; } - - let currentWindow = this.getWindow(); - if (currentWindow.openTrustedLinkIn) { - let where = lazy.BrowserUtils.whereToOpenLink( - e.detail.originalEvent, - false, - true - ); - if (where == "current") { - where = "tab"; - } - currentWindow.openTrustedLinkIn(e.originalTarget.url, where); - } } onSecondaryAction(e) { @@ -282,24 +166,29 @@ class HistoryInView extends ViewPage { } deleteFromHistory(e) { - lazy.PlacesUtils.history.remove(this.triggerNode.url); + this.controller.deleteFromHistory(); this.recordContextMenuTelemetry("delete-from-history", e); } async onChangeSortOption(e) { - this.sortOption = e.target.value; + await this.controller.onChangeSortOption(e); Services.telemetry.recordEvent( "firefoxview_next", "sort_history", "tabs", null, { - sort_type: this.sortOption, - search_start: this.searchQuery ? "true" : "false", + sort_type: this.controller.sortOption, + search_start: this.controller.searchQuery ? "true" : "false", } ); - await this.updateHistoryData(); - await this.#updateSearchResults(); + } + + async onSearchQuery(e) { + await this.controller.onSearchQuery(e); + this.cumulativeSearches = this.controller.searchQuery + ? this.cumulativeSearches + 1 + : 0; } showAllHistory() { @@ -396,9 +285,9 @@ class HistoryInView extends ViewPage { * The template to use for cards-container. */ get cardsTemplate() { - if (this.searchResults) { + if (this.controller.searchResults) { return this.#searchResultsTemplate(); - } else if (this.allHistoryItems.size) { + } else if (this.controller.allHistoryItems.size) { return this.#historyCardsTemplate(); } return this.#emptyMessageTemplate(); @@ -406,8 +295,11 @@ class HistoryInView extends ViewPage { #historyCardsTemplate() { let cardsTemplate = []; - if (this.sortOption === "date" && this.historyMapByDate.length) { - this.historyMapByDate.forEach(historyItem => { + if ( + this.controller.sortOption === "date" && + this.controller.historyMapByDate.length + ) { + this.controller.historyMapByDate.forEach(historyItem => { if (historyItem.items.length) { let dateArg = JSON.stringify({ date: historyItem.items[0].time }); cardsTemplate.push(html` @@ -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 { `); } }); - } 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`

@@ -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), })} >

${when( - this.searchResults.length, + this.controller.searchResults.length, () => html`

` )} @@ -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()} @@ -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} />
@@ -624,32 +518,24 @@ class HistoryInView extends ViewPage { `; } - 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``; + }; + + render() { + if (this.searchQuery && this.tabItems.length === 0) { + return this.emptySearchResultsTemplate(); + } + return html` + ${this.stylesheets()} + + ${when( + this.pinnedTabsGridView && this.pinnedTabs.length, + () => html` +
+ ${this.pinnedTabs.map((tabItem, i) => + this.customItemTemplate + ? this.customItemTemplate(tabItem, i) + : this.itemTemplate(tabItem, i) + )} +
+ ` + )} +
+ ${when( + lazy.virtualListEnabledPref, + () => html` + + `, + () => + html`${this.tabItems.map((tabItem, i) => + this.itemTemplate(tabItem, i) + )}` + )} +
+ + `; + } +} +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` + + ${when( + this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + (this.indicators?.includes("muted") || + this.indicators?.includes("soundplaying")), + () => html` + + ` + )} + `; + } + + #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``, + () => html`` + )}`; + } + + #containerIndicatorTemplate() { + let tabList = this.getRootNode().host; + let tabsToCheck = tabList.pinnedTabsGridView + ? tabList.unpinnedTabs + : tabList.tabItems; + return html`${when( + tabsToCheck.some(tab => tab.containerObj), + () => html`` + )}`; + } + + #pinnedTabItemTemplate() { + return html` + + ${this.#faviconTemplate()} + + `; + } + + #unpinnedTabItemTemplate() { + return html` + ${this.#faviconTemplate()} ${this.titleTemplate()} + ${when( + !this.compact, + () => html`${this.#containerIndicatorTemplate()} ${this.urlTemplate()} + ${this.dateTemplate()} ${this.timeTemplate()}` + )} + + ${this.#mediaButtonTemplate()} ${this.secondaryButtonTemplate()} + ${this.tertiaryButtonTemplate()}`; + } + + render() { + return html` + ${this.stylesheets()} + + ${when( + this.containerObj, + () => html` + + ` + )} + ${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 { >`; } - 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`

${this.title}

` )}
- - +
${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` + 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` this.controller.handleEvent(e)} aria-details="empty-container" > @@ -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), })} > `, @@ -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" > ${when( - isSearchEnabled() || this._currentSetupStateIndex === 4, + isSearchEnabled() || this.controller.currentSetupStateIndex === 4, () => html`
${when( isSearchEnabled(), @@ -606,12 +419,12 @@ class SyncedTabsInView extends ViewPage {
` )} ${when( - this._currentSetupStateIndex === 4, + this.controller.currentSetupStateIndex === 4, () => html` + + `; + } + + /** + * 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` + + `; + } + + /** + * The template shown for a device that has no tabs. + * + * @param {string} deviceName + * @param {string} deviceType + * @returns {TemplateResult} + */ + noDeviceTabsTemplate(deviceName, deviceType) { + return html` + `; + } + + /** + * 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` + `; + } + + /** + * 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()} + + ${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 @@ + + + + + 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 - 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` - -
-
-
${this._displayTitle}
-
${this._displayURI}
-
- ${this.thumbnailCanShow - ? html` -
- ${this._displayImg} -
- ` - : ""} -
- `; - } -} -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,11 +11,42 @@ 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, @@ -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} + */ + 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. @@ -45,6 +89,18 @@ 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. * @@ -52,7 +108,7 @@ export class TranslationsPanelShared { * - 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?} */ @@ -373,21 +380,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. @@ -396,8 +388,9 @@ var FullPageTranslationsPanel = new (class { * @returns {Promise} */ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + orient="vertical" + onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)" + onpopuphidden="SelectTranslationsPanel.handlePanelPopupHiddenEvent(event)"> + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + - - - + + + - - - - - - + + + + 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?} */ @@ -39,6 +51,69 @@ var SelectTranslationsPanel = new (class { return this.#console; } + /** + * 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. * @@ -46,6 +121,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. * @@ -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", }); @@ -90,6 +189,43 @@ var SelectTranslationsPanel = new (class { return this.#lazyElements; } + /** + * 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} - 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 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} */ - 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} */ - 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} */ - 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} + */ + #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} 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} 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_engine_unsupported_lang.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_engine_unsupported_lang.js deleted file mode 100644 index 21f7e8fdb7..0000000000 --- a/browser/components/translations/tests/browser/browser_translations_full_page_panel_engine_unsupported_lang.js +++ /dev/null @@ -1,28 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests how the unsupported language flow works. - */ -add_task(async function test_unsupported_lang() { - const { cleanup } = await loadTestPage({ - page: SPANISH_PAGE_URL, - languagePairs: [ - // Do not include Spanish. - { fromLang: "fr", toLang: "en" }, - { fromLang: "en", toLang: "fr" }, - ], - }); - - await FullPageTranslationsTestUtils.openPanel({ - openFromAppMenu: true, - onOpenPanel: - FullPageTranslationsTestUtils.assertPanelViewUnsupportedLanguage, - }); - - await FullPageTranslationsTestUtils.clickChangeSourceLanguageButton(); - - await 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_unsupported_lang.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_unsupported_lang.js new file mode 100644 index 0000000000..59be1e329b --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_unsupported_lang.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests how the unsupported language flow works. + */ +add_task(async function test_unsupported_lang() { + const { cleanup } = await loadTestPage({ + page: SPANISH_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + }); + + await FullPageTranslationsTestUtils.openPanel({ + openFromAppMenu: true, + onOpenPanel: + FullPageTranslationsTestUtils.assertPanelViewUnsupportedLanguage, + }); + + 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); } @@ -284,6 +283,19 @@ async function toggleReaderMode() { * using functions from this class directly. */ 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. * @@ -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} */ @@ -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, @@ -743,6 +745,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. */ @@ -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} 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} */ 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( @@ -1398,6 +1500,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} options.downloadHandler - The function to resolve or reject the downloads. + * @param {boolean} [options.pivotTranslation] - Whether to expect a pivot translation. + * + * @returns {Promise} + */ + 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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} */ - 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 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"; -- cgit v1.2.3