From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001
From: Daniel Baumann
Date: Fri, 19 Apr 2024 02:47:55 +0200
Subject: Adding upstream version 124.0.1.
Signed-off-by: Daniel Baumann
---
.../extensions/test/AppUiTestDelegate.sys.mjs | 227 +++
.../extensions/test/browser/.eslintrc.js | 11 +
.../extensions/test/browser/authenticate.sjs | 85 +
.../extensions/test/browser/browser-private.toml | 11 +
.../extensions/test/browser/browser.toml | 702 ++++++++
.../browser/browser_AMBrowserExtensionsImport.js | 286 ++++
.../browser/browser_ExtensionControlledPopup.js | 241 +++
.../browser_ext_action_popup_allowed_urls.js | 283 ++++
.../test/browser/browser_ext_activeScript.js | 480 ++++++
.../browser_ext_addon_debugging_netmonitor.js | 116 ++
.../test/browser/browser_ext_autocompletepopup.js | 90 +
.../browser/browser_ext_autoplayInBackground.js | 52 +
.../browser/browser_ext_browserAction_activeTab.js | 195 +++
.../test/browser/browser_ext_browserAction_area.js | 126 ++
.../browser_ext_browserAction_click_types.js | 269 +++
.../browser/browser_ext_browserAction_context.js | 1194 ++++++++++++++
.../browser_ext_browserAction_contextMenu.js | 880 ++++++++++
.../browser/browser_ext_browserAction_disabled.js | 101 ++
.../browser_ext_browserAction_experiment.js | 159 ++
.../browser_ext_browserAction_getUserSettings.js | 244 +++
.../browser/browser_ext_browserAction_incognito.js | 48 +
.../browser/browser_ext_browserAction_keyclick.js | 68 +
.../browser_ext_browserAction_pageAction_icon.js | 651 ++++++++
...xt_browserAction_pageAction_icon_permissions.js | 239 +++
.../browser/browser_ext_browserAction_popup.js | 370 +++++
.../browser_ext_browserAction_popup_port.js | 56 +
.../browser_ext_browserAction_popup_preload.js | 472 ++++++
...er_ext_browserAction_popup_preload_smoketest.js | 194 +++
.../browser_ext_browserAction_popup_resize.js | 79 +
...rowser_ext_browserAction_popup_resize_bottom.js | 39 +
.../browser/browser_ext_browserAction_simple.js | 105 ++
.../browser/browser_ext_browserAction_telemetry.js | 386 +++++
.../browser_ext_browserAction_theme_icons.js | 370 +++++
.../browser_ext_browsingData_cookieStoreId.js | 86 +
.../browser/browser_ext_browsingData_formData.js | 175 ++
.../browser/browser_ext_browsingData_history.js | 123 ++
.../browser_ext_chrome_settings_overrides_home.js | 843 ++++++++++
.../browser_ext_commands_execute_browser_action.js | 194 +++
.../browser_ext_commands_execute_page_action.js | 204 +++
.../browser_ext_commands_execute_sidebar_action.js | 56 +
.../test/browser/browser_ext_commands_getAll.js | 142 ++
.../test/browser/browser_ext_commands_onChanged.js | 59 +
.../test/browser/browser_ext_commands_onCommand.js | 442 +++++
.../test/browser/browser_ext_commands_update.js | 428 +++++
.../browser/browser_ext_connect_and_move_tabs.js | 104 ++
.../browser/browser_ext_contentscript_animate.js | 135 ++
.../browser/browser_ext_contentscript_connect.js | 94 ++
...er_ext_contentscript_cross_docGroup_adoption.js | 63 +
...xt_contentscript_cross_docGroup_adoption_xhr.js | 56 +
...browser_ext_contentscript_dataTransfer_files.js | 104 ++
.../browser/browser_ext_contentscript_in_parent.js | 101 ++
.../browser/browser_ext_contentscript_incognito.js | 42 +
.../browser_ext_contentscript_nontab_connect.js | 116 ++
.../browser_ext_contentscript_sender_url.js | 67 +
.../test/browser/browser_ext_contextMenus.js | 854 ++++++++++
.../browser/browser_ext_contextMenus_bookmarks.js | 115 ++
.../browser/browser_ext_contextMenus_checkboxes.js | 157 ++
.../browser/browser_ext_contextMenus_commands.js | 158 ++
.../test/browser/browser_ext_contextMenus_icons.js | 493 ++++++
.../browser/browser_ext_contextMenus_onclick.js | 297 ++++
.../browser_ext_contextMenus_radioGroups.js | 140 ++
.../browser_ext_contextMenus_srcUrl_redirect.js | 69 +
.../browser_ext_contextMenus_targetUrlPatterns.js | 317 ++++
.../browser/browser_ext_contextMenus_uninstall.js | 114 ++
.../browser_ext_contextMenus_urlPatterns.js | 337 ++++
.../test/browser/browser_ext_currentWindow.js | 183 +++
.../browser_ext_devtools_inspectedWindow.js | 540 ++++++
...r_ext_devtools_inspectedWindow_eval_bindings.js | 270 +++
...owser_ext_devtools_inspectedWindow_eval_file.js | 54 +
.../browser_ext_devtools_inspectedWindow_reload.js | 481 ++++++
...er_ext_devtools_inspectedWindow_targetSwitch.js | 128 ++
.../test/browser/browser_ext_devtools_network.js | 298 ++++
.../browser_ext_devtools_network_targetSwitch.js | 74 +
.../test/browser/browser_ext_devtools_optional.js | 169 ++
.../test/browser/browser_ext_devtools_page.js | 304 ++++
.../browser/browser_ext_devtools_page_incognito.js | 92 ++
.../test/browser/browser_ext_devtools_panel.js | 812 +++++++++
.../browser_ext_devtools_panels_elements.js | 124 ++
...browser_ext_devtools_panels_elements_sidebar.js | 323 ++++
.../extensions/test/browser/browser_ext_find.js | 468 ++++++
.../test/browser/browser_ext_getViews.js | 439 +++++
.../test/browser/browser_ext_history_redirect.js | 72 +
.../browser/browser_ext_identity_indication.js | 141 ++
.../test/browser/browser_ext_incognito_popup.js | 209 +++
.../test/browser/browser_ext_incognito_views.js | 269 +++
.../test/browser/browser_ext_lastError.js | 61 +
.../test/browser/browser_ext_management.js | 139 ++
.../extensions/test/browser/browser_ext_menus.js | 458 ++++++
.../test/browser/browser_ext_menus_accesskey.js | 209 +++
.../test/browser/browser_ext_menus_activeTab.js | 115 ++
.../browser_ext_menus_capture_secondary_click.js | 140 ++
.../test/browser/browser_ext_menus_errors.js | 164 ++
.../test/browser/browser_ext_menus_event_order.js | 87 +
.../test/browser/browser_ext_menus_eventpage.js | 277 ++++
.../test/browser/browser_ext_menus_events.js | 911 +++++++++++
...owser_ext_menus_events_after_context_destroy.js | 64 +
.../test/browser/browser_ext_menus_incognito.js | 155 ++
.../test/browser/browser_ext_menus_refresh.js | 438 +++++
.../test/browser/browser_ext_menus_replace_menu.js | 525 ++++++
.../browser_ext_menus_replace_menu_context.js | 475 ++++++
.../browser_ext_menus_replace_menu_permissions.js | 220 +++
.../browser/browser_ext_menus_targetElement.js | 326 ++++
.../browser_ext_menus_targetElement_extension.js | 198 +++
.../browser_ext_menus_targetElement_shadow.js | 108 ++
.../test/browser/browser_ext_menus_viewType.js | 122 ++
.../test/browser/browser_ext_menus_visible.js | 95 ++
.../test/browser/browser_ext_mousewheel_zoom.js | 186 +++
.../browser/browser_ext_nontab_process_switch.js | 154 ++
.../extensions/test/browser/browser_ext_omnibox.js | 504 ++++++
.../test/browser/browser_ext_openPanel.js | 152 ++
.../browser/browser_ext_optionsPage_activity.js | 79 +
.../browser_ext_optionsPage_browser_style.js | 155 ++
.../browser_ext_optionsPage_links_open_in_tabs.js | 68 +
.../test/browser/browser_ext_optionsPage_modals.js | 100 ++
.../test/browser/browser_ext_optionsPage_popups.js | 249 +++
.../browser/browser_ext_optionsPage_privileges.js | 86 +
.../test/browser/browser_ext_originControls.js | 867 ++++++++++
.../browser/browser_ext_pageAction_activeTab.js | 107 ++
.../browser/browser_ext_pageAction_click_types.js | 240 +++
.../test/browser/browser_ext_pageAction_context.js | 453 +++++
.../browser/browser_ext_pageAction_contextMenu.js | 128 ++
.../test/browser/browser_ext_pageAction_popup.js | 305 ++++
.../browser/browser_ext_pageAction_popup_resize.js | 192 +++
.../browser/browser_ext_pageAction_show_matches.js | 329 ++++
.../test/browser/browser_ext_pageAction_simple.js | 213 +++
.../browser/browser_ext_pageAction_telemetry.js | 228 +++
.../test/browser/browser_ext_pageAction_title.js | 275 ++++
...ext_persistent_storage_permission_indication.js | 131 ++
.../browser/browser_ext_popup_api_injection.js | 113 ++
.../test/browser/browser_ext_popup_background.js | 160 ++
.../test/browser/browser_ext_popup_corners.js | 165 ++
.../test/browser/browser_ext_popup_focus.js | 88 +
.../browser_ext_popup_links_open_in_tabs.js | 56 +
.../browser/browser_ext_popup_requestPermission.js | 67 +
.../test/browser/browser_ext_popup_select.js | 115 ++
.../browser/browser_ext_popup_select_in_oopif.js | 131 ++
.../test/browser/browser_ext_popup_sendMessage.js | 135 ++
.../test/browser/browser_ext_popup_shutdown.js | 80 +
.../browser_ext_port_disconnect_on_crash.js | 113 ++
.../browser_ext_port_disconnect_on_window_close.js | 39 +
.../browser/browser_ext_reload_manifest_cache.js | 72 +
.../browser/browser_ext_request_permissions.js | 121 ++
.../browser_ext_runtime_onPerformanceWarning.js | 144 ++
.../browser/browser_ext_runtime_openOptionsPage.js | 442 +++++
...rowser_ext_runtime_openOptionsPage_uninstall.js | 122 ++
.../browser/browser_ext_runtime_setUninstallURL.js | 134 ++
.../extensions/test/browser/browser_ext_search.js | 351 ++++
.../test/browser/browser_ext_search_favicon.js | 184 +++
.../test/browser/browser_ext_search_query.js | 174 ++
.../browser_ext_sessions_forgetClosedTab.js | 145 ++
.../browser_ext_sessions_forgetClosedWindow.js | 121 ++
.../browser_ext_sessions_getRecentlyClosed.js | 216 +++
...owser_ext_sessions_getRecentlyClosed_private.js | 93 ++
.../browser_ext_sessions_getRecentlyClosed_tabs.js | 292 ++++
.../test/browser/browser_ext_sessions_incognito.js | 113 ++
.../test/browser/browser_ext_sessions_restore.js | 234 +++
.../browser/browser_ext_sessions_restoreTab.js | 137 ++
.../browser_ext_sessions_restore_private.js | 236 +++
.../browser_ext_sessions_window_tab_value.js | 398 +++++
...rowser_ext_settings_overrides_default_search.js | 881 ++++++++++
.../test/browser/browser_ext_sidebarAction.js | 268 +++
.../browser_ext_sidebarAction_browser_style.js | 90 +
.../browser/browser_ext_sidebarAction_click.js | 74 +
.../browser/browser_ext_sidebarAction_context.js | 683 ++++++++
.../browser_ext_sidebarAction_contextMenu.js | 133 ++
.../browser/browser_ext_sidebarAction_httpAuth.js | 72 +
.../browser/browser_ext_sidebarAction_incognito.js | 139 ++
.../browser/browser_ext_sidebarAction_runtime.js | 76 +
.../test/browser/browser_ext_sidebarAction_tabs.js | 48 +
.../browser/browser_ext_sidebarAction_windows.js | 69 +
.../browser_ext_sidebar_requestPermission.js | 43 +
.../extensions/test/browser/browser_ext_simple.js | 60 +
.../test/browser/browser_ext_slow_script.js | 72 +
.../test/browser/browser_ext_tab_runtimeConnect.js | 100 ++
.../test/browser/browser_ext_tabs_attention.js | 64 +
.../test/browser/browser_ext_tabs_audio.js | 261 +++
.../browser/browser_ext_tabs_autoDiscardable.js | 177 ++
.../browser/browser_ext_tabs_containerIsolation.js | 360 ++++
.../test/browser/browser_ext_tabs_cookieStoreId.js | 328 ++++
.../browser_ext_tabs_cookieStoreId_private.js | 44 +
.../test/browser/browser_ext_tabs_create.js | 299 ++++
.../browser/browser_ext_tabs_create_invalid_url.js | 79 +
.../test/browser/browser_ext_tabs_create_url.js | 230 +++
.../test/browser/browser_ext_tabs_discard.js | 98 ++
.../browser/browser_ext_tabs_discard_reversed.js | 129 ++
.../test/browser/browser_ext_tabs_discarded.js | 386 +++++
.../test/browser/browser_ext_tabs_duplicate.js | 316 ++++
.../test/browser/browser_ext_tabs_events.js | 794 +++++++++
.../test/browser/browser_ext_tabs_events_order.js | 208 +++
.../test/browser/browser_ext_tabs_executeScript.js | 453 +++++
.../browser_ext_tabs_executeScript_about_blank.js | 33 +
.../browser/browser_ext_tabs_executeScript_bad.js | 361 ++++
.../browser/browser_ext_tabs_executeScript_file.js | 93 ++
.../browser/browser_ext_tabs_executeScript_good.js | 190 +++
.../browser_ext_tabs_executeScript_multiple.js | 61 +
.../browser_ext_tabs_executeScript_no_create.js | 80 +
.../browser_ext_tabs_executeScript_runAt.js | 134 ++
.../test/browser/browser_ext_tabs_getCurrent.js | 86 +
.../browser/browser_ext_tabs_goBack_goForward.js | 113 ++
.../test/browser/browser_ext_tabs_hide.js | 375 +++++
.../test/browser/browser_ext_tabs_hide_update.js | 146 ++
.../test/browser/browser_ext_tabs_highlight.js | 118 ++
.../browser_ext_tabs_incognito_not_allowed.js | 155 ++
.../test/browser/browser_ext_tabs_insertCSS.js | 312 ++++
.../test/browser/browser_ext_tabs_lastAccessed.js | 52 +
.../test/browser/browser_ext_tabs_lazy.js | 49 +
.../test/browser/browser_ext_tabs_move_array.js | 95 ++
...browser_ext_tabs_move_array_multiple_windows.js | 160 ++
.../browser/browser_ext_tabs_move_discarded.js | 94 ++
.../test/browser/browser_ext_tabs_move_window.js | 178 ++
.../browser_ext_tabs_move_window_multiple.js | 64 +
.../browser/browser_ext_tabs_move_window_pinned.js | 44 +
.../browser/browser_ext_tabs_newtab_private.js | 96 ++
.../test/browser/browser_ext_tabs_onCreated.js | 35 +
.../test/browser/browser_ext_tabs_onHighlighted.js | 130 ++
.../test/browser/browser_ext_tabs_onUpdated.js | 339 ++++
.../browser/browser_ext_tabs_onUpdated_filter.js | 354 ++++
.../test/browser/browser_ext_tabs_opener.js | 130 ++
.../test/browser/browser_ext_tabs_printPreview.js | 44 +
.../test/browser/browser_ext_tabs_query.js | 468 ++++++
.../test/browser/browser_ext_tabs_readerMode.js | 138 ++
.../test/browser/browser_ext_tabs_reload.js | 53 +
.../browser_ext_tabs_reload_bypass_cache.js | 89 +
.../test/browser/browser_ext_tabs_remove.js | 258 +++
.../test/browser/browser_ext_tabs_removeCSS.js | 151 ++
.../test/browser/browser_ext_tabs_saveAsPDF.js | 197 +++
.../test/browser/browser_ext_tabs_sendMessage.js | 385 +++++
.../test/browser/browser_ext_tabs_sharingState.js | 110 ++
.../test/browser/browser_ext_tabs_successors.js | 396 +++++
.../test/browser/browser_ext_tabs_update.js | 54 +
.../browser/browser_ext_tabs_update_highlighted.js | 183 +++
.../test/browser/browser_ext_tabs_update_url.js | 235 +++
.../test/browser/browser_ext_tabs_warmup.js | 40 +
.../test/browser/browser_ext_tabs_zoom.js | 346 ++++
.../test/browser/browser_ext_themes_validation.js | 55 +
.../test/browser/browser_ext_topSites.js | 413 +++++
.../browser/browser_ext_url_overrides_newtab.js | 794 +++++++++
.../test/browser/browser_ext_user_events.js | 271 +++
...browser_ext_webNavigation_containerIsolation.js | 169 ++
.../browser/browser_ext_webNavigation_frameId0.js | 43 +
.../browser/browser_ext_webNavigation_getFrames.js | 323 ++++
..._ext_webNavigation_onCreatedNavigationTarget.js | 194 +++
...gation_onCreatedNavigationTarget_contextmenu.js | 182 +++
...ation_onCreatedNavigationTarget_named_window.js | 100 ++
...CreatedNavigationTarget_subframe_window_open.js | 168 ++
...gation_onCreatedNavigationTarget_window_open.js | 168 ++
...browser_ext_webNavigation_urlbar_transitions.js | 314 ++++
.../test/browser/browser_ext_webRequest.js | 142 ++
...ext_webRequest_error_after_stopped_or_closed.js | 110 ++
.../extensions/test/browser/browser_ext_webrtc.js | 131 ++
.../extensions/test/browser/browser_ext_windows.js | 348 ++++
.../browser_ext_windows_allowScriptsToClose.js | 69 +
.../test/browser/browser_ext_windows_create.js | 205 +++
.../browser_ext_windows_create_cookieStoreId.js | 345 ++++
.../browser/browser_ext_windows_create_params.js | 249 +++
.../browser/browser_ext_windows_create_tabId.js | 387 +++++
.../test/browser/browser_ext_windows_create_url.js | 253 +++
.../test/browser/browser_ext_windows_events.js | 222 +++
.../test/browser/browser_ext_windows_incognito.js | 84 +
.../test/browser/browser_ext_windows_remove.js | 53 +
.../test/browser/browser_ext_windows_size.js | 122 ++
.../test/browser/browser_ext_windows_update.js | 390 +++++
.../test/browser/browser_legacy_recent_tabs.toml | 34 +
.../browser_toolbar_prefers_color_scheme.js | 266 +++
.../test/browser/browser_unified_extensions.js | 1545 ++++++++++++++++++
.../browser_unified_extensions_accessibility.js | 302 ++++
.../browser_unified_extensions_context_menu.js | 1006 ++++++++++++
.../test/browser/browser_unified_extensions_cui.js | 159 ++
.../browser_unified_extensions_doorhangers.js | 116 ++
.../browser/browser_unified_extensions_messages.js | 222 +++
...wser_unified_extensions_overflowable_toolbar.js | 1389 ++++++++++++++++
.../extensions/test/browser/context.html | 44 +
.../extensions/test/browser/context_frame.html | 8 +
.../browser/context_tabs_onUpdated_iframe.html | 19 +
.../test/browser/context_tabs_onUpdated_page.html | 18 +
.../test/browser/context_with_redirect.html | 4 +
.../extensions/test/browser/ctxmenu-image.png | Bin 0 -> 5401 bytes
.../components/extensions/test/browser/empty.xpi | 0
.../extensions/test/browser/file_bypass_cache.sjs | 13 +
.../test/browser/file_dataTransfer_files.html | 36 +
.../extensions/test/browser/file_dummy.html | 10 +
.../extensions/test/browser/file_find_frames.html | 19 +
...ile_has_non_web_controlled_blank_page_link.html | 5 +
.../test/browser/file_iframe_document.html | 11 +
.../test/browser/file_inspectedwindow_eval.html | 29 +
.../browser/file_inspectedwindow_reload_target.sjs | 130 ++
.../test/browser/file_popup_api_injection_a.html | 10 +
.../test/browser/file_popup_api_injection_b.html | 10 +
.../test/browser/file_slowed_document.sjs | 49 +
.../extensions/test/browser/file_title.html | 9 +
.../test/browser/file_with_example_com_frame.html | 5 +
.../test/browser/file_with_xorigin_frame.html | 5 +
browser/components/extensions/test/browser/head.js | 1046 ++++++++++++
.../extensions/test/browser/head_browserAction.js | 368 +++++
.../extensions/test/browser/head_devtools.js | 162 ++
.../extensions/test/browser/head_pageAction.js | 232 +++
.../extensions/test/browser/head_sessions.js | 64 +
.../test/browser/head_unified_extensions.js | 199 +++
.../extensions/test/browser/head_webNavigation.js | 49 +
.../extensions/test/browser/redirect_to.sjs | 9 +
.../browser/search-engines/another/manifest.json | 19 +
.../browser/search-engines/basic/manifest.json | 19 +
.../test/browser/search-engines/engines.json | 38 +
.../browser/search-engines/simple/manifest.json | 29 +
.../test/browser/searchSuggestionEngine.sjs | 10 +
.../test/browser/searchSuggestionEngine.xml | 9 +
.../components/extensions/test/browser/silence.ogg | Bin 0 -> 3557 bytes
.../extensions/test/browser/wait-a-bit.sjs | 23 +
.../test/browser/webNav_createdTarget.html | 10 +
.../test/browser/webNav_createdTargetSource.html | 45 +
.../webNav_createdTargetSource_subframe.html | 42 +
.../extensions/test/mochitest/.eslintrc.js | 8 +
.../extensions/test/mochitest/mochitest.toml | 14 +
.../test/mochitest/test_ext_all_apis.html | 83 +
.../extensions/test/xpcshell/.eslintrc.js | 9 +
.../test/xpcshell/data/test/manifest.json | 80 +
.../test/xpcshell/data/test2/manifest.json | 23 +
.../components/extensions/test/xpcshell/head.js | 78 +
.../extensions/test/xpcshell/test_ext_bookmarks.js | 1725 ++++++++++++++++++++
.../xpcshell/test_ext_browsingData_downloads.js | 126 ++
.../xpcshell/test_ext_browsingData_passwords.js | 96 ++
.../xpcshell/test_ext_browsingData_settings.js | 147 ++
.../test_ext_chrome_settings_overrides_home.js | 231 +++
.../test_ext_chrome_settings_overrides_update.js | 794 +++++++++
.../test/xpcshell/test_ext_distribution_popup.js | 56 +
.../extensions/test/xpcshell/test_ext_history.js | 864 ++++++++++
.../test_ext_homepage_overrides_private.js | 134 ++
.../extensions/test/xpcshell/test_ext_manifest.js | 105 ++
.../test/xpcshell/test_ext_manifest_commands.js | 52 +
.../test/xpcshell/test_ext_manifest_omnibox.js | 62 +
.../test/xpcshell/test_ext_manifest_permissions.js | 85 +
.../test/xpcshell/test_ext_menu_caller.js | 53 +
.../test/xpcshell/test_ext_menu_startup.js | 432 +++++
.../test/xpcshell/test_ext_normandyAddonStudy.js | 243 +++
.../test/xpcshell/test_ext_pageAction_shutdown.js | 81 +
.../test/xpcshell/test_ext_pkcs11_management.js | 300 ++++
.../test_ext_settings_overrides_defaults.js | 263 +++
.../xpcshell/test_ext_settings_overrides_search.js | 597 +++++++
.../test_ext_settings_overrides_search_mozParam.js | 239 +++
.../test_ext_settings_overrides_shutdown.js | 109 ++
.../test/xpcshell/test_ext_settings_validate.js | 193 +++
.../extensions/test/xpcshell/test_ext_topSites.js | 293 ++++
.../test/xpcshell/test_ext_url_overrides_newtab.js | 340 ++++
.../test_ext_url_overrides_newtab_update.js | 127 ++
.../extensions/test/xpcshell/xpcshell.toml | 69 +
345 files changed, 72962 insertions(+)
create mode 100644 browser/components/extensions/test/AppUiTestDelegate.sys.mjs
create mode 100644 browser/components/extensions/test/browser/.eslintrc.js
create mode 100644 browser/components/extensions/test/browser/authenticate.sjs
create mode 100644 browser/components/extensions/test/browser/browser-private.toml
create mode 100644 browser/components/extensions/test/browser/browser.toml
create mode 100644 browser/components/extensions/test/browser/browser_AMBrowserExtensionsImport.js
create mode 100644 browser/components/extensions/test/browser/browser_ExtensionControlledPopup.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_action_popup_allowed_urls.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_activeScript.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_addon_debugging_netmonitor.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_autocompletepopup.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_autoplayInBackground.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_activeTab.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_area.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_click_types.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_context.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_experiment.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_getUserSettings.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_incognito.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_keyclick.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_popup_port.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize_bottom.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_simple.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_telemetry.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browserAction_theme_icons.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browsingData_cookieStoreId.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browsingData_formData.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_browsingData_history.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_commands_execute_sidebar_action.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_commands_getAll.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_commands_onChanged.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_commands_update.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_connect_and_move_tabs.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_animate.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_connect.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_cross_docGroup_adoption.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_cross_docGroup_adoption_xhr.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_dataTransfer_files.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_in_parent.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_incognito.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_checkboxes.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_commands.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_radioGroups.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_srcUrl_redirect.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_targetUrlPatterns.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_uninstall.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_currentWindow.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_file.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_reload.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_targetSwitch.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_network.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_network_targetSwitch.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_optional.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_page.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_page_incognito.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_panel.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_panels_elements.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_find.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_getViews.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_history_redirect.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_identity_indication.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_incognito_popup.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_incognito_views.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_lastError.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_management.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_accesskey.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_activeTab.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_capture_secondary_click.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_errors.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_event_order.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_eventpage.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_events.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_events_after_context_destroy.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_incognito.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_refresh.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_targetElement.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_targetElement_extension.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_targetElement_shadow.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_viewType.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_visible.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_nontab_process_switch.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_omnibox.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_openPanel.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_optionsPage_activity.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_optionsPage_browser_style.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_optionsPage_links_open_in_tabs.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_optionsPage_popups.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_originControls.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_activeTab.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_click_types.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_context.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_contextMenu.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_simple.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_telemetry.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_pageAction_title.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_persistent_storage_permission_indication.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_api_injection.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_background.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_corners.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_focus.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_links_open_in_tabs.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_requestPermission.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_select.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_port_disconnect_on_crash.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_port_disconnect_on_window_close.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_reload_manifest_cache.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_request_permissions.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_runtime_onPerformanceWarning.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_search.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_search_favicon.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_search_query.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedTab.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedWindow.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_incognito.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_restore.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_restore_private.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sessions_window_tab_value.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_browser_style.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_sidebar_requestPermission.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_simple.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_slow_script.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_attention.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_audio.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_autoDiscardable.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_containerIsolation.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId_private.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_create.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_create_url.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_discard.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_discard_reversed.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_events.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_events_order.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_executeScript_about_blank.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_executeScript_file.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_executeScript_multiple.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_goBack_goForward.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_hide.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_hide_update.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_incognito_not_allowed.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_lazy.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_move_array.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_move_array_multiple_windows.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_move_discarded.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_move_window.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_onUpdated_filter.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_opener.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_printPreview.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_query.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_reload.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_remove.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_successors.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_update.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_update_url.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_warmup.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_themes_validation.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_topSites.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_user_events.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_containerIsolation.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_contextmenu.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_named_window.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_subframe_window_open.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webRequest.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webRequest_error_after_stopped_or_closed.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_webrtc.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_create.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_create_params.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_create_url.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_events.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_incognito.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_remove.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_size.js
create mode 100644 browser/components/extensions/test/browser/browser_ext_windows_update.js
create mode 100644 browser/components/extensions/test/browser/browser_legacy_recent_tabs.toml
create mode 100644 browser/components/extensions/test/browser/browser_toolbar_prefers_color_scheme.js
create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions.js
create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js
create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js
create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions_cui.js
create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js
create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions_messages.js
create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions_overflowable_toolbar.js
create mode 100644 browser/components/extensions/test/browser/context.html
create mode 100644 browser/components/extensions/test/browser/context_frame.html
create mode 100644 browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html
create mode 100644 browser/components/extensions/test/browser/context_tabs_onUpdated_page.html
create mode 100644 browser/components/extensions/test/browser/context_with_redirect.html
create mode 100644 browser/components/extensions/test/browser/ctxmenu-image.png
create mode 100644 browser/components/extensions/test/browser/empty.xpi
create mode 100644 browser/components/extensions/test/browser/file_bypass_cache.sjs
create mode 100644 browser/components/extensions/test/browser/file_dataTransfer_files.html
create mode 100644 browser/components/extensions/test/browser/file_dummy.html
create mode 100644 browser/components/extensions/test/browser/file_find_frames.html
create mode 100644 browser/components/extensions/test/browser/file_has_non_web_controlled_blank_page_link.html
create mode 100644 browser/components/extensions/test/browser/file_iframe_document.html
create mode 100644 browser/components/extensions/test/browser/file_inspectedwindow_eval.html
create mode 100644 browser/components/extensions/test/browser/file_inspectedwindow_reload_target.sjs
create mode 100644 browser/components/extensions/test/browser/file_popup_api_injection_a.html
create mode 100644 browser/components/extensions/test/browser/file_popup_api_injection_b.html
create mode 100644 browser/components/extensions/test/browser/file_slowed_document.sjs
create mode 100644 browser/components/extensions/test/browser/file_title.html
create mode 100644 browser/components/extensions/test/browser/file_with_example_com_frame.html
create mode 100644 browser/components/extensions/test/browser/file_with_xorigin_frame.html
create mode 100644 browser/components/extensions/test/browser/head.js
create mode 100644 browser/components/extensions/test/browser/head_browserAction.js
create mode 100644 browser/components/extensions/test/browser/head_devtools.js
create mode 100644 browser/components/extensions/test/browser/head_pageAction.js
create mode 100644 browser/components/extensions/test/browser/head_sessions.js
create mode 100644 browser/components/extensions/test/browser/head_unified_extensions.js
create mode 100644 browser/components/extensions/test/browser/head_webNavigation.js
create mode 100644 browser/components/extensions/test/browser/redirect_to.sjs
create mode 100644 browser/components/extensions/test/browser/search-engines/another/manifest.json
create mode 100644 browser/components/extensions/test/browser/search-engines/basic/manifest.json
create mode 100644 browser/components/extensions/test/browser/search-engines/engines.json
create mode 100644 browser/components/extensions/test/browser/search-engines/simple/manifest.json
create mode 100644 browser/components/extensions/test/browser/searchSuggestionEngine.sjs
create mode 100644 browser/components/extensions/test/browser/searchSuggestionEngine.xml
create mode 100644 browser/components/extensions/test/browser/silence.ogg
create mode 100644 browser/components/extensions/test/browser/wait-a-bit.sjs
create mode 100644 browser/components/extensions/test/browser/webNav_createdTarget.html
create mode 100644 browser/components/extensions/test/browser/webNav_createdTargetSource.html
create mode 100644 browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html
create mode 100644 browser/components/extensions/test/mochitest/.eslintrc.js
create mode 100644 browser/components/extensions/test/mochitest/mochitest.toml
create mode 100644 browser/components/extensions/test/mochitest/test_ext_all_apis.html
create mode 100644 browser/components/extensions/test/xpcshell/.eslintrc.js
create mode 100644 browser/components/extensions/test/xpcshell/data/test/manifest.json
create mode 100644 browser/components/extensions/test/xpcshell/data/test2/manifest.json
create mode 100644 browser/components/extensions/test/xpcshell/head.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_bookmarks.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_history.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_manifest.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_menu_caller.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_menu_startup.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_validate.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_topSites.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js
create mode 100644 browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js
create mode 100644 browser/components/extensions/test/xpcshell/xpcshell.toml
(limited to 'browser/components/extensions/test')
diff --git a/browser/components/extensions/test/AppUiTestDelegate.sys.mjs b/browser/components/extensions/test/AppUiTestDelegate.sys.mjs
new file mode 100644
index 0000000000..f68392800d
--- /dev/null
+++ b/browser/components/extensions/test/AppUiTestDelegate.sys.mjs
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+import { Assert } from "resource://testing-common/Assert.sys.mjs";
+import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
+});
+
+async function promiseAnimationFrame(window) {
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+
+ let { tm } = Services;
+ return new Promise(resolve => tm.dispatchToMainThread(resolve));
+}
+
+function makeWidgetId(id) {
+ id = id.toLowerCase();
+ return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+async function getPageActionButtonId(window, extensionId) {
+ // This would normally be set automatically on navigation, and cleared
+ // when the user types a value into the URL bar, to show and hide page
+ // identity info and icons such as page action buttons.
+ //
+ // Unfortunately, that doesn't happen automatically in browser chrome
+ // tests.
+ window.gURLBar.setPageProxyState("valid");
+
+ let { gIdentityHandler } = window.gBrowser.ownerGlobal;
+ // If the current tab is blank and the previously selected tab was an internal
+ // page, the urlbar will now be showing the internal identity box due to the
+ // setPageProxyState call above. The page action button is hidden in that
+ // case, so make sure we're not showing the internal identity box.
+ gIdentityHandler._identityBox.classList.remove("chromeUI");
+
+ await promiseAnimationFrame(window);
+
+ return window.BrowserPageActions.urlbarButtonNodeIDForActionID(
+ makeWidgetId(extensionId)
+ );
+}
+
+async function getPageActionButton(window, extensionId) {
+ let pageActionId = await getPageActionButtonId(window, extensionId);
+ return window.document.getElementById(pageActionId);
+}
+
+async function clickPageAction(window, extensionId, modifiers = {}) {
+ let pageActionId = await getPageActionButtonId(window, extensionId);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ `#${pageActionId}`,
+ modifiers,
+ window.browsingContext
+ );
+ return new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+}
+
+function getBrowserActionWidgetId(extensionId) {
+ return makeWidgetId(extensionId) + "-browser-action";
+}
+
+function getBrowserActionWidget(extensionId) {
+ return lazy.CustomizableUI.getWidget(getBrowserActionWidgetId(extensionId));
+}
+
+async function showBrowserAction(window, extensionId) {
+ let group = getBrowserActionWidget(extensionId);
+ let widget = group.forWindow(window);
+ if (!widget.node) {
+ return;
+ }
+
+ let navbar = window.document.getElementById("nav-bar");
+ if (group.areaType == lazy.CustomizableUI.TYPE_TOOLBAR) {
+ Assert.equal(
+ widget.overflowed,
+ navbar.hasAttribute("overflowing"),
+ "Expect widget overflow state to match toolbar"
+ );
+ } else if (group.areaType == lazy.CustomizableUI.TYPE_PANEL) {
+ let panel = window.gUnifiedExtensions.panel;
+ let shown = BrowserTestUtils.waitForPopupEvent(panel, "shown");
+ window.gUnifiedExtensions.togglePanel();
+ await shown;
+ }
+}
+
+async function clickBrowserAction(window, extensionId, modifiers) {
+ await promiseAnimationFrame(window);
+ await showBrowserAction(window, extensionId);
+
+ if (modifiers) {
+ let widgetId = getBrowserActionWidgetId(extensionId);
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ `#${widgetId}`,
+ modifiers,
+ window.browsingContext
+ );
+ } else {
+ let widget = getBrowserActionWidget(extensionId).forWindow(window);
+ widget.node.firstElementChild.click();
+ }
+}
+
+async function promisePopupShown(popup) {
+ return new Promise(resolve => {
+ if (popup.state == "open") {
+ resolve();
+ } else {
+ let onPopupShown = event => {
+ popup.removeEventListener("popupshown", onPopupShown);
+ resolve();
+ };
+ popup.addEventListener("popupshown", onPopupShown);
+ }
+ });
+}
+
+function awaitBrowserLoaded(browser) {
+ if (
+ browser.ownerGlobal.document.readyState === "complete" &&
+ browser.currentURI.spec !== "about:blank"
+ ) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve => {
+ const listener = ev => {
+ if (browser.currentURI.spec === "about:blank") {
+ return;
+ }
+ browser.removeEventListener("AppTestDelegate:load", listener);
+ resolve();
+ };
+ browser.addEventListener("AppTestDelegate:load", listener);
+ });
+}
+
+function getPanelForNode(node) {
+ return node.closest("panel");
+}
+
+async function awaitExtensionPanel(window, extensionId, awaitLoad = true) {
+ let { originalTarget: browser } = await BrowserTestUtils.waitForEvent(
+ window.document,
+ "WebExtPopupLoaded",
+ true,
+ event => event.detail.extension.id === extensionId
+ );
+
+ await Promise.all([
+ promisePopupShown(getPanelForNode(browser)),
+
+ awaitLoad && awaitBrowserLoaded(browser),
+ ]);
+
+ return browser;
+}
+
+function closeBrowserAction(window, extensionId) {
+ let group = getBrowserActionWidget(extensionId);
+
+ let node = window.document.getElementById(group.viewId);
+ lazy.CustomizableUI.hidePanelForNode(node);
+
+ return Promise.resolve();
+}
+
+function getPageActionPopup(window, extensionId) {
+ let panelId = makeWidgetId(extensionId) + "-panel";
+ return window.document.getElementById(panelId);
+}
+
+function closePageAction(window, extensionId) {
+ let node = getPageActionPopup(window, extensionId);
+ if (node) {
+ return promisePopupShown(node).then(() => {
+ node.hidePopup();
+ });
+ }
+
+ return Promise.resolve();
+}
+
+function openNewForegroundTab(window, url, waitForLoad = true) {
+ return BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ url,
+ waitForLoad
+ );
+}
+
+async function removeTab(tab) {
+ BrowserTestUtils.removeTab(tab);
+}
+
+// These methods are exported so that they can be used in head.js but are
+// *not* part of the AppUiTestDelegate API.
+export var AppUiTestInternals = {
+ awaitBrowserLoaded,
+ getBrowserActionWidget,
+ getBrowserActionWidgetId,
+ getPageActionButton,
+ getPageActionPopup,
+ getPanelForNode,
+ makeWidgetId,
+ promiseAnimationFrame,
+ promisePopupShown,
+ showBrowserAction,
+};
+
+// These methods are part of the AppUiTestDelegate API. All implementations need
+// to be kept in sync. For details, see:
+// testing/specialpowers/content/AppTestDelegateParent.sys.mjs
+export var AppUiTestDelegate = {
+ awaitExtensionPanel,
+ clickBrowserAction,
+ clickPageAction,
+ closeBrowserAction,
+ closePageAction,
+ openNewForegroundTab,
+ removeTab,
+};
diff --git a/browser/components/extensions/test/browser/.eslintrc.js b/browser/components/extensions/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..a0509253d6
--- /dev/null
+++ b/browser/components/extensions/test/browser/.eslintrc.js
@@ -0,0 +1,11 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+
+ rules: {
+ "no-shadow": 0,
+ },
+};
diff --git a/browser/components/extensions/test/browser/authenticate.sjs b/browser/components/extensions/test/browser/authenticate.sjs
new file mode 100644
index 0000000000..be1aac246b
--- /dev/null
+++ b/browser/components/extensions/test/browser/authenticate.sjs
@@ -0,0 +1,85 @@
+"use strict";
+
+function handleRequest(request, response) {
+ let match;
+ let requestAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ let query = "?" + request.queryString;
+
+ let expected_user = "test",
+ expected_pass = "testpass",
+ realm = "mochitest";
+
+ // user=xxx
+ match = /[^_]user=([^&]*)/.exec(query);
+ if (match) {
+ expected_user = match[1];
+ }
+
+ // pass=xxx
+ match = /[^_]pass=([^&]*)/.exec(query);
+ if (match) {
+ expected_pass = match[1];
+ }
+
+ // realm=xxx
+ match = /[^_]realm=([^&]*)/.exec(query);
+ if (match) {
+ realm = match[1];
+ }
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ let actual_user = "",
+ actual_pass = "",
+ authHeader;
+ if (request.hasHeader("Authorization")) {
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2) {
+ throw new Error("Couldn't parse auth header: " + authHeader);
+ }
+ /* eslint-disable-next-line no-use-before-define */
+ let userpass = atob(match[1]);
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3) {
+ throw new Error("Couldn't decode auth header: " + userpass);
+ }
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user && expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+
+ if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ response.setHeader("WWW-Authenticate", 'basic realm="' + realm + '"', true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("");
+ response.write(
+ "Login: " +
+ (requestAuth ? "FAIL" : "PASS") +
+ "
\n"
+ );
+ response.write("Auth: " + authHeader + "
\n");
+ response.write("User: " + actual_user + "
\n");
+ response.write("Pass: " + actual_pass + "
\n");
+ response.write("");
+}
diff --git a/browser/components/extensions/test/browser/browser-private.toml b/browser/components/extensions/test/browser/browser-private.toml
new file mode 100644
index 0000000000..6d4c44f16c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser-private.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+#
+# This manifest lists tests that use permanent private browsing mode.
+#
+tags = "webextensions"
+prefs = ["browser.privatebrowsing.autostart=true"]
+support-files = ["head.js"]
+
+["browser_ext_tabs_cookieStoreId_private.js"]
+
+["browser_ext_tabs_newtab_private.js"]
diff --git a/browser/components/extensions/test/browser/browser.toml b/browser/components/extensions/test/browser/browser.toml
new file mode 100644
index 0000000000..c185ebd4e7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser.toml
@@ -0,0 +1,702 @@
+[DEFAULT]
+tags = "webextensions"
+prefs = [
+ "browser.sessionstore.closedTabsFromAllWindows=true",
+ "browser.sessionstore.closedTabsFromClosedWindows=true",
+ "dom.ipc.keepProcessesAlive.extension=1", # We don't want to reset this at the end of the test, so that we don't have to spawn a new extension child process for each test unit.
+ "dom.animations-api.core.enabled=true",
+ "dom.animations-api.timelines.enabled=true",
+ "javascript.options.asyncstack_capture_debuggee_only=false",
+]
+support-files = [
+ "head.js",
+ "head_devtools.js",
+ "silence.ogg",
+ "head_browserAction.js",
+ "head_pageAction.js",
+ "head_sessions.js",
+ "head_unified_extensions.js",
+ "head_webNavigation.js",
+ "context.html",
+ "context_frame.html",
+ "ctxmenu-image.png",
+ "context_with_redirect.html",
+ "context_tabs_onUpdated_page.html",
+ "context_tabs_onUpdated_iframe.html",
+ "file_dataTransfer_files.html",
+ "file_find_frames.html",
+ "file_popup_api_injection_a.html",
+ "file_popup_api_injection_b.html",
+ "file_iframe_document.html",
+ "file_inspectedwindow_eval.html",
+ "file_inspectedwindow_reload_target.sjs",
+ "file_slowed_document.sjs",
+ "file_bypass_cache.sjs",
+ "file_dummy.html",
+ "file_title.html",
+ "file_with_xorigin_frame.html",
+ "file_with_example_com_frame.html",
+ "webNav_createdTarget.html",
+ "webNav_createdTargetSource.html",
+ "webNav_createdTargetSource_subframe.html",
+ "redirect_to.sjs",
+ "search-engines/*",
+ "searchSuggestionEngine.xml",
+ "searchSuggestionEngine.sjs",
+ "empty.xpi",
+ "../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js",
+ "../../../../../toolkit/components/extensions/test/mochitest/redirection.sjs",
+ "../../../../../toolkit/components/reader/test/readerModeNonArticle.html",
+ "../../../../../toolkit/components/reader/test/readerModeArticle.html",
+]
+skip-if = ["os == 'linux' && asan"] # Bug 1721945 - Software WebRender
+
+["browser_AMBrowserExtensionsImport.js"]
+
+["browser_ExtensionControlledPopup.js"]
+
+["browser_ext_action_popup_allowed_urls.js"]
+
+["browser_ext_activeScript.js"]
+
+["browser_ext_addon_debugging_netmonitor.js"]
+
+["browser_ext_autocompletepopup.js"]
+disabled = "bug 1438663 # same focus issue as Bug 1438663"
+
+["browser_ext_autoplayInBackground.js"]
+
+["browser_ext_browserAction_activeTab.js"]
+
+["browser_ext_browserAction_area.js"]
+
+["browser_ext_browserAction_click_types.js"]
+
+["browser_ext_browserAction_context.js"]
+https_first_disabled = true
+skip-if = [
+ "os == 'linux' && debug", # Bug 1504096
+ "os == 'linux' && socketprocess_networking",
+]
+
+["browser_ext_browserAction_contextMenu.js"]
+skip-if = ["os == 'linux'"] # bug 1369197
+
+["browser_ext_browserAction_disabled.js"]
+
+["browser_ext_browserAction_experiment.js"]
+
+["browser_ext_browserAction_getUserSettings.js"]
+
+["browser_ext_browserAction_incognito.js"]
+
+["browser_ext_browserAction_keyclick.js"]
+
+["browser_ext_browserAction_pageAction_icon.js"]
+
+["browser_ext_browserAction_pageAction_icon_permissions.js"]
+
+["browser_ext_browserAction_popup.js"]
+
+["browser_ext_browserAction_popup_port.js"]
+
+["browser_ext_browserAction_popup_preload.js"]
+skip-if = [
+ "os == 'win' && !debug",
+ "verify && debug && os == 'mac'", # bug 1352668
+]
+
+["browser_ext_browserAction_popup_preload_smoketest.js"]
+skip-if = ["debug"] # Bug 1746047
+
+["browser_ext_browserAction_popup_resize.js"]
+
+["browser_ext_browserAction_popup_resize_bottom.js"]
+skip-if = ["debug"] # Bug 1522164
+
+["browser_ext_browserAction_simple.js"]
+
+["browser_ext_browserAction_telemetry.js"]
+
+["browser_ext_browserAction_theme_icons.js"]
+
+["browser_ext_browsingData_cookieStoreId.js"]
+
+["browser_ext_browsingData_formData.js"]
+
+["browser_ext_browsingData_history.js"]
+
+["browser_ext_chrome_settings_overrides_home.js"]
+
+["browser_ext_commands_execute_browser_action.js"]
+
+["browser_ext_commands_execute_page_action.js"]
+skip-if = [
+ "verify && os == 'linux'",
+ "verify && os == 'mac'",
+]
+
+["browser_ext_commands_execute_sidebar_action.js"]
+
+["browser_ext_commands_getAll.js"]
+
+["browser_ext_commands_onChanged.js"]
+
+["browser_ext_commands_onCommand.js"]
+skip-if = ["debug"] # bug 1553577
+
+["browser_ext_commands_update.js"]
+
+["browser_ext_connect_and_move_tabs.js"]
+
+["browser_ext_contentscript_animate.js"]
+
+["browser_ext_contentscript_connect.js"]
+
+["browser_ext_contentscript_cross_docGroup_adoption.js"]
+https_first_disabled = true
+
+["browser_ext_contentscript_cross_docGroup_adoption_xhr.js"]
+https_first_disabled = true
+
+["browser_ext_contentscript_dataTransfer_files.js"]
+
+["browser_ext_contentscript_in_parent.js"]
+
+["browser_ext_contentscript_incognito.js"]
+
+["browser_ext_contentscript_nontab_connect.js"]
+
+["browser_ext_contentscript_sender_url.js"]
+skip-if = ["debug"] # The nature of the reduced STR test triggers an unrelated debug assertion in DOM IPC code, see bug 1736590.
+
+["browser_ext_contextMenus.js"]
+support-files = [ "!/browser/components/places/tests/browser/head.js"]
+
+["browser_ext_contextMenus_bookmarks.js"]
+support-files = [ "!/browser/components/places/tests/browser/head.js"]
+
+["browser_ext_contextMenus_checkboxes.js"]
+
+["browser_ext_contextMenus_commands.js"]
+
+["browser_ext_contextMenus_icons.js"]
+
+["browser_ext_contextMenus_onclick.js"]
+https_first_disabled = true
+
+["browser_ext_contextMenus_radioGroups.js"]
+
+["browser_ext_contextMenus_srcUrl_redirect.js"]
+
+["browser_ext_contextMenus_targetUrlPatterns.js"]
+skip-if = ["apple_silicon"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+
+["browser_ext_contextMenus_uninstall.js"]
+
+["browser_ext_contextMenus_urlPatterns.js"]
+
+["browser_ext_currentWindow.js"]
+
+["browser_ext_devtools_inspectedWindow.js"]
+
+["browser_ext_devtools_inspectedWindow_eval_bindings.js"]
+
+["browser_ext_devtools_inspectedWindow_eval_file.js"]
+
+["browser_ext_devtools_inspectedWindow_reload.js"]
+
+["browser_ext_devtools_inspectedWindow_targetSwitch.js"]
+
+["browser_ext_devtools_network.js"]
+https_first_disabled = true
+
+["browser_ext_devtools_network_targetSwitch.js"]
+https_first_disabled = true
+
+["browser_ext_devtools_optional.js"]
+
+["browser_ext_devtools_page.js"]
+
+["browser_ext_devtools_page_incognito.js"]
+
+["browser_ext_devtools_panel.js"]
+
+["browser_ext_devtools_panels_elements.js"]
+
+["browser_ext_devtools_panels_elements_sidebar.js"]
+support-files = ["../../../../../devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js"]
+
+["browser_ext_find.js"]
+https_first_disabled = true
+skip-if = [
+ "verify && os == 'linux'",
+ "verify && os == 'mac'",
+]
+
+["browser_ext_getViews.js"]
+
+["browser_ext_history_redirect.js"]
+
+["browser_ext_identity_indication.js"]
+
+["browser_ext_incognito_popup.js"]
+
+["browser_ext_incognito_views.js"]
+skip-if = [
+ "apple_silicon && !fission", # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+]
+
+["browser_ext_lastError.js"]
+
+["browser_ext_management.js"]
+
+["browser_ext_menus.js"]
+https_first_disabled = true
+
+["browser_ext_menus_accesskey.js"]
+
+["browser_ext_menus_activeTab.js"]
+
+["browser_ext_menus_capture_secondary_click.js"]
+
+["browser_ext_menus_errors.js"]
+
+["browser_ext_menus_event_order.js"]
+
+["browser_ext_menus_eventpage.js"]
+
+["browser_ext_menus_events.js"]
+
+["browser_ext_menus_events_after_context_destroy.js"]
+
+["browser_ext_menus_incognito.js"]
+
+["browser_ext_menus_refresh.js"]
+skip-if = ["a11y_checks"] # Bug 1858041 and 1835079 for causing intermittent crashes
+
+["browser_ext_menus_replace_menu.js"]
+
+["browser_ext_menus_replace_menu_context.js"]
+https_first_disabled = true
+
+["browser_ext_menus_replace_menu_permissions.js"]
+
+["browser_ext_menus_targetElement.js"]
+
+["browser_ext_menus_targetElement_extension.js"]
+skip-if = ["apple_silicon"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+
+["browser_ext_menus_targetElement_shadow.js"]
+
+["browser_ext_menus_viewType.js"]
+skip-if = ["apple_silicon"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+
+["browser_ext_menus_visible.js"]
+
+["browser_ext_mousewheel_zoom.js"]
+
+["browser_ext_nontab_process_switch.js"]
+https_first_disabled = true
+
+["browser_ext_omnibox.js"]
+
+["browser_ext_openPanel.js"]
+skip-if = [
+ "verify && !debug && os == 'linux'",
+ "verify && !debug && os == 'mac'",
+]
+
+["browser_ext_optionsPage_activity.js"]
+
+["browser_ext_optionsPage_browser_style.js"]
+
+["browser_ext_optionsPage_links_open_in_tabs.js"]
+
+["browser_ext_optionsPage_modals.js"]
+
+["browser_ext_optionsPage_popups.js"]
+
+["browser_ext_optionsPage_privileges.js"]
+https_first_disabled = true
+
+["browser_ext_originControls.js"]
+
+["browser_ext_pageAction_activeTab.js"]
+
+["browser_ext_pageAction_click_types.js"]
+
+["browser_ext_pageAction_context.js"]
+https_first_disabled = true
+skip-if = ["verify && !debug && os == 'linux'"]
+
+["browser_ext_pageAction_contextMenu.js"]
+
+["browser_ext_pageAction_popup.js"]
+
+["browser_ext_pageAction_popup_resize.js"]
+skip-if = ["verify && debug && os == 'mac'"]
+
+["browser_ext_pageAction_show_matches.js"]
+https_first_disabled = true
+
+["browser_ext_pageAction_simple.js"]
+
+["browser_ext_pageAction_telemetry.js"]
+
+["browser_ext_pageAction_title.js"]
+
+["browser_ext_persistent_storage_permission_indication.js"]
+
+["browser_ext_popup_api_injection.js"]
+
+["browser_ext_popup_background.js"]
+
+["browser_ext_popup_corners.js"]
+
+["browser_ext_popup_focus.js"]
+
+["browser_ext_popup_links_open_in_tabs.js"]
+
+["browser_ext_popup_requestPermission.js"]
+
+["browser_ext_popup_select.js"]
+skip-if = [
+ "debug", # FIXME: re-enable on debug build (bug 1442822)
+ "os != 'win'",
+]
+
+["browser_ext_popup_select_in_oopif.js"]
+skip-if = ["os == 'linux' && swgl && fission && tsan"] # high frequency intermittent
+
+["browser_ext_popup_sendMessage.js"]
+
+["browser_ext_popup_shutdown.js"]
+
+["browser_ext_port_disconnect_on_crash.js"]
+https_first_disabled = true
+run-if = ["crashreporter"]
+
+["browser_ext_port_disconnect_on_window_close.js"]
+
+["browser_ext_reload_manifest_cache.js"]
+
+["browser_ext_request_permissions.js"]
+
+["browser_ext_runtime_onPerformanceWarning.js"]
+
+["browser_ext_runtime_openOptionsPage.js"]
+
+["browser_ext_runtime_openOptionsPage_uninstall.js"]
+
+["browser_ext_runtime_setUninstallURL.js"]
+
+["browser_ext_search.js"]
+
+["browser_ext_search_favicon.js"]
+
+["browser_ext_search_query.js"]
+
+["browser_ext_sessions_forgetClosedTab.js"]
+
+["browser_ext_sessions_forgetClosedWindow.js"]
+
+["browser_ext_sessions_getRecentlyClosed.js"]
+https_first_disabled = true
+
+["browser_ext_sessions_getRecentlyClosed_private.js"]
+
+["browser_ext_sessions_getRecentlyClosed_tabs.js"]
+support-files = [
+ "file_has_non_web_controlled_blank_page_link.html",
+ "wait-a-bit.sjs",
+]
+
+["browser_ext_sessions_incognito.js"]
+
+["browser_ext_sessions_restore.js"]
+
+["browser_ext_sessions_restoreTab.js"]
+https_first_disabled = true
+
+["browser_ext_sessions_restore_private.js"]
+
+["browser_ext_sessions_window_tab_value.js"]
+https_first_disabled = true
+skip-if = ["debug"] # Bug 1394984 disable debug builds on all platforms
+
+["browser_ext_settings_overrides_default_search.js"]
+
+["browser_ext_sidebarAction.js"]
+
+["browser_ext_sidebarAction_browser_style.js"]
+
+["browser_ext_sidebarAction_click.js"]
+
+["browser_ext_sidebarAction_context.js"]
+
+["browser_ext_sidebarAction_contextMenu.js"]
+skip-if = ["apple_silicon"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+
+["browser_ext_sidebarAction_httpAuth.js"]
+support-files = ["authenticate.sjs"]
+
+["browser_ext_sidebarAction_incognito.js"]
+skip-if = ["true"] # Bug 1575369
+
+["browser_ext_sidebarAction_runtime.js"]
+
+["browser_ext_sidebarAction_tabs.js"]
+
+["browser_ext_sidebarAction_windows.js"]
+
+["browser_ext_sidebar_requestPermission.js"]
+
+["browser_ext_simple.js"]
+
+["browser_ext_slow_script.js"]
+https_first_disabled = true
+skip-if = [
+ "debug",
+ "asan",
+ "tsan" # Bug 1874317
+]
+
+["browser_ext_tab_runtimeConnect.js"]
+
+["browser_ext_tabs_attention.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_audio.js"]
+
+["browser_ext_tabs_autoDiscardable.js"]
+
+["browser_ext_tabs_containerIsolation.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_cookieStoreId.js"]
+
+["browser_ext_tabs_create.js"]
+
+["browser_ext_tabs_create_invalid_url.js"]
+
+["browser_ext_tabs_create_url.js"]
+
+["browser_ext_tabs_discard.js"]
+
+["browser_ext_tabs_discard_reversed.js"]
+https_first_disabled = true
+skip-if = [
+ "os == 'mac'", # Bug 1722607
+ "os == 'linux' && debug", #Bug 1722607
+]
+
+["browser_ext_tabs_discarded.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_duplicate.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_events.js"]
+
+["browser_ext_tabs_events_order.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_executeScript.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_executeScript_about_blank.js"]
+
+["browser_ext_tabs_executeScript_bad.js"]
+
+["browser_ext_tabs_executeScript_file.js"]
+
+["browser_ext_tabs_executeScript_good.js"]
+
+["browser_ext_tabs_executeScript_multiple.js"]
+
+["browser_ext_tabs_executeScript_no_create.js"]
+
+["browser_ext_tabs_executeScript_runAt.js"]
+
+["browser_ext_tabs_getCurrent.js"]
+
+["browser_ext_tabs_goBack_goForward.js"]
+
+["browser_ext_tabs_hide.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_hide_update.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_highlight.js"]
+
+["browser_ext_tabs_incognito_not_allowed.js"]
+
+["browser_ext_tabs_insertCSS.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_lastAccessed.js"]
+
+["browser_ext_tabs_lazy.js"]
+
+["browser_ext_tabs_move_array.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_move_array_multiple_windows.js"]
+
+["browser_ext_tabs_move_discarded.js"]
+
+["browser_ext_tabs_move_window.js"]
+skip-if = ["win10_2009 && debug"] # Bug 1730374
+
+["browser_ext_tabs_move_window_multiple.js"]
+
+["browser_ext_tabs_move_window_pinned.js"]
+
+["browser_ext_tabs_onCreated.js"]
+
+["browser_ext_tabs_onHighlighted.js"]
+
+["browser_ext_tabs_onUpdated.js"]
+
+["browser_ext_tabs_onUpdated_filter.js"]
+
+["browser_ext_tabs_opener.js"]
+
+["browser_ext_tabs_printPreview.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_query.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_readerMode.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_reload.js"]
+
+["browser_ext_tabs_reload_bypass_cache.js"]
+
+["browser_ext_tabs_remove.js"]
+
+["browser_ext_tabs_removeCSS.js"]
+
+["browser_ext_tabs_saveAsPDF.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_sendMessage.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_sharingState.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_successors.js"]
+
+["browser_ext_tabs_update.js"]
+
+["browser_ext_tabs_update_highlighted.js"]
+
+["browser_ext_tabs_update_url.js"]
+
+["browser_ext_tabs_warmup.js"]
+https_first_disabled = true
+
+["browser_ext_tabs_zoom.js"]
+
+["browser_ext_themes_validation.js"]
+
+["browser_ext_topSites.js"]
+
+["browser_ext_url_overrides_newtab.js"]
+skip-if = [
+ "os == 'linux' && os_version == '18.04'", # Bug 1651261
+ "win11_2009 && asan", # Bug 1797751
+ "win11_2009 && debug", # Bug 1797751
+]
+
+["browser_ext_user_events.js"]
+
+["browser_ext_webNavigation_containerIsolation.js"]
+https_first_disabled = true
+
+["browser_ext_webNavigation_frameId0.js"]
+
+["browser_ext_webNavigation_getFrames.js"]
+
+["browser_ext_webNavigation_onCreatedNavigationTarget.js"]
+
+["browser_ext_webNavigation_onCreatedNavigationTarget_contextmenu.js"]
+
+["browser_ext_webNavigation_onCreatedNavigationTarget_named_window.js"]
+
+["browser_ext_webNavigation_onCreatedNavigationTarget_subframe_window_open.js"]
+
+["browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js"]
+
+["browser_ext_webNavigation_urlbar_transitions.js"]
+https_first_disabled = true
+
+["browser_ext_webRequest.js"]
+
+["browser_ext_webRequest_error_after_stopped_or_closed.js"]
+
+["browser_ext_webrtc.js"]
+skip-if = ["os == 'mac'"] # Bug 1565738
+
+["browser_ext_windows.js"]
+https_first_disabled = true
+
+["browser_ext_windows_allowScriptsToClose.js"]
+https_first_disabled = true
+
+["browser_ext_windows_create.js"]
+skip-if = ["verify && os == 'mac'"]
+tags = "fullscreen"
+
+["browser_ext_windows_create_cookieStoreId.js"]
+
+["browser_ext_windows_create_params.js"]
+
+["browser_ext_windows_create_tabId.js"]
+https_first_disabled = true
+
+["browser_ext_windows_create_url.js"]
+
+["browser_ext_windows_events.js"]
+
+["browser_ext_windows_incognito.js"]
+
+["browser_ext_windows_remove.js"]
+
+["browser_ext_windows_size.js"]
+skip-if = [
+ "os == 'mac'",
+ "os == 'linux' && os_version == '18.04' && debug", # Fails when windows are randomly opened in fullscreen mode, Bug 1638027
+]
+
+["browser_ext_windows_update.js"]
+skip-if = [
+ "verify && os == 'mac'",
+ "os == 'mac' && os_version == '10.15' && debug", # Bug 1780998
+ "os == 'linux' && os_version == '18.04'", # Bug 1533982 for linux1804
+]
+tags = "fullscreen"
+
+["browser_toolbar_prefers_color_scheme.js"]
+
+["browser_unified_extensions.js"]
+fail-if = ["a11y_checks"] # Bug 1854460 clicked browser may not be accessible
+
+["browser_unified_extensions_accessibility.js"]
+
+["browser_unified_extensions_context_menu.js"]
+skip-if = ["true"] #Bug 1800712
+
+["browser_unified_extensions_cui.js"]
+
+["browser_unified_extensions_doorhangers.js"]
+
+["browser_unified_extensions_messages.js"]
+
+["browser_unified_extensions_overflowable_toolbar.js"]
+tags = "overflowable-toolbar"
diff --git a/browser/components/extensions/test/browser/browser_AMBrowserExtensionsImport.js b/browser/components/extensions/test/browser/browser_AMBrowserExtensionsImport.js
new file mode 100644
index 0000000000..a680edb454
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_AMBrowserExtensionsImport.js
@@ -0,0 +1,286 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { AMBrowserExtensionsImport } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+// This test verifies the appmenu UI when there are pending imported add-ons.
+// The UI in `about:addons` is covered by tests in
+// `toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js`.
+
+AddonTestUtils.initMochitest(this);
+
+const TEST_SERVER = AddonTestUtils.createHttpServer();
+
+const ADDONS = {
+ ext1: {
+ manifest: {
+ name: "Ext 1",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "ff@ext-1" } },
+ permissions: ["history"],
+ },
+ },
+ ext2: {
+ manifest: {
+ name: "Ext 2",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "ff@ext-2" } },
+ permissions: ["history"],
+ },
+ },
+};
+// Populated in `setup()`.
+const XPIS = {};
+// Populated in `setup()`.
+const ADDON_SEARCH_RESULTS = {};
+
+const mockAddonRepository = ({ addons = [] }) => {
+ return {
+ async getMappedAddons(browserID, extensionIDs) {
+ return Promise.resolve({
+ addons,
+ matchedIDs: [],
+ unmatchedIDs: [],
+ });
+ },
+ };
+};
+
+add_setup(async function setup() {
+ for (const [name, data] of Object.entries(ADDONS)) {
+ XPIS[name] = AddonTestUtils.createTempWebExtensionFile(data);
+ TEST_SERVER.registerFile(`/addons/${name}.xpi`, XPIS[name]);
+
+ ADDON_SEARCH_RESULTS[name] = {
+ id: data.manifest.browser_specific_settings.gecko.id,
+ name: data.name,
+ version: data.version,
+ sourceURI: Services.io.newURI(
+ `http://localhost:${TEST_SERVER.identity.primaryPort}/addons/${name}.xpi`
+ ),
+ icons: {},
+ };
+ }
+
+ registerCleanupFunction(() => {
+ // Clear the add-on repository override.
+ AMBrowserExtensionsImport._addonRepository = null;
+ });
+});
+
+add_task(async function test_appmenu_notification() {
+ const browserID = "some-browser-id";
+ const extensionIDs = ["ext-1", "ext-2"];
+ AMBrowserExtensionsImport._addonRepository = mockAddonRepository({
+ addons: Object.values(ADDON_SEARCH_RESULTS),
+ });
+ const menuButton = document.getElementById("PanelUI-menu-button");
+
+ let promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-pending"
+ );
+ // Start a first import...
+ await AMBrowserExtensionsImport.stageInstalls(browserID, extensionIDs);
+ await promiseTopic;
+ Assert.equal(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "expected menu button to have the addon-alert badge"
+ );
+
+ // ...then cancel it.
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-cancelled"
+ );
+ await AMBrowserExtensionsImport.cancelInstalls();
+ await promiseTopic;
+ Assert.ok(
+ !menuButton.hasAttribute("badge-status"),
+ "expected menu button to no longer have the addon-alert badge"
+ );
+
+ // We start a second import here, then we complete it with the UI.
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-pending"
+ );
+ const result = await AMBrowserExtensionsImport.stageInstalls(
+ browserID,
+ extensionIDs
+ );
+ await promiseTopic;
+ Assert.equal(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "expected menu button to have the addon-alert badge"
+ );
+
+ // Open the appmenu panel in order to verify the notification.
+ const menuPanelShown = BrowserTestUtils.waitForEvent(
+ PanelUI.panel,
+ "ViewShown"
+ );
+ menuButton.click();
+ await menuPanelShown;
+
+ const notifications = PanelUI.addonNotificationContainer;
+ Assert.equal(
+ notifications.children.length,
+ 1,
+ "expected a notification about the imported add-ons"
+ );
+
+ const endedPromises = result.importedAddonIDs.map(id =>
+ AddonTestUtils.promiseInstallEvent("onInstallEnded")
+ );
+ const menuPanelHidden = BrowserTestUtils.waitForEvent(
+ PanelUI.panel,
+ "popuphidden"
+ );
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-complete"
+ );
+ // Complete the installation of the add-ons by clicking on the notification.
+ notifications.children[0].click();
+ await Promise.all([...endedPromises, promiseTopic, menuPanelHidden]);
+ Assert.ok(
+ !menuButton.hasAttribute("badge-status"),
+ "expected menu button to no longer have the addon-alert badge"
+ );
+
+ for (const id of result.importedAddonIDs) {
+ const addon = await AddonManager.getAddonByID(id);
+ Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`);
+ await addon.uninstall();
+ }
+});
+
+add_task(async function test_appmenu_notification_with_sideloaded_addon() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.autoDisableScopes", 15]],
+ });
+
+ const menuButton = document.getElementById("PanelUI-menu-button");
+
+ // Load a sideloaded add-on, which will be user-disabled by default.
+ const sideloadedXPI = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ name: "sideloaded add-on",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "ff@sideloaded" } },
+ },
+ });
+ await AddonTestUtils.manuallyInstall(sideloadedXPI);
+ const changePromise = new Promise(resolve =>
+ ExtensionsUI.once("change", resolve)
+ );
+ ExtensionsUI._checkForSideloaded();
+ await changePromise;
+
+ // We should have a badge for the sideloaded add-on on the appmenu button.
+ Assert.equal(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "expected menu button to have the addon-alert badge"
+ );
+
+ // Now, we start an import...
+ const browserID = "some-browser-id";
+ const extensionIDs = ["ext-1", "ext-2"];
+ AMBrowserExtensionsImport._addonRepository = mockAddonRepository({
+ addons: Object.values(ADDON_SEARCH_RESULTS),
+ });
+
+ let promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-pending"
+ );
+ await AMBrowserExtensionsImport.stageInstalls(browserID, extensionIDs);
+ await promiseTopic;
+
+ // Badge should still be shown.
+ Assert.equal(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "expected menu button to have the addon-alert badge"
+ );
+
+ // Open the appmenu panel in order to verify the notifications.
+ const menuPanelShown = BrowserTestUtils.waitForEvent(
+ PanelUI.panel,
+ "ViewShown"
+ );
+ menuButton.click();
+ await menuPanelShown;
+
+ // We expect two notifications: one for the imported add-ons (listed first),
+ // and the second about the sideloaded add-on.
+ const notifications = PanelUI.addonNotificationContainer;
+ Assert.equal(notifications.children.length, 2, "expected two notifications");
+ Assert.equal(
+ notifications.children[0].id,
+ "webext-imported-addons",
+ "expected a notification for the imported add-ons"
+ );
+ Assert.equal(
+ notifications.children[1].id,
+ "webext-perms-sideload-menu-item",
+ "expected a notification for the sideloaded add-on"
+ );
+
+ // Cancel the import to make sure the UI is updated accordingly.
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-cancelled"
+ );
+ await AMBrowserExtensionsImport.cancelInstalls();
+ await promiseTopic;
+
+ // Badge should still be shown since there is still the sideloaded
+ // notification but we do not expect the notification for the imported
+ // add-ons anymore.
+ Assert.ok(
+ menuButton.hasAttribute("badge-status"),
+ "expected menu button to have the addon-alert badge"
+ );
+ Assert.equal(notifications.children.length, 1, "expected a notification");
+ Assert.equal(
+ notifications.children[0].id,
+ "webext-perms-sideload-menu-item",
+ "expected a notification for the sideloaded add-on"
+ );
+
+ // Click the sideloaded add-on notification.
+ const menuPanelHidden = BrowserTestUtils.waitForEvent(
+ PanelUI.panel,
+ "popuphidden"
+ );
+ const popupPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ );
+ notifications.children[0].click();
+ const [popup] = await Promise.all([popupPromise, menuPanelHidden]);
+
+ // The user should see a new popup asking them about enabling the sideloaded
+ // add-on. Let's keep the add-on disabled.
+ popup.secondaryButton.click();
+
+ // The badge should be hidden now, and there shouldn't be any notification in
+ // the panel anymore.
+ Assert.ok(
+ !menuButton.hasAttribute("badge-status"),
+ "expected menu button to no longer have the addon-alert badge"
+ );
+ Assert.equal(notifications.children.length, 0, "expected no notification");
+
+ // Unload the sideloaded add-on.
+ const addon = await AddonManager.getAddonByID("ff@sideloaded");
+ Assert.ok(addon.userDisabled, "expected add-on to be disabled");
+ await addon.uninstall();
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ExtensionControlledPopup.js b/browser/components/extensions/test/browser/browser_ExtensionControlledPopup.js
new file mode 100644
index 0000000000..2e1e178215
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ExtensionControlledPopup.js
@@ -0,0 +1,241 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionControlledPopup:
+ "resource:///modules/ExtensionControlledPopup.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+
+function createMarkup(doc, popup) {
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(doc);
+ let popupnotification = doc.createXULElement("popupnotification");
+ let attributes = {
+ id: "extension-controlled-notification",
+ class: "extension-controlled-notification",
+ popupid: "extension-controlled",
+ hidden: "true",
+ label: "ExtControlled",
+ buttonlabel: "Keep Changes",
+ buttonaccesskey: "K",
+ secondarybuttonlabel: "Restore Settings",
+ secondarybuttonaccesskey: "R",
+ closebuttonhidden: "true",
+ dropmarkerhidden: "true",
+ checkboxhidden: "true",
+ };
+ Object.entries(attributes).forEach(([key, value]) => {
+ popupnotification.setAttribute(key, value);
+ });
+ let content = doc.createXULElement("popupnotificationcontent");
+ content.setAttribute("orient", "vertical");
+ let description = doc.createXULElement("description");
+ description.setAttribute("id", "extension-controlled-description");
+ content.appendChild(description);
+ popupnotification.appendChild(content);
+ panel.appendChild(popupnotification);
+
+ registerCleanupFunction(function removePopup() {
+ popupnotification.remove();
+ });
+
+ return { panel, popupnotification };
+}
+
+/*
+ * This function is a unit test for ExtensionControlledPopup. It is also tested
+ * where it is being used (currently New Tab and homepage). An empty extension
+ * is used along with the expected markup as an example.
+ */
+add_task(async function testExtensionControlledPopup() {
+ let id = "ext-controlled@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ name: "Ext Controlled",
+ },
+ // We need to be able to find the extension using AddonManager.
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ let addon = await AddonManager.getAddonByID(id);
+ await ExtensionSettingsStore.initialize();
+
+ let confirmedType = "extension-controlled-confirmed";
+ let onObserverAdded = sinon.spy();
+ let onObserverRemoved = sinon.spy();
+ let observerTopic = "extension-controlled-event";
+ let beforeDisableAddon = sinon.spy();
+ let settingType = "extension-controlled";
+ let settingKey = "some-key";
+ let popup = new ExtensionControlledPopup({
+ confirmedType,
+ observerTopic,
+ popupnotificationId: "extension-controlled-notification",
+ settingType,
+ settingKey,
+ descriptionId: "extension-controlled-description",
+ descriptionMessageId: "newTabControlled.message2",
+ learnMoreLink: "extension-controlled",
+ onObserverAdded,
+ onObserverRemoved,
+ beforeDisableAddon,
+ });
+
+ let doc = Services.wm.getMostRecentWindow("navigator:browser").document;
+ let { panel, popupnotification } = createMarkup(doc, popup);
+
+ function openPopupWithEvent() {
+ let popupShown = promisePopupShown(panel);
+ Services.obs.notifyObservers(null, observerTopic);
+ return popupShown;
+ }
+
+ function closePopupWithAction(action, extensionId) {
+ let done;
+ if (action == "ignore") {
+ panel.hidePopup();
+ } else if (action == "button") {
+ done = TestUtils.waitForCondition(() => {
+ return ExtensionSettingsStore.getSetting(confirmedType, id, id).value;
+ });
+ popupnotification.button.click();
+ } else if (action == "secondarybutton") {
+ done = awaitEvent("shutdown", id);
+ popupnotification.secondaryButton.click();
+ }
+ return done;
+ }
+
+ // No callbacks are initially called.
+ ok(!onObserverAdded.called, "No observer has been added");
+ ok(!onObserverRemoved.called, "No observer has been removed");
+ ok(!beforeDisableAddon.called, "Settings have not been restored");
+
+ // Add the setting and observer.
+ await ExtensionSettingsStore.addSetting(
+ id,
+ settingType,
+ settingKey,
+ "controlled",
+ () => "init"
+ );
+ await popup.addObserver(id);
+
+ // Ensure the panel isn't open.
+ ok(onObserverAdded.called, "Observing the event");
+ onObserverAdded.resetHistory();
+ ok(!onObserverRemoved.called, "Observing the event");
+ ok(!beforeDisableAddon.called, "Settings have not been restored");
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The panel is closed"
+ );
+ is(popupnotification.hidden, true, "The popup is hidden");
+ is(addon.userDisabled, false, "The extension is enabled");
+ is(
+ await popup.userHasConfirmed(id),
+ false,
+ "The user is not initially confirmed"
+ );
+
+ // The popup should opened based on the observer event.
+ await openPopupWithEvent();
+
+ ok(!onObserverAdded.called, "Only one observer has been registered");
+ ok(onObserverRemoved.called, "The observer was removed");
+ onObserverRemoved.resetHistory();
+ ok(!beforeDisableAddon.called, "Settings have not been restored");
+ is(panel.getAttribute("panelopen"), "true", "The panel is open");
+ is(popupnotification.hidden, false, "The popup content is visible");
+ is(await popup.userHasConfirmed(id), false, "The user has not confirmed yet");
+
+ // Verify the description is populated.
+ let description = doc.getElementById("extension-controlled-description");
+ is(
+ description.textContent,
+ "An extension, Ext Controlled, changed the page you see when you open a new tab.Learn more",
+ "The extension name is in the description"
+ );
+ let link = description.querySelector("a.learnMore");
+ is(
+ link.href,
+ "http://127.0.0.1:8888/support-dummy/extension-controlled",
+ "The link has the href set from learnMoreLink"
+ );
+
+ // Force close the popup, as if a user clicked away from it.
+ await closePopupWithAction("ignore");
+
+ // Nothing was recorded, but we won't show it again.
+ ok(!onObserverAdded.called, "The observer hasn't changed");
+ ok(!onObserverRemoved.called, "The observer hasn't changed");
+ is(await popup.userHasConfirmed(id), false, "The user has not confirmed");
+ is(addon.userDisabled, false, "The extension is still enabled");
+
+ // Force add the observer again to keep changes.
+ await popup.addObserver(id);
+ ok(onObserverAdded.called, "The observer was added again");
+ onObserverAdded.resetHistory();
+ ok(!onObserverRemoved.called, "The observer is still registered");
+ is(await popup.userHasConfirmed(id), false, "The user has not confirmed");
+
+ // Wait for popup.
+ await openPopupWithEvent();
+
+ // Keep the changes.
+ await closePopupWithAction("button");
+
+ // The observer is removed, but the notification is saved.
+ ok(!onObserverAdded.called, "The observer wasn't added");
+ ok(onObserverRemoved.called, "The observer was removed");
+ onObserverRemoved.resetHistory();
+ is(await popup.userHasConfirmed(id), true, "The user has confirmed");
+ is(addon.userDisabled, false, "The extension is still enabled");
+
+ // Adding the observer again for this add-on won't work, since it is
+ // confirmed.
+ await popup.addObserver(id);
+ ok(!onObserverAdded.called, "The observer isn't added");
+ ok(!onObserverRemoved.called, "The observer isn't removed");
+ is(await popup.userHasConfirmed(id), true, "The user has confirmed");
+
+ // Clear that the user was notified.
+ await popup.clearConfirmation(id);
+ is(
+ await popup.userHasConfirmed(id),
+ false,
+ "The user confirmation has been cleared"
+ );
+
+ // Force add the observer again to restore changes.
+ await popup.addObserver(id);
+ ok(onObserverAdded.called, "The observer was added a third time");
+ onObserverAdded.resetHistory();
+ ok(!onObserverRemoved.called, "The observer is still active");
+ ok(!beforeDisableAddon.called, "We haven't disabled the add-on yet");
+ is(await popup.userHasConfirmed(id), false, "The user has not confirmed");
+
+ // Wait for popup.
+ await openPopupWithEvent();
+
+ // Restore the settings.
+ await closePopupWithAction("secondarybutton");
+
+ // The observer is removed and the add-on is now disabled.
+ ok(!onObserverAdded.called, "There is no observer");
+ ok(onObserverRemoved.called, "The observer has been removed");
+ ok(beforeDisableAddon.called, "The beforeDisableAddon callback was fired");
+ is(await popup.userHasConfirmed(id), false, "The user has not confirmed");
+ is(addon.userDisabled, true, "The extension is now disabled");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_action_popup_allowed_urls.js b/browser/components/extensions/test/browser/browser_ext_action_popup_allowed_urls.js
new file mode 100644
index 0000000000..8a985161ce
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_action_popup_allowed_urls.js
@@ -0,0 +1,283 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_actions_setPopup_allowed_urls() {
+ const otherExtension = ExtensionTestUtils.loadExtension({});
+ const extensionDefinition = {
+ background() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg === "set-popup") {
+ const apiNs = args[0];
+ const popupOptions = args[1];
+ if (apiNs === "pageAction") {
+ popupOptions.tabId = (
+ await browser.tabs.query({ active: true })
+ )[0].id;
+ }
+
+ let error;
+ try {
+ await browser[apiNs].setPopup(popupOptions);
+ } catch (err) {
+ error = err;
+ }
+ browser.test.sendMessage("set-popup:done", {
+ error: error && String(error),
+ });
+ return;
+ }
+
+ browser.test.fail(`Unexpected test message: ${msg}`);
+ });
+ },
+ };
+
+ await otherExtension.startup();
+
+ const testCases = [
+ // https urls are disallowed on mv3 but currently allowed on mv2.
+ [
+ "action",
+ "https://example.com",
+ {
+ manifest_version: 3,
+ action: {},
+ },
+ {
+ disallowed: true,
+ errorMessage: "Access denied for URL https://example.com",
+ },
+ ],
+
+ [
+ "pageAction",
+ "https://example.com",
+ {
+ manifest_version: 3,
+ page_action: {},
+ },
+ {
+ disallowed: true,
+ errorMessage: "Access denied for URL https://example.com",
+ },
+ ],
+
+ [
+ "browserAction",
+ "https://example.com",
+ {
+ manifest_version: 2,
+ browser_action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ [
+ "pageAction",
+ "https://example.com",
+ {
+ manifest_version: 2,
+ page_action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ // absolute moz-extension url from same extension expected to be allowed in MV3 and MV2.
+
+ [
+ "action",
+ extension => `moz-extension://${extension.uuid}/page.html`,
+ {
+ manifest_version: 3,
+ action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ [
+ "browserAction",
+ extension => `moz-extension://${extension.uuid}/page.html`,
+ {
+ manifest_version: 2,
+ browser_action: { default_popup: "popup.html" },
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ [
+ "pageAction",
+ extension => `moz-extension://${extension.uuid}/page.html`,
+ {
+ manifest_version: 3,
+ page_action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ [
+ "pageAction",
+ extension => `moz-extension://${extension.uuid}/page.html`,
+ {
+ manifest_version: 2,
+ page_action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ // absolute moz-extension url from other extensions expected to be disallowed in MV3 and MV2.
+
+ [
+ "action",
+ `moz-extension://${otherExtension.uuid}/page.html`,
+ {
+ manifest_version: 3,
+ action: {},
+ },
+ {
+ disallowed: true,
+ errorMessage: `Access denied for URL moz-extension://${otherExtension.uuid}/page.html`,
+ },
+ ],
+
+ [
+ "browserAction",
+ `moz-extension://${otherExtension.uuid}/page.html`,
+ {
+ manifest_version: 2,
+ browser_action: {},
+ },
+ {
+ disallowed: true,
+ errorMessage: `Access denied for URL moz-extension://${otherExtension.uuid}/page.html`,
+ },
+ ],
+
+ [
+ "pageAction",
+ `moz-extension://${otherExtension.uuid}/page.html`,
+ {
+ manifest_version: 3,
+ page_action: {},
+ },
+ {
+ disallowed: true,
+ errorMessage: `Access denied for URL moz-extension://${otherExtension.uuid}/page.html`,
+ },
+ ],
+
+ [
+ "pageAction",
+ `moz-extension://${otherExtension.uuid}/page.html`,
+ {
+ manifest_version: 2,
+ page_action: {},
+ },
+ {
+ disallowed: true,
+ errorMessage: `Access denied for URL moz-extension://${otherExtension.uuid}/page.html`,
+ },
+ ],
+
+ // Empty url should also be allowed (as it resets the popup url currently set).
+ [
+ "action",
+ null,
+ {
+ manifest_version: 3,
+ action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ [
+ "browserAction",
+ null,
+ {
+ manifest_version: 2,
+ browser_action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ [
+ "pageAction",
+ null,
+ {
+ manifest_version: 3,
+ page_action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+
+ [
+ "pageAction",
+ null,
+ {
+ manifest_version: 2,
+ page_action: {},
+ },
+ {
+ disallowed: false,
+ },
+ ],
+ ];
+
+ for (const [apiNs, popupUrl, manifest, expects] of testCases) {
+ const extension = ExtensionTestUtils.loadExtension({
+ ...extensionDefinition,
+ manifest,
+ });
+ await extension.startup();
+
+ const popup =
+ typeof popupUrl === "function" ? popupUrl(extension) : popupUrl;
+
+ info(
+ `Testing ${apiNs}.setPopup({ popup: ${popup} }) on manifest_version ${
+ manifest.manifest_version ?? 2
+ }`
+ );
+
+ const popupOptions = { popup };
+ extension.sendMessage("set-popup", apiNs, popupOptions);
+
+ const { error } = await extension.awaitMessage("set-popup:done");
+ if (expects.disallowed) {
+ ok(
+ error?.includes(expects.errorMessage),
+ `Got expected error on url ${popup}: ${error}`
+ );
+ } else {
+ is(error, undefined, `Expected url ${popup} to be allowed`);
+ }
+ await extension.unload();
+ }
+
+ await otherExtension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_activeScript.js b/browser/components/extensions/test/browser/browser_ext_activeScript.js
new file mode 100644
index 0000000000..231825795c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_activeScript.js
@@ -0,0 +1,480 @@
+"use strict";
+
+requestLongerTimeout(2);
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+loadTestSubscript("head_unified_extensions.js");
+
+function makeRunAtScript(runAt) {
+ return `
+ window.order ??= [];
+ window.order.push("${runAt}");
+ browser.test.sendMessage("injected", "order@" + window.order.join());
+ `;
+}
+
+async function makeExtension({
+ id,
+ manifest_version = 3,
+ granted = [],
+ noStaticScript = false,
+ verifyExtensionsPanel,
+}) {
+ info(`Loading extension ` + JSON.stringify({ id, granted }));
+
+ let manifest = {
+ manifest_version,
+ browser_specific_settings: { gecko: { id } },
+ permissions: ["activeTab", "scripting"],
+ content_scripts: noStaticScript
+ ? []
+ : [
+ {
+ matches: ["*://*/*"],
+ js: ["static.js"],
+ },
+ ],
+ };
+
+ if (!verifyExtensionsPanel) {
+ // Pin the browser action widget to the navbar (toolbar).
+ if (manifest_version === 3) {
+ manifest.action = {
+ default_area: "navbar",
+ };
+ } else {
+ manifest.browser_action = {
+ default_area: "navbar",
+ };
+ }
+ }
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest,
+ useAddonManager: "temporary",
+
+ background() {
+ let expectCount = 0;
+
+ const executeCountScript = tab =>
+ browser.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: expectCount => {
+ let retryCount = 0;
+
+ function tryScriptCount() {
+ let id = browser.runtime.id.split("@")[0];
+ let count = document.body.dataset[id] | 0;
+ if (count < expectCount && retryCount < 100) {
+ // This needs to run after all scripts, to confirm the correct
+ // number of scripts was injected. The two paths are inherently
+ // independant, and since there's a variable number of content
+ // scripts, there's no easy/better way to do it than a delay
+ // and retry for up to 100 frames.
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(tryScriptCount, 30);
+ retryCount++;
+ return;
+ }
+ browser.test.sendMessage("scriptCount", count);
+ }
+
+ tryScriptCount();
+ },
+ args: [expectCount],
+ });
+
+ browser.test.onMessage.addListener(async (msg, arg) => {
+ if (msg === "dynamic-script") {
+ await browser.scripting.registerContentScripts([arg]);
+ browser.test.sendMessage("dynamic-script-done");
+ } else if (msg === "injected-flush?") {
+ browser.test.sendMessage("injected", "flush");
+ } else if (msg === "expect-count") {
+ expectCount = arg;
+ browser.test.sendMessage("expect-done");
+ } else if (msg === "execute-count-script") {
+ const [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.withHandlingUserInput(() => executeCountScript(tab));
+ }
+ });
+
+ let action = browser.action || browser.browserAction;
+ // For some test cases, we don't define a browser action in the manifest.
+ if (action) {
+ action.onClicked.addListener(executeCountScript);
+ }
+ },
+
+ files: {
+ "static.js"() {
+ // Need to use DOM attributes (or the dataset), because two different
+ // content script sandboxes (from top frame and the same-origin iframe)
+ // get different wrappers, they don't see each other's top expandos.
+
+ // Need to avoid using the @ character (from the extension id)
+ // because it's not allowed as part of the DOM attribute name.
+
+ let id = browser.runtime.id.split("@")[0];
+ top.document.body.dataset[id] = (top.document.body.dataset[id] | 0) + 1;
+
+ browser.test.log(
+ `Static content script from ${id} running on ${location.href}.`
+ );
+
+ browser.test.sendMessage("injected", "static@" + location.host);
+ },
+ "dynamic.js"() {
+ let id = browser.runtime.id.split("@")[0];
+ top.document.body.dataset[id] = (top.document.body.dataset[id] | 0) + 1;
+
+ browser.test.log(
+ `Dynamic content script from ${id} running on ${location.href}.`
+ );
+
+ let frame = window === top ? "top" : "frame";
+ browser.test.sendMessage(
+ "injected",
+ `dynamic-${frame}@${location.host}`
+ );
+ },
+ "document_start.js": makeRunAtScript("document_start"),
+ "document_end.js": makeRunAtScript("document_end"),
+ "document_idle.js": makeRunAtScript("document_idle"),
+ },
+ });
+
+ if (granted?.length) {
+ info("Granting initial permissions.");
+ await ExtensionPermissions.add(id, { permissions: [], origins: granted });
+ }
+
+ await ext.startup();
+ return ext;
+}
+
+async function testActiveScript(
+ extension,
+ expectCount,
+ expectHosts,
+ win,
+ verifyExtensionsPanel
+) {
+ info(`Testing ${extension.id} on ${gBrowser.currentURI.spec}.`);
+
+ extension.sendMessage("expect-count", expectCount);
+ await extension.awaitMessage("expect-done");
+
+ if (verifyExtensionsPanel) {
+ await clickUnifiedExtensionsItem(win, extension.id, true);
+ } else {
+ await clickBrowserAction(extension, win);
+ }
+
+ let received = [];
+ for (let host of expectHosts) {
+ info(`Waiting for a script to run in a ${host} frame.`);
+ received.push(await extension.awaitMessage("injected"));
+ }
+
+ extension.sendMessage("injected-flush?");
+ info("Waiting for the flush message between test runs.");
+ let flush = await extension.awaitMessage("injected");
+ is(flush, "flush", "Messages properly flushed.");
+
+ is(received.sort().join(), expectHosts.join(), "All messages received.");
+
+ // To cover the activeTab counter assertion for extensions that don't trigger
+ // an action/browserAction onClicked event, we send an explicit test message
+ // here.
+ // The test extension queries the current active tab and then execute the
+ // counter content script from inside a `browser.test.withHandlingUserInput()`
+ // callback.
+ if (verifyExtensionsPanel) {
+ extension.sendMessage("execute-count-script");
+ }
+
+ info(`Awaiting the counter from the activeTab content script.`);
+ let scriptCount = await extension.awaitMessage("scriptCount");
+ is(scriptCount | 0, expectCount, "Expected number of scripts running");
+}
+
+const verifyActionActiveScript = async ({
+ win = window,
+ verifyExtensionsPanel = false,
+} = {}) => {
+ // Static MV2 extension content scripts are not affected.
+ let ext0 = await makeExtension({
+ id: "ext0@test",
+ manifest_version: 2,
+ granted: ["*://example.com/*"],
+ verifyExtensionsPanel,
+ });
+
+ let ext1 = await makeExtension({
+ id: "ext1@test",
+ verifyExtensionsPanel,
+ });
+
+ let ext2 = await makeExtension({
+ id: "ext2@test",
+ granted: ["*://example.com/*"],
+ verifyExtensionsPanel,
+ });
+
+ let ext3 = await makeExtension({
+ id: "ext3@test",
+ granted: ["*://mochi.test/*"],
+ verifyExtensionsPanel,
+ });
+
+ // Test run_at script ordering.
+ let ext4 = await makeExtension({
+ id: "ext4@test",
+ verifyExtensionsPanel,
+ });
+
+ // Test without static scripts in the manifest, because they add optional
+ // permissions, and we specifically want to test activeTab without them.
+ let ext5 = await makeExtension({
+ id: "ext5@test",
+ noStaticScript: true,
+ verifyExtensionsPanel,
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ info("No content scripts run on top level about:blank.");
+ await testActiveScript(ext0, 0, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext1, 0, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext2, 0, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext3, 0, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext4, 0, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext5, 0, [], win, verifyExtensionsPanel);
+ });
+
+ let dynamicScript = {
+ id: "script",
+ js: ["dynamic.js"],
+ matches: [""],
+ allFrames: true,
+ persistAcrossSessions: false,
+ };
+
+ // MV2 extensions don't support activeScript. This dynamic script won't run
+ // when action button is clicked, but will run on example.com automatically.
+ ext0.sendMessage("dynamic-script", dynamicScript);
+ await ext0.awaitMessage("dynamic-script-done");
+
+ // Only ext3 will have a dynamic script, matching with allFrames.
+ ext3.sendMessage("dynamic-script", dynamicScript);
+ await ext3.awaitMessage("dynamic-script-done");
+
+ // ext5 will have only dynamic scripts and activeTab, so no host permissions.
+ ext5.sendMessage("dynamic-script", dynamicScript);
+ await ext5.awaitMessage("dynamic-script-done");
+
+ let url =
+ "https://example.com/browser/browser/components/extensions/test/browser/file_with_xorigin_frame.html";
+
+ await BrowserTestUtils.withNewTab(url, async browser => {
+ info("ext0 is MV2, static content script should run automatically.");
+ info("ext0 has example.com permission, dynamic scripts should also run.");
+ let received = [
+ await ext0.awaitMessage("injected"),
+ await ext0.awaitMessage("injected"),
+ await ext0.awaitMessage("injected"),
+ ];
+ is(
+ received.sort().join(),
+ "dynamic-frame@example.com,dynamic-top@example.com,static@example.com",
+ "All messages received"
+ );
+
+ info("Clicking ext0 button should not run content script again.");
+ await testActiveScript(ext0, 3, [], win, verifyExtensionsPanel);
+
+ info("ext2 has host permission, content script should run automatically.");
+ let static2 = await ext2.awaitMessage("injected");
+ is(static2, "static@example.com", "Script ran automatically");
+
+ info("Clicking ext2 button should not run content script again.");
+ await testActiveScript(ext2, 1, [], win, verifyExtensionsPanel);
+
+ await testActiveScript(
+ ext1,
+ 1,
+ ["static@example.com"],
+ win,
+ verifyExtensionsPanel
+ );
+
+ await testActiveScript(
+ ext3,
+ 3,
+ [
+ "dynamic-frame@example.com",
+ "dynamic-top@example.com",
+ "static@example.com",
+ ],
+ win,
+ verifyExtensionsPanel
+ );
+
+ await testActiveScript(
+ ext4,
+ 1,
+ ["static@example.com"],
+ win,
+ verifyExtensionsPanel
+ );
+
+ info("ext5 only has dynamic scripts that run with activeTab.");
+ await testActiveScript(
+ ext5,
+ 2,
+ ["dynamic-frame@example.com", "dynamic-top@example.com"],
+ win,
+ verifyExtensionsPanel
+ );
+
+ // Navigate same-origin iframe to another page, activeScripts shouldn't run.
+ let bc = browser.browsingContext.children[0].children[0];
+ SpecialPowers.spawn(bc, [], () => {
+ content.location.href = "file_dummy.html";
+ });
+ // But dynamic script from ext0 should run automatically again.
+ let dynamic0 = await ext0.awaitMessage("injected");
+ is(dynamic0, "dynamic-frame@example.com", "Script ran automatically");
+
+ info("Clicking all buttons again should not activeScripts.");
+ await testActiveScript(ext0, 4, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext1, 1, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext2, 1, [], win, verifyExtensionsPanel);
+ // Except ext3 dynamic allFrames script runs in the new navigated page.
+ await testActiveScript(
+ ext3,
+ 4,
+ ["dynamic-frame@example.com"],
+ win,
+ verifyExtensionsPanel
+ );
+ await testActiveScript(ext4, 1, [], win, verifyExtensionsPanel);
+
+ // ext5 dynamic allFrames script also runs in the new navigated page.
+ await testActiveScript(
+ ext5,
+ 3,
+ ["dynamic-frame@example.com"],
+ win,
+ verifyExtensionsPanel
+ );
+ });
+
+ // Register run_at content scripts in reverse order.
+ for (let runAt of ["document_idle", "document_end", "document_start"]) {
+ ext4.sendMessage("dynamic-script", {
+ id: runAt,
+ runAt: runAt,
+ js: [`${runAt}.js`],
+ matches: ["http://mochi.test/*"],
+ persistAcrossSessions: false,
+ });
+ await ext4.awaitMessage("dynamic-script-done");
+ }
+
+ await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
+ info("ext0 is MV2, static content script should run automatically.");
+ let static0 = await ext0.awaitMessage("injected");
+ is(static0, "static@mochi.test:8888", "Script ran automatically.");
+
+ info("Clicking ext0 button should not run content script again.");
+ await testActiveScript(ext0, 1, [], win, verifyExtensionsPanel);
+
+ info("ext3 has host permission, content script should run automatically.");
+ let received3 = [
+ await ext3.awaitMessage("injected"),
+ await ext3.awaitMessage("injected"),
+ ];
+ is(
+ received3.sort().join(),
+ "dynamic-top@mochi.test:8888,static@mochi.test:8888",
+ "All messages received."
+ );
+
+ info("Clicking ext3 button should not run content script again.");
+ await testActiveScript(ext3, 2, [], win, verifyExtensionsPanel);
+
+ await testActiveScript(
+ ext1,
+ 1,
+ ["static@mochi.test:8888"],
+ win,
+ verifyExtensionsPanel
+ );
+ await testActiveScript(
+ ext2,
+ 1,
+ ["static@mochi.test:8888"],
+ win,
+ verifyExtensionsPanel
+ );
+
+ // Expect run_at content scripts to run in the correct order.
+ await testActiveScript(
+ ext4,
+ 1,
+ [
+ "order@document_start",
+ "order@document_start,document_end",
+ "order@document_start,document_end,document_idle",
+ "static@mochi.test:8888",
+ ],
+ win,
+ verifyExtensionsPanel
+ );
+
+ info("ext5 dynamic scripts with activeTab should run when activated.");
+ await testActiveScript(
+ ext5,
+ 1,
+ ["dynamic-top@mochi.test:8888"],
+ win,
+ verifyExtensionsPanel
+ );
+
+ info("Clicking all buttons again should not run content scripts.");
+ await testActiveScript(ext0, 1, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext1, 1, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext2, 1, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext3, 2, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext4, 1, [], win, verifyExtensionsPanel);
+ await testActiveScript(ext5, 1, [], win, verifyExtensionsPanel);
+
+ // TODO: We must unload the extensions here, not after we close the tab but
+ // this should not be needed ideally. Bug 1768532 describes why we need to
+ // unload the extensions from within `.withNewTab()` for now. Once this bug
+ // is fixed, we should move the unload calls below to after the
+ // `.withNewTab()` block.
+ await ext0.unload();
+ await ext1.unload();
+ await ext2.unload();
+ await ext3.unload();
+ await ext4.unload();
+ await ext5.unload();
+ });
+};
+
+add_task(async function test_action_activeScript() {
+ await verifyActionActiveScript();
+});
+
+add_task(async function test_activeScript_with_unified_extensions_panel() {
+ await verifyActionActiveScript({ verifyExtensionsPanel: true });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_addon_debugging_netmonitor.js b/browser/components/extensions/test/browser/browser_ext_addon_debugging_netmonitor.js
new file mode 100644
index 0000000000..f436a19657
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_addon_debugging_netmonitor.js
@@ -0,0 +1,116 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+async function setupToolboxTest(extensionId) {
+ const toolbox = await gDevTools.showToolboxForWebExtension(extensionId);
+
+ async function waitFor(condition) {
+ while (!condition()) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(done => window.setTimeout(done, 1000));
+ }
+ }
+
+ const netmonitor = await toolbox.selectTool("netmonitor");
+
+ const expectedURL = "http://mochi.test:8888/?test_netmonitor=1";
+
+ // Call a function defined in the target extension to make it
+ // fetch from an expected http url.
+ await toolbox.commands.scriptCommand.execute(
+ `doFetchHTTPRequest("${expectedURL}");`
+ );
+
+ await waitFor(() => {
+ return !netmonitor.panelWin.document.querySelector(
+ ".request-list-empty-notice"
+ );
+ });
+
+ let { store } = netmonitor.panelWin;
+
+ // NOTE: we need to filter the requests to the ones that we expect until
+ // the network monitor is not yet filtering out the requests that are not
+ // coming from an extension window or a descendent of an extension window,
+ // in both oop and non-oop extension mode (filed as Bug 1442621).
+ function filterRequest(request) {
+ return request.url === expectedURL;
+ }
+
+ let requests;
+
+ await waitFor(() => {
+ requests = Array.from(store.getState().requests.requests.values()).filter(
+ filterRequest
+ );
+
+ return !!requests.length;
+ });
+
+ // Call a function defined in the target extension to make assertions
+ // on the network requests collected by the netmonitor panel.
+ await toolbox.commands.scriptCommand.execute(
+ `testNetworkRequestReceived(${JSON.stringify(requests)});`
+ );
+
+ await toolbox.destroy();
+}
+
+add_task(async function test_addon_debugging_netmonitor_panel() {
+ const EXTENSION_ID = "test-monitor-panel@mozilla";
+
+ function background() {
+ let expectedURL;
+ window.doFetchHTTPRequest = async function (urlToFetch) {
+ expectedURL = urlToFetch;
+ await fetch(urlToFetch);
+ };
+ window.testNetworkRequestReceived = async function (requests) {
+ browser.test.log(
+ "Addon Debugging Netmonitor panel collected requests: " +
+ JSON.stringify(requests)
+ );
+ browser.test.assertEq(1, requests.length, "Got one request logged");
+ browser.test.assertEq("GET", requests[0].method, "Got a GET request");
+ browser.test.assertEq(
+ expectedURL,
+ requests[0].url,
+ "Got the expected request url"
+ );
+
+ browser.test.notifyPass("netmonitor_request_logged");
+ };
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ useAddonManager: "temporary",
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ browser_specific_settings: {
+ gecko: { id: EXTENSION_ID },
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ const onToolboxClose = setupToolboxTest(EXTENSION_ID);
+ await Promise.all([
+ extension.awaitFinish("netmonitor_request_logged"),
+ onToolboxClose,
+ ]);
+
+ info("Addon Toolbox closed");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_autocompletepopup.js b/browser/components/extensions/test/browser/browser_ext_autocompletepopup.js
new file mode 100644
index 0000000000..7188d61ca6
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_autocompletepopup.js
@@ -0,0 +1,90 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testAutocompletePopup() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "page.html",
+ browser_style: false,
+ },
+ page_action: {
+ default_popup: "page.html",
+ browser_style: false,
+ },
+ },
+ background: async function () {
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("ready");
+ },
+ files: {
+ "page.html": `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ });
+
+ async function testDatalist(browser, doc) {
+ let autocompletePopup = doc.getElementById("PopupAutoComplete");
+ let opened = promisePopupShown(autocompletePopup);
+ info("click in test-input now");
+ // two clicks to open
+ await BrowserTestUtils.synthesizeMouseAtCenter("#test-input", {}, browser);
+ await BrowserTestUtils.synthesizeMouseAtCenter("#test-input", {}, browser);
+ info("wait for opened event");
+ await opened;
+ // third to close
+ let closed = promisePopupHidden(autocompletePopup);
+ info("click in test-input now");
+ await BrowserTestUtils.synthesizeMouseAtCenter("#test-input", {}, browser);
+ info("wait for closed event");
+ await closed;
+ // If this didn't work, we hang. Other tests deal with testing the actual functionality of datalist.
+ ok(true, "datalist popup has been shown");
+ }
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ clickPageAction(extension);
+ // intentional misspell so eslint is ok with browser in background script.
+ let bowser = await awaitExtensionPanel(extension);
+ ok(!!bowser, "panel opened with browser");
+ await testDatalist(bowser, document);
+ closePageAction(extension);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ clickBrowserAction(extension);
+ bowser = await awaitExtensionPanel(extension);
+ ok(!!bowser, "panel opened with browser");
+ await testDatalist(bowser, document);
+ closeBrowserAction(extension);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_autoplayInBackground.js b/browser/components/extensions/test/browser/browser_ext_autoplayInBackground.js
new file mode 100644
index 0000000000..e082b1e9bf
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_autoplayInBackground.js
@@ -0,0 +1,52 @@
+"use strict";
+
+function setup_test_preference(enableScript) {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", 1],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.allow-extension-background-pages", enableScript],
+ ],
+ });
+}
+
+async function testAutoplayInBackgroundScript(enableScript) {
+ info(`- setup test preference, enableScript=${enableScript} -`);
+ await setup_test_preference(enableScript);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.log("- create audio in background page -");
+ let audio = new Audio();
+ audio.src =
+ "https://example.com/browser/browser/components/extensions/test/browser/silence.ogg";
+ audio.play().then(
+ function () {
+ browser.test.log("play succeed!");
+ browser.test.sendMessage("play-succeed");
+ },
+ function () {
+ browser.test.log("play promise was rejected!");
+ browser.test.sendMessage("play-failed");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+
+ if (enableScript) {
+ await extension.awaitMessage("play-succeed");
+ ok(true, "play promise was resolved!");
+ } else {
+ await extension.awaitMessage("play-failed");
+ ok(true, "play promise was rejected!");
+ }
+
+ await extension.unload();
+}
+
+add_task(async function testMain() {
+ await testAutoplayInBackgroundScript(true /* enable autoplay */);
+ await testAutoplayInBackgroundScript(false /* enable autoplay */);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_activeTab.js b/browser/components/extensions/test/browser/browser_ext_browserAction_activeTab.js
new file mode 100644
index 0000000000..4bc24a2df2
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_activeTab.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_middle_click_with_activeTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ permissions: ["activeTab"],
+ },
+
+ background() {
+ browser.browserAction.onClicked.addListener(async (tab, info) => {
+ browser.test.assertEq(1, info.button, "Expected button value");
+ browser.test.assertEq(
+ "https://example.com/",
+ tab.url,
+ "tab.url has the expected url"
+ );
+ await browser.tabs.insertCSS(tab.id, {
+ code: "body { border: 20px solid red; }",
+ });
+ browser.test.sendMessage("onClick");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+ is(
+ ext.tabManager.hasActiveTabPermission(tab),
+ false,
+ "Active tab was not granted permission"
+ );
+
+ await clickBrowserAction(extension, window, { button: 1 });
+ await extension.awaitMessage("onClick");
+
+ is(
+ ext.tabManager.hasActiveTabPermission(tab),
+ true,
+ "Active tab was granted permission"
+ );
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_middle_click_with_activeTab_and_popup() {
+ const { browserActionFor } = Management.global;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ default_area: "navbar",
+ },
+ permissions: ["activeTab"],
+ },
+
+ files: {
+ "popup.html": ` `,
+ },
+
+ background() {
+ browser.browserAction.onClicked.addListener(async (tab, info) => {
+ browser.test.assertEq(1, info.button, "Expected button value");
+ browser.test.assertEq(
+ "https://example.com/",
+ tab.url,
+ "tab.url has the expected url"
+ );
+ await browser.tabs.insertCSS(tab.id, {
+ code: "body { border: 20px solid red; }",
+ });
+ browser.test.sendMessage("onClick");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mouseover" },
+ window
+ );
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let widget = getBrowserActionWidget(extension).forWindow(window);
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+ let browserAction = browserActionFor(ext);
+
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseover" },
+ window
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ window
+ );
+
+ isnot(browserAction.pendingPopup, null, "Have pending popup");
+ is(browserAction.action.activeTabForPreload, tab, "Tab to revoke was saved");
+
+ await clickBrowserAction(extension, window, { button: 1 });
+ await extension.awaitMessage("onClick");
+
+ is(
+ browserAction.action.activeTabForPreload,
+ null,
+ "Tab to revoke was removed"
+ );
+
+ is(
+ browserAction.tabManager.hasActiveTabPermission(tab),
+ true,
+ "Active tab was granted permission"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseup", button: 0 },
+ window
+ );
+
+ EventUtils.synthesizeMouseAtCenter(widget.node, { type: "mouseout" }, window);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_middle_click_without_activeTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+
+ background() {
+ browser.browserAction.onClicked.addListener(async (tab, info) => {
+ browser.test.assertEq(1, info.button, "Expected button value");
+ browser.test.assertEq(tab.url, undefined, "tab.url is undefined");
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, {
+ code: "body { border: 20px solid red; }",
+ }),
+ "Missing host permission for the tab",
+ "expected failure of tabs.insertCSS without permission"
+ );
+ browser.test.sendMessage("onClick");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await clickBrowserAction(extension, window, { button: 1 });
+ await extension.awaitMessage("onClick");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_area.js b/browser/components/extensions/test/browser/browser_ext_browserAction_area.js
new file mode 100644
index 0000000000..90ed744a78
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_area.js
@@ -0,0 +1,126 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+var browserAreas = {
+ navbar: CustomizableUI.AREA_NAVBAR,
+ menupanel: getCustomizableUIPanelID(),
+ tabstrip: CustomizableUI.AREA_TABSTRIP,
+ personaltoolbar: CustomizableUI.AREA_BOOKMARKS,
+};
+
+async function testInArea(area) {
+ let manifest = {
+ browser_action: {
+ browser_style: true,
+ },
+ };
+ if (area) {
+ manifest.browser_action.default_area = area;
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ });
+ await extension.startup();
+ let widget = getBrowserActionWidget(extension);
+ let placement = CustomizableUI.getPlacementOfWidget(widget.id);
+ let fallbackDefaultArea = CustomizableUI.AREA_ADDONS;
+ is(
+ placement && placement.area,
+ browserAreas[area] || fallbackDefaultArea,
+ `widget located in correct area`
+ );
+ await extension.unload();
+}
+
+add_task(async function testBrowserActionDefaultArea() {
+ await testInArea();
+});
+
+add_task(async function testBrowserActionInToolbar() {
+ await testInArea("navbar");
+});
+
+add_task(async function testBrowserActionInMenuPanel() {
+ await testInArea("menupanel");
+});
+
+add_task(async function testBrowserActionInTabStrip() {
+ await testInArea("tabstrip");
+});
+
+add_task(async function testBrowserActionInPersonalToolbar() {
+ await testInArea("personaltoolbar");
+});
+
+add_task(async function testPolicyOverridesBrowserActionToNavbar() {
+ const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+ );
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ "policyBrowserActionAreaNavBarTest@mozilla.com": {
+ default_area: "navbar",
+ },
+ },
+ },
+ });
+ let manifest = {
+ browser_action: {},
+ browser_specific_settings: {
+ gecko: {
+ id: "policyBrowserActionAreaNavBarTest@mozilla.com",
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ });
+ await extension.startup();
+ let widget = getBrowserActionWidget(extension);
+ let placement = CustomizableUI.getPlacementOfWidget(widget.id);
+ is(
+ placement && placement.area,
+ CustomizableUI.AREA_NAVBAR,
+ `widget located in nav bar`
+ );
+ await extension.unload();
+});
+
+add_task(async function testPolicyOverridesBrowserActionToMenuPanel() {
+ const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+ );
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ "policyBrowserActionAreaMenuPanelTest@mozilla.com": {
+ default_area: "menupanel",
+ },
+ },
+ },
+ });
+ let manifest = {
+ browser_action: {
+ default_area: "navbar",
+ },
+ browser_specific_settings: {
+ gecko: {
+ id: "policyBrowserActionAreaMenuPanelTest@mozilla.com",
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ });
+ await extension.startup();
+ let widget = getBrowserActionWidget(extension);
+ let placement = CustomizableUI.getPlacementOfWidget(widget.id);
+ is(
+ placement && placement.area,
+ getCustomizableUIPanelID(),
+ `widget located in extensions menu`
+ );
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_click_types.js b/browser/components/extensions/test/browser/browser_ext_browserAction_click_types.js
new file mode 100644
index 0000000000..6614bb62c2
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_click_types.js
@@ -0,0 +1,269 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function test_clickData({ manifest_version, persistent }) {
+ const action = manifest_version < 3 ? "browser_action" : "action";
+ const background = { scripts: ["background.js"] };
+
+ if (persistent != null) {
+ background.persistent = persistent;
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ [action]: {},
+ background,
+ },
+
+ files: {
+ "background.js": function backgroundScript() {
+ function onClicked(tab, info) {
+ let button = info.button;
+ let modifiers = info.modifiers;
+ browser.test.sendMessage("onClick", { button, modifiers });
+ }
+
+ const apiNS =
+ browser.runtime.getManifest().manifest_version >= 3
+ ? "action"
+ : "browserAction";
+
+ browser[apiNS].onClicked.addListener(onClicked);
+ browser.test.sendMessage("ready");
+ },
+ },
+ });
+
+ const map = {
+ shiftKey: "Shift",
+ altKey: "Alt",
+ metaKey: "Command",
+ ctrlKey: "Ctrl",
+ };
+
+ function assertSingleModifier(info, modifier, area) {
+ if (modifier === "ctrlKey" && AppConstants.platform === "macosx") {
+ is(
+ info.modifiers.length,
+ 2,
+ `MacCtrl modifier with control click on Mac`
+ );
+ is(
+ info.modifiers[1],
+ "MacCtrl",
+ `MacCtrl modifier with control click on Mac`
+ );
+ } else {
+ is(
+ info.modifiers.length,
+ 1,
+ `No unnecessary modifiers for exactly one key on event`
+ );
+ }
+
+ is(info.modifiers[0], map[modifier], `Correct modifier on ${area} click`);
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (manifest_version >= 3 || persistent === false) {
+ // NOTE: even in MV3 where the API namespace is technically "action",
+ // the event listeners will be persisted into the startup data
+ // with "browserAction" as the module, because that is the name
+ // of the module shared by both MV2 browserAction and MV3 action APIs.
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: false,
+ });
+ await extension.terminateBackground();
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: true,
+ });
+ }
+
+ for (let area of [CustomizableUI.AREA_NAVBAR, getCustomizableUIPanelID()]) {
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, area);
+
+ for (let modifier of Object.keys(map)) {
+ for (let i = 0; i < 2; i++) {
+ info(`test click with button ${i} modifier ${modifier}`);
+ let clickEventData = { button: i };
+ clickEventData[modifier] = true;
+ await clickBrowserAction(extension, window, clickEventData);
+ let details = await extension.awaitMessage("onClick");
+
+ is(details.button, i, `Correct button in ${area} click`);
+ assertSingleModifier(details, modifier, area);
+ }
+
+ info(`test keypress with modifier ${modifier}`);
+ let keypressEventData = {};
+ keypressEventData[modifier] = true;
+ await triggerBrowserActionWithKeyboard(extension, " ", keypressEventData);
+ let details = await extension.awaitMessage("onClick");
+
+ is(details.button, 0, `Key command emulates left click`);
+ assertSingleModifier(details, modifier, area);
+ }
+ }
+
+ if (manifest_version >= 3 || persistent === false) {
+ // The background event page is expected to have been
+ // spawned again to handle the action onClicked event.
+ await extension.awaitMessage("ready");
+ }
+
+ await extension.unload();
+}
+
+async function test_clickData_reset({ manifest_version }) {
+ const action = manifest_version < 3 ? "browser_action" : "action";
+ const browser_action_command =
+ manifest_version < 3 ? "_execute_browser_action" : "_execute_action";
+ const browser_action_key = "j";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ [action]: {
+ default_area: "navbar",
+ },
+ page_action: {},
+ commands: {
+ [browser_action_command]: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ },
+
+ async background() {
+ function onBrowserActionClicked(tab, info) {
+ browser.test.sendMessage("onClick", info);
+ }
+
+ function onPageActionClicked(tab, info) {
+ browser.test.sendMessage("open-popup");
+ }
+
+ const { manifest_version } = browser.runtime.getManifest();
+ const apiNS = manifest_version >= 3 ? "action" : "browserAction";
+
+ browser[apiNS].onClicked.addListener(onBrowserActionClicked);
+
+ // pageAction should only be available in MV2 extensions.
+ if (manifest_version < 3) {
+ browser.pageAction.onClicked.addListener(onPageActionClicked);
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ }
+
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ // Pollute the state of the browserAction's lastClickInfo
+ async function clickBrowserActionWithModifiers() {
+ await clickBrowserAction(extension, window, { button: 1, shiftKey: true });
+ let info = await extension.awaitMessage("onClick");
+ is(info.button, 1, "Got expected ClickData button details");
+ is(info.modifiers[0], "Shift", "Got expected ClickData modifiers details");
+ }
+
+ function assertInfoReset(info) {
+ is(info.button, 0, `ClickData button reset properly`);
+ is(info.modifiers.length, 0, `ClickData modifiers reset properly`);
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (manifest_version >= 3) {
+ // NOTE: even in MV3 where the API namespace is technically "action",
+ // the event listeners will be persisted into the startup data
+ // with "browserAction" as the module, because that is the name
+ // of the module shared by both MV2 browserAction and MV3 action APIs.
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: false,
+ });
+ await extension.terminateBackground();
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: true,
+ });
+ }
+
+ await clickBrowserActionWithModifiers();
+
+ if (manifest_version >= 3) {
+ // The background event page is expected to have been
+ // spawned again to handle the action onClicked event.
+ await extension.awaitMessage("ready");
+ } else {
+ extension.onMessage("open-popup", () => {
+ EventUtils.synthesizeKey(browser_action_key, {
+ altKey: true,
+ shiftKey: true,
+ });
+ });
+
+ // pageAction should only be available in MV2 extensions.
+ await clickPageAction(extension);
+
+ // NOTE: the pageAction event listener then triggers browserAction.onClicked
+ assertInfoReset(await extension.awaitMessage("onClick"));
+ }
+
+ await clickBrowserActionWithModifiers();
+
+ await triggerBrowserActionWithKeyboard(extension, " ");
+ assertInfoReset(await extension.awaitMessage("onClick"));
+
+ await clickBrowserActionWithModifiers();
+
+ await triggerBrowserActionWithKeyboard(extension, " ");
+ assertInfoReset(await extension.awaitMessage("onClick"));
+
+ await extension.unload();
+}
+
+add_task(function test_clickData_MV2() {
+ return test_clickData({ manifest_version: 2 });
+});
+
+add_task(async function test_clickData_MV2_eventpage() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+ await test_clickData({
+ manifest_version: 2,
+ persistent: false,
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_clickData_MV3() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+ await test_clickData({ manifest_version: 3 });
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(function test_clickData_reset_MV2() {
+ return test_clickData_reset({ manifest_version: 2 });
+});
+
+add_task(async function test_clickData_reset_MV3() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+ await test_clickData_reset({ manifest_version: 3 });
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
new file mode 100644
index 0000000000..c1f8184c78
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -0,0 +1,1194 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function runTests(options) {
+ async function background(getTests) {
+ let manifest = browser.runtime.getManifest();
+ let { manifest_version } = manifest;
+ const action = manifest_version < 3 ? "browserAction" : "action";
+ async function checkExtAPIDetails(expecting, details) {
+ let title = await browser[action].getTitle(details);
+ browser.test.assertEq(
+ expecting.title,
+ title,
+ "expected value from getTitle"
+ );
+
+ let popup = await browser[action].getPopup(details);
+ browser.test.assertEq(
+ expecting.popup,
+ popup,
+ "expected value from getPopup"
+ );
+
+ let badge = await browser[action].getBadgeText(details);
+ browser.test.assertEq(
+ expecting.badge,
+ badge,
+ "expected value from getBadge"
+ );
+
+ let badgeBackgroundColor = await browser[action].getBadgeBackgroundColor(
+ details
+ );
+ browser.test.assertEq(
+ String(expecting.badgeBackgroundColor),
+ String(badgeBackgroundColor),
+ "expected value from getBadgeBackgroundColor"
+ );
+
+ let badgeTextColor = await browser[action].getBadgeTextColor(details);
+ browser.test.assertEq(
+ String(expecting.badgeTextColor),
+ String(badgeTextColor),
+ "expected value from getBadgeTextColor"
+ );
+
+ let enabled = await browser[action].isEnabled(details);
+ browser.test.assertEq(
+ expecting.enabled,
+ enabled,
+ "expected value from isEnabled"
+ );
+ }
+
+ let tabs = [];
+ let windows = [];
+ let tests = getTests(tabs, windows);
+
+ {
+ let tabId = 0xdeadbeef;
+ let calls = [
+ () => browser[action].enable(tabId),
+ () => browser[action].disable(tabId),
+ () => browser[action].setTitle({ tabId, title: "foo" }),
+ () => browser[action].setIcon({ tabId, path: "foo.png" }),
+ () => browser[action].setPopup({ tabId, popup: "foo.html" }),
+ () => browser[action].setBadgeText({ tabId, text: "foo" }),
+ () =>
+ browser[action].setBadgeBackgroundColor({
+ tabId,
+ color: [0xff, 0, 0, 0xff],
+ }),
+ () =>
+ browser[action].setBadgeTextColor({
+ tabId,
+ color: [0, 0xff, 0xff, 0xff],
+ }),
+ ];
+
+ for (let call of calls) {
+ await browser.test.assertRejects(
+ new Promise(resolve => resolve(call())),
+ RegExp(`Invalid tab ID: ${tabId}`),
+ "Expected invalid tab ID error"
+ );
+ }
+ }
+
+ // Runs the next test in the `tests` array, checks the results,
+ // and passes control back to the outer test scope.
+ function nextTest() {
+ let test = tests.shift();
+
+ test(async (expectTab, expectWindow, expectGlobal, expectDefault) => {
+ expectGlobal = { ...expectDefault, ...expectGlobal };
+ expectWindow = { ...expectGlobal, ...expectWindow };
+ expectTab = { ...expectWindow, ...expectTab };
+
+ // Check that the API returns the expected values, and then
+ // run the next test.
+ let [{ windowId, id: tabId }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await checkExtAPIDetails(expectTab, { tabId });
+ await checkExtAPIDetails(expectWindow, { windowId });
+ await checkExtAPIDetails(expectGlobal, {});
+
+ // Check that the actual icon has the expected values, then
+ // run the next test.
+ browser.test.sendMessage("nextTest", expectTab, windowId, tests.length);
+ });
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg != "runNextTest") {
+ browser.test.fail("Expecting 'runNextTest' message");
+ }
+
+ nextTest();
+ });
+
+ let [{ id, windowId }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ tabs.push(id);
+ windows.push(windowId);
+ nextTest();
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: options.manifest,
+
+ files: options.files || {},
+
+ background: `(${background})(${options.getTests})`,
+ });
+
+ function serializeColor([r, g, b, a]) {
+ if (a === 255) {
+ return `rgb(${r}, ${g}, ${b})`;
+ }
+ return `rgba(${r}, ${g}, ${b}, ${a / 255})`;
+ }
+
+ let browserActionId;
+ async function checkWidgetDetails(details, windowId) {
+ let { document } = Services.wm.getOuterWindowWithId(windowId);
+ if (!browserActionId) {
+ browserActionId = `${makeWidgetId(extension.id)}-browser-action`;
+ }
+
+ let node = document.getElementById(browserActionId);
+ let button = node.firstElementChild;
+
+ ok(button, "button exists");
+
+ let title = details.title || options.manifest.name;
+
+ // NOTE: resorting to waitForCondition to prevent frequent
+ // intermittent failures due to multiple action API calls
+ // being queued.
+ if (getListStyleImage(button) !== details.icon) {
+ info(`wait for action icon url to be set to ${details.icon}`);
+ await TestUtils.waitForCondition(
+ () => getListStyleImage(button) === details.icon,
+ "Wait for the expected icon URL to be set"
+ );
+ }
+
+ // NOTE: resorting to waitForCondition to prevent frequent
+ // intermittent failures due to multiple action API calls
+ // being queued.
+ if (button.getAttribute("tooltiptext") !== title) {
+ info(`wait for action tooltiptext to be set to ${title}`);
+ await TestUtils.waitForCondition(
+ () => button.getAttribute("tooltiptext") === title,
+ "Wait for expected title to be set"
+ );
+ }
+
+ is(getListStyleImage(button), details.icon, "icon URL is correct");
+ is(button.getAttribute("tooltiptext"), title, "image title is correct");
+ is(button.getAttribute("label"), title, "image label is correct");
+ is(button.getAttribute("badge"), details.badge, "badge text is correct");
+ is(
+ button.getAttribute("disabled") == "true",
+ !details.enabled,
+ "disabled state is correct"
+ );
+
+ if (details.badge) {
+ let badge = button.badgeLabel;
+ let style = window.getComputedStyle(badge);
+ let expected = {
+ backgroundColor: serializeColor(details.badgeBackgroundColor),
+ color: serializeColor(details.badgeTextColor),
+ };
+ for (let [prop, value] of Object.entries(expected)) {
+ // NOTE: resorting to waitForCondition to prevent frequent
+ // intermittent failures due to multiple action API calls
+ // being queued.
+ if (style[prop] !== value) {
+ info(`wait for badge ${prop} to be set to ${value}`);
+ await TestUtils.waitForCondition(
+ () => window.getComputedStyle(badge)[prop] === value,
+ `Wait for expected badge ${prop} to be set`
+ );
+ }
+ }
+ }
+
+ // TODO: Popup URL.
+ }
+
+ let awaitFinish = new Promise(resolve => {
+ extension.onMessage(
+ "nextTest",
+ async (expecting, windowId, testsRemaining) => {
+ await promiseAnimationFrame();
+ await checkWidgetDetails(expecting, windowId);
+
+ if (testsRemaining) {
+ extension.sendMessage("runNextTest");
+ } else {
+ resolve();
+ }
+ }
+ );
+ });
+
+ await extension.startup();
+
+ await awaitFinish;
+
+ await extension.unload();
+}
+
+let tabSwitchTestData = {
+ files: {
+ "_locales/en/messages.json": {
+ popup: {
+ message: "default.html",
+ description: "Popup",
+ },
+
+ title: {
+ message: "Title",
+ description: "Title",
+ },
+ },
+
+ "default.png": imageBuffer,
+ "global.png": imageBuffer,
+ "1.png": imageBuffer,
+ "2.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let manifest = browser.runtime.getManifest();
+ let { manifest_version } = manifest;
+ const action = manifest_version < 3 ? "browserAction" : "action";
+
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ badge: "",
+ badgeBackgroundColor: [0xd9, 0, 0, 255],
+ badgeTextColor: [0xff, 0xff, 0xff, 0xff],
+ enabled: true,
+ },
+ { icon: browser.runtime.getURL("1.png") },
+ {
+ icon: browser.runtime.getURL("2.png"),
+ popup: browser.runtime.getURL("2.html"),
+ title: "Title 2",
+ badge: "2",
+ badgeBackgroundColor: [0xff, 0, 0, 0xff],
+ badgeTextColor: [0, 0xff, 0xff, 0xff],
+ enabled: false,
+ },
+ {
+ icon: browser.runtime.getURL("global.png"),
+ popup: browser.runtime.getURL("global.html"),
+ title: "Global Title",
+ badge: "g",
+ badgeBackgroundColor: [0, 0xff, 0, 0xff],
+ badgeTextColor: [0xff, 0, 0xff, 0xff],
+ enabled: false,
+ },
+ {
+ icon: browser.runtime.getURL("global.png"),
+ popup: browser.runtime.getURL("global.html"),
+ title: "Global Title",
+ badge: "g",
+ badgeBackgroundColor: [0, 0xff, 0, 0xff],
+ badgeTextColor: [0xff, 0, 0xff, 0xff],
+ },
+ ];
+
+ let promiseTabLoad = details => {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
+ if (tabId == details.id && changed.url == details.url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ };
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Change the icon in the current tab. Expect default properties excluding the icon."
+ );
+ browser[action].setIcon({ tabId: tabs[0], path: "1.png" });
+
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. Expect default properties.");
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?0",
+ });
+ tabs.push(tab.id);
+
+ browser.test.log("Await tab load.");
+ let promise = promiseTabLoad({ id: tabs[1], url: "about:blank?0" });
+ let { url } = await browser.tabs.get(tabs[1]);
+ if (url === "about:blank") {
+ await promise;
+ }
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change properties. Expect new properties.");
+ let tabId = tabs[1];
+ browser[action].setIcon({ tabId, path: "2.png" });
+ browser[action].setPopup({ tabId, popup: "2.html" });
+ browser[action].setTitle({ tabId, title: "Title 2" });
+ browser[action].setBadgeText({ tabId, text: "2" });
+ browser[action].setBadgeBackgroundColor({
+ tabId,
+ color: "#ff0000",
+ });
+ browser[action].setBadgeTextColor({ tabId, color: "#00ffff" });
+ browser[action].disable(tabId);
+
+ expect(details[2], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to the first tab. Expect previously set properties."
+ );
+ await browser.tabs.update(tabs[0], { active: true });
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Change global values, expect those changes reflected."
+ );
+ browser[action].setIcon({ path: "global.png" });
+ browser[action].setPopup({ popup: "global.html" });
+ browser[action].setTitle({ title: "Global Title" });
+ browser[action].setBadgeText({ text: "g" });
+ browser[action].setBadgeBackgroundColor({
+ color: [0, 0xff, 0, 0xff],
+ });
+ browser[action].setBadgeTextColor({
+ color: [0xff, 0, 0xff, 0xff],
+ });
+ browser[action].disable();
+
+ expect(details[1], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Re-enable globally. Expect enabled.");
+ browser[action].enable();
+
+ expect(details[1], null, details[4], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to tab 2. Expect former tab values, and new global values from previous steps."
+ );
+ await browser.tabs.update(tabs[1], { active: true });
+
+ expect(details[2], null, details[4], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Navigate to a new page. Expect tab-specific values to be cleared."
+ );
+
+ let promise = promiseTabLoad({ id: tabs[1], url: "about:blank?1" });
+ browser.tabs.update(tabs[1], { url: "about:blank?1" });
+ await promise;
+
+ expect(null, null, details[4], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Delete tab, switch back to tab 1. Expect previous results again."
+ );
+ await browser.tabs.remove(tabs[1]);
+ expect(details[1], null, details[4], details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. Expect new global properties.");
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?2",
+ });
+ tabs.push(tab.id);
+ expect(null, null, details[4], details[0]);
+ },
+ async expect => {
+ browser.test.log("Delete tab.");
+ await browser.tabs.remove(tabs[2]);
+ expect(details[1], null, details[4], details[0]);
+ },
+ ];
+ },
+};
+
+add_task(async function testTabSwitchContext() {
+ await runTests({
+ manifest: {
+ browser_action: {
+ default_icon: "default.png",
+ default_popup: "__MSG_popup__",
+ default_title: "Default __MSG_title__",
+ default_area: "navbar",
+ },
+
+ default_locale: "en",
+
+ permissions: ["tabs"],
+ },
+ ...tabSwitchTestData,
+ });
+});
+
+add_task(async function testTabSwitchActionContext() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+
+ await runTests({
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_icon: "default.png",
+ default_popup: "__MSG_popup__",
+ default_title: "Default __MSG_title__",
+ default_area: "navbar",
+ },
+ default_locale: "en",
+ permissions: ["tabs"],
+ },
+ ...tabSwitchTestData,
+ });
+});
+
+add_task(async function testDefaultTitle() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ browser_action: {
+ default_icon: "icon.png",
+ default_area: "navbar",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "icon.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let details = [
+ {
+ title: "Foo Extension",
+ popup: "",
+ badge: "",
+ badgeBackgroundColor: [0xd9, 0, 0, 255],
+ badgeTextColor: [0xff, 0xff, 0xff, 0xff],
+ icon: browser.runtime.getURL("icon.png"),
+ enabled: true,
+ },
+ { title: "Foo Title" },
+ { title: "Bar Title" },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state. Expect default properties.");
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change the tab title. Expect new title.");
+ browser.browserAction.setTitle({
+ tabId: tabs[0],
+ title: "Foo Title",
+ });
+
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change the global title. Expect same properties.");
+ browser.browserAction.setTitle({ title: "Bar Title" });
+
+ expect(details[1], null, details[2], details[0]);
+ },
+ async expect => {
+ browser.test.log("Clear the tab title. Expect new global title.");
+ browser.browserAction.setTitle({ tabId: tabs[0], title: null });
+
+ expect(null, null, details[2], details[0]);
+ },
+ async expect => {
+ browser.test.log("Clear the global title. Expect default title.");
+ browser.browserAction.setTitle({ title: null });
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.assertRejects(
+ browser.browserAction.setPopup({ popup: "about:addons" }),
+ /Access denied for URL about:addons/,
+ "unable to set popup to about:addons"
+ );
+
+ expect(null, null, null, details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testBadgeColorPersistence() {
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.onMessage.addListener((msg, arg) => {
+ browser.browserAction[msg](arg);
+ });
+ },
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ });
+ await extension.startup();
+
+ function getBadgeForWindow(win) {
+ const widget = getBrowserActionWidget(extension).forWindow(win).node;
+ return widget.firstElementChild.badgeLabel;
+ }
+
+ let badge = getBadgeForWindow(window);
+ const badgeChanged = new Promise(resolve => {
+ const observer = new MutationObserver(() => resolve());
+ observer.observe(badge, { attributes: true, attributeFilter: ["style"] });
+ });
+
+ extension.sendMessage("setBadgeText", { text: "hi" });
+ extension.sendMessage("setBadgeBackgroundColor", { color: [0, 255, 0, 255] });
+
+ await badgeChanged;
+
+ is(badge.textContent, "hi", "badge text is set in first window");
+ is(
+ badge.style.backgroundColor,
+ "rgb(0, 255, 0)",
+ "badge color is set in first window"
+ );
+
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow();
+ await windowOpenedPromise;
+
+ badge = getBadgeForWindow(win);
+ is(badge.textContent, "hi", "badge text is set in new window");
+ is(
+ badge.style.backgroundColor,
+ "rgb(0, 255, 0)",
+ "badge color is set in new window"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await extension.unload();
+});
+
+add_task(async function testPropertyRemoval() {
+ await runTests({
+ manifest: {
+ name: "Generated extension",
+ browser_action: {
+ default_icon: "default.png",
+ default_popup: "default.html",
+ default_title: "Default Title",
+ default_area: "navbar",
+ },
+ },
+
+ files: {
+ "default.png": imageBuffer,
+ "global.png": imageBuffer,
+ "global2.png": imageBuffer,
+ "window.png": imageBuffer,
+ "tab.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ badge: "",
+ badgeBackgroundColor: [0xd9, 0x00, 0x00, 0xff],
+ badgeTextColor: [0xff, 0xff, 0xff, 0xff],
+ enabled: true,
+ },
+ {
+ icon: browser.runtime.getURL("global.png"),
+ popup: browser.runtime.getURL("global.html"),
+ title: "global",
+ badge: "global",
+ badgeBackgroundColor: [0x11, 0x11, 0x11, 0xff],
+ badgeTextColor: [0x99, 0x99, 0x99, 0xff],
+ },
+ {
+ icon: browser.runtime.getURL("window.png"),
+ popup: browser.runtime.getURL("window.html"),
+ title: "window",
+ badge: "window",
+ badgeBackgroundColor: [0x22, 0x22, 0x22, 0xff],
+ badgeTextColor: [0x88, 0x88, 0x88, 0xff],
+ },
+ {
+ icon: browser.runtime.getURL("tab.png"),
+ popup: browser.runtime.getURL("tab.html"),
+ title: "tab",
+ badge: "tab",
+ badgeBackgroundColor: [0x33, 0x33, 0x33, 0xff],
+ badgeTextColor: [0x77, 0x77, 0x77, 0xff],
+ },
+ {
+ icon: defaultIcon,
+ popup: "",
+ title: "",
+ badge: "",
+ badgeBackgroundColor: [0x33, 0x33, 0x33, 0xff],
+ badgeTextColor: [0x77, 0x77, 0x77, 0xff],
+ },
+ {
+ icon: browser.runtime.getURL("global2.png"),
+ popup: browser.runtime.getURL("global2.html"),
+ title: "global2",
+ badge: "global2",
+ badgeBackgroundColor: [0x44, 0x44, 0x44, 0xff],
+ badgeTextColor: [0x66, 0x66, 0x66, 0xff],
+ },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set global values, expect the new values.");
+ browser.browserAction.setIcon({ path: "global.png" });
+ browser.browserAction.setPopup({ popup: "global.html" });
+ browser.browserAction.setTitle({ title: "global" });
+ browser.browserAction.setBadgeText({ text: "global" });
+ browser.browserAction.setBadgeBackgroundColor({ color: "#111" });
+ browser.browserAction.setBadgeTextColor({ color: "#999" });
+ expect(null, null, details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[0];
+ browser.browserAction.setIcon({ windowId, path: "window.png" });
+ browser.browserAction.setPopup({ windowId, popup: "window.html" });
+ browser.browserAction.setTitle({ windowId, title: "window" });
+ browser.browserAction.setBadgeText({ windowId, text: "window" });
+ browser.browserAction.setBadgeBackgroundColor({
+ windowId,
+ color: "#222",
+ });
+ browser.browserAction.setBadgeTextColor({ windowId, color: "#888" });
+ expect(null, details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set tab values, expect the new values.");
+ let tabId = tabs[0];
+ browser.browserAction.setIcon({ tabId, path: "tab.png" });
+ browser.browserAction.setPopup({ tabId, popup: "tab.html" });
+ browser.browserAction.setTitle({ tabId, title: "tab" });
+ browser.browserAction.setBadgeText({ tabId, text: "tab" });
+ browser.browserAction.setBadgeBackgroundColor({
+ tabId,
+ color: "#333",
+ });
+ browser.browserAction.setBadgeTextColor({ tabId, color: "#777" });
+ expect(details[3], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Set empty tab values, expect empty values except for colors."
+ );
+ let tabId = tabs[0];
+ browser.browserAction.setIcon({ tabId, path: "" });
+ browser.browserAction.setPopup({ tabId, popup: "" });
+ browser.browserAction.setTitle({ tabId, title: "" });
+ browser.browserAction.setBadgeText({ tabId, text: "" });
+ await browser.test.assertRejects(
+ browser.browserAction.setBadgeBackgroundColor({ tabId, color: "" }),
+ /^Invalid badge background color: ""$/,
+ "Expected invalid badge background color error"
+ );
+ await browser.test.assertRejects(
+ browser.browserAction.setBadgeTextColor({ tabId, color: "" }),
+ /^Invalid badge text color: ""$/,
+ "Expected invalid badge text color error"
+ );
+ expect(details[4], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove tab values, expect window values.");
+ let tabId = tabs[0];
+ browser.browserAction.setIcon({ tabId, path: null });
+ browser.browserAction.setPopup({ tabId, popup: null });
+ browser.browserAction.setTitle({ tabId, title: null });
+ browser.browserAction.setBadgeText({ tabId, text: null });
+ browser.browserAction.setBadgeBackgroundColor({ tabId, color: null });
+ browser.browserAction.setBadgeTextColor({ tabId, color: null });
+ expect(null, details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove window values, expect global values.");
+ let windowId = windows[0];
+ browser.browserAction.setIcon({ windowId, path: null });
+ browser.browserAction.setPopup({ windowId, popup: null });
+ browser.browserAction.setTitle({ windowId, title: null });
+ browser.browserAction.setBadgeText({ windowId, text: null });
+ browser.browserAction.setBadgeBackgroundColor({
+ windowId,
+ color: null,
+ });
+ browser.browserAction.setBadgeTextColor({ windowId, color: null });
+ expect(null, null, details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Change global values, expect the new values.");
+ browser.browserAction.setIcon({ path: "global2.png" });
+ browser.browserAction.setPopup({ popup: "global2.html" });
+ browser.browserAction.setTitle({ title: "global2" });
+ browser.browserAction.setBadgeText({ text: "global2" });
+ browser.browserAction.setBadgeBackgroundColor({ color: "#444" });
+ browser.browserAction.setBadgeTextColor({ color: "#666" });
+ expect(null, null, details[5], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove global values, expect defaults.");
+ browser.browserAction.setIcon({ path: null });
+ browser.browserAction.setPopup({ popup: null });
+ browser.browserAction.setBadgeText({ text: null });
+ browser.browserAction.setTitle({ title: null });
+ browser.browserAction.setBadgeBackgroundColor({ color: null });
+ browser.browserAction.setBadgeTextColor({ color: null });
+ expect(null, null, null, details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testMultipleWindows() {
+ await runTests({
+ manifest: {
+ browser_action: {
+ default_icon: "default.png",
+ default_popup: "default.html",
+ default_title: "Default Title",
+ default_area: "navbar",
+ },
+ },
+
+ files: {
+ "default.png": imageBuffer,
+ "window1.png": imageBuffer,
+ "window2.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ badge: "",
+ badgeBackgroundColor: [0xd9, 0x00, 0x00, 0xff],
+ badgeTextColor: [0xff, 0xff, 0xff, 0xff],
+ enabled: true,
+ },
+ {
+ icon: browser.runtime.getURL("window1.png"),
+ popup: browser.runtime.getURL("window1.html"),
+ title: "window1",
+ badge: "w1",
+ badgeBackgroundColor: [0x11, 0x11, 0x11, 0xff],
+ badgeTextColor: [0x99, 0x99, 0x99, 0xff],
+ },
+ {
+ icon: browser.runtime.getURL("window2.png"),
+ popup: browser.runtime.getURL("window2.html"),
+ title: "window2",
+ badge: "w2",
+ badgeBackgroundColor: [0x22, 0x22, 0x22, 0xff],
+ badgeTextColor: [0x88, 0x88, 0x88, 0xff],
+ },
+ { title: "tab" },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[0];
+ browser.browserAction.setIcon({ windowId, path: "window1.png" });
+ browser.browserAction.setPopup({ windowId, popup: "window1.html" });
+ browser.browserAction.setTitle({ windowId, title: "window1" });
+ browser.browserAction.setBadgeText({ windowId, text: "w1" });
+ browser.browserAction.setBadgeBackgroundColor({
+ windowId,
+ color: "#111",
+ });
+ browser.browserAction.setBadgeTextColor({ windowId, color: "#999" });
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab, expect window values.");
+ let tab = await browser.tabs.create({ active: true });
+ tabs.push(tab.id);
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a tab title, expect it.");
+ await browser.browserAction.setTitle({
+ tabId: tabs[1],
+ title: "tab",
+ });
+ expect(details[3], details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Open a new window, expect default values.");
+ let { id } = await browser.windows.create();
+ windows.push(id);
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[1];
+ browser.browserAction.setIcon({ windowId, path: "window2.png" });
+ browser.browserAction.setPopup({ windowId, popup: "window2.html" });
+ browser.browserAction.setTitle({ windowId, title: "window2" });
+ browser.browserAction.setBadgeText({ windowId, text: "w2" });
+ browser.browserAction.setBadgeBackgroundColor({
+ windowId,
+ color: "#222",
+ });
+ browser.browserAction.setBadgeTextColor({ windowId, color: "#888" });
+ expect(null, details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Move tab from old window to the new one. Tab-specific data" +
+ " is preserved but inheritance is from the new window"
+ );
+ await browser.tabs.move(tabs[1], { windowId: windows[1], index: -1 });
+ await browser.tabs.update(tabs[1], { active: true });
+ expect(details[3], details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Close the initial tab of the new window.");
+ let [{ id }] = await browser.tabs.query({
+ windowId: windows[1],
+ index: 0,
+ });
+ await browser.tabs.remove(id);
+ expect(details[3], details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Move the previous tab to a 3rd window, the 2nd one will close."
+ );
+ await browser.windows.create({ tabId: tabs[1] });
+ expect(details[3], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Close the tab, go back to the 1st window.");
+ await browser.tabs.remove(tabs[1]);
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Assert failures for bad parameters. Expect no change"
+ );
+
+ let calls = {
+ setIcon: { path: "default.png" },
+ setPopup: { popup: "default.html" },
+ setTitle: { title: "Default Title" },
+ setBadgeText: { text: "" },
+ setBadgeBackgroundColor: { color: [0xd9, 0x00, 0x00, 0xff] },
+ setBadgeTextColor: { color: [0xff, 0xff, 0xff, 0xff] },
+ getPopup: {},
+ getTitle: {},
+ getBadgeText: {},
+ getBadgeBackgroundColor: {},
+ };
+ for (let [method, arg] of Object.entries(calls)) {
+ browser.test.assertThrows(
+ () => browser.browserAction[method]({ ...arg, windowId: -3 }),
+ /-3 is too small \(must be at least -2\)/,
+ method + " with invalid windowId"
+ );
+ await browser.test.assertRejects(
+ browser.browserAction[method]({
+ ...arg,
+ tabId: tabs[0],
+ windowId: windows[0],
+ }),
+ /Only one of tabId and windowId can be specified/,
+ method + " with both tabId and windowId"
+ );
+ }
+
+ expect(null, details[1], null, details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testDefaultBadgeTextColor() {
+ await runTests({
+ manifest: {
+ browser_action: {
+ default_icon: "default.png",
+ default_popup: "default.html",
+ default_title: "Default Title",
+ default_area: "navbar",
+ },
+ },
+
+ files: {
+ "default.png": imageBuffer,
+ "window1.png": imageBuffer,
+ "window2.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ badge: "",
+ badgeBackgroundColor: [0xd9, 0x00, 0x00, 0xff],
+ badgeTextColor: [0xff, 0xff, 0xff, 0xff],
+ enabled: true,
+ },
+ {
+ badgeBackgroundColor: [0xff, 0xff, 0x00, 0xff],
+ badgeTextColor: [0x00, 0x00, 0x00, 0xff],
+ },
+ {
+ badgeBackgroundColor: [0x00, 0x00, 0xff, 0xff],
+ badgeTextColor: [0xff, 0xff, 0xff, 0xff],
+ },
+ {
+ badgeBackgroundColor: [0xff, 0xff, 0xff, 0x00],
+ badgeTextColor: [0x00, 0x00, 0x00, 0xff],
+ },
+ {
+ badgeBackgroundColor: [0x00, 0x00, 0xff, 0xff],
+ badgeTextColor: [0xff, 0x00, 0xff, 0xff],
+ },
+ { badgeBackgroundColor: [0xff, 0xff, 0xff, 0x00] },
+ {
+ badgeBackgroundColor: [0x00, 0x00, 0x00, 0x00],
+ badgeTextColor: [0xff, 0xff, 0xff, 0xff],
+ },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a global light bgcolor, expect black text.");
+ browser.browserAction.setBadgeBackgroundColor({ color: "#ff0" });
+ expect(null, null, details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Set a window-specific dark bgcolor, expect white text."
+ );
+ let windowId = windows[0];
+ browser.browserAction.setBadgeBackgroundColor({
+ windowId,
+ color: "#00f",
+ });
+ expect(null, details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Set a tab-specific transparent-white bgcolor, expect black text."
+ );
+ let tabId = tabs[0];
+ browser.browserAction.setBadgeBackgroundColor({
+ tabId,
+ color: "#fff0",
+ });
+ expect(details[3], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Set a window-specific text color, expect it in the tab."
+ );
+ let windowId = windows[0];
+ browser.browserAction.setBadgeTextColor({ windowId, color: "#f0f" });
+ expect(details[5], details[4], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Remove the window-specific text color, expect black again."
+ );
+ let windowId = windows[0];
+ browser.browserAction.setBadgeTextColor({ windowId, color: null });
+ expect(details[3], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Set a tab-specific transparent-black bgcolor, expect white text."
+ );
+ let tabId = tabs[0];
+ browser.browserAction.setBadgeBackgroundColor({
+ tabId,
+ color: "#0000",
+ });
+ expect(details[6], details[2], details[1], details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testNavigationClearsData() {
+ let url = "http://example.com/";
+ let default_title = "Default title";
+ let tab_title = "Tab title";
+
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+ let extension,
+ tabs = [];
+ async function addTab(...args) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, ...args);
+ tabs.push(tab);
+ return tab;
+ }
+ async function sendMessage(method, param, expect, msg) {
+ extension.sendMessage({ method, param, expect, msg });
+ await extension.awaitMessage("done");
+ }
+ async function expectTabSpecificData(tab, msg) {
+ let tabId = tabTracker.getId(tab);
+ await sendMessage("getBadgeText", { tabId }, "foo", msg);
+ await sendMessage("getTitle", { tabId }, tab_title, msg);
+ }
+ async function expectDefaultData(tab, msg) {
+ let tabId = tabTracker.getId(tab);
+ await sendMessage("getBadgeText", { tabId }, "", msg);
+ await sendMessage("getTitle", { tabId }, default_title, msg);
+ }
+ async function setTabSpecificData(tab) {
+ let tabId = tabTracker.getId(tab);
+ await expectDefaultData(
+ tab,
+ "Expect default data before setting tab-specific data."
+ );
+ await sendMessage("setBadgeText", { tabId, text: "foo" });
+ await sendMessage("setTitle", { tabId, title: tab_title });
+ await expectTabSpecificData(
+ tab,
+ "Expect tab-specific data after setting it."
+ );
+ }
+
+ info("Load a tab before installing the extension");
+ let tab1 = await addTab(url, true, true);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: { default_title },
+ },
+ background: function () {
+ browser.test.onMessage.addListener(
+ async ({ method, param, expect, msg }) => {
+ let result = await browser.browserAction[method](param);
+ if (expect !== undefined) {
+ browser.test.assertEq(expect, result, msg);
+ }
+ browser.test.sendMessage("done");
+ }
+ );
+ },
+ });
+ await extension.startup();
+
+ info("Set tab-specific data to the existing tab.");
+ await setTabSpecificData(tab1);
+
+ info("Add a hash. Does not cause navigation.");
+ await navigateTab(tab1, url + "#hash");
+ await expectTabSpecificData(
+ tab1,
+ "Adding a hash does not clear tab-specific data"
+ );
+
+ info("Remove the hash. Causes navigation.");
+ await navigateTab(tab1, url);
+ await expectDefaultData(tab1, "Removing hash clears tab-specific data");
+
+ info("Open a new tab, set tab-specific data to it.");
+ let tab2 = await addTab("about:newtab", false, false);
+ await setTabSpecificData(tab2);
+
+ info("Load a page in that tab.");
+ await navigateTab(tab2, url);
+ await expectDefaultData(tab2, "Loading a page clears tab-specific data.");
+
+ info("Set tab-specific data.");
+ await setTabSpecificData(tab2);
+
+ info("Push history state. Does not cause navigation.");
+ await historyPushState(tab2, url + "/path");
+ await expectTabSpecificData(
+ tab2,
+ "history.pushState() does not clear tab-specific data"
+ );
+
+ info("Navigate when the tab is not selected");
+ gBrowser.selectedTab = tab1;
+ await navigateTab(tab2, url);
+ await expectDefaultData(
+ tab2,
+ "Navigating clears tab-specific data, even when not selected."
+ );
+
+ for (let tab of tabs) {
+ await BrowserTestUtils.removeTab(tab);
+ }
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
new file mode 100644
index 0000000000..9544ed43a4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
@@ -0,0 +1,880 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "ABUSE_REPORT_ENABLED",
+ "extensions.abuseReport.enabled",
+ false
+);
+
+let extData = {
+ manifest: {
+ permissions: ["contextMenus"],
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "popup.html": `
+
+
+
+
+
+ A Test Popup
+
+
+ `,
+ },
+
+ background: function () {
+ browser.contextMenus.create({
+ id: "clickme-page",
+ title: "Click me!",
+ contexts: ["all"],
+ });
+ },
+};
+
+let contextMenuItems = {
+ "context-sep-navigation": "hidden",
+ "context-viewsource": "",
+ "inspect-separator": "hidden",
+ "context-inspect": "hidden",
+ "context-inspect-a11y": "hidden",
+ "context-bookmarkpage": "hidden",
+};
+if (AppConstants.platform == "macosx") {
+ contextMenuItems["context-back"] = "hidden";
+ contextMenuItems["context-forward"] = "hidden";
+ contextMenuItems["context-reload"] = "hidden";
+ contextMenuItems["context-stop"] = "hidden";
+} else {
+ contextMenuItems["context-navigation"] = "hidden";
+}
+
+const TOOLBAR_CONTEXT_MENU = "toolbar-context-menu";
+const UNIFIED_CONTEXT_MENU = "unified-extensions-context-menu";
+
+loadTestSubscript("head_unified_extensions.js");
+
+add_setup(async function test_setup() {
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar");
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("home-button")
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "extensions.abuseReport.amoFormURL",
+ "https://example.org/%LOCALE%/%APP%/feedback/addon/%addonID%/",
+ ],
+ ],
+ });
+});
+
+async function browseraction_popup_contextmenu_helper() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+
+ await clickBrowserAction(extension);
+
+ let contentAreaContextMenu = await openContextMenuInPopup(extension);
+ let item = contentAreaContextMenu.getElementsByAttribute(
+ "label",
+ "Click me!"
+ );
+ is(item.length, 1, "contextMenu item for page was found");
+ await closeContextMenu(contentAreaContextMenu);
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+}
+
+async function browseraction_popup_contextmenu_hidden_items_helper() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+
+ await clickBrowserAction(extension);
+
+ let contentAreaContextMenu = await openContextMenuInPopup(extension, "#text");
+
+ let item, state;
+ for (const itemID in contextMenuItems) {
+ info(`Checking ${itemID}`);
+ item = contentAreaContextMenu.querySelector(`#${itemID}`);
+ state = contextMenuItems[itemID];
+
+ if (state !== "") {
+ ok(item[state], `${itemID} is ${state}`);
+
+ if (state !== "hidden") {
+ ok(!item.hidden, `Disabled ${itemID} is not hidden`);
+ }
+ } else {
+ ok(!item.hidden, `${itemID} is not hidden`);
+ ok(!item.disabled, `${itemID} is not disabled`);
+ }
+ }
+
+ await closeContextMenu(contentAreaContextMenu);
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+}
+
+async function browseraction_popup_image_contextmenu_helper() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+
+ await clickBrowserAction(extension);
+
+ let contentAreaContextMenu = await openContextMenuInPopup(
+ extension,
+ "#testimg"
+ );
+
+ let item = contentAreaContextMenu.querySelector("#context-copyimage");
+ ok(!item.hidden);
+ ok(!item.disabled);
+
+ await closeContextMenu(contentAreaContextMenu);
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+}
+
+function openContextMenu(menuId, targetId) {
+ info(`Open context menu ${menuId} at ${targetId}`);
+ return openChromeContextMenu(menuId, "#" + CSS.escape(targetId));
+}
+
+function waitForElementShown(element) {
+ let win = element.ownerGlobal;
+ let dwu = win.windowUtils;
+ return BrowserTestUtils.waitForCondition(() => {
+ info("Waiting for overflow button to have non-0 size");
+ let bounds = dwu.getBoundsWithoutFlushing(element);
+ return bounds.width > 0 && bounds.height > 0;
+ });
+}
+
+async function browseraction_contextmenu_manage_extension_helper() {
+ let id = "addon_id@example.com";
+ let buttonId = `${makeWidgetId(id)}-BAP`;
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ browser_action: {
+ default_area: "navbar",
+ },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "options.html": ``,
+ "options.js": `browser.test.sendMessage("options-loaded");`,
+ },
+ });
+
+ function checkVisibility(menu, visible) {
+ let removeExtension = menu.querySelector(
+ ".customize-context-removeExtension"
+ );
+ let manageExtension = menu.querySelector(
+ ".customize-context-manageExtension"
+ );
+ let reportExtension = menu.querySelector(
+ ".customize-context-reportExtension"
+ );
+ let separator = reportExtension.nextElementSibling;
+
+ info(`Check visibility: ${visible}`);
+ let expected = visible ? "visible" : "hidden";
+ is(
+ removeExtension.hidden,
+ !visible,
+ `Remove Extension should be ${expected}`
+ );
+ is(
+ manageExtension.hidden,
+ !visible,
+ `Manage Extension should be ${expected}`
+ );
+ is(
+ reportExtension.hidden,
+ !ABUSE_REPORT_ENABLED || !visible,
+ `Report Extension should be ${expected}`
+ );
+ is(
+ separator.hidden,
+ !visible,
+ `Separator after Manage Extension should be ${expected}`
+ );
+ }
+
+ async function testContextMenu(menuId, customizing) {
+ info(`Open browserAction context menu in ${menuId} on ${buttonId}`);
+ let menu = await openContextMenu(menuId, buttonId);
+ await checkVisibility(menu, true);
+
+ info(`Choosing 'Manage Extension' in ${menuId} should load options`);
+ let addonManagerPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+ let manageExtension = menu.querySelector(
+ ".customize-context-manageExtension"
+ );
+ await closeChromeContextMenu(menuId, manageExtension);
+ let managerWindow = (await addonManagerPromise).linkedBrowser.contentWindow;
+
+ // Check the UI to make sure that the correct view is loaded.
+ is(
+ managerWindow.gViewController.currentViewId,
+ `addons://detail/${encodeURIComponent(id)}`,
+ "Expected extension details view in about:addons"
+ );
+ // In HTML about:addons, the default view does not show the inline
+ // options browser, so we should not receive an "options-loaded" event.
+ // (if we do, the test will fail due to the unexpected message).
+
+ info(
+ `Remove the opened tab, and await customize mode to be restored if necessary`
+ );
+ let tab = gBrowser.selectedTab;
+ is(tab.linkedBrowser.currentURI.spec, "about:addons");
+ if (customizing) {
+ let customizationReady = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gBrowser.removeTab(tab);
+ await customizationReady;
+ } else {
+ gBrowser.removeTab(tab);
+ }
+
+ return menu;
+ }
+
+ async function main(customizing) {
+ if (customizing) {
+ info("Enter customize mode");
+ let customizationReady = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gCustomizeMode.enter();
+ await customizationReady;
+ }
+
+ info("Test toolbar context menu in browserAction");
+ let toolbarCtxMenu = await testContextMenu(
+ TOOLBAR_CONTEXT_MENU,
+ customizing
+ );
+
+ info("Check toolbar context menu in another button");
+ let otherButtonId = "home-button";
+ await openContextMenu(TOOLBAR_CONTEXT_MENU, otherButtonId);
+ checkVisibility(toolbarCtxMenu, false);
+ toolbarCtxMenu.hidePopup();
+
+ info("Check toolbar context menu without triggerNode");
+ toolbarCtxMenu.openPopup();
+ checkVisibility(toolbarCtxMenu, false);
+ toolbarCtxMenu.hidePopup();
+
+ CustomizableUI.addWidgetToArea(
+ otherButtonId,
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+
+ info("Wait until the overflow menu is ready");
+ let overflowButton = document.getElementById("nav-bar-overflow-button");
+ let icon = overflowButton.icon;
+ await waitForElementShown(icon);
+
+ if (!customizing) {
+ info("Open overflow menu");
+ let menu = document.getElementById("widget-overflow");
+ let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ overflowButton.click();
+ await shown;
+ }
+
+ info("Check overflow menu context menu in another button");
+ let overflowMenuCtxMenu = await openContextMenu(
+ "customizationPanelItemContextMenu",
+ otherButtonId
+ );
+ checkVisibility(overflowMenuCtxMenu, false);
+ overflowMenuCtxMenu.hidePopup();
+
+ info("Put other button action back in nav-bar");
+ CustomizableUI.addWidgetToArea(otherButtonId, CustomizableUI.AREA_NAVBAR);
+
+ if (customizing) {
+ info("Exit customize mode");
+ let afterCustomization = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "aftercustomization"
+ );
+ gCustomizeMode.exit();
+ await afterCustomization;
+ }
+ }
+
+ await extension.startup();
+
+ info(
+ "Add a dummy tab to prevent about:addons from being loaded in the initial about:blank tab"
+ );
+ let dummyTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com",
+ true,
+ true
+ );
+
+ info("Run tests in normal mode");
+ await main(false);
+
+ info("Run tests in customize mode");
+ await main(true);
+
+ info("Close the dummy tab and finish");
+ gBrowser.removeTab(dummyTab);
+ await extension.unload();
+}
+
+async function runTestContextMenu({ id, customizing, testContextMenu }) {
+ let widgetId = makeWidgetId(id);
+ let nodeId = `${widgetId}-browser-action`;
+ if (customizing) {
+ info("Enter customize mode");
+ let customizationReady = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gCustomizeMode.enter();
+ await customizationReady;
+ }
+
+ info("Test toolbar context menu in browserAction");
+ await testContextMenu(TOOLBAR_CONTEXT_MENU, customizing);
+
+ info("Pin the browserAction to the addons panel");
+ CustomizableUI.addWidgetToArea(nodeId, CustomizableUI.AREA_ADDONS);
+
+ if (!customizing) {
+ info("Open addons panel");
+ gUnifiedExtensions.togglePanel();
+ await BrowserTestUtils.waitForEvent(gUnifiedExtensions.panel, "popupshown");
+ info("Test browserAction in addons panel");
+ await testContextMenu(UNIFIED_CONTEXT_MENU, customizing);
+ } else {
+ todo(
+ false,
+ "The browserAction cannot be accessed from customize " +
+ "mode when in the addons panel."
+ );
+ }
+
+ info("Restore initial state");
+ CustomizableUI.addWidgetToArea(nodeId, CustomizableUI.AREA_NAVBAR);
+
+ if (customizing) {
+ info("Exit customize mode");
+ let afterCustomization = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "aftercustomization"
+ );
+ gCustomizeMode.exit();
+ await afterCustomization;
+ }
+}
+
+async function browseraction_contextmenu_remove_extension_helper() {
+ let id = "addon_id@example.com";
+ let name = "Awesome Add-on";
+ let buttonId = `${makeWidgetId(id)}-BAP`;
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name,
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ let brand = Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShorterName");
+ let { prompt } = Services;
+ let promptService = {
+ _response: 1,
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx: function (...args) {
+ promptService._resolveArgs(args);
+ return promptService._response;
+ },
+ confirmArgs() {
+ return new Promise(resolve => {
+ promptService._resolveArgs = resolve;
+ });
+ },
+ };
+ Services.prompt = promptService;
+ registerCleanupFunction(() => {
+ Services.prompt = prompt;
+ });
+
+ async function testContextMenu(menuId, customizing) {
+ info(`Open browserAction context menu in ${menuId}`);
+ let confirmArgs = promptService.confirmArgs();
+ let menu = await openContextMenu(menuId, buttonId);
+
+ info(`Choosing 'Remove Extension' in ${menuId} should show confirm dialog`);
+ let removeItemQuery =
+ menuId == UNIFIED_CONTEXT_MENU
+ ? ".unified-extensions-context-menu-remove-extension"
+ : ".customize-context-removeExtension";
+ let removeExtension = menu.querySelector(removeItemQuery);
+ await closeChromeContextMenu(menuId, removeExtension);
+ let args = await confirmArgs;
+ is(args[1], `Remove ${name}?`);
+ if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) {
+ is(args[2], `Remove ${name} from ${brand}?`);
+ }
+ is(args[4], "Remove");
+ return menu;
+ }
+
+ await extension.startup();
+
+ info("Run tests in normal mode");
+ await runTestContextMenu({
+ id,
+ customizing: false,
+ testContextMenu,
+ });
+
+ info("Run tests in customize mode");
+ await runTestContextMenu({
+ id,
+ customizing: true,
+ testContextMenu,
+ });
+
+ // We'll only get one of these because in customize mode, the browserAction
+ // is not accessible when in the addons panel.
+ todo(
+ false,
+ "Should record a second removal event when browserAction " +
+ "becomes available in customize mode."
+ );
+
+ let addon = await AddonManager.getAddonByID(id);
+ ok(addon, "Addon is still installed");
+
+ promptService._response = 0;
+ let uninstalled = new Promise(resolve => {
+ AddonManager.addAddonListener({
+ onUninstalled(addon) {
+ is(addon.id, id, "The expected add-on has been uninstalled");
+ AddonManager.removeAddonListener(this);
+ resolve();
+ },
+ });
+ });
+ await testContextMenu(TOOLBAR_CONTEXT_MENU, false);
+ await uninstalled;
+
+ addon = await AddonManager.getAddonByID(id);
+ ok(!addon, "Addon has been uninstalled");
+
+ await extension.unload();
+
+ // We've got a cleanup function registered to restore this, but on debug
+ // builds, it seems that sometimes the cleanup function won't run soon
+ // enough and we'll leak this window because of the fake prompt function
+ // staying alive on Services. We work around this by restoring prompt
+ // here within the test if we've gotten here without throwing.
+ Services.prompt = prompt;
+}
+
+// This test case verify reporting an extension from the browserAction
+// context menu (when the browserAction is in the toolbox and in the
+// overwflow menu, and repeat the test with and without the customize
+// mode enabled).
+async function browseraction_contextmenu_report_extension_helper() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.enabled", true]],
+ });
+
+ let id = "addon_id@example.com";
+ let name = "Bad Add-on";
+ let buttonId = `${makeWidgetId(id)}-browser-action`;
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name,
+ author: "Bad author",
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ async function testReportDialog(viaUnifiedContextMenu) {
+ const reportDialogWindow = await BrowserTestUtils.waitForCondition(
+ () => AbuseReporter.getOpenDialog(),
+ "Wait for the abuse report dialog to have been opened"
+ );
+
+ const reportDialogParams = reportDialogWindow.arguments[0].wrappedJSObject;
+ is(
+ reportDialogParams.report.addon.id,
+ id,
+ "Abuse report dialog has the expected addon id"
+ );
+ is(
+ reportDialogParams.report.reportEntryPoint,
+ viaUnifiedContextMenu ? "unified_context_menu" : "toolbar_context_menu",
+ "Abuse report dialog has the expected reportEntryPoint"
+ );
+
+ info("Wait the report dialog to complete rendering");
+ await reportDialogParams.promiseReportPanel;
+ info("Close the report dialog");
+ reportDialogWindow.close();
+ is(
+ await reportDialogParams.promiseReport,
+ undefined,
+ "Report resolved as user cancelled when the window is closed"
+ );
+ }
+
+ async function testContextMenu(menuId, customizing) {
+ info(`Open browserAction context menu in ${menuId}`);
+ let menu = await openContextMenu(menuId, buttonId);
+
+ info(`Choosing 'Report Extension' in ${menuId}`);
+
+ let usingUnifiedContextMenu = menuId == UNIFIED_CONTEXT_MENU;
+ let reportItemQuery = usingUnifiedContextMenu
+ ? ".unified-extensions-context-menu-report-extension"
+ : ".customize-context-reportExtension";
+ let reportExtension = menu.querySelector(reportItemQuery);
+
+ ok(!reportExtension.hidden, "Report extension should be visibile");
+
+ let aboutAddonsBrowser;
+
+ if (AbuseReporter.amoFormEnabled) {
+ const reportURL = Services.urlFormatter
+ .formatURLPref("extensions.abuseReport.amoFormURL")
+ .replace("%addonID%", id);
+
+ const promiseReportTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ reportURL,
+ /* waitForLoad */ false,
+ // Expect it to be the next tab opened
+ /* waitForAnyTab */ false
+ );
+ await closeChromeContextMenu(menuId, reportExtension);
+ const reportTab = await promiseReportTab;
+ // Remove the report tab and expect the selected tab
+ // to become the about:addons tab.
+ BrowserTestUtils.removeTab(reportTab);
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:blank",
+ "Expect about:addons tab to not have been opened (amoFormEnabled=true)"
+ );
+ } else {
+ // When running in customizing mode "about:addons" will load in a new tab,
+ // otherwise it will replace the existing blank tab.
+ const onceAboutAddonsTab = customizing
+ ? BrowserTestUtils.waitForNewTab(gBrowser, "about:addons")
+ : BrowserTestUtils.waitForCondition(() => {
+ return gBrowser.currentURI.spec === "about:addons";
+ }, "Wait an about:addons tab to be opened");
+ await closeChromeContextMenu(menuId, reportExtension);
+ await onceAboutAddonsTab;
+ const browser = gBrowser.selectedBrowser;
+ is(
+ browser.currentURI.spec,
+ "about:addons",
+ "Got about:addons tab selected (amoFormEnabled=false)"
+ );
+ // Do not wait for the about:addons tab to be loaded if its
+ // document is already readyState==complete.
+ // This prevents intermittent timeout failures while running
+ // this test in optimized builds.
+ if (browser.contentDocument?.readyState != "complete") {
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+ await testReportDialog(usingUnifiedContextMenu);
+ aboutAddonsBrowser = browser;
+ }
+
+ // Close the new about:addons tab when running in customize mode,
+ // or cancel the abuse report if the about:addons page has been
+ // loaded in the existing blank tab.
+ if (customizing) {
+ info("Closing the about:addons tab");
+ let customizationReady = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gBrowser.removeTab(gBrowser.selectedTab);
+ await customizationReady;
+ } else if (aboutAddonsBrowser) {
+ info("Navigate the about:addons tab to about:blank");
+ BrowserTestUtils.startLoadingURIString(aboutAddonsBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(aboutAddonsBrowser);
+ }
+
+ return menu;
+ }
+
+ await extension.startup();
+
+ info("Run tests in normal mode");
+ await runTestContextMenu({
+ id,
+ customizing: false,
+ testContextMenu,
+ });
+
+ info("Run tests in customize mode");
+ await runTestContextMenu({
+ id,
+ customizing: true,
+ testContextMenu,
+ });
+
+ await extension.unload();
+
+ // Opening the about:addons page will reuse an about:blank tab if it was
+ // a new tab that was never navigated, otherwise opens in a new tab when
+ // the test is triggering the report action (then we navigate it to
+ // about:blank). Here we cleanup all the blank tabs lets behind but one
+ // (otherwise the window running the test will be closed and the test
+ // would be failing with a timeout).
+ info("Cleanup about:blank tabs");
+ while (gBrowser.visibleTabs.length > 1) {
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:blank",
+ "Expect an about:blank tab"
+ );
+ const promiseRemovedTab = BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabClose"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await promiseRemovedTab;
+ }
+}
+
+/**
+ * Tests that built-in buttons see the Pin to Overflow and Remove items in
+ * the toolbar context menu and don't see the Pin to Toolbar item, since
+ * that's reserved for extension widgets.
+ *
+ * @returns {Promise}
+ */
+async function test_no_toolbar_pinning_on_builtin_helper() {
+ let menu = await openContextMenu(TOOLBAR_CONTEXT_MENU, "home-button");
+ info(`Pin to Overflow and Remove from Toolbar should be visible.`);
+ let pinToOverflow = menu.querySelector(".customize-context-moveToPanel");
+ let removeFromToolbar = menu.querySelector(
+ ".customize-context-removeFromToolbar"
+ );
+ Assert.ok(!pinToOverflow.hidden, "Pin to Overflow is visible.");
+ Assert.ok(!removeFromToolbar.hidden, "Remove from Toolbar is visible.");
+ info(`This button should have "Pin to Toolbar" hidden`);
+ let pinToToolbar = menu.querySelector(".customize-context-pinToToolbar");
+ Assert.ok(pinToToolbar.hidden, "Pin to Overflow is hidden.");
+ menu.hidePopup();
+}
+
+add_task(async function test_unified_extensions_ui() {
+ await browseraction_popup_contextmenu_helper();
+ await browseraction_popup_contextmenu_hidden_items_helper();
+ await browseraction_popup_image_contextmenu_helper();
+ await browseraction_contextmenu_manage_extension_helper();
+ await browseraction_contextmenu_remove_extension_helper();
+ await test_no_toolbar_pinning_on_builtin_helper();
+});
+
+add_task(async function test_report_amoFormEnabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amoFormEnabled", true]],
+ });
+ await browseraction_contextmenu_report_extension_helper();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_report_amoFormDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amoFormEnabled", false]],
+ });
+ await browseraction_contextmenu_report_extension_helper();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that if Unified Extensions is enabled, that browser actions can
+ * be unpinned from the toolbar to the addons panel and back again, via
+ * a context menu item.
+ */
+add_task(async function test_unified_extensions_toolbar_pinning() {
+ let id = "addon_id@example.com";
+ let nodeId = `${makeWidgetId(id)}-browser-action`;
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ Assert.equal(
+ CustomizableUI.getPlacementOfWidget(nodeId).area,
+ CustomizableUI.AREA_NAVBAR,
+ "Should start placed in the nav-bar."
+ );
+
+ let menu = await openContextMenu(TOOLBAR_CONTEXT_MENU, nodeId);
+
+ info(`Pin to Overflow and Remove from Toolbar should be hidden.`);
+ let pinToOverflow = menu.querySelector(".customize-context-moveToPanel");
+ let removeFromToolbar = menu.querySelector(
+ ".customize-context-removeFromToolbar"
+ );
+ Assert.ok(pinToOverflow.hidden, "Pin to Overflow is hidden.");
+ Assert.ok(removeFromToolbar.hidden, "Remove from Toolbar is hidden.");
+
+ info(
+ `This button should have "Pin to Toolbar" visible and checked by default.`
+ );
+ let pinToToolbar = menu.querySelector(".customize-context-pinToToolbar");
+ Assert.ok(!pinToToolbar.hidden, "Pin to Toolbar is visible.");
+ Assert.equal(
+ pinToToolbar.getAttribute("checked"),
+ "true",
+ "Pin to Toolbar is checked."
+ );
+
+ info("Pinning addon to the addons panel.");
+ await closeChromeContextMenu(TOOLBAR_CONTEXT_MENU, pinToToolbar);
+
+ Assert.equal(
+ CustomizableUI.getPlacementOfWidget(nodeId).area,
+ CustomizableUI.AREA_ADDONS,
+ "Should have moved the button to the addons panel."
+ );
+
+ info("Opening addons panel");
+ gUnifiedExtensions.togglePanel();
+ await BrowserTestUtils.waitForEvent(gUnifiedExtensions.panel, "popupshown");
+ info("Testing unpinning in the addons panel");
+
+ menu = await openContextMenu(UNIFIED_CONTEXT_MENU, nodeId);
+
+ // The UNIFIED_CONTEXT_MENU has a different node for pinToToolbar, so
+ // we have to requery for it.
+ pinToToolbar = menu.querySelector(
+ ".unified-extensions-context-menu-pin-to-toolbar"
+ );
+
+ Assert.ok(!pinToToolbar.hidden, "Pin to Toolbar is visible.");
+ Assert.equal(
+ pinToToolbar.getAttribute("checked"),
+ "false",
+ "Pin to Toolbar is not checked."
+ );
+ await closeChromeContextMenu(UNIFIED_CONTEXT_MENU, pinToToolbar);
+
+ Assert.equal(
+ CustomizableUI.getPlacementOfWidget(nodeId).area,
+ CustomizableUI.AREA_NAVBAR,
+ "Should have moved the button back to the nav-bar."
+ );
+
+ await extension.unload();
+});
+
+/**
+ * Tests that there's no Pin to Toolbar option for unified-extensions-item's
+ * in the add-ons panel, since these do not represent browser action buttons.
+ */
+add_task(async function test_unified_extensions_item_no_pinning() {
+ let id = "addon_id@example.com";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ info("Opening addons panel");
+ let panel = gUnifiedExtensions.panel;
+ await openExtensionsPanel();
+
+ let items = panel.querySelectorAll("unified-extensions-item");
+ Assert.ok(
+ !!items.length,
+ "There should be at least one unified-extensions-item."
+ );
+
+ let menu = await openChromeContextMenu(
+ UNIFIED_CONTEXT_MENU,
+ `unified-extensions-item[extension-id='${id}']`
+ );
+ let pinToToolbar = menu.querySelector(
+ ".unified-extensions-context-menu-pin-to-toolbar"
+ );
+ Assert.ok(pinToToolbar.hidden, "Pin to Toolbar is hidden.");
+ menu.hidePopup();
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js b/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js
new file mode 100644
index 0000000000..b4581bcd0e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js
@@ -0,0 +1,101 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testDisabled() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+
+ background: function () {
+ let clicked = false;
+
+ browser.browserAction.onClicked.addListener(() => {
+ browser.test.log("Got click event");
+ clicked = true;
+ });
+
+ browser.test.onMessage.addListener((msg, expectClick) => {
+ if (msg == "enable") {
+ browser.test.log("enable browserAction");
+ browser.browserAction.enable();
+ } else if (msg == "disable") {
+ browser.test.log("disable browserAction");
+ browser.browserAction.disable();
+ } else if (msg == "check-clicked") {
+ browser.test.assertEq(expectClick, clicked, "got click event?");
+ clicked = false;
+ } else {
+ browser.test.fail("Unexpected message");
+ }
+
+ browser.test.sendMessage("next-test");
+ });
+
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await clickBrowserAction(extension);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ extension.sendMessage("check-clicked", true);
+ await extension.awaitMessage("next-test");
+
+ extension.sendMessage("disable");
+ await extension.awaitMessage("next-test");
+
+ await clickBrowserAction(extension);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ extension.sendMessage("check-clicked", false);
+ await extension.awaitMessage("next-test");
+
+ // We intentionally turn off this a11y check, because the following click
+ // is targeting a disabled control to confirm the click event won't come through.
+ // It is not meant to be interactive and is not expected to be accessible:
+ AccessibilityUtils.setEnv({
+ mustBeEnabled: false,
+ });
+ await clickBrowserAction(extension, window, { button: 1 });
+ await new Promise(resolve => setTimeout(resolve, 0));
+ AccessibilityUtils.resetEnv();
+
+ extension.sendMessage("check-clicked", false);
+ await extension.awaitMessage("next-test");
+
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
+
+ // We intentionally turn off this a11y check, because the following click
+ // is targeting a disabled control to confirm the click event won't come through.
+ // It is not meant to be interactive and is not expected to be accessible:
+ AccessibilityUtils.setEnv({
+ mustBeEnabled: false,
+ });
+ await clickBrowserAction(extension, window, { button: 1 });
+ await new Promise(resolve => setTimeout(resolve, 0));
+ AccessibilityUtils.resetEnv();
+
+ extension.sendMessage("check-clicked", false);
+ await extension.awaitMessage("next-test");
+
+ CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_NAVBAR);
+
+ extension.sendMessage("enable");
+ await extension.awaitMessage("next-test");
+
+ await clickBrowserAction(extension);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ extension.sendMessage("check-clicked", true);
+ await extension.awaitMessage("next-test");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_experiment.js b/browser/components/extensions/test/browser/browser_ext_browserAction_experiment.js
new file mode 100644
index 0000000000..d33553146e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_experiment.js
@@ -0,0 +1,159 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let fooExperimentAPIs = {
+ foo: {
+ schema: "schema.json",
+ parent: {
+ scopes: ["addon_parent"],
+ script: "parent.js",
+ paths: [["experiments", "foo", "parent"]],
+ },
+ child: {
+ scopes: ["addon_child"],
+ script: "child.js",
+ paths: [["experiments", "foo", "child"]],
+ },
+ },
+};
+
+let fooExperimentFiles = {
+ "schema.json": JSON.stringify([
+ {
+ namespace: "experiments.foo",
+ types: [
+ {
+ id: "Meh",
+ type: "object",
+ properties: {},
+ },
+ ],
+ functions: [
+ {
+ name: "parent",
+ type: "function",
+ async: true,
+ parameters: [],
+ },
+ {
+ name: "child",
+ type: "function",
+ parameters: [],
+ returns: { type: "string" },
+ },
+ ],
+ },
+ ]),
+
+ /* globals ExtensionAPI */
+ "parent.js": () => {
+ this.foo = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ experiments: {
+ foo: {
+ parent() {
+ return Promise.resolve("parent");
+ },
+ },
+ },
+ };
+ }
+ };
+ },
+
+ "child.js": () => {
+ this.foo = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ experiments: {
+ foo: {
+ child() {
+ return "child";
+ },
+ },
+ },
+ };
+ }
+ };
+ },
+};
+
+async function testFooExperiment() {
+ browser.test.assertEq(
+ "object",
+ typeof browser.experiments,
+ "typeof browser.experiments"
+ );
+
+ browser.test.assertEq(
+ "object",
+ typeof browser.experiments.foo,
+ "typeof browser.experiments.foo"
+ );
+
+ browser.test.assertEq(
+ "function",
+ typeof browser.experiments.foo.child,
+ "typeof browser.experiments.foo.child"
+ );
+
+ browser.test.assertEq(
+ "function",
+ typeof browser.experiments.foo.parent,
+ "typeof browser.experiments.foo.parent"
+ );
+
+ browser.test.assertEq(
+ "child",
+ browser.experiments.foo.child(),
+ "foo.child()"
+ );
+
+ browser.test.assertEq(
+ "parent",
+ await browser.experiments.foo.parent(),
+ "await foo.parent()"
+ );
+}
+
+add_task(async function test_browseraction_with_experiment() {
+ async function background() {
+ await new Promise(resolve =>
+ browser.browserAction.onClicked.addListener(resolve)
+ );
+ browser.test.log("Got browserAction.onClicked");
+
+ await testFooExperiment();
+
+ browser.test.notifyPass("background-browserAction-experiments.foo");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+
+ experiment_apis: fooExperimentAPIs,
+ },
+
+ background: `
+ ${testFooExperiment}
+ (${background})();
+ `,
+
+ files: fooExperimentFiles,
+ });
+
+ await extension.startup();
+
+ await clickBrowserAction(extension, window);
+
+ await extension.awaitFinish("background-browserAction-experiments.foo");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_getUserSettings.js b/browser/components/extensions/test/browser/browser_ext_browserAction_getUserSettings.js
new file mode 100644
index 0000000000..97ff709788
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_getUserSettings.js
@@ -0,0 +1,244 @@
+"use strict";
+
+async function background(test) {
+ let resolvers = {};
+ let tests = {};
+
+ browser.test.onMessage.addListener(id => {
+ let resolver = resolvers[id];
+ browser.test.assertTrue(resolver, `Found resolver for ${id}`);
+ browser.test.assertTrue(resolver.resolve, `${id} was not resolved yet`);
+ resolver.resolve(id);
+ resolver.resolve = null; // resolve can be used only once.
+ });
+
+ async function pinToToolbar(shouldPinToToolbar) {
+ let identifier = `${
+ shouldPinToToolbar ? "pin-to-toolbar" : "unpin-from-toolbar"
+ }-${Object.keys(resolvers).length}`;
+ resolvers[identifier] = {};
+ resolvers[identifier].promise = new Promise(
+ _resolve => (resolvers[identifier].resolve = _resolve)
+ );
+ browser.test.sendMessage("pinToToolbar", {
+ identifier,
+ shouldPinToToolbar,
+ });
+ await resolvers[identifier].promise;
+ }
+
+ let { manifest_version } = await browser.runtime.getManifest();
+ let action = browser.browserAction;
+
+ if (manifest_version === 3) {
+ action = browser.action;
+ }
+
+ tests.getUserSettings = async function () {
+ let userSettings = await action.getUserSettings();
+
+ await pinToToolbar(true);
+ userSettings = await action.getUserSettings();
+ browser.test.assertTrue(
+ userSettings.isOnToolbar,
+ "isOnToolbar should be true after pinning to toolbar"
+ );
+
+ await pinToToolbar(false);
+ userSettings = await action.getUserSettings();
+ browser.test.assertFalse(
+ userSettings.isOnToolbar,
+ "isOnToolbar should be false after unpinning"
+ );
+
+ await pinToToolbar(true);
+ userSettings = await action.getUserSettings();
+ browser.test.assertTrue(
+ userSettings.isOnToolbar,
+ "isOnToolbar should be true after repinning"
+ );
+
+ await pinToToolbar(false);
+ userSettings = await action.getUserSettings();
+ browser.test.assertFalse(
+ userSettings.isOnToolbar,
+ "isOnToolbar should be false after unpinning"
+ );
+
+ await browser.test.notifyPass("getUserSettings");
+ };
+
+ tests.default_area_getUserSettings = async function () {
+ let userSettings = await action.getUserSettings();
+
+ browser.test.assertTrue(
+ userSettings.isOnToolbar,
+ "isOnToolbar should be true when one of ['navbar', 'tabstrip', 'personaltoolbar'] default_area is specified in manifest.json"
+ );
+
+ await pinToToolbar(false);
+ userSettings = await action.getUserSettings();
+ browser.test.assertFalse(
+ userSettings.isOnToolbar,
+ "isOnToolbar should be false after unpinning"
+ );
+
+ await browser.test.notifyPass("getUserSettings");
+ };
+
+ tests.menupanel_default_area_getUserSettings = async function () {
+ let userSettings = await action.getUserSettings();
+
+ browser.test.assertFalse(
+ userSettings.isOnToolbar,
+ "isOnToolbar should be false when default_area is 'menupanel' in manifest.json"
+ );
+
+ await pinToToolbar(true);
+ userSettings = await action.getUserSettings();
+ browser.test.assertTrue(
+ userSettings.isOnToolbar,
+ "isOnToolbar should be true after pinning"
+ );
+
+ await browser.test.notifyPass("getUserSettings");
+ };
+
+ tests[test]();
+}
+
+function pinToToolbar(shouldPinToToolbar, extension) {
+ let newArea = shouldPinToToolbar
+ ? CustomizableUI.AREA_NAVBAR
+ : CustomizableUI.AREA_ADDONS;
+ let newPosition = shouldPinToToolbar ? undefined : 0;
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, newArea, newPosition);
+}
+
+add_task(async function browserAction_getUserSettings() {
+ let manifest = {
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ },
+ background: `(${background})('getUserSettings')`,
+ };
+ let extension = ExtensionTestUtils.loadExtension(manifest);
+ extension.onMessage("pinToToolbar", ({ identifier, shouldPinToToolbar }) => {
+ pinToToolbar(shouldPinToToolbar, extension);
+ extension.sendMessage(identifier);
+ });
+ await extension.startup();
+ await extension.awaitFinish("getUserSettings");
+ await extension.unload();
+});
+
+add_task(async function action_getUserSettings() {
+ let manifest = {
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ background: `(${background})('getUserSettings')`,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(manifest);
+ extension.onMessage("pinToToolbar", ({ identifier, shouldPinToToolbar }) => {
+ pinToToolbar(shouldPinToToolbar, extension);
+ extension.sendMessage(identifier);
+ });
+ await extension.startup();
+ await extension.awaitFinish("getUserSettings");
+ await extension.unload();
+});
+
+add_task(async function browserAction_getUserSettings_default_area() {
+ for (let default_area of ["navbar", "tabstrip", "personaltoolbar"]) {
+ let manifest = {
+ manifest: {
+ manifest_version: 2,
+ browser_action: {
+ default_area,
+ },
+ },
+ background: `(${background})('default_area_getUserSettings')`,
+ };
+ let extension = ExtensionTestUtils.loadExtension(manifest);
+ extension.onMessage(
+ "pinToToolbar",
+ ({ identifier, shouldPinToToolbar }) => {
+ pinToToolbar(shouldPinToToolbar, extension);
+ extension.sendMessage(identifier);
+ }
+ );
+ await extension.startup();
+ await extension.awaitFinish("getUserSettings");
+ await extension.unload();
+ }
+});
+
+add_task(async function action_getUserSettings_default_area() {
+ for (let default_area of ["navbar", "tabstrip", "personaltoolbar"]) {
+ let manifest = {
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_area,
+ },
+ },
+ background: `(${background})('default_area_getUserSettings')`,
+ };
+ let extension = ExtensionTestUtils.loadExtension(manifest);
+ extension.onMessage(
+ "pinToToolbar",
+ ({ identifier, shouldPinToToolbar }) => {
+ pinToToolbar(shouldPinToToolbar, extension);
+ extension.sendMessage(identifier);
+ }
+ );
+ await extension.startup();
+ await extension.awaitFinish("getUserSettings");
+ await extension.unload();
+ }
+});
+
+add_task(async function browserAction_getUserSettings_menupanel_default_area() {
+ let manifest = {
+ manifest: {
+ manifest_version: 2,
+ browser_action: {
+ default_area: "menupanel",
+ },
+ },
+ background: `(${background})('menupanel_default_area_getUserSettings')`,
+ };
+ let extension = ExtensionTestUtils.loadExtension(manifest);
+ extension.onMessage("pinToToolbar", ({ identifier, shouldPinToToolbar }) => {
+ pinToToolbar(shouldPinToToolbar, extension);
+ extension.sendMessage(identifier);
+ });
+ await extension.startup();
+ await extension.awaitFinish("getUserSettings");
+ await extension.unload();
+});
+
+add_task(async function action_getUserSettings_menupanel_default_area() {
+ let manifest = {
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_area: "menupanel",
+ },
+ },
+ background: `(${background})('menupanel_default_area_getUserSettings')`,
+ };
+ let extension = ExtensionTestUtils.loadExtension(manifest);
+ extension.onMessage("pinToToolbar", ({ identifier, shouldPinToToolbar }) => {
+ pinToToolbar(shouldPinToToolbar, extension);
+ extension.sendMessage(identifier);
+ });
+ await extension.startup();
+ await extension.awaitFinish("getUserSettings");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_incognito.js b/browser/components/extensions/test/browser/browser_ext_browserAction_incognito.js
new file mode 100644
index 0000000000..e3f9ea97fa
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_incognito.js
@@ -0,0 +1,48 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testIncognito(incognitoOverride) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ incognitoOverride,
+ });
+
+ // We test three windows, the public window, a private window prior
+ // to extension start, and one created after. This tests that CUI
+ // creates the widgets (or not) as it should.
+ let p1 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ await extension.startup();
+ let p2 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ let action = getBrowserActionWidget(extension);
+ await showBrowserAction(extension);
+ await showBrowserAction(extension, p1);
+ await showBrowserAction(extension, p2);
+
+ ok(!!action.forWindow(window).node, "popup exists in non-private window");
+
+ for (let win of [p1, p2]) {
+ let node = action.forWindow(win).node;
+ if (incognitoOverride == "spanning") {
+ ok(!!node, "popup exists in private window");
+ } else {
+ ok(!node, "popup does not exist in private window");
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+ await extension.unload();
+}
+
+add_task(async function test_browserAction_not_allowed() {
+ await testIncognito();
+});
+
+add_task(async function test_browserAction_allowed() {
+ await testIncognito("spanning");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_keyclick.js b/browser/components/extensions/test/browser/browser_ext_browserAction_keyclick.js
new file mode 100644
index 0000000000..1cf5dbb8b3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_keyclick.js
@@ -0,0 +1,68 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Like focusButtonAndPressKey, but leaves time between keydown and keyup
+// rather than dispatching both synchronously. This allows time for the
+// element's `open` property to go back to being `false` if forced to true
+// synchronously in response to keydown.
+async function focusButtonAndPressKeyWithDelay(key, elem, modifiers) {
+ let focused = BrowserTestUtils.waitForEvent(elem, "focus", true);
+ elem.setAttribute("tabindex", "-1");
+ elem.focus();
+ elem.removeAttribute("tabindex");
+ await focused;
+
+ EventUtils.synthesizeKey(key, { type: "keydown", modifiers });
+ await new Promise(executeSoon);
+ EventUtils.synthesizeKey(key, { type: "keyup", modifiers });
+ let blurred = BrowserTestUtils.waitForEvent(elem, "blur", true);
+ elem.blur();
+ await blurred;
+}
+
+// This test verifies that pressing enter while a page action is focused
+// triggers the action once and only once.
+add_task(async function testKeyBrowserAction() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+
+ async background() {
+ let counter = 0;
+
+ browser.browserAction.onClicked.addListener(() => {
+ counter++;
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.assertEq(
+ "checkCounter",
+ msg,
+ "expected check counter message"
+ );
+ browser.test.sendMessage("counter", counter);
+ });
+
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let elem = getBrowserActionWidget(extension).forWindow(window).node;
+
+ await promiseAnimationFrame(window);
+ await showBrowserAction(extension, window);
+ await focusButtonAndPressKeyWithDelay(" ", elem.firstElementChild, {});
+
+ extension.sendMessage("checkCounter");
+ let counter = await extension.awaitMessage("counter");
+ is(counter, 1, "Key only triggered button once");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
new file mode 100644
index 0000000000..1561e4d6a8
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -0,0 +1,651 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function testHiDpiImage(button, images1x, images2x, prop) {
+ let image = getRawListStyleImage(button);
+ info(image);
+ info(button.outerHTML);
+ let image1x = images1x[prop];
+ let image2x = images2x[prop];
+ is(
+ image,
+ `image-set(url("${image1x}") 1dppx, url("${image2x}") 2dppx)`,
+ prop
+ );
+}
+
+// Test that various combinations of icon details specs, for both paths
+// and ImageData objects, result in the correct image being displayed in
+// all display resolutions.
+add_task(async function testDetailsObjects() {
+ function background() {
+ function getImageData(color) {
+ let canvas = document.createElement("canvas");
+ canvas.width = 2;
+ canvas.height = 2;
+ let canvasContext = canvas.getContext("2d");
+
+ canvasContext.clearRect(0, 0, canvas.width, canvas.height);
+ canvasContext.fillStyle = color;
+ canvasContext.fillRect(0, 0, 1, 1);
+
+ return {
+ url: canvas.toDataURL("image/png"),
+ imageData: canvasContext.getImageData(
+ 0,
+ 0,
+ canvas.width,
+ canvas.height
+ ),
+ };
+ }
+
+ let imageData = {
+ red: getImageData("red"),
+ green: getImageData("green"),
+ };
+
+ // eslint-disable indent, indent-legacy
+ let iconDetails = [
+ // Only paths.
+ {
+ details: { path: "a.png" },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ },
+ },
+ {
+ details: { path: "/a.png" },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("a.png"),
+ pageActionImageURL: browser.runtime.getURL("a.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("a.png"),
+ pageActionImageURL: browser.runtime.getURL("a.png"),
+ },
+ },
+ },
+ {
+ details: { path: { 19: "a.png" } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ },
+ },
+ {
+ details: { path: { 38: "a.png" } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ },
+ },
+ {
+ details: { path: { 19: "a.png", 38: "a-x2.png" } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a-x2.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a-x2.png"),
+ },
+ },
+ },
+ {
+ details: {
+ path: { 16: "a-16.png", 32: "a-32.png", 64: "a-64.png" },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a-16.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a-16.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a-32.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a-32.png"),
+ },
+ },
+ },
+
+ // Test that CSS strings are escaped properly.
+ {
+ details: { path: 'a.png#" \\' },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL(
+ "data/a.png#%22%20%5C"
+ ),
+ pageActionImageURL: browser.runtime.getURL("data/a.png#%22%20%5C"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL(
+ "data/a.png#%22%20%5C"
+ ),
+ pageActionImageURL: browser.runtime.getURL("data/a.png#%22%20%5C"),
+ },
+ },
+ },
+
+ // Only ImageData objects.
+ {
+ details: { imageData: imageData.red.imageData },
+ resolutions: {
+ 1: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ 2: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ },
+ },
+ {
+ details: { imageData: { 19: imageData.red.imageData } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ 2: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ },
+ },
+ {
+ details: { imageData: { 38: imageData.red.imageData } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ 2: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ },
+ },
+ {
+ details: {
+ imageData: {
+ 19: imageData.red.imageData,
+ 38: imageData.green.imageData,
+ },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ 2: {
+ browserActionImageURL: imageData.green.url,
+ pageActionImageURL: imageData.green.url,
+ },
+ },
+ },
+
+ // Mixed path and imageData objects.
+ //
+ // The behavior is currently undefined if both |path| and
+ // |imageData| specify icons of the same size.
+ {
+ details: {
+ path: { 19: "a.png" },
+ imageData: { 38: imageData.red.imageData },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ 2: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ },
+ },
+ {
+ details: {
+ path: { 38: "a.png" },
+ imageData: { 19: imageData.red.imageData },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ },
+ },
+
+ // A path or ImageData object by itself is treated as a 19px icon.
+ {
+ details: {
+ path: "a.png",
+ imageData: { 38: imageData.red.imageData },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ 2: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ },
+ },
+ {
+ details: {
+ path: { 38: "a.png" },
+ imageData: imageData.red.imageData,
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: imageData.red.url,
+ pageActionImageURL: imageData.red.url,
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ },
+ },
+
+ // Various resolutions
+ {
+ details: { path: { 18: "a.png", 36: "a-x2.png" } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a-x2.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a-x2.png"),
+ },
+ },
+ },
+ {
+ details: { path: { 16: "a.png", 30: "a-x2.png" } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/a.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/a-x2.png"),
+ pageActionImageURL: browser.runtime.getURL("data/a-x2.png"),
+ },
+ },
+ },
+ {
+ details: { path: { 16: "16.png", 100: "100.png" } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/16.png"),
+ pageActionImageURL: browser.runtime.getURL("data/16.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/100.png"),
+ pageActionImageURL: browser.runtime.getURL("data/100.png"),
+ },
+ },
+ },
+ {
+ details: { path: { 2: "2.png" } },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/2.png"),
+ pageActionImageURL: browser.runtime.getURL("data/2.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/2.png"),
+ pageActionImageURL: browser.runtime.getURL("data/2.png"),
+ },
+ },
+ },
+ {
+ details: {
+ path: {
+ 16: "16.svg",
+ 18: "18.svg",
+ },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/16.svg"),
+ pageActionImageURL: browser.runtime.getURL("data/16.svg"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/18.svg"),
+ pageActionImageURL: browser.runtime.getURL("data/18.svg"),
+ },
+ },
+ },
+ {
+ details: {
+ path: {
+ 6: "6.png",
+ 18: "18.png",
+ 36: "36.png",
+ 48: "48.png",
+ 128: "128.png",
+ },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/18.png"),
+ pageActionImageURL: browser.runtime.getURL("data/18.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/36.png"),
+ pageActionImageURL: browser.runtime.getURL("data/36.png"),
+ },
+ },
+ menuResolutions: {
+ 1: browser.runtime.getURL("data/36.png"),
+ 2: browser.runtime.getURL("data/128.png"),
+ },
+ },
+ {
+ details: {
+ path: {
+ 16: "16.png",
+ 18: "18.png",
+ 32: "32.png",
+ 48: "48.png",
+ 64: "64.png",
+ 128: "128.png",
+ },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/16.png"),
+ pageActionImageURL: browser.runtime.getURL("data/16.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/32.png"),
+ pageActionImageURL: browser.runtime.getURL("data/32.png"),
+ },
+ },
+ menuResolutions: {
+ 1: browser.runtime.getURL("data/32.png"),
+ 2: browser.runtime.getURL("data/64.png"),
+ },
+ },
+ {
+ details: {
+ path: {
+ 18: "18.png",
+ 32: "32.png",
+ 48: "48.png",
+ 128: "128.png",
+ },
+ },
+ resolutions: {
+ 1: {
+ browserActionImageURL: browser.runtime.getURL("data/32.png"),
+ pageActionImageURL: browser.runtime.getURL("data/32.png"),
+ },
+ 2: {
+ browserActionImageURL: browser.runtime.getURL("data/32.png"),
+ pageActionImageURL: browser.runtime.getURL("data/32.png"),
+ },
+ },
+ },
+ ];
+ /* eslint-enable indent, indent-legacy */
+
+ // Allow serializing ImageData objects for logging.
+ ImageData.prototype.toJSON = () => "";
+
+ let tabId;
+
+ browser.test.onMessage.addListener((msg, test) => {
+ if (msg != "setIcon") {
+ browser.test.fail("expecting 'setIcon' message");
+ }
+
+ let details = iconDetails[test.index];
+
+ let detailString = JSON.stringify(details);
+ browser.test.log(
+ `Setting browerAction/pageAction to ${detailString} expecting URLs ${JSON.stringify(
+ details.resolutions
+ )}`
+ );
+
+ Promise.all([
+ browser.browserAction.setIcon(
+ Object.assign({ tabId }, details.details)
+ ),
+ browser.pageAction.setIcon(Object.assign({ tabId }, details.details)),
+ ]).then(() => {
+ browser.test.sendMessage("iconSet");
+ });
+ });
+
+ // Generate a list of tests and resolutions to send back to the test
+ // context.
+ //
+ // This process is a bit convoluted, because the outer test context needs
+ // to handle checking the button nodes and changing the screen resolution,
+ // but it can't pass us icon definitions with ImageData objects. This
+ // shouldn't be a problem, since structured clones should handle ImageData
+ // objects without issue. Unfortunately, |cloneInto| implements a slightly
+ // different algorithm than we use in web APIs, and does not handle them
+ // correctly.
+ let tests = [];
+ for (let [idx, icon] of iconDetails.entries()) {
+ tests.push({
+ index: idx,
+ menuResolutions: icon.menuResolutions,
+ resolutions: icon.resolutions,
+ });
+ }
+
+ // Sort by resolution, so we don't needlessly switch back and forth
+ // between each test.
+ tests.sort(test => test.resolution);
+
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ tabId = tabs[0].id;
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("ready", tests);
+ });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ page_action: {},
+ background: {
+ page: "data/background.html",
+ },
+ },
+
+ files: {
+ "data/background.html": ``,
+ "data/background.js": background,
+
+ "data/16.svg": imageBuffer,
+ "data/18.svg": imageBuffer,
+
+ "data/16.png": imageBuffer,
+ "data/18.png": imageBuffer,
+ "data/32.png": imageBuffer,
+ "data/36.png": imageBuffer,
+ "data/48.png": imageBuffer,
+ "data/64.png": imageBuffer,
+ "data/128.png": imageBuffer,
+
+ "a.png": imageBuffer,
+ "data/2.png": imageBuffer,
+ "data/100.png": imageBuffer,
+ "data/a.png": imageBuffer,
+ "data/a-x2.png": imageBuffer,
+ },
+ });
+
+ await extension.startup();
+
+ let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+ let browserActionWidget = getBrowserActionWidget(extension);
+
+ let tests = await extension.awaitMessage("ready");
+ await promiseAnimationFrame();
+
+ // The initial icon should be the default icon since no icon is in the manifest.
+ const DEFAULT_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ let browserActionButton =
+ browserActionWidget.forWindow(window).node.firstElementChild;
+ let pageActionImage = document.getElementById(pageActionId);
+ is(
+ getListStyleImage(browserActionButton),
+ DEFAULT_ICON,
+ `browser action has the correct default image`
+ );
+ is(
+ getListStyleImage(pageActionImage),
+ DEFAULT_ICON,
+ `page action has the correct default image`
+ );
+
+ for (let test of tests) {
+ extension.sendMessage("setIcon", test);
+ await extension.awaitMessage("iconSet");
+
+ await promiseAnimationFrame();
+
+ testHiDpiImage(
+ browserActionButton,
+ test.resolutions[1],
+ test.resolutions[2],
+ "browserActionImageURL"
+ );
+ testHiDpiImage(
+ pageActionImage,
+ test.resolutions[1],
+ test.resolutions[2],
+ "pageActionImageURL"
+ );
+
+ if (!test.menuResolutions) {
+ continue;
+ }
+ }
+
+ await extension.unload();
+});
+
+// NOTE: The current goal of this test is ensuring that Bug 1397196 has been fixed,
+// and so this test extension manifest has a browser action which specify
+// a theme based icon and a pageAction, both the pageAction and the browserAction
+// have a common default_icon.
+//
+// Once Bug 1398156 will be fixed, this test should be converted into testing that
+// the browserAction and pageAction themed icons (as well as any other cached icon,
+// e.g. the sidebar and devtools panel icons) can be specified in the same extension
+// and do not conflict with each other.
+//
+// This test currently fails without the related fix, but only if the browserAction
+// API has been already loaded before the pageAction, otherwise the icons will be cached
+// in the opposite order and the test is not able to reproduce the issue anymore.
+add_task(async function testPageActionIconLoadingOnBrowserActionThemedIcon() {
+ async function background() {
+ const tabs = await browser.tabs.query({ active: true });
+ await browser.pageAction.show(tabs[0].id);
+
+ browser.test.sendMessage("ready");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ name: "Foo Extension",
+
+ browser_action: {
+ default_icon: "common_cached_icon.png",
+ default_popup: "default_popup.html",
+ default_title: "BrowserAction title",
+ default_area: "navbar",
+ theme_icons: [
+ {
+ dark: "1.png",
+ light: "2.png",
+ size: 16,
+ },
+ ],
+ },
+
+ page_action: {
+ default_icon: "common_cached_icon.png",
+ default_popup: "default_popup.html",
+ default_title: "PageAction title",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "common_cached_icon.png": imageBuffer,
+ "1.png": imageBuffer,
+ "2.png": imageBuffer,
+ "default_popup.html": "popup",
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("ready");
+
+ await promiseAnimationFrame();
+
+ let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+ let pageActionImage = document.getElementById(pageActionId);
+
+ const iconURL = new URL(getListStyleImage(pageActionImage));
+
+ is(
+ iconURL.pathname,
+ "/common_cached_icon.png",
+ "Got the expected pageAction icon url"
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js
new file mode 100644
index 0000000000..5a94a0dde1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js
@@ -0,0 +1,239 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+PromiseTestUtils.allowMatchingRejectionsGlobally(/packaging errors/);
+
+// Test that an error is thrown when providing invalid icon sizes
+add_task(async function testInvalidIconSizes() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ page_action: {},
+ },
+
+ background: function () {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ let tabId = tabs[0].id;
+
+ let promises = [];
+ for (let api of ["pageAction", "browserAction"]) {
+ // helper function to run setIcon and check if it fails
+ let assertSetIconThrows = function (detail, error, message) {
+ detail.tabId = tabId;
+ browser.test.assertThrows(
+ () => browser[api].setIcon(detail),
+ /an unexpected .* property/,
+ "setIcon with invalid icon size"
+ );
+ };
+
+ let imageData = new ImageData(1, 1);
+
+ // test invalid icon size inputs
+ for (let type of ["path", "imageData"]) {
+ let img = type == "imageData" ? imageData : "test.png";
+
+ assertSetIconThrows({ [type]: { abcdef: img } });
+ assertSetIconThrows({ [type]: { "48px": img } });
+ assertSetIconThrows({ [type]: { 20.5: img } });
+ assertSetIconThrows({ [type]: { "5.0": img } });
+ assertSetIconThrows({ [type]: { "-300": img } });
+ assertSetIconThrows({ [type]: { abc: img, 5: img } });
+ }
+
+ assertSetIconThrows({
+ imageData: { abcdef: imageData },
+ path: { 5: "test.png" },
+ });
+ assertSetIconThrows({
+ path: { abcdef: "test.png" },
+ imageData: { 5: imageData },
+ });
+ }
+
+ Promise.all(promises).then(() => {
+ browser.test.notifyPass("setIcon with invalid icon size");
+ });
+ });
+ },
+ });
+
+ await Promise.all([
+ extension.startup(),
+ extension.awaitFinish("setIcon with invalid icon size"),
+ ]);
+
+ await extension.unload();
+});
+
+// Test that default icon details in the manifest.json file are handled
+// correctly.
+add_task(async function testDefaultDetails() {
+ // TODO: Test localized variants.
+ let icons = [
+ "foo/bar.png",
+ "/foo/bar.png",
+ { 38: "foo/bar.png" },
+ { 70: "foo/bar.png" },
+ ];
+
+ if (window.devicePixelRatio > 1) {
+ icons.push({ 38: "baz/quux.png", 70: "foo/bar.png" });
+ } else {
+ icons.push({ 38: "foo/bar.png", 70: "baz/quux@2x.png" });
+ }
+
+ let expectedURL = new RegExp(
+ String.raw`^moz-extension://[^/]+/foo/bar\.png$`
+ );
+
+ for (let icon of icons) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: { default_icon: icon, default_area: "navbar" },
+ page_action: { default_icon: icon },
+ },
+
+ background: function () {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ let tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("ready");
+ });
+ });
+ },
+
+ files: {
+ "foo/bar.png": imageBuffer,
+ "baz/quux.png": imageBuffer,
+ "baz/quux@2x.png": imageBuffer,
+ },
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ let browserActionId = makeWidgetId(extension.id) + "-browser-action";
+ let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+
+ await promiseAnimationFrame();
+
+ let browserActionButton =
+ document.getElementById(browserActionId).firstElementChild;
+ let image = getListStyleImage(browserActionButton);
+
+ ok(
+ expectedURL.test(image),
+ `browser action image ${image} matches ${expectedURL}`
+ );
+
+ let pageActionImage = document.getElementById(pageActionId);
+ image = getListStyleImage(pageActionImage);
+
+ ok(
+ expectedURL.test(image),
+ `page action image ${image} matches ${expectedURL}`
+ );
+
+ await extension.unload();
+
+ let node = document.getElementById(pageActionId);
+ is(node, null, "pageAction image removed from document");
+ }
+});
+
+// Check that attempts to load a privileged URL as an icon image fail.
+add_task(async function testSecureURLsDenied() {
+ // Test URLs passed to setIcon.
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: { default_area: "navbar" },
+ page_action: {},
+ },
+
+ background: function () {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ let tabId = tabs[0].id;
+
+ let urls = [
+ "chrome://browser/content/browser.xhtml",
+ "javascript:true",
+ ];
+
+ let promises = [];
+ for (let url of urls) {
+ for (let api of ["pageAction", "browserAction"]) {
+ promises.push(
+ browser.test.assertRejects(
+ browser[api].setIcon({ tabId, path: url }),
+ /Illegal URL/,
+ `Load of '${url}' should fail.`
+ )
+ );
+ }
+ }
+
+ Promise.all(promises).then(() => {
+ browser.test.notifyPass("setIcon security tests");
+ });
+ });
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("setIcon security tests");
+ await extension.unload();
+});
+
+add_task(async function testSecureManifestURLsDenied() {
+ // Test URLs included in the manifest.
+
+ let urls = ["chrome://browser/content/browser.xhtml", "javascript:true"];
+
+ let apis = ["browser_action", "page_action"];
+
+ for (let url of urls) {
+ for (let api of apis) {
+ info(`TEST ${api} icon url: ${url}`);
+
+ let matchURLForbidden = url => ({
+ message: new RegExp(`match the format "strictRelativeUrl"`),
+ });
+
+ let messages = [matchURLForbidden(url)];
+
+ let waitForConsole = new Promise(resolve => {
+ // Not necessary in browser-chrome tests, but monitorConsole gripes
+ // if we don't call it.
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.monitorConsole(resolve, messages);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ [api]: {
+ default_icon: url,
+ default_area: "navbar",
+ },
+ },
+ });
+
+ await Assert.rejects(
+ extension.startup(),
+ /startup failed/,
+ "Manifest rejected"
+ );
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+ }
+ }
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
new file mode 100644
index 0000000000..136f30eb41
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
@@ -0,0 +1,370 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function getBrowserAction(extension) {
+ const {
+ global: { browserActionFor },
+ } = Management;
+
+ let ext = WebExtensionPolicy.getByID(extension.id)?.extension;
+ return browserActionFor(ext);
+}
+
+async function assertViewCount(extension, count, waitForPromise) {
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+
+ if (waitForPromise) {
+ await waitForPromise;
+ }
+
+ is(
+ ext.views.size,
+ count,
+ "Should have the expected number of extension views"
+ );
+}
+
+function promiseExtensionPageClosed(extension, viewType) {
+ return new Promise(resolve => {
+ const policy = WebExtensionPolicy.getByID(extension.id);
+
+ const listener = (evtType, context) => {
+ if (context.viewType === viewType) {
+ policy.extension.off("extension-proxy-context-load", listener);
+
+ context.callOnClose({
+ close: resolve,
+ });
+ }
+ };
+ policy.extension.on("extension-proxy-context-load", listener);
+ });
+}
+
+let scriptPage = url =>
+ ` ${url}`;
+
+async function testInArea(area) {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ let middleClickShowPopup = false;
+ browser.browserAction.onClicked.addListener((tabs, info) => {
+ browser.test.sendMessage("browserAction-onClicked");
+ if (info.button === 1 && middleClickShowPopup) {
+ browser.browserAction.openPopup();
+ }
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg.type === "setBrowserActionPopup") {
+ let opts = { popup: msg.popup };
+ if (msg.onCurrentWindowId) {
+ let { id } = await browser.windows.getCurrent();
+ opts = { ...opts, windowId: id };
+ } else if (msg.onActiveTabId) {
+ let [{ id }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ opts = { ...opts, tabId: id };
+ }
+ await browser.browserAction.setPopup(opts);
+ browser.test.sendMessage("setBrowserActionPopup:done");
+ } else if (msg.type === "setMiddleClickShowPopup") {
+ middleClickShowPopup = msg.show;
+ browser.test.sendMessage("setMiddleClickShowPopup:done");
+ }
+ });
+
+ browser.test.sendMessage("background-page-ready");
+ },
+ manifest: {
+ browser_action: {
+ default_popup: "popup-a.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup-a.html": scriptPage("popup-a.js"),
+ "popup-a.js": function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "close-popup-using-window.close") {
+ window.close();
+ }
+ });
+
+ window.onload = () => {
+ let color = window.getComputedStyle(document.body).color;
+ browser.test.assertEq("rgb(34, 36, 38)", color);
+ browser.test.sendMessage("from-popup", "popup-a");
+ };
+ },
+
+ "data/popup-b.html": scriptPage("popup-b.js"),
+ "data/popup-b.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("from-popup", "popup-b");
+ };
+ },
+
+ "data/popup-c.html": scriptPage("popup-c.js"),
+ "data/popup-c.js": function () {
+ // Close the popup before the document is fully-loaded to make sure that
+ // we handle this case sanely.
+ browser.test.sendMessage("from-popup", "popup-c");
+ window.close();
+ },
+ },
+ });
+
+ await Promise.all([
+ extension.startup(),
+ extension.awaitMessage("background-page-ready"),
+ ]);
+
+ let widget = getBrowserActionWidget(extension);
+
+ // Move the browserAction widget to the area targeted by this test.
+ CustomizableUI.addWidgetToArea(widget.id, area);
+
+ async function setBrowserActionPopup(opts) {
+ extension.sendMessage({ type: "setBrowserActionPopup", ...opts });
+ await extension.awaitMessage("setBrowserActionPopup:done");
+ }
+
+ async function setShowPopupOnMiddleClick(show) {
+ extension.sendMessage({ type: "setMiddleClickShowPopup", show });
+ await extension.awaitMessage("setMiddleClickShowPopup:done");
+ }
+
+ async function runTest({
+ actionType,
+ waitForPopupLoaded,
+ expectPopup,
+ expectOnClicked,
+ closePopup,
+ }) {
+ const oncePopupPageClosed = promiseExtensionPageClosed(extension, "popup");
+ const oncePopupLoaded = waitForPopupLoaded
+ ? awaitExtensionPanel(extension)
+ : undefined;
+
+ if (actionType === "click") {
+ clickBrowserAction(extension);
+ } else if (actionType === "trigger") {
+ getBrowserAction(extension).triggerAction(window);
+ } else if (actionType === "middleClick") {
+ clickBrowserAction(extension, window, { button: 1 });
+ }
+
+ if (expectOnClicked) {
+ await extension.awaitMessage("browserAction-onClicked");
+ }
+
+ if (expectPopup) {
+ info(`Waiting for popup: ${expectPopup}`);
+ is(
+ await extension.awaitMessage("from-popup"),
+ expectPopup,
+ "expected popup opened"
+ );
+ }
+
+ await oncePopupLoaded;
+
+ if (closePopup) {
+ info("Closing popup");
+ await closeBrowserAction(extension);
+ await assertViewCount(extension, 1, oncePopupPageClosed);
+ }
+
+ return { oncePopupPageClosed };
+ }
+
+ // Run the sequence of test cases.
+ const tests = [
+ async () => {
+ info(`Click browser action, expect popup "a".`);
+
+ await runTest({
+ actionType: "click",
+ expectPopup: "popup-a",
+ closePopup: true,
+ });
+ },
+ async () => {
+ info(`Click browser action again, expect popup "a".`);
+
+ await runTest({
+ actionType: "click",
+ expectPopup: "popup-a",
+ waitForPopupLoaded: true,
+ closePopup: true,
+ });
+ },
+ async () => {
+ info(`Call triggerAction, expect popup "a" again. Leave popup open.`);
+
+ const { oncePopupPageClosed } = await runTest({
+ actionType: "trigger",
+ expectPopup: "popup-a",
+ waitForPopupLoaded: true,
+ });
+
+ await assertViewCount(extension, 2);
+
+ info(`Call triggerAction again. Expect remaining popup closed.`);
+ getBrowserAction(extension).triggerAction(window);
+
+ await assertViewCount(extension, 1, oncePopupPageClosed);
+ },
+ async () => {
+ info(`Call triggerAction again. Expect popup "a" again.`);
+
+ await runTest({
+ actionType: "trigger",
+ expectPopup: "popup-a",
+ closePopup: true,
+ });
+ },
+ async () => {
+ info(`Set popup to "c" and click browser action. Expect popup "c".`);
+
+ await setBrowserActionPopup({ popup: "data/popup-c.html" });
+
+ const { oncePopupPageClosed } = await runTest({
+ actionType: "click",
+ expectPopup: "popup-c",
+ });
+
+ await assertViewCount(extension, 1, oncePopupPageClosed);
+ },
+ async () => {
+ info(`Set popup to "b" and click browser action. Expect popup "b".`);
+
+ await setBrowserActionPopup({ popup: "data/popup-b.html" });
+
+ await runTest({
+ actionType: "click",
+ expectPopup: "popup-b",
+ closePopup: true,
+ });
+ },
+ async () => {
+ info(`Click browser action again, expect popup "b".`);
+
+ await runTest({
+ actionType: "click",
+ expectPopup: "popup-b",
+ closePopup: true,
+ });
+ },
+ async () => {
+ info(`Middle-click browser action, expect an event only.`);
+
+ await setShowPopupOnMiddleClick(false);
+
+ await runTest({
+ actionType: "middleClick",
+ expectOnClicked: true,
+ });
+ },
+ async () => {
+ info(
+ `Middle-click browser action again, expect a click event then a popup.`
+ );
+
+ await setShowPopupOnMiddleClick(true);
+
+ await runTest({
+ actionType: "middleClick",
+ expectOnClicked: true,
+ expectPopup: "popup-b",
+ closePopup: true,
+ });
+ },
+ async () => {
+ info(`Clear popup URL. Click browser action. Expect click event.`);
+
+ await setBrowserActionPopup({ popup: "" });
+
+ await runTest({
+ actionType: "click",
+ expectOnClicked: true,
+ });
+ },
+ async () => {
+ info(`Click browser action again. Expect another click event.`);
+
+ await runTest({
+ actionType: "click",
+ expectOnClicked: true,
+ });
+ },
+ async () => {
+ info(`Call triggerAction. Expect click event.`);
+
+ await runTest({
+ actionType: "trigger",
+ expectOnClicked: true,
+ });
+ },
+ async () => {
+ info(
+ `Set window-specific popup to "b" and click browser action. Expect popup "b".`
+ );
+
+ await setBrowserActionPopup({
+ popup: "data/popup-b.html",
+ onCurrentWindowId: true,
+ });
+
+ await runTest({
+ actionType: "click",
+ expectPopup: "popup-b",
+ closePopup: true,
+ });
+ },
+ async () => {
+ info(
+ `Set tab-specific popup to "a" and click browser action. Expect popup "a", and leave open.`
+ );
+
+ await setBrowserActionPopup({
+ popup: "/popup-a.html",
+ onActiveTabId: true,
+ });
+
+ const { oncePopupPageClosed } = await runTest({
+ actionType: "click",
+ expectPopup: "popup-a",
+ });
+ assertViewCount(extension, 2);
+
+ info(`Tell popup "a" to call window.close(). Expect popup closed.`);
+ extension.sendMessage("close-popup-using-window.close");
+
+ await assertViewCount(extension, 1, oncePopupPageClosed);
+ },
+ ];
+
+ for (let test of tests) {
+ await test();
+ }
+
+ // Unload the extension and verify that the browserAction widget is gone.
+ await extension.unload();
+
+ let view = document.getElementById(widget.viewId);
+ is(view, null, "browserAction view removed from document");
+}
+
+add_task(async function testBrowserActionInToolbar() {
+ await testInArea(CustomizableUI.AREA_NAVBAR);
+});
+
+add_task(async function testBrowserActionInPanel() {
+ await testInArea(getCustomizableUIPanelID());
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_port.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_port.js
new file mode 100644
index 0000000000..415d69738d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_port.js
@@ -0,0 +1,56 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let scriptPage = url =>
+ ` ${url}`;
+
+// Tests that message ports still function correctly after a browserAction popup
+// has been reparented.
+add_task(async function test_browserActionPort() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ },
+
+ background() {
+ new Promise(resolve => {
+ browser.runtime.onConnect.addListener(port => {
+ resolve(
+ Promise.all([
+ new Promise(r => port.onMessage.addListener(r)),
+ new Promise(r => port.onDisconnect.addListener(r)),
+ ])
+ );
+ });
+ }).then(([msg]) => {
+ browser.test.assertEq("Hallo.", msg, "Got expected message");
+ browser.test.notifyPass("browserAction-popup-port");
+ });
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js"() {
+ let port = browser.runtime.connect();
+ window.onload = () => {
+ setTimeout(() => {
+ port.postMessage("Hallo.");
+ window.close();
+ }, 0);
+ };
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await clickBrowserAction(extension);
+
+ await extension.awaitFinish("browserAction-popup-port");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload.js
new file mode 100644
index 0000000000..4c47c1b13b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload.js
@@ -0,0 +1,472 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+let scriptPage = url =>
+ ` ${url}`;
+
+add_task(async function testBrowserActionClickCanceled() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mousemove" },
+ window
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ permissions: ["activeTab"],
+ },
+
+ files: {
+ "popup.html": ` `,
+ },
+ });
+
+ await extension.startup();
+
+ const {
+ Management: {
+ global: { browserActionFor },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let ext = WebExtensionPolicy.getByID(extension.id)?.extension;
+ let browserAction = browserActionFor(ext);
+
+ let widget = getBrowserActionWidget(extension).forWindow(window);
+
+ // Test canceled click.
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousemove" },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ window
+ );
+
+ isnot(browserAction.pendingPopup, null, "Have pending popup");
+ is(
+ browserAction.pendingPopup.window,
+ window,
+ "Have pending popup for the correct window"
+ );
+
+ is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+
+ is(browserAction.action.activeTabForPreload, tab, "Tab to revoke was saved");
+ is(
+ browserAction.tabManager.hasActiveTabPermission(tab),
+ true,
+ "Active tab was granted permission"
+ );
+
+ // We intentionally turn off this a11y check, because the following click
+ // is send on the to dismiss the pending popup using an alternative way
+ // of the popup dismissal, where the other way like `Esc` key is available,
+ // therefore this test can be ignored.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ EventUtils.synthesizeMouseAtCenter(
+ document.documentElement,
+ { type: "mousemove" },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.documentElement,
+ { type: "mouseup", button: 0 },
+ window
+ );
+ AccessibilityUtils.resetEnv();
+
+ is(browserAction.pendingPopup, null, "Pending popup was cleared");
+ is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+
+ is(
+ browserAction.action.activeTabForPreload,
+ null,
+ "Tab to revoke was removed"
+ );
+ is(
+ browserAction.tabManager.hasActiveTabPermission(tab),
+ false,
+ "Permission was revoked from tab"
+ );
+
+ // Test completed click.
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousemove" },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ window
+ );
+
+ isnot(browserAction.pendingPopup, null, "Have pending popup");
+ is(
+ browserAction.pendingPopup.window,
+ window,
+ "Have pending popup for the correct window"
+ );
+
+ is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+
+ // We need to do these tests during the mouseup event cycle, since the click
+ // and command events will be dispatched immediately after mouseup, and void
+ // the results.
+ let mouseUpPromise = BrowserTestUtils.waitForEvent(
+ widget.node,
+ "mouseup",
+ false,
+ event => {
+ isnot(browserAction.pendingPopup, null, "Pending popup was not cleared");
+ isnot(
+ browserAction.pendingPopupTimeout,
+ null,
+ "Have a pending popup timeout"
+ );
+ return true;
+ }
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseup", button: 0 },
+ window
+ );
+
+ await mouseUpPromise;
+
+ is(browserAction.pendingPopup, null, "Pending popup was cleared");
+ is(
+ browserAction.pendingPopupTimeout,
+ null,
+ "Pending popup timeout was cleared"
+ );
+
+ await promisePopupShown(getBrowserActionPopup(extension));
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testBrowserActionDisabled() {
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mousemove" },
+ window
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ },
+
+ background: async function () {
+ await browser.browserAction.disable();
+ browser.test.sendMessage("browserAction-disabled");
+ },
+
+ files: {
+ "popup.html": ` `,
+ "popup.js"() {
+ browser.test.fail("Should not get here");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("browserAction-disabled");
+ await promiseAnimationFrame();
+
+ const {
+ Management: {
+ global: { browserActionFor },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let ext = WebExtensionPolicy.getByID(extension.id)?.extension;
+ let browserAction = browserActionFor(ext);
+
+ let widget = getBrowserActionWidget(extension).forWindow(window);
+ let button = widget.node.firstElementChild;
+
+ is(button.getAttribute("disabled"), "true", "Button is disabled");
+ is(browserAction.pendingPopup, null, "Have no pending popup prior to click");
+
+ // Test canceled click.
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousemove" },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ window
+ );
+
+ is(browserAction.pendingPopup, null, "Have no pending popup");
+ is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+
+ // We intentionally turn off this a11y check, because the following click
+ // is send on the to dismiss the pending popup using an alternative way
+ // of the popup dismissal, where the other way like `Esc` key is available,
+ // therefore this test can be ignored.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ EventUtils.synthesizeMouseAtCenter(
+ document.documentElement,
+ { type: "mousemove" },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.documentElement,
+ { type: "mouseup", button: 0 },
+ window
+ );
+ AccessibilityUtils.resetEnv();
+
+ is(browserAction.pendingPopup, null, "Have no pending popup");
+ is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+
+ // Test completed click.
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousemove" },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ window
+ );
+
+ is(browserAction.pendingPopup, null, "Have no pending popup");
+ is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+
+ // We need to do these tests during the mouseup event cycle, since the click
+ // and command events will be dispatched immediately after mouseup, and void
+ // the results.
+ let mouseUpPromise = BrowserTestUtils.waitForEvent(
+ widget.node,
+ "mouseup",
+ false,
+ event => {
+ is(browserAction.pendingPopup, null, "Have no pending popup");
+ is(
+ browserAction.pendingPopupTimeout,
+ null,
+ "Have no pending popup timeout"
+ );
+ return true;
+ }
+ );
+
+ // We intentionally turn off a11y_checks, because the following click
+ // is targeting a disabled control to confirm the click event won't come through.
+ // It is not meant to be interactive and is not expected to be accessible:
+ AccessibilityUtils.setEnv({
+ mustBeEnabled: false,
+ });
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseup", button: 0 },
+ window
+ );
+ AccessibilityUtils.resetEnv();
+
+ await mouseUpPromise;
+
+ is(browserAction.pendingPopup, null, "Have no pending popup");
+ is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+
+ // Give the popup a chance to load and trigger a failure, if it was
+ // erroneously opened.
+ await new Promise(resolve => setTimeout(resolve, 250));
+
+ await extension.unload();
+});
+
+add_task(async function testBrowserActionTabPopulation() {
+ // Note: This test relates to https://bugzilla.mozilla.org/show_bug.cgi?id=1310019
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ permissions: ["activeTab"],
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": function () {
+ browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {
+ browser.test.assertEq(
+ "mochitest index /",
+ tabs[0].title,
+ "Tab has the expected title on first click"
+ );
+ browser.test.sendMessage("tabTitle");
+ });
+ },
+ },
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.startLoadingURIString(
+ win.gBrowser.selectedBrowser,
+ "http://example.com/"
+ );
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ win.gURLBar.textbox,
+ { type: "mousemove" },
+ win
+ );
+
+ await extension.startup();
+
+ let widget = getBrowserActionWidget(extension).forWindow(win);
+ EventUtils.synthesizeMouseAtCenter(widget.node, { type: "mousemove" }, win);
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ win
+ );
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseup", button: 0 },
+ win
+ );
+
+ await extension.awaitMessage("tabTitle");
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function testClosePopupDuringPreload() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": function () {
+ browser.test.sendMessage("popup_loaded");
+ window.close();
+ },
+ },
+ });
+
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mousemove" },
+ window
+ );
+
+ await extension.startup();
+
+ const {
+ Management: {
+ global: { browserActionFor },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let ext = WebExtensionPolicy.getByID(extension.id)?.extension;
+ let browserAction = browserActionFor(ext);
+
+ let widget = getBrowserActionWidget(extension).forWindow(window);
+
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousemove" },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ window
+ );
+
+ isnot(browserAction.pendingPopup, null, "Have pending popup");
+ is(
+ browserAction.pendingPopup.window,
+ window,
+ "Have pending popup for the correct window"
+ );
+
+ await extension.awaitMessage("popup_loaded");
+ try {
+ await browserAction.pendingPopup.browserLoaded;
+ } catch (e) {
+ is(
+ e.message,
+ "Popup destroyed",
+ "Popup content should have been destroyed"
+ );
+ }
+
+ let promiseViewShowing = BrowserTestUtils.waitForEvent(
+ document,
+ "ViewShowing",
+ false,
+ ev => ev.target.id === browserAction.viewId
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseup", button: 0 },
+ window
+ );
+
+ // The popup panel may become visible after the ViewShowing event. Wait a bit
+ // to ensure that the popup is not shown when window.close() was used.
+ await promiseViewShowing;
+ await new Promise(resolve => setTimeout(resolve, 50));
+
+ let panel = getBrowserActionPopup(extension);
+ is(panel, null, "Popup panel should have been closed");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js
new file mode 100644
index 0000000000..98e66e6c7a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js
@@ -0,0 +1,194 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+// This test does create and cancel the preloaded popup
+// multiple times and in some cases it takes longer than
+// the default timeouts allows.
+requestLongerTimeout(4);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+async function installTestAddon(addonId, unpacked = false) {
+ let xpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ browser_specific_settings: { gecko: { id: addonId } },
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "popup.html": `
+
+
+
+
+
+
+
+
+ `,
+ "popup.css": `@import "imported.css";`,
+ "imported.css": `
+ /* Increasing the imported.css file size to increase the
+ * chances to trigger the stylesheet preload issue that
+ * has been fixed by Bug 1735899 consistently and with
+ * a smaller number of preloaded popup cancelled.
+ *
+ * ${new Array(600000).fill("x").join("\n")}
+ */
+ body { width: 300px; height: 300px; background: red; }
+ `,
+ },
+ });
+
+ if (unpacked) {
+ // This temporary directory is going to be removed from the
+ // cleanup function, but also make it unique as we do for the
+ // other temporary files (e.g. like getTemporaryFile as defined
+ // in XPInstall.jsm).
+ const random = Math.round(Math.random() * 36 ** 3).toString(36);
+ const tmpDirName = `mochitest_unpacked_addons_${random}`;
+ let tmpExtPath = FileUtils.getDir("TmpD", [tmpDirName]);
+ tmpExtPath.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ registerCleanupFunction(() => {
+ tmpExtPath.remove(true);
+ });
+
+ // Unpacking the xpi file into the tempoary directory.
+ const extDir = await AddonTestUtils.manuallyInstall(
+ xpi,
+ tmpExtPath,
+ null,
+ /* unpacked */ true
+ );
+
+ // Install temporarily as unpacked.
+ return AddonManager.installTemporaryAddon(extDir);
+ }
+
+ // Install temporarily as packed.
+ return AddonManager.installTemporaryAddon(xpi);
+}
+
+async function waitForExtensionAndBrowserAction(addonId) {
+ const {
+ Management: {
+ global: { browserActionFor },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ // trigger a number of preloads
+ let extension;
+ let browserAction;
+ await TestUtils.waitForCondition(() => {
+ extension = WebExtensionPolicy.getByID(addonId)?.extension;
+ browserAction = extension && browserActionFor(extension);
+ return browserAction;
+ }, "got the extension and browserAction");
+
+ let widget = getBrowserActionWidget(extension).forWindow(window);
+
+ return { extension, browserAction, widget };
+}
+
+async function testCancelPreloadedPopup({ browserAction, widget }) {
+ // Trigger the preloaded popup.
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseover", button: 0 },
+ window
+ );
+ await TestUtils.waitForCondition(
+ () => browserAction.pendingPopup,
+ "Wait for the preloaded popup"
+ );
+ // Cancel the preloaded popup.
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseout", button: 0 },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ window.gURLBar.textbox,
+ { type: "mouseover" },
+ window
+ );
+ await TestUtils.waitForCondition(
+ () => !browserAction.pendingPopup,
+ "Wait for the preloaded popup to be cancelled"
+ );
+}
+
+async function testPopupLoadCompleted({ extension, browserAction, widget }) {
+ const promiseViewShowing = BrowserTestUtils.waitForEvent(
+ document,
+ "ViewShowing",
+ false,
+ ev => ev.target.id === browserAction.viewId
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseup", button: 0 },
+ window
+ );
+
+ info("Await the browserAction popup to be shown");
+ await promiseViewShowing;
+
+ info("Await the browserAction popup to be fully loaded");
+ const browser = await awaitExtensionPanel(
+ extension,
+ window,
+ /* awaitLoad */ true
+ );
+
+ await TestUtils.waitForCondition(async () => {
+ const docReadyState = await SpecialPowers.spawn(browser, [], () => {
+ return this.content.document.readyState;
+ });
+
+ return docReadyState === "complete";
+ }, "Wait the popup document to get to readyState complete");
+
+ ok(true, "Popup document was fully loaded");
+}
+
+// This test is covering a scenario similar to the one fixed in Bug 1735899,
+// and possibly some other similar ones that may slip through unnoticed.
+add_task(async function testCancelPopupPreloadRaceOnUnpackedAddon() {
+ const ID = "preloaded-popup@test";
+ const addon = await installTestAddon(ID, /* unpacked */ true);
+ const { extension, browserAction, widget } =
+ await waitForExtensionAndBrowserAction(ID);
+ info("Preload popup and cancel it multiple times");
+ for (let i = 0; i < 200; i++) {
+ await testCancelPreloadedPopup({ browserAction, widget });
+ }
+ await testPopupLoadCompleted({ extension, browserAction, widget });
+ await addon.uninstall();
+});
+
+// This test is covering a scenario similar to the one fixed in Bug 1735899,
+// and possibly some other similar ones that may slip through unnoticed.
+add_task(async function testCancelPopupPreloadRaceOnPackedAddon() {
+ const ID = "preloaded-popup@test";
+ const addon = await installTestAddon(ID, /* unpacked */ false);
+ const { extension, browserAction, widget } =
+ await waitForExtensionAndBrowserAction(ID);
+ info("Preload popup and cancel it multiple times");
+ for (let i = 0; i < 200; i++) {
+ await testCancelPreloadedPopup({ browserAction, widget });
+ }
+ await testPopupLoadCompleted({ extension, browserAction, widget });
+ await addon.uninstall();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
new file mode 100644
index 0000000000..4a7ec8f2b7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
@@ -0,0 +1,79 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_browserAction.js");
+
+add_task(async function testBrowserActionPopupResize() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html":
+ ' ',
+ },
+ });
+
+ await extension.startup();
+
+ let browser = await openBrowserActionPanel(extension, undefined, true);
+
+ async function checkSize(expected) {
+ let dims = await promiseContentDimensions(browser);
+
+ Assert.lessOrEqual(
+ Math.abs(dims.window.innerHeight - expected),
+ 1,
+ `Panel window should be ${expected}px tall (was ${dims.window.innerHeight})`
+ );
+ is(
+ dims.body.clientHeight,
+ dims.body.scrollHeight,
+ "Panel body should be tall enough to fit its contents"
+ );
+
+ // Tolerate if it is 1px too wide, as that may happen with the current resizing method.
+ Assert.lessOrEqual(
+ Math.abs(dims.window.innerWidth - expected),
+ 1,
+ `Panel window should be ${expected}px wide`
+ );
+ is(
+ dims.body.clientWidth,
+ dims.body.scrollWidth,
+ "Panel body should be wide enough to fit its contents"
+ );
+ }
+
+ function setSize(size) {
+ content.document.body.style.height = `${size}px`;
+ content.document.body.style.width = `${size}px`;
+ }
+
+ let sizes = [200, 400, 300];
+
+ for (let size of sizes) {
+ await alterContent(browser, setSize, size);
+ await checkSize(size);
+ }
+
+ let popup = getBrowserActionPopup(extension);
+ await closeBrowserAction(extension);
+ is(popup.state, "closed", "browserAction popup has been closed");
+
+ await extension.unload();
+});
+
+add_task(async function testBrowserActionMenuResizeStandards() {
+ await testPopupSize(true);
+});
+
+add_task(async function testBrowserActionMenuResizeQuirks() {
+ await testPopupSize(false);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize_bottom.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize_bottom.js
new file mode 100644
index 0000000000..3370be0053
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize_bottom.js
@@ -0,0 +1,39 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_browserAction.js");
+
+// Test that we still make reasonable maximum size calculations when the window
+// is close enough to the bottom of the screen that the menu panel opens above,
+// rather than below, its button.
+add_task(async function testBrowserActionMenuResizeBottomArrow() {
+ const WIDTH = 800;
+ const HEIGHT = 80;
+
+ let left = screen.availLeft + screen.availWidth - WIDTH;
+ let top = screen.availTop + screen.availHeight - HEIGHT;
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ win.resizeTo(WIDTH, HEIGHT);
+
+ // Sometimes we run into problems on Linux with resizing being asynchronous
+ // and window managers not allowing us to move the window so that any part of
+ // it is off-screen, so we need to try more than once.
+ for (let i = 0; i < 20; i++) {
+ win.moveTo(left, top);
+
+ if (win.screenX == left && win.screenY == top) {
+ break;
+ }
+
+ await delay(100);
+ }
+
+ await SimpleTest.promiseFocus(win);
+
+ await testPopupSize(true, win, "bottom");
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_simple.js b/browser/components/extensions/test/browser/browser_ext_browserAction_simple.js
new file mode 100644
index 0000000000..c9ac05c17f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_simple.js
@@ -0,0 +1,105 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testAction(manifest_version) {
+ const action = manifest_version < 3 ? "browser_action" : "action";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ [action]: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ unrecognized_property: "with-a-random-value",
+ },
+ icons: { 32: "icon.png" },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+
+ `,
+
+ "popup.js": function () {
+ window.onload = () => {
+ browser.runtime.sendMessage("from-popup");
+ };
+ },
+ "icon.png": imageBuffer,
+ },
+
+ background: function () {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "from-popup", "correct message received");
+ browser.test.sendMessage("popup");
+ });
+
+ // Test what api namespace is valid, make sure both are not.
+ let manifest = browser.runtime.getManifest();
+ let { manifest_version } = manifest;
+ browser.test.assertEq(
+ manifest_version == 2,
+ "browserAction" in browser,
+ "browserAction is available"
+ );
+ browser.test.assertEq(
+ manifest_version !== 2,
+ "action" in browser,
+ "action is available"
+ );
+ },
+ });
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message: new RegExp(
+ `Reading manifest: Warning processing ${action}.unrecognized_property: An unexpected property was found`
+ ),
+ },
+ ]);
+ });
+
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+
+ let widgetGroup = getBrowserActionWidget(extension);
+ ok(widgetGroup.webExtension, "The extension property was set.");
+
+ // Do this a few times to make sure the pop-up is reloaded each time.
+ for (let i = 0; i < 3; i++) {
+ clickBrowserAction(extension);
+
+ let widget = widgetGroup.forWindow(window);
+ let image = getComputedStyle(widget.node.firstElementChild).listStyleImage;
+
+ ok(image.includes("/icon.png"), "The extension's icon is used");
+ await extension.awaitMessage("popup");
+
+ closeBrowserAction(extension);
+ }
+
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_browserAction() {
+ await testAction(2);
+});
+
+add_task(async function test_action() {
+ await testAction(3);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_telemetry.js b/browser/components/extensions/test/browser/browser_ext_browserAction_telemetry.js
new file mode 100644
index 0000000000..9e55b49c7d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_telemetry.js
@@ -0,0 +1,386 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const TIMING_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_OPEN_MS";
+const TIMING_HISTOGRAM_KEYED = "WEBEXT_BROWSERACTION_POPUP_OPEN_MS_BY_ADDONID";
+const RESULT_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT";
+const RESULT_HISTOGRAM_KEYED =
+ "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT_BY_ADDONID";
+
+const EXTENSION_ID1 = "@test-extension1";
+const EXTENSION_ID2 = "@test-extension2";
+
+// Keep this in sync with the order in Histograms.json for
+// WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT
+const CATEGORIES = ["popupShown", "clearAfterHover", "clearAfterMousedown"];
+const GLEAN_RESULT_LABELS = [...CATEGORIES, "__other__"];
+
+function assertGleanPreloadResultLabelCounter(expectedLabelsValue) {
+ for (const label of GLEAN_RESULT_LABELS) {
+ const expectedLabelValue = expectedLabelsValue[label];
+ Assert.deepEqual(
+ Glean.extensionsCounters.browserActionPreloadResult[label].testGetValue(),
+ expectedLabelValue,
+ `Expect Glean browserActionPreloadResult metric label ${label} to be ${
+ expectedLabelValue > 0 ? expectedLabelValue : "empty"
+ }`
+ );
+ }
+}
+
+function assertGleanPreloadResultLabelCounterEmpty() {
+ // All empty labels passed to the other helpers to make it
+ // assert that all labels are empty.
+ assertGleanPreloadResultLabelCounter({});
+}
+
+/**
+ * Takes a Telemetry histogram snapshot and makes sure
+ * that the index for that value (as defined by CATEGORIES)
+ * has a count of 1, and that it's the only value that
+ * has been incremented.
+ *
+ * @param {object} snapshot
+ * The Telemetry histogram snapshot to examine.
+ * @param {string} category
+ * The category in CATEGORIES whose index we expect to have
+ * been set to 1.
+ */
+function assertOnlyOneTypeSet(snapshot, category) {
+ let categoryIndex = CATEGORIES.indexOf(category);
+ Assert.equal(
+ snapshot.values[categoryIndex],
+ 1,
+ `Should have seen the ${category} count increment.`
+ );
+ // Use Array.prototype.reduce to sum up all of the
+ // snapshot.count entries
+ Assert.equal(
+ Object.values(snapshot.values).reduce((a, b) => a + b, 0),
+ 1,
+ "Should only be 1 collected value."
+ );
+}
+
+add_task(async function testBrowserActionTelemetryTiming() {
+ let extensionOptions = {
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
`,
+ },
+ };
+ let extension1 = ExtensionTestUtils.loadExtension({
+ ...extensionOptions,
+ manifest: {
+ ...extensionOptions.manifest,
+ browser_specific_settings: {
+ gecko: { id: EXTENSION_ID1 },
+ },
+ },
+ });
+ let extension2 = ExtensionTestUtils.loadExtension({
+ ...extensionOptions,
+ manifest: {
+ ...extensionOptions.manifest,
+ browser_specific_settings: {
+ gecko: { id: EXTENSION_ID2 },
+ },
+ },
+ });
+
+ let histogram = Services.telemetry.getHistogramById(TIMING_HISTOGRAM);
+ let histogramKeyed = Services.telemetry.getKeyedHistogramById(
+ TIMING_HISTOGRAM_KEYED
+ );
+
+ histogram.clear();
+ histogramKeyed.clear();
+ Services.fog.testResetFOG();
+
+ is(
+ histogram.snapshot().sum,
+ 0,
+ `No data recorded for histogram: ${TIMING_HISTOGRAM}.`
+ );
+ is(
+ Object.keys(histogramKeyed).length,
+ 0,
+ `No data recorded for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ Assert.deepEqual(
+ Glean.extensionsTiming.browserActionPopupOpen.testGetValue(),
+ undefined,
+ "No data recorded for glean metric extensionsTiming.browserActionPopupOpen"
+ );
+
+ await extension1.startup();
+ await extension2.startup();
+
+ is(
+ histogram.snapshot().sum,
+ 0,
+ `No data recorded for histogram after startup: ${TIMING_HISTOGRAM}.`
+ );
+ is(
+ Object.keys(histogramKeyed).length,
+ 0,
+ `No data recorded for histogram after startup: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ Assert.deepEqual(
+ Glean.extensionsTiming.browserActionPopupOpen.testGetValue(),
+ undefined,
+ "No data recorded for glean metric extensionsTiming.browserActionPopupOpen"
+ );
+
+ clickBrowserAction(extension1);
+ await awaitExtensionPanel(extension1);
+ let sumOld = histogram.snapshot().sum;
+ Assert.greater(
+ sumOld,
+ 0,
+ `Data recorded for first extension for histogram: ${TIMING_HISTOGRAM}.`
+ );
+
+ let oldKeyedSnapshot = histogramKeyed.snapshot();
+ Assert.deepEqual(
+ Object.keys(oldKeyedSnapshot),
+ [EXTENSION_ID1],
+ `Data recorded for first extension for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ Assert.greater(
+ oldKeyedSnapshot[EXTENSION_ID1].sum,
+ 0,
+ `Data recorded for first extension for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+
+ let gleanSumOld =
+ Glean.extensionsTiming.browserActionPopupOpen.testGetValue()?.sum;
+ Assert.greater(
+ gleanSumOld,
+ 0,
+ "Data recorded for first extension on glean metric extensionsTiming.browserActionPopupOpen"
+ );
+
+ await closeBrowserAction(extension1);
+
+ clickBrowserAction(extension2);
+ await awaitExtensionPanel(extension2);
+ let sumNew = histogram.snapshot().sum;
+ Assert.greater(
+ sumNew,
+ sumOld,
+ `Data recorded for second extension for histogram: ${TIMING_HISTOGRAM}.`
+ );
+ sumOld = sumNew;
+
+ let gleanSumNew =
+ Glean.extensionsTiming.browserActionPopupOpen.testGetValue()?.sum;
+ Assert.greater(
+ gleanSumNew,
+ gleanSumOld,
+ "Data recorded for second extension on glean metric extensionsTiming.browserActionPopupOpen"
+ );
+ gleanSumOld = gleanSumNew;
+
+ let newKeyedSnapshot = histogramKeyed.snapshot();
+ Assert.deepEqual(
+ Object.keys(newKeyedSnapshot).sort(),
+ [EXTENSION_ID1, EXTENSION_ID2],
+ `Data recorded for second extension for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ Assert.greater(
+ newKeyedSnapshot[EXTENSION_ID2].sum,
+ 0,
+ `Data recorded for second extension for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ is(
+ newKeyedSnapshot[EXTENSION_ID1].sum,
+ oldKeyedSnapshot[EXTENSION_ID1].sum,
+ `Data recorded for first extension should not change for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ oldKeyedSnapshot = newKeyedSnapshot;
+
+ await closeBrowserAction(extension2);
+
+ clickBrowserAction(extension2);
+ await awaitExtensionPanel(extension2);
+ sumNew = histogram.snapshot().sum;
+ Assert.greater(
+ sumNew,
+ sumOld,
+ `Data recorded for second opening of popup for histogram: ${TIMING_HISTOGRAM}.`
+ );
+ sumOld = sumNew;
+
+ gleanSumNew =
+ Glean.extensionsTiming.browserActionPopupOpen.testGetValue()?.sum;
+ Assert.greater(
+ gleanSumNew,
+ gleanSumOld,
+ "Data recorded for second popup opening on glean metric extensionsTiming.browserActionPopupOpen"
+ );
+ gleanSumOld = gleanSumNew;
+
+ newKeyedSnapshot = histogramKeyed.snapshot();
+ Assert.greater(
+ newKeyedSnapshot[EXTENSION_ID2].sum,
+ oldKeyedSnapshot[EXTENSION_ID2].sum,
+ `Data recorded for second opening of popup for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ is(
+ newKeyedSnapshot[EXTENSION_ID1].sum,
+ oldKeyedSnapshot[EXTENSION_ID1].sum,
+ `Data recorded for first extension should not change for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ oldKeyedSnapshot = newKeyedSnapshot;
+
+ await closeBrowserAction(extension2);
+
+ clickBrowserAction(extension1);
+ await awaitExtensionPanel(extension1);
+ sumNew = histogram.snapshot().sum;
+ Assert.greater(
+ sumNew,
+ sumOld,
+ `Data recorded for third opening of popup for histogram: ${TIMING_HISTOGRAM}.`
+ );
+
+ gleanSumNew =
+ Glean.extensionsTiming.browserActionPopupOpen.testGetValue()?.sum;
+ Assert.greater(
+ gleanSumNew,
+ gleanSumOld,
+ "Data recorded for third popup opening on glean metric extensionsTiming.browserActionPopupOpen"
+ );
+
+ newKeyedSnapshot = histogramKeyed.snapshot();
+ Assert.greater(
+ newKeyedSnapshot[EXTENSION_ID1].sum,
+ oldKeyedSnapshot[EXTENSION_ID1].sum,
+ `Data recorded for second opening of popup for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+ is(
+ newKeyedSnapshot[EXTENSION_ID2].sum,
+ oldKeyedSnapshot[EXTENSION_ID2].sum,
+ `Data recorded for second extension should not change for histogram: ${TIMING_HISTOGRAM_KEYED}.`
+ );
+
+ await closeBrowserAction(extension1);
+
+ await extension1.unload();
+ await extension2.unload();
+});
+
+add_task(async function testBrowserActionTelemetryResults() {
+ let extensionOptions = {
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: EXTENSION_ID1 },
+ },
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
`,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionOptions);
+
+ let histogram = Services.telemetry.getHistogramById(RESULT_HISTOGRAM);
+ let histogramKeyed = Services.telemetry.getKeyedHistogramById(
+ RESULT_HISTOGRAM_KEYED
+ );
+
+ histogram.clear();
+ histogramKeyed.clear();
+ Services.fog.testResetFOG();
+
+ is(
+ histogram.snapshot().sum,
+ 0,
+ `No data recorded for histogram: ${RESULT_HISTOGRAM}.`
+ );
+ is(
+ Object.keys(histogramKeyed).length,
+ 0,
+ `No data recorded for histogram: ${RESULT_HISTOGRAM_KEYED}.`
+ );
+ assertGleanPreloadResultLabelCounterEmpty();
+
+ await extension.startup();
+
+ // Make sure the mouse isn't hovering over the browserAction widget to start.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mouseover" },
+ window
+ );
+
+ let widget = getBrowserActionWidget(extension).forWindow(window);
+
+ // Hover the mouse over the browserAction widget and then move it away.
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseover", button: 0 },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseout", button: 0 },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.documentElement,
+ { type: "mousemove" },
+ window
+ );
+
+ assertOnlyOneTypeSet(histogram.snapshot(), "clearAfterHover");
+ assertGleanPreloadResultLabelCounter({ clearAfterHover: 1 });
+
+ let keyedSnapshot = histogramKeyed.snapshot();
+ Assert.deepEqual(
+ Object.keys(keyedSnapshot),
+ [EXTENSION_ID1],
+ `Data recorded for histogram: ${RESULT_HISTOGRAM_KEYED}.`
+ );
+ assertOnlyOneTypeSet(keyedSnapshot[EXTENSION_ID1], "clearAfterHover");
+
+ histogram.clear();
+ histogramKeyed.clear();
+ Services.fog.testResetFOG();
+
+ // TODO: Create a test for cancel after mousedown.
+ // This is tricky because calling mouseout after mousedown causes a
+ // "Hover" event to be added to the queue in ext-browserAction.js.
+
+ clickBrowserAction(extension);
+ await awaitExtensionPanel(extension);
+
+ assertOnlyOneTypeSet(histogram.snapshot(), "popupShown");
+ assertGleanPreloadResultLabelCounter({ popupShown: 1 });
+
+ keyedSnapshot = histogramKeyed.snapshot();
+ Assert.deepEqual(
+ Object.keys(keyedSnapshot),
+ [EXTENSION_ID1],
+ `Data recorded for histogram: ${RESULT_HISTOGRAM_KEYED}.`
+ );
+ assertOnlyOneTypeSet(keyedSnapshot[EXTENSION_ID1], "popupShown");
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_theme_icons.js b/browser/components/extensions/test/browser/browser_ext_browserAction_theme_icons.js
new file mode 100644
index 0000000000..6e3add4a1c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_theme_icons.js
@@ -0,0 +1,370 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const LIGHT_THEME_COLORS = {
+ frame: "#FFF",
+ tab_background_text: "#000",
+};
+
+const DARK_THEME_COLORS = {
+ frame: "#000",
+ tab_background_text: "#FFF",
+};
+
+const TOOLBAR_MAPPING = {
+ navbar: "nav-bar",
+ tabstrip: "TabsToolbar",
+};
+
+async function testBrowserAction(extension, expectedIcon) {
+ let browserActionWidget = getBrowserActionWidget(extension);
+ await promiseAnimationFrame();
+ let browserActionButton = browserActionWidget.forWindow(window).node;
+ let image = getListStyleImage(browserActionButton.firstElementChild);
+ ok(
+ image.includes(expectedIcon),
+ `Expected browser action icon (${image}) to be ${expectedIcon}`
+ );
+}
+
+async function testStaticTheme(options) {
+ let {
+ themeData,
+ themeIcons,
+ withDefaultIcon,
+ expectedIcon,
+ defaultArea = "navbar",
+ } = options;
+
+ let manifest = {
+ browser_action: {
+ theme_icons: themeIcons,
+ default_area: defaultArea,
+ },
+ };
+
+ if (withDefaultIcon) {
+ manifest.browser_action.default_icon = "default.png";
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({ manifest });
+
+ await extension.startup();
+
+ // Ensure we show the menupanel at least once. This makes sure that the
+ // elements we're going to query the style of are in the flat tree.
+ if (defaultArea == "menupanel") {
+ let shown = BrowserTestUtils.waitForPopupEvent(
+ window.gUnifiedExtensions.panel,
+ "shown"
+ );
+ window.gUnifiedExtensions.togglePanel();
+ await shown;
+ }
+
+ // Confirm that the browser action has the correct default icon before a theme is loaded.
+ let toolbarId = TOOLBAR_MAPPING[defaultArea];
+ let expectedDefaultIcon;
+ // Some platforms have dark toolbars by default, take it in account when picking the default icon.
+ if (
+ toolbarId &&
+ document.getElementById(toolbarId).hasAttribute("brighttext")
+ ) {
+ expectedDefaultIcon = "light.png";
+ } else {
+ expectedDefaultIcon = withDefaultIcon ? "default.png" : "dark.png";
+ }
+ await testBrowserAction(extension, expectedDefaultIcon);
+
+ let theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: themeData,
+ },
+ },
+ });
+
+ await theme.startup();
+
+ // Confirm that the correct icon is used when the theme is loaded.
+ if (expectedIcon == "dark") {
+ // The dark icon should be used if the area is light.
+ await testBrowserAction(extension, "dark.png");
+ } else {
+ // The light icon should be used if the area is dark.
+ await testBrowserAction(extension, "light.png");
+ }
+
+ await theme.unload();
+
+ // Confirm that the browser action has the correct default icon when the theme is unloaded.
+ await testBrowserAction(extension, expectedDefaultIcon);
+
+ await extension.unload();
+}
+
+add_task(async function browseraction_theme_icons_light_theme() {
+ await testStaticTheme({
+ themeData: LIGHT_THEME_COLORS,
+ expectedIcon: "dark",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 19,
+ },
+ ],
+ withDefaultIcon: true,
+ });
+ await testStaticTheme({
+ themeData: LIGHT_THEME_COLORS,
+ expectedIcon: "dark",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 16,
+ },
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 32,
+ },
+ ],
+ withDefaultIcon: false,
+ });
+});
+
+add_task(async function browseraction_theme_icons_dark_theme() {
+ await testStaticTheme({
+ themeData: DARK_THEME_COLORS,
+ expectedIcon: "light",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 19,
+ },
+ ],
+ withDefaultIcon: true,
+ });
+ await testStaticTheme({
+ themeData: DARK_THEME_COLORS,
+ expectedIcon: "light",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 16,
+ },
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 32,
+ },
+ ],
+ withDefaultIcon: false,
+ });
+});
+
+add_task(async function browseraction_theme_icons_different_toolbars() {
+ let themeData = {
+ frame: "#000",
+ tab_background_text: "#fff",
+ toolbar: "#fff",
+ bookmark_text: "#000",
+ };
+ await testStaticTheme({
+ themeData,
+ expectedIcon: "dark",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 19,
+ },
+ ],
+ withDefaultIcon: true,
+ });
+ await testStaticTheme({
+ themeData,
+ expectedIcon: "dark",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 16,
+ },
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 32,
+ },
+ ],
+ });
+ await testStaticTheme({
+ themeData,
+ expectedIcon: "light",
+ defaultArea: "tabstrip",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 19,
+ },
+ ],
+ withDefaultIcon: true,
+ });
+ await testStaticTheme({
+ themeData,
+ expectedIcon: "light",
+ defaultArea: "tabstrip",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 16,
+ },
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 32,
+ },
+ ],
+ });
+});
+
+add_task(async function browseraction_theme_icons_overflow_panel() {
+ let themeData = {
+ popup: "#000",
+ popup_text: "#fff",
+ };
+ await testStaticTheme({
+ themeData,
+ expectedIcon: "dark",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 19,
+ },
+ ],
+ withDefaultIcon: true,
+ });
+ await testStaticTheme({
+ themeData,
+ expectedIcon: "dark",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 16,
+ },
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 32,
+ },
+ ],
+ });
+
+ await testStaticTheme({
+ themeData,
+ expectedIcon: "light",
+ defaultArea: "menupanel",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 19,
+ },
+ ],
+ withDefaultIcon: true,
+ });
+ await testStaticTheme({
+ themeData,
+ expectedIcon: "light",
+ defaultArea: "menupanel",
+ themeIcons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 16,
+ },
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 32,
+ },
+ ],
+ });
+});
+
+add_task(async function browseraction_theme_icons_dynamic_theme() {
+ let themeExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["theme"],
+ },
+ background() {
+ browser.test.onMessage.addListener((msg, colors) => {
+ if (msg === "update-theme") {
+ browser.theme.update({
+ colors: colors,
+ });
+ browser.test.sendMessage("theme-updated");
+ }
+ });
+ },
+ });
+
+ await themeExtension.startup();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_icon: "default.png",
+ default_area: "navbar",
+ theme_icons: [
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 16,
+ },
+ {
+ light: "light.png",
+ dark: "dark.png",
+ size: 32,
+ },
+ ],
+ },
+ },
+ });
+
+ await extension.startup();
+
+ // Confirm that the browser action has the default icon before a theme is set.
+ await testBrowserAction(extension, "default.png");
+
+ // Update the theme to a light theme.
+ themeExtension.sendMessage("update-theme", LIGHT_THEME_COLORS);
+ await themeExtension.awaitMessage("theme-updated");
+
+ // Confirm that the dark icon is used for the light theme.
+ await testBrowserAction(extension, "dark.png");
+
+ // Update the theme to a dark theme.
+ themeExtension.sendMessage("update-theme", DARK_THEME_COLORS);
+ await themeExtension.awaitMessage("theme-updated");
+
+ // Confirm that the light icon is used for the dark theme.
+ await testBrowserAction(extension, "light.png");
+
+ // Unload the theme.
+ await themeExtension.unload();
+
+ // Confirm that the default icon is used when the theme is unloaded.
+ await testBrowserAction(extension, "default.png");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browsingData_cookieStoreId.js b/browser/components/extensions/test/browser/browser_ext_browsingData_cookieStoreId.js
new file mode 100644
index 0000000000..a7acbcfdf3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_cookieStoreId.js
@@ -0,0 +1,86 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_remove_unsupported() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ async background() {
+ for (let dataType of [
+ "cache",
+ "downloads",
+ "formData",
+ "history",
+ "passwords",
+ "pluginData",
+ "serviceWorkers",
+ ]) {
+ await browser.test.assertRejects(
+ browser.browsingData.remove(
+ { cookieStoreId: "firefox-default" },
+ {
+ [dataType]: true,
+ }
+ ),
+ `Firefox does not support clearing ${dataType} with 'cookieStoreId'.`,
+ `Should reject for unsupported dataType: ${dataType}`
+ );
+ }
+
+ // Smoke test that doesn't delete anything.
+ await browser.browsingData.remove(
+ { cookieStoreId: "firefox-container-1" },
+ {}
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_invalid_id() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ async background() {
+ for (let cookieStoreId of [
+ "firefox-DEFAULT", // should be "firefox-default"
+ "firefox-private222",
+ "firefox",
+ "firefox-container-",
+ "firefox-container-100000",
+ ]) {
+ await browser.test.assertRejects(
+ browser.browsingData.remove({ cookieStoreId }, { cookies: true }),
+ `Invalid cookieStoreId: ${cookieStoreId}`,
+ `Should reject invalid cookieStoreId: ${cookieStoreId}`
+ );
+ }
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browsingData_formData.js b/browser/components/extensions/test/browser/browser_ext_browsingData_formData.js
new file mode 100644
index 0000000000..06d928b9b1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_formData.js
@@ -0,0 +1,175 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+const REFERENCE_DATE = Date.now();
+
+async function countEntries(fieldname, message, expected) {
+ let count = await FormHistory.count({ fieldname });
+ is(count, expected, message);
+}
+
+async function setupFormHistory() {
+ async function searchFirstEntry(terms, params) {
+ return (await FormHistory.search(terms, params))[0];
+ }
+
+ // Make sure we've got a clean DB to start with, then add the entries we'll be testing.
+ await FormHistory.update([
+ { op: "remove" },
+ {
+ op: "add",
+ fieldname: "reference",
+ value: "reference",
+ },
+ {
+ op: "add",
+ fieldname: "10secondsAgo",
+ value: "10s",
+ },
+ {
+ op: "add",
+ fieldname: "10minutesAgo",
+ value: "10m",
+ },
+ ]);
+
+ // Age the entries to the proper vintage.
+ let timestamp = PlacesUtils.toPRTime(REFERENCE_DATE);
+ let result = await searchFirstEntry(["guid"], { fieldname: "reference" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: result.guid,
+ });
+
+ timestamp = PlacesUtils.toPRTime(REFERENCE_DATE - 10000);
+ result = await searchFirstEntry(["guid"], { fieldname: "10secondsAgo" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: result.guid,
+ });
+
+ timestamp = PlacesUtils.toPRTime(REFERENCE_DATE - 10000 * 60);
+ result = await searchFirstEntry(["guid"], { fieldname: "10minutesAgo" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: result.guid,
+ });
+
+ // Sanity check.
+ await countEntries(
+ "reference",
+ "Checking for 10minutes form history entry creation",
+ 1
+ );
+ await countEntries(
+ "10secondsAgo",
+ "Checking for 1hour form history entry creation",
+ 1
+ );
+ await countEntries(
+ "10minutesAgo",
+ "Checking for 1hour10minutes form history entry creation",
+ 1
+ );
+}
+
+add_task(async function testFormData() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, options) => {
+ if (msg == "removeFormData") {
+ await browser.browsingData.removeFormData(options);
+ } else {
+ await browser.browsingData.remove(options, { formData: true });
+ }
+ browser.test.sendMessage("formDataRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ async function testRemovalMethod(method) {
+ // Clear form data with no since value.
+ await setupFormHistory();
+ extension.sendMessage(method, {});
+ await extension.awaitMessage("formDataRemoved");
+
+ await countEntries(
+ "reference",
+ "reference form entry should be deleted.",
+ 0
+ );
+ await countEntries(
+ "10secondsAgo",
+ "10secondsAgo form entry should be deleted.",
+ 0
+ );
+ await countEntries(
+ "10minutesAgo",
+ "10minutesAgo form entry should be deleted.",
+ 0
+ );
+
+ // Clear form data with recent since value.
+ await setupFormHistory();
+ extension.sendMessage(method, { since: REFERENCE_DATE });
+ await extension.awaitMessage("formDataRemoved");
+
+ await countEntries(
+ "reference",
+ "reference form entry should be deleted.",
+ 0
+ );
+ await countEntries(
+ "10secondsAgo",
+ "10secondsAgo form entry should still exist.",
+ 1
+ );
+ await countEntries(
+ "10minutesAgo",
+ "10minutesAgo form entry should still exist.",
+ 1
+ );
+
+ // Clear form data with old since value.
+ await setupFormHistory();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 1000000 });
+ await extension.awaitMessage("formDataRemoved");
+
+ await countEntries(
+ "reference",
+ "reference form entry should be deleted.",
+ 0
+ );
+ await countEntries(
+ "10secondsAgo",
+ "10secondsAgo form entry should be deleted.",
+ 0
+ );
+ await countEntries(
+ "10minutesAgo",
+ "10minutesAgo form entry should be deleted.",
+ 0
+ );
+ }
+
+ await extension.startup();
+
+ await testRemovalMethod("removeFormData");
+ await testRemovalMethod("remove");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_browsingData_history.js b/browser/components/extensions/test/browser/browser_ext_browsingData_history.js
new file mode 100644
index 0000000000..2f696f3154
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_history.js
@@ -0,0 +1,123 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+const OLD_URL = "http://example.com/";
+const RECENT_URL = "http://example.com/2/";
+const REFERENCE_DATE = new Date();
+
+// Visits to add via addVisits
+const PLACEINFO = [
+ {
+ uri: RECENT_URL,
+ title: `test visit for ${RECENT_URL}`,
+ visitDate: REFERENCE_DATE,
+ },
+ {
+ uri: OLD_URL,
+ title: `test visit for ${OLD_URL}`,
+ visitDate: new Date(Number(REFERENCE_DATE) - 1000),
+ },
+ {
+ uri: OLD_URL,
+ title: `test visit for ${OLD_URL}`,
+ visitDate: new Date(Number(REFERENCE_DATE) - 2000),
+ },
+];
+
+async function setupHistory() {
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(PLACEINFO);
+ is(
+ await PlacesTestUtils.visitsInDB(RECENT_URL),
+ 1,
+ "Expected number of visits found in history database."
+ );
+ is(
+ await PlacesTestUtils.visitsInDB(OLD_URL),
+ 2,
+ "Expected number of visits found in history database."
+ );
+}
+
+add_task(async function testHistory() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, options) => {
+ if (msg == "removeHistory") {
+ await browser.browsingData.removeHistory(options);
+ } else {
+ await browser.browsingData.remove(options, { history: true });
+ }
+ browser.test.sendMessage("historyRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ async function testRemovalMethod(method) {
+ // Clear history with no since value.
+ await setupHistory();
+ extension.sendMessage(method, {});
+ await extension.awaitMessage("historyRemoved");
+
+ is(
+ await PlacesTestUtils.visitsInDB(RECENT_URL),
+ 0,
+ "Expected number of visits removed from history database."
+ );
+ is(
+ await PlacesTestUtils.visitsInDB(OLD_URL),
+ 0,
+ "Expected number of visits removed from history database."
+ );
+
+ // Clear history with recent since value.
+ await setupHistory();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 1000 });
+ await extension.awaitMessage("historyRemoved");
+
+ is(
+ await PlacesTestUtils.visitsInDB(RECENT_URL),
+ 0,
+ "Expected number of visits removed from history database."
+ );
+ is(
+ await PlacesTestUtils.visitsInDB(OLD_URL),
+ 1,
+ "Expected number of visits removed from history database."
+ );
+
+ // Clear history with old since value.
+ await setupHistory();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 100000 });
+ await extension.awaitMessage("historyRemoved");
+
+ is(
+ await PlacesTestUtils.visitsInDB(RECENT_URL),
+ 0,
+ "Expected number of visits removed from history database."
+ );
+ is(
+ await PlacesTestUtils.visitsInDB(OLD_URL),
+ 0,
+ "Expected number of visits removed from history database."
+ );
+ }
+
+ await extension.startup();
+
+ await testRemovalMethod("removeHistory");
+ await testRemovalMethod("remove");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
new file mode 100644
index 0000000000..b67952c03c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
@@ -0,0 +1,843 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+requestLongerTimeout(4);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ ExtensionControlledPopup:
+ "resource:///modules/ExtensionControlledPopup.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+ HomePage: "resource:///modules/HomePage.sys.mjs",
+});
+
+// Named this way so they correspond to the extensions
+const HOME_URI_2 = "http://example.com/";
+const HOME_URI_3 = "http://example.org/";
+const HOME_URI_4 = "http://example.net/";
+
+const CONTROLLED_BY_THIS = "controlled_by_this_extension";
+const CONTROLLED_BY_OTHER = "controlled_by_other_extensions";
+const NOT_CONTROLLABLE = "not_controllable";
+
+const HOMEPAGE_URL_PREF = "browser.startup.homepage";
+
+const getHomePageURL = () => {
+ return Services.prefs.getStringPref(HOMEPAGE_URL_PREF);
+};
+
+function isConfirmed(id) {
+ let item = ExtensionSettingsStore.getSetting("homepageNotification", id);
+ return !!(item && item.value);
+}
+
+async function assertPreferencesShown(_spotlight) {
+ await TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:preferences#home",
+ "Should open about:preferences."
+ );
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [_spotlight],
+ async spotlight => {
+ let doc = content.document;
+ let section = await ContentTaskUtils.waitForCondition(
+ () => doc.querySelector(".spotlight"),
+ "The spotlight should appear."
+ );
+ Assert.equal(
+ section.getAttribute("data-subcategory"),
+ spotlight,
+ "The correct section is spotlighted."
+ );
+ }
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+add_task(async function test_multiple_extensions_overriding_home_page() {
+ let defaultHomePage = getHomePageURL();
+
+ function background() {
+ browser.test.onMessage.addListener(async msg => {
+ switch (msg) {
+ case "checkHomepage":
+ let homepage = await browser.browserSettings.homepageOverride.get({});
+ browser.test.sendMessage("homepage", homepage);
+ break;
+ case "trySet":
+ let setResult = await browser.browserSettings.homepageOverride.set({
+ value: "foo",
+ });
+ browser.test.assertFalse(
+ setResult,
+ "Calling homepageOverride.set returns false."
+ );
+ browser.test.sendMessage("homepageSet");
+ break;
+ case "tryClear":
+ let clearResult =
+ await browser.browserSettings.homepageOverride.clear({});
+ browser.test.assertFalse(
+ clearResult,
+ "Calling homepageOverride.clear returns false."
+ );
+ browser.test.sendMessage("homepageCleared");
+ break;
+ }
+ });
+ }
+
+ let extObj = {
+ manifest: {
+ chrome_settings_overrides: {},
+ permissions: ["browserSettings"],
+ },
+ useAddonManager: "temporary",
+ background,
+ };
+
+ let ext1 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_settings_overrides = { homepage: HOME_URI_2 };
+ let ext2 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_settings_overrides = { homepage: HOME_URI_3 };
+ let ext3 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_settings_overrides = { homepage: HOME_URI_4 };
+ let ext4 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_settings_overrides = {};
+ let ext5 = ExtensionTestUtils.loadExtension(extObj);
+
+ async function checkHomepageOverride(
+ ext,
+ expectedValue,
+ expectedLevelOfControl
+ ) {
+ ext.sendMessage("checkHomepage");
+ let homepage = await ext.awaitMessage("homepage");
+ is(
+ homepage.value,
+ expectedValue,
+ `homepageOverride setting returns the expected value: ${expectedValue}.`
+ );
+ is(
+ homepage.levelOfControl,
+ expectedLevelOfControl,
+ `homepageOverride setting returns the expected levelOfControl: ${expectedLevelOfControl}.`
+ );
+ }
+
+ await ext1.startup();
+
+ is(getHomePageURL(), defaultHomePage, "Home url should be the default");
+ await checkHomepageOverride(ext1, getHomePageURL(), NOT_CONTROLLABLE);
+
+ // Because we are expecting the pref to change when we start or unload, we
+ // need to wait on a pref change. This is because the pref management is
+ // async and can happen after the startup/unload is finished.
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext2.startup();
+ await prefPromise;
+
+ ok(
+ getHomePageURL().endsWith(HOME_URI_2),
+ "Home url should be overridden by the second extension."
+ );
+
+ await checkHomepageOverride(ext1, HOME_URI_2, CONTROLLED_BY_OTHER);
+
+ // Verify that calling set and clear do nothing.
+ ext2.sendMessage("trySet");
+ await ext2.awaitMessage("homepageSet");
+ await checkHomepageOverride(ext1, HOME_URI_2, CONTROLLED_BY_OTHER);
+
+ ext2.sendMessage("tryClear");
+ await ext2.awaitMessage("homepageCleared");
+ await checkHomepageOverride(ext1, HOME_URI_2, CONTROLLED_BY_OTHER);
+
+ // Because we are unloading an earlier extension, browser.startup.homepage won't change
+ await ext1.unload();
+
+ await checkHomepageOverride(ext2, HOME_URI_2, CONTROLLED_BY_THIS);
+
+ ok(
+ getHomePageURL().endsWith(HOME_URI_2),
+ "Home url should be overridden by the second extension."
+ );
+
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext3.startup();
+ await prefPromise;
+
+ ok(
+ getHomePageURL().endsWith(HOME_URI_3),
+ "Home url should be overridden by the third extension."
+ );
+
+ await checkHomepageOverride(ext3, HOME_URI_3, CONTROLLED_BY_THIS);
+
+ // Because we are unloading an earlier extension, browser.startup.homepage won't change
+ await ext2.unload();
+
+ ok(
+ getHomePageURL().endsWith(HOME_URI_3),
+ "Home url should be overridden by the third extension."
+ );
+
+ await checkHomepageOverride(ext3, HOME_URI_3, CONTROLLED_BY_THIS);
+
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext4.startup();
+ await prefPromise;
+
+ ok(
+ getHomePageURL().endsWith(HOME_URI_4),
+ "Home url should be overridden by the third extension."
+ );
+
+ await checkHomepageOverride(ext3, HOME_URI_4, CONTROLLED_BY_OTHER);
+
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext4.unload();
+ await prefPromise;
+
+ ok(
+ getHomePageURL().endsWith(HOME_URI_3),
+ "Home url should be overridden by the third extension."
+ );
+
+ await checkHomepageOverride(ext3, HOME_URI_3, CONTROLLED_BY_THIS);
+
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext3.unload();
+ await prefPromise;
+
+ is(getHomePageURL(), defaultHomePage, "Home url should be reset to default");
+
+ await ext5.startup();
+ await checkHomepageOverride(ext5, defaultHomePage, NOT_CONTROLLABLE);
+ await ext5.unload();
+});
+
+const HOME_URI_1 = "http://example.com/";
+const USER_URI = "http://example.edu/";
+
+add_task(async function test_extension_setting_home_page_back() {
+ let defaultHomePage = getHomePageURL();
+
+ Services.prefs.setStringPref(HOMEPAGE_URL_PREF, USER_URI);
+
+ is(getHomePageURL(), USER_URI, "Home url should be the user set value");
+
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: { chrome_settings_overrides: { homepage: HOME_URI_1 } },
+ useAddonManager: "temporary",
+ });
+
+ // Because we are expecting the pref to change when we start or unload, we
+ // need to wait on a pref change. This is because the pref management is
+ // async and can happen after the startup/unload is finished.
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext1.startup();
+ await prefPromise;
+
+ ok(
+ getHomePageURL().endsWith(HOME_URI_1),
+ "Home url should be overridden by the second extension."
+ );
+
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext1.unload();
+ await prefPromise;
+
+ is(getHomePageURL(), USER_URI, "Home url should be the user set value");
+
+ Services.prefs.clearUserPref(HOMEPAGE_URL_PREF);
+
+ is(getHomePageURL(), defaultHomePage, "Home url should be the default");
+});
+
+add_task(async function test_disable() {
+ const ID = "id@tests.mozilla.org";
+ let defaultHomePage = getHomePageURL();
+
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOME_URI_1,
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext1.startup();
+ await prefPromise;
+
+ is(
+ getHomePageURL(),
+ HOME_URI_1,
+ "Home url should be overridden by the extension."
+ );
+
+ let addon = await AddonManager.getAddonByID(ID);
+ is(addon.id, ID, "Found the correct add-on.");
+
+ let disabledPromise = awaitEvent("shutdown", ID);
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await addon.disable();
+ await Promise.all([disabledPromise, prefPromise]);
+
+ is(getHomePageURL(), defaultHomePage, "Home url should be the default");
+
+ let enabledPromise = awaitEvent("ready", ID);
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await addon.enable();
+ await Promise.all([enabledPromise, prefPromise]);
+
+ is(
+ getHomePageURL(),
+ HOME_URI_1,
+ "Home url should be overridden by the extension."
+ );
+
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext1.unload();
+ await prefPromise;
+
+ is(getHomePageURL(), defaultHomePage, "Home url should be the default");
+});
+
+add_task(async function test_local() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: { chrome_settings_overrides: { homepage: "home.html" } },
+ useAddonManager: "temporary",
+ });
+
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext1.startup();
+ await prefPromise;
+
+ let homepage = getHomePageURL();
+ ok(
+ homepage.startsWith("moz-extension") && homepage.endsWith("home.html"),
+ "Home url should be relative to extension."
+ );
+
+ await ext1.unload();
+});
+
+add_task(async function test_multiple() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ homepage:
+ "https://mozilla.org/|https://developer.mozilla.org/|https://addons.mozilla.org/",
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await extension.startup();
+ await prefPromise;
+
+ is(
+ getHomePageURL(),
+ "https://mozilla.org/%7Chttps://developer.mozilla.org/%7Chttps://addons.mozilla.org/",
+ "The homepage encodes | so only one homepage is allowed"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_doorhanger_homepage_button() {
+ let defaultHomePage = getHomePageURL();
+ // These extensions are temporarily loaded so that the AddonManager can see
+ // them and the extension's shutdown handlers are called.
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: { chrome_settings_overrides: { homepage: "ext1.html" } },
+ files: { "ext1.html": "1 " },
+ useAddonManager: "temporary",
+ });
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: { chrome_settings_overrides: { homepage: "ext2.html" } },
+ files: { "ext2.html": "2 " },
+ useAddonManager: "temporary",
+ });
+
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(document);
+ let popupnotification = document.getElementById(
+ "extension-homepage-notification"
+ );
+
+ await ext1.startup();
+ await ext2.startup();
+
+ let popupShown = promisePopupShown(panel);
+ BrowserHome();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, () =>
+ gURLBar.value.endsWith("ext2.html")
+ );
+ await popupShown;
+
+ // Click Manage.
+ let popupHidden = promisePopupHidden(panel);
+ // Ensures the preferences tab opens, checks the spotlight, and then closes it
+ let spotlightShown = assertPreferencesShown("homeOverride");
+ popupnotification.secondaryButton.click();
+ await popupHidden;
+ await spotlightShown;
+
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext2.unload();
+ await prefPromise;
+
+ // Expect a new doorhanger for the next extension.
+ popupShown = promisePopupShown(panel);
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+ let openHomepage = TestUtils.topicObserved("browser-open-homepage-start");
+ BrowserHome();
+ await openHomepage;
+ await popupShown;
+ await TestUtils.waitForCondition(
+ () => gURLBar.value.endsWith("ext1.html"),
+ "ext1 is in control"
+ );
+
+ // Click manage again.
+ popupHidden = promisePopupHidden(panel);
+ // Ensures the preferences tab opens, checks the spotlight, and then closes it
+ spotlightShown = assertPreferencesShown("homeOverride");
+ popupnotification.secondaryButton.click();
+ await popupHidden;
+ await spotlightShown;
+
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext1.unload();
+ await prefPromise;
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ openHomepage = TestUtils.topicObserved("browser-open-homepage-start");
+ BrowserHome();
+ await openHomepage;
+
+ is(getHomePageURL(), defaultHomePage, "The homepage is set back to default");
+});
+
+add_task(async function test_doorhanger_new_window() {
+ // These extensions are temporarily loaded so that the AddonManager can see
+ // them and the extension's shutdown handlers are called.
+ let ext1Id = "ext1@mochi.test";
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: { homepage: "ext1.html" },
+ browser_specific_settings: {
+ gecko: { id: ext1Id },
+ },
+ name: "Ext1",
+ },
+ files: { "ext1.html": "1 " },
+ useAddonManager: "temporary",
+ });
+ let ext2 = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("url", browser.runtime.getURL("ext2.html"));
+ },
+ manifest: {
+ chrome_settings_overrides: { homepage: "ext2.html" },
+ name: "Ext2",
+ },
+ files: { "ext2.html": "2 " },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await ext2.startup();
+ let url = await ext2.awaitMessage("url");
+
+ await SpecialPowers.pushPrefEnv({ set: [["browser.startup.page", 1]] });
+
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow({ url });
+ let win = OpenBrowserWindow();
+ await windowOpenedPromise;
+ let doc = win.document;
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(doc);
+ await promisePopupShown(panel);
+
+ let description = doc.getElementById(
+ "extension-homepage-notification-description"
+ );
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.value.endsWith("ext2.html"),
+ "ext2 is in control"
+ );
+
+ is(
+ description.textContent,
+ "An extension, Ext2, changed what you see when you open your homepage and new windows.Learn more",
+ "The extension name is in the popup"
+ );
+
+ // Click Manage.
+ let popupHidden = promisePopupHidden(panel);
+ let popupnotification = doc.getElementById("extension-homepage-notification");
+ popupnotification.secondaryButton.click();
+ await popupHidden;
+
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ await ext2.unload();
+ await prefPromise;
+
+ // Expect a new doorhanger for the next extension.
+ let popupShown = promisePopupShown(panel);
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank");
+ let openHomepage = TestUtils.topicObserved("browser-open-homepage-start");
+ win.BrowserHome();
+ await openHomepage;
+ await popupShown;
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.value.endsWith("ext1.html"),
+ "ext1 is in control"
+ );
+
+ is(
+ description.textContent,
+ "An extension, Ext1, changed what you see when you open your homepage and new windows.Learn more",
+ "The extension name is in the popup"
+ );
+
+ // Click Keep Changes.
+ popupnotification.button.click();
+ await TestUtils.waitForCondition(() => isConfirmed(ext1Id));
+
+ ok(
+ getHomePageURL().endsWith("ext1.html"),
+ "The homepage is still the first eextension"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await ext1.unload();
+
+ ok(!isConfirmed(ext1Id), "The confirmation is cleaned up on uninstall");
+ // Skipping for window leak in debug builds, follow up bug: 1678412
+}).skip(AppConstants.DEBUG);
+
+async function testHomePageWindow(options = {}) {
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow(options.options);
+ let openHomepage = TestUtils.topicObserved("browser-open-homepage-start");
+ await windowOpenedPromise;
+ let doc = win.document;
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(doc);
+
+ let popupShown = options.expectPanel && promisePopupShown(panel);
+ win.BrowserHome();
+ await Promise.all([
+ BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser),
+ openHomepage,
+ popupShown,
+ ]);
+
+ await options.test(win);
+
+ if (options.expectPanel) {
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+ }
+ await BrowserTestUtils.closeWindow(win);
+}
+
+add_task(async function test_overriding_home_page_incognito_not_allowed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.startup.page", 1]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: { homepage: "home.html" },
+ name: "extension",
+ },
+ files: { "home.html": "1 " },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ let url = `moz-extension://${extension.uuid}/home.html`;
+
+ await testHomePageWindow({
+ expectPanel: true,
+ test(win) {
+ let doc = win.document;
+ let description = doc.getElementById(
+ "extension-homepage-notification-description"
+ );
+ let popupnotification = description.closest("popupnotification");
+ is(
+ description.textContent,
+ "An extension, extension, changed what you see when you open your homepage and new windows.Learn more",
+ "The extension name is in the popup"
+ );
+ is(
+ popupnotification.hidden,
+ false,
+ "The expected popup notification is visible"
+ );
+
+ Assert.equal(HomePage.get(win), url, "The homepage is not set");
+ Assert.equal(
+ win.gURLBar.value,
+ url,
+ "home page not used in private window"
+ );
+ },
+ });
+
+ await testHomePageWindow({
+ expectPanel: false,
+ options: { private: true },
+ test(win) {
+ Assert.notEqual(HomePage.get(win), url, "The homepage is not set");
+ Assert.notEqual(
+ win.gURLBar.value,
+ url,
+ "home page not used in private window"
+ );
+ },
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_overriding_home_page_incognito_spanning() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: { homepage: "home.html" },
+ name: "private extension",
+ browser_specific_settings: {
+ gecko: { id: "@spanning-home" },
+ },
+ },
+ files: { "home.html": "1 " },
+ useAddonManager: "permanent",
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+
+ // private window uses extension homepage
+ await testHomePageWindow({
+ expectPanel: true,
+ options: { private: true },
+ test(win) {
+ Assert.equal(
+ HomePage.get(win),
+ `moz-extension://${extension.uuid}/home.html`,
+ "The homepage is set"
+ );
+ Assert.equal(
+ win.gURLBar.value,
+ `moz-extension://${extension.uuid}/home.html`,
+ "extension is control in window"
+ );
+ },
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_overriding_home_page_incognito_external() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: { homepage: "/home.html" },
+ name: "extension",
+ },
+ useAddonManager: "temporary",
+ files: { "home.html": "non-private home " },
+ });
+
+ await extension.startup();
+
+ // non-private window uses extension homepage
+ await testHomePageWindow({
+ expectPanel: true,
+ test(win) {
+ Assert.equal(
+ HomePage.get(win),
+ `moz-extension://${extension.uuid}/home.html`,
+ "The homepage is set"
+ );
+ Assert.equal(
+ win.gURLBar.value,
+ `moz-extension://${extension.uuid}/home.html`,
+ "extension is control in window"
+ );
+ },
+ });
+
+ // private window does not use extension window
+ await testHomePageWindow({
+ expectPanel: false,
+ options: { private: true },
+ test(win) {
+ Assert.notEqual(
+ HomePage.get(win),
+ `moz-extension://${extension.uuid}/home.html`,
+ "The homepage is not set"
+ );
+ Assert.notEqual(
+ win.gURLBar.value,
+ `moz-extension://${extension.uuid}/home.html`,
+ "home page not used in private window"
+ );
+ },
+ });
+
+ await extension.unload();
+});
+
+// This tests that the homepage provided by an extension can be opened by any extension
+// and does not require web_accessible_resource entries.
+async function _test_overriding_home_page_open(manifest_version) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ chrome_settings_overrides: { homepage: "home.html" },
+ name: "homepage provider",
+ browser_specific_settings: {
+ gecko: { id: "homepage@mochitest" },
+ },
+ },
+ files: {
+ "home.html": `Home Page! `,
+ "home.js": () => {
+ document.querySelector("#result").textContent = "homepage loaded";
+ },
+ },
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+
+ // ensure it works and deal with initial panel prompt.
+ await testHomePageWindow({
+ expectPanel: true,
+ async test(win) {
+ Assert.equal(
+ HomePage.get(win),
+ `moz-extension://${extension.uuid}/home.html`,
+ "The homepage is set"
+ );
+ Assert.equal(
+ win.gURLBar.value,
+ `moz-extension://${extension.uuid}/home.html`,
+ "extension is control in window"
+ );
+ const { selectedBrowser } = win.gBrowser;
+ const result = await SpecialPowers.spawn(
+ selectedBrowser,
+ [],
+ async () => {
+ const { document } = this.content;
+ if (document.readyState !== "complete") {
+ await new Promise(resolve => (document.onload = resolve));
+ }
+ return document.querySelector("#result").textContent;
+ }
+ );
+ Assert.equal(
+ result,
+ "homepage loaded",
+ "Overridden homepage loaded successfully"
+ );
+ },
+ });
+
+ // Extension used to open the homepage in a new window.
+ let opener = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ let win;
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
+ if (tab.windowId !== win.id || tab.status !== "complete") {
+ return;
+ }
+ browser.test.sendMessage("created", tab.url);
+ });
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "create") {
+ win = await browser.windows.create({});
+ browser.test.assertTrue(
+ win.id !== browser.windows.WINDOW_ID_NONE,
+ "New window was created."
+ );
+ }
+ });
+ },
+ });
+
+ function listener(msg) {
+ Assert.ok(!/may not load or link to moz-extension/.test(msg.message));
+ }
+ Services.console.registerListener(listener);
+ registerCleanupFunction(() => {
+ Services.console.unregisterListener(listener);
+ });
+
+ await opener.startup();
+ const promiseNewWindow = BrowserTestUtils.waitForNewWindow();
+ await opener.sendMessage("create");
+ let homepageUrl = await opener.awaitMessage("created");
+
+ Assert.equal(
+ homepageUrl,
+ `moz-extension://${extension.uuid}/home.html`,
+ "The homepage is set"
+ );
+
+ const newWin = await promiseNewWindow;
+ Assert.equal(
+ await SpecialPowers.spawn(newWin.gBrowser.selectedBrowser, [], async () => {
+ const { document } = this.content;
+ if (document.readyState !== "complete") {
+ await new Promise(resolve => (document.onload = resolve));
+ }
+ return document.querySelector("#result").textContent;
+ }),
+ "homepage loaded",
+ "Overridden homepage loaded as expected"
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+ await opener.unload();
+ await extension.unload();
+}
+
+add_task(async function test_overriding_home_page_open_mv2() {
+ await _test_overriding_home_page_open(2);
+});
+
+add_task(async function test_overriding_home_page_open_mv3() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+ await _test_overriding_home_page_open(3);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js b/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
new file mode 100644
index 0000000000..526dfbbeeb
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
@@ -0,0 +1,194 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testTabSwitchActionContext() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+async function testExecuteBrowserActionWithOptions(options = {}) {
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mouseover" },
+ window
+ );
+
+ let extensionOptions = {};
+
+ let browser_action =
+ options.manifest_version > 2 ? "action" : "browser_action";
+ let browser_action_key = options.manifest_version > 2 ? "a" : "j";
+
+ // We accept any command in the manifest, so here we add commands for
+ // both V2 and V3, but only the command that matches the manifest version
+ // should ever work.
+ extensionOptions.manifest = {
+ manifest_version: options.manifest_version || 2,
+ commands: {
+ _execute_browser_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ _execute_action: {
+ suggested_key: {
+ default: "Alt+Shift+A",
+ },
+ },
+ },
+ [browser_action]: {
+ browser_style: true,
+ },
+ };
+
+ if (options.withPopup) {
+ extensionOptions.manifest[browser_action].default_popup = "popup.html";
+
+ extensionOptions.files = {
+ "popup.html": `
+
+
+
+
+
+
+
+ `,
+
+ "popup.js": function () {
+ browser.runtime.sendMessage("from-browser-action-popup");
+ },
+ };
+ }
+
+ extensionOptions.background = () => {
+ let manifest = browser.runtime.getManifest();
+ let { manifest_version } = manifest;
+ const action = manifest_version < 3 ? "browserAction" : "action";
+
+ browser.test.onMessage.addListener((message, options) => {
+ browser.commands.onCommand.addListener(commandName => {
+ if (
+ ["_execute_browser_action", "_execute_action"].includes(commandName)
+ ) {
+ browser.test.assertEq(
+ commandName,
+ options.expectedCommand,
+ `The onCommand listener fired for ${commandName}.`
+ );
+ browser[action].openPopup();
+ }
+ });
+
+ if (!options.expectedCommand) {
+ browser[action].onClicked.addListener(() => {
+ if (options.withPopup) {
+ browser.test.fail(
+ "The onClick listener should never fire if the browserAction has a popup."
+ );
+ browser.test.notifyFail("execute-browser-action-on-clicked-fired");
+ } else {
+ browser.test.notifyPass("execute-browser-action-on-clicked-fired");
+ }
+ });
+ }
+
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "from-browser-action-popup") {
+ browser.test.notifyPass("execute-browser-action-popup-opened");
+ }
+ });
+
+ browser.test.sendMessage("send-keys");
+ });
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionOptions);
+
+ extension.onMessage("send-keys", () => {
+ EventUtils.synthesizeKey(browser_action_key, {
+ altKey: true,
+ shiftKey: true,
+ });
+ });
+
+ await extension.startup();
+
+ await SimpleTest.promiseFocus(window);
+
+ if (options.inArea) {
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, options.inArea);
+ }
+
+ extension.sendMessage("options", options);
+
+ if (options.withPopup) {
+ await extension.awaitFinish("execute-browser-action-popup-opened");
+
+ if (!getBrowserActionPopup(extension)) {
+ await awaitExtensionPanel(extension);
+ }
+ await closeBrowserAction(extension);
+ } else {
+ await extension.awaitFinish("execute-browser-action-on-clicked-fired");
+ }
+ await extension.unload();
+}
+
+add_task(async function test_execute_browser_action_with_popup() {
+ await testExecuteBrowserActionWithOptions({
+ withPopup: true,
+ });
+});
+
+add_task(async function test_execute_browser_action_without_popup() {
+ await testExecuteBrowserActionWithOptions();
+});
+
+add_task(async function test_execute_browser_action_command() {
+ await testExecuteBrowserActionWithOptions({
+ withPopup: true,
+ expectedCommand: "_execute_browser_action",
+ });
+});
+
+add_task(async function test_execute_action_with_popup() {
+ await testExecuteBrowserActionWithOptions({
+ withPopup: true,
+ manifest_version: 3,
+ });
+});
+
+add_task(async function test_execute_action_without_popup() {
+ await testExecuteBrowserActionWithOptions({
+ manifest_version: 3,
+ });
+});
+
+add_task(async function test_execute_action_command() {
+ await testExecuteBrowserActionWithOptions({
+ withPopup: true,
+ expectedCommand: "_execute_action",
+ });
+});
+
+add_task(
+ async function test_execute_browser_action_in_hamburger_menu_with_popup() {
+ await testExecuteBrowserActionWithOptions({
+ withPopup: true,
+ inArea: getCustomizableUIPanelID(),
+ });
+ }
+);
+
+add_task(
+ async function test_execute_browser_action_in_hamburger_menu_without_popup() {
+ await testExecuteBrowserActionWithOptions({
+ inArea: getCustomizableUIPanelID(),
+ });
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js b/browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js
new file mode 100644
index 0000000000..12c4bb7ef8
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js
@@ -0,0 +1,204 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const scriptPage = url =>
+ ` Test Popup`;
+
+add_task(async function test_execute_page_action_without_popup() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: {
+ _execute_page_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ "send-keys-command": {
+ suggested_key: {
+ default: "Alt+Shift+3",
+ },
+ },
+ },
+ page_action: {},
+ },
+
+ background: function () {
+ let isShown = false;
+
+ browser.commands.onCommand.addListener(commandName => {
+ if (commandName == "_execute_page_action") {
+ browser.test.fail(
+ `The onCommand listener should never fire for ${commandName}.`
+ );
+ } else if (commandName == "send-keys-command") {
+ if (!isShown) {
+ isShown = true;
+ browser.tabs.query({ currentWindow: true, active: true }, tabs => {
+ tabs.forEach(tab => {
+ browser.pageAction.show(tab.id);
+ });
+ browser.test.sendMessage("send-keys");
+ });
+ }
+ }
+ });
+
+ browser.pageAction.onClicked.addListener(() => {
+ browser.test.assertTrue(
+ isShown,
+ "The onClicked event should fire if the page action is shown."
+ );
+ browser.test.notifyPass("page-action-without-popup");
+ });
+
+ browser.test.sendMessage("send-keys");
+ },
+ });
+
+ extension.onMessage("send-keys", () => {
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("page-action-without-popup");
+ await extension.unload();
+});
+
+add_task(async function test_execute_page_action_with_popup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: {
+ _execute_page_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ "send-keys-command": {
+ suggested_key: {
+ default: "Alt+Shift+3",
+ },
+ },
+ },
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": function () {
+ browser.runtime.sendMessage("popup-opened");
+ },
+ },
+
+ background: function () {
+ let isShown = false;
+
+ browser.commands.onCommand.addListener(message => {
+ if (message == "_execute_page_action") {
+ browser.test.fail(
+ `The onCommand listener should never fire for ${message}.`
+ );
+ }
+
+ if (message == "send-keys-command") {
+ if (!isShown) {
+ isShown = true;
+ browser.tabs.query({ currentWindow: true, active: true }, tabs => {
+ tabs.forEach(tab => {
+ browser.pageAction.show(tab.id);
+ });
+ browser.test.sendMessage("send-keys");
+ });
+ }
+ }
+ });
+
+ browser.pageAction.onClicked.addListener(() => {
+ browser.test.fail(
+ `The onClicked listener should never fire when the pageAction has a popup.`
+ );
+ });
+
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "popup-opened", "expected popup opened");
+ browser.test.assertTrue(
+ isShown,
+ "The onClicked event should fire if the page action is shown."
+ );
+ browser.test.notifyPass("page-action-with-popup");
+ });
+
+ browser.test.sendMessage("send-keys");
+ },
+ });
+
+ extension.onMessage("send-keys", () => {
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("page-action-with-popup");
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_execute_page_action_with_matching() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: {
+ _execute_page_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ page_action: {
+ default_popup: "popup.html",
+ show_matches: [""],
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": function () {
+ window.addEventListener(
+ "load",
+ () => {
+ browser.test.notifyPass("page-action-with-popup");
+ },
+ { once: true }
+ );
+ },
+ },
+ });
+
+ await extension.startup();
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "http://example.com/"
+ );
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ info("Waiting for pageAction open.");
+ await extension.awaitFinish("page-action-with-popup");
+
+ // Bug 1447796 make sure the key command can close the page action
+ let panel = document.getElementById(`${makeWidgetId(extension.id)}-panel`);
+ let hidden = promisePopupHidden(panel);
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ info("Waiting for pageAction close.");
+ await hidden;
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_commands_execute_sidebar_action.js b/browser/components/extensions/test/browser/browser_ext_commands_execute_sidebar_action.js
new file mode 100644
index 0000000000..0f96138d09
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_sidebar_action.js
@@ -0,0 +1,56 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_execute_sidebar_action() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: {
+ _execute_sidebar_action: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ // We want to confirm that sidebar is not shown on every extension start,
+ // so we use an explicit APP_STARTUP.
+ startupReason: "APP_STARTUP",
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+
+ `,
+
+ "sidebar.js": function () {
+ browser.runtime.sendMessage("from-sidebar-action");
+ },
+ },
+ background() {
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "from-sidebar-action") {
+ browser.test.notifyPass("execute-sidebar-action-opened");
+ }
+ });
+ },
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ `Sidebar box is not visible after "not-first" startup.`
+ );
+ // Send the key to open the sidebar.
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ await extension.awaitFinish("execute-sidebar-action-opened");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_commands_getAll.js b/browser/components/extensions/test/browser/browser_ext_commands_getAll.js
new file mode 100644
index 0000000000..d9b4c16272
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_getAll.js
@@ -0,0 +1,142 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "_locales/en/messages.json": {
+ with_translation: {
+ message: "The description",
+ description: "A description",
+ },
+ },
+ },
+ manifest: {
+ name: "Commands Extension",
+ default_locale: "en",
+ commands: {
+ "with-desciption": {
+ suggested_key: {
+ default: "Ctrl+Shift+Y",
+ },
+ description: "should have a description",
+ },
+ "without-description": {
+ suggested_key: {
+ default: "Ctrl+Shift+D",
+ },
+ },
+ "with-platform-info": {
+ suggested_key: {
+ mac: "Ctrl+Shift+M",
+ linux: "Ctrl+Shift+L",
+ windows: "Ctrl+Shift+W",
+ android: "Ctrl+Shift+A",
+ },
+ },
+ "with-translation": {
+ description: "__MSG_with_translation__",
+ },
+ "without-suggested-key": {
+ description: "has no suggested_key",
+ },
+ "without-suggested-key-nor-description": {},
+ },
+ },
+
+ background: function () {
+ browser.test.onMessage.addListener((message, additionalScope) => {
+ browser.commands.getAll(commands => {
+ let errorMessage = "getAll should return an array of commands";
+ browser.test.assertEq(commands.length, 6, errorMessage);
+
+ let command = commands.find(c => c.name == "with-desciption");
+
+ errorMessage =
+ "The description should match what is provided in the manifest";
+ browser.test.assertEq(
+ "should have a description",
+ command.description,
+ errorMessage
+ );
+
+ errorMessage =
+ "The shortcut should match the default shortcut provided in the manifest";
+ browser.test.assertEq("Ctrl+Shift+Y", command.shortcut, errorMessage);
+
+ command = commands.find(c => c.name == "without-description");
+
+ errorMessage =
+ "The description should be empty when it is not provided";
+ browser.test.assertEq(null, command.description, errorMessage);
+
+ errorMessage =
+ "The shortcut should match the default shortcut provided in the manifest";
+ browser.test.assertEq("Ctrl+Shift+D", command.shortcut, errorMessage);
+
+ let platformKeys = {
+ macosx: "M",
+ linux: "L",
+ win: "W",
+ android: "A",
+ };
+
+ command = commands.find(c => c.name == "with-platform-info");
+ let platformKey = platformKeys[additionalScope.platform];
+ let shortcut = `Ctrl+Shift+${platformKey}`;
+ errorMessage = `The shortcut should match the one provided in the manifest for OS='${additionalScope.platform}'`;
+ browser.test.assertEq(shortcut, command.shortcut, errorMessage);
+
+ command = commands.find(c => c.name == "with-translation");
+ browser.test.assertEq(
+ command.description,
+ "The description",
+ "The description can be localized"
+ );
+
+ command = commands.find(c => c.name == "without-suggested-key");
+
+ browser.test.assertEq(
+ "has no suggested_key",
+ command.description,
+ "The description should match what is provided in the manifest"
+ );
+
+ browser.test.assertEq(
+ "",
+ command.shortcut,
+ "The shortcut should be empty if not provided"
+ );
+
+ command = commands.find(
+ c => c.name == "without-suggested-key-nor-description"
+ );
+
+ browser.test.assertEq(
+ null,
+ command.description,
+ "The description should be empty when it is not provided"
+ );
+
+ browser.test.assertEq(
+ "",
+ command.shortcut,
+ "The shortcut should be empty if not provided"
+ );
+
+ browser.test.notifyPass("commands");
+ });
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ extension.sendMessage("additional-scope", {
+ platform: AppConstants.platform,
+ });
+ await extension.awaitFinish("commands");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_commands_onChanged.js b/browser/components/extensions/test/browser/browser_ext_commands_onChanged.js
new file mode 100644
index 0000000000..2b44d472cc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_onChanged.js
@@ -0,0 +1,59 @@
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "commands@mochi.test" } },
+ commands: {
+ foo: {
+ suggested_key: {
+ default: "Ctrl+Shift+V",
+ },
+ description: "The foo command",
+ },
+ },
+ },
+ background: async function () {
+ const { commands } = browser.runtime.getManifest();
+
+ const originalFoo = commands.foo;
+
+ let resolver = {};
+ resolver.promise = new Promise(resolve => (resolver.resolve = resolve));
+
+ browser.commands.onChanged.addListener(update => {
+ browser.test.assertDeepEq(
+ update,
+ {
+ name: "foo",
+ newShortcut: "Ctrl+Shift+L",
+ oldShortcut: originalFoo.suggested_key.default,
+ },
+ `The name should match what was provided in the manifest.
+ The new shortcut should match what was provided in the update.
+ The old shortcut should match what was provided in the manifest
+ `
+ );
+ browser.test.assertFalse(
+ resolver.hasResolvedAlready,
+ `resolver was not resolved yet`
+ );
+ resolver.resolve();
+ resolver.hasResolvedAlready = true;
+ });
+
+ await browser.commands.update({ name: "foo", shortcut: "Ctrl+Shift+L" });
+ // We're checking that nothing emits when
+ // the new shortcut is identical to the old one
+ await browser.commands.update({ name: "foo", shortcut: "Ctrl+Shift+L" });
+
+ await resolver.promise;
+
+ browser.test.notifyPass("commands");
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("commands");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
new file mode 100644
index 0000000000..db900f7ea4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
@@ -0,0 +1,442 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_user_defined_commands() {
+ const testCommands = [
+ // Ctrl Shortcuts
+ {
+ name: "toggle-ctrl-a",
+ shortcut: "Ctrl+A",
+ key: "A",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-up",
+ shortcut: "Ctrl+Up",
+ key: "VK_UP",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ // Alt Shortcuts
+ {
+ name: "toggle-alt-a",
+ shortcut: "Alt+A",
+ key: "A",
+ modifiers: {
+ altKey: true,
+ },
+ },
+ {
+ name: "toggle-alt-down",
+ shortcut: "Alt+Down",
+ key: "VK_DOWN",
+ modifiers: {
+ altKey: true,
+ },
+ },
+ // Mac Shortcuts
+ {
+ name: "toggle-command-shift-page-up",
+ shortcutMac: "Command+Shift+PageUp",
+ key: "VK_PAGE_UP",
+ modifiers: {
+ accelKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "toggle-mac-control-shift+period",
+ shortcut: "Ctrl+Shift+Period",
+ shortcutMac: "MacCtrl+Shift+Period",
+ key: "VK_PERIOD",
+ modifiers: {
+ ctrlKey: true,
+ shiftKey: true,
+ },
+ },
+ // Ctrl+Shift Shortcuts
+ {
+ name: "toggle-ctrl-shift-left",
+ shortcut: "Ctrl+Shift+Left",
+ key: "VK_LEFT",
+ modifiers: {
+ accelKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-shift-1",
+ shortcut: "Ctrl+Shift+1",
+ key: "1",
+ modifiers: {
+ accelKey: true,
+ shiftKey: true,
+ },
+ },
+ // Alt+Shift Shortcuts
+ {
+ name: "toggle-alt-shift-1",
+ shortcut: "Alt+Shift+1",
+ key: "1",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "toggle-alt-shift-a",
+ shortcut: "Alt+Shift+A",
+ key: "A",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "toggle-alt-shift-right",
+ shortcut: "Alt+Shift+Right",
+ key: "VK_RIGHT",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ // Function keys
+ {
+ name: "function-keys-Alt+Shift+F3",
+ shortcut: "Alt+Shift+F3",
+ key: "VK_F3",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "function-keys-F2",
+ shortcut: "F2",
+ key: "VK_F2",
+ modifiers: {
+ altKey: false,
+ shiftKey: false,
+ },
+ },
+ // Misc Shortcuts
+ {
+ name: "valid-command-with-unrecognized-property-name",
+ shortcut: "Alt+Shift+3",
+ key: "3",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ unrecognized_property: "with-a-random-value",
+ },
+ {
+ name: "spaces-in-shortcut-name",
+ shortcut: " Alt + Shift + 2 ",
+ key: "2",
+ modifiers: {
+ altKey: true,
+ shiftKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-space",
+ shortcut: "Ctrl+Space",
+ key: "VK_SPACE",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-comma",
+ shortcut: "Ctrl+Comma",
+ key: "VK_COMMA",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-period",
+ shortcut: "Ctrl+Period",
+ key: "VK_PERIOD",
+ modifiers: {
+ accelKey: true,
+ },
+ },
+ {
+ name: "toggle-ctrl-alt-v",
+ shortcut: "Ctrl+Alt+V",
+ key: "V",
+ modifiers: {
+ accelKey: true,
+ altKey: true,
+ },
+ },
+ ];
+
+ // Create a window before the extension is loaded.
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.startLoadingURIString(
+ win1.gBrowser.selectedBrowser,
+ "about:robots"
+ );
+ await BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
+
+ // We would have previously focused the window's content area after the
+ // navigation from about:blank to about:robots, but bug 1596738 changed this
+ // to prevent the browser element from stealing focus from the urlbar.
+ //
+ // Some of these command tests (specifically alt-a on linux) were designed
+ // based on focus being in the browser content, so we need to manually focus
+ // the browser here to preserve that assumption.
+ win1.gBrowser.selectedBrowser.focus();
+
+ let commands = {};
+ let isMac = AppConstants.platform == "macosx";
+ let totalMacOnlyCommands = 0;
+ let numberNumericCommands = 4;
+
+ for (let testCommand of testCommands) {
+ let command = {
+ suggested_key: {},
+ };
+
+ if (testCommand.shortcut) {
+ command.suggested_key.default = testCommand.shortcut;
+ }
+
+ if (testCommand.shortcutMac) {
+ command.suggested_key.mac = testCommand.shortcutMac;
+ }
+
+ if (testCommand.shortcutMac && !testCommand.shortcut) {
+ totalMacOnlyCommands++;
+ }
+
+ if (testCommand.unrecognized_property) {
+ command.unrecognized_property = testCommand.unrecognized_property;
+ }
+
+ commands[testCommand.name] = command;
+ }
+
+ function background() {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.sendMessage("oncommand", commandName);
+ });
+ browser.test.sendMessage("ready");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: commands,
+ },
+ background,
+ });
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ /Reading manifest: Warning processing commands.*.unrecognized_property: An unexpected property was found/,
+ },
+ ]);
+ });
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ await extension.awaitMessage("ready");
+
+ async function runTest(window) {
+ for (let testCommand of testCommands) {
+ if (testCommand.shortcutMac && !testCommand.shortcut && !isMac) {
+ continue;
+ }
+ EventUtils.synthesizeKey(testCommand.key, testCommand.modifiers, window);
+ let message = await extension.awaitMessage("oncommand");
+ is(
+ message,
+ testCommand.name,
+ `Expected onCommand listener to fire with the correct name: ${testCommand.name}`
+ );
+ }
+ }
+
+ // Create another window after the extension is loaded.
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.startLoadingURIString(
+ win2.gBrowser.selectedBrowser,
+ "about:robots"
+ );
+ await BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser);
+
+ // See comment above.
+ win2.gBrowser.selectedBrowser.focus();
+
+ let totalTestCommands =
+ Object.keys(testCommands).length + numberNumericCommands;
+ let expectedCommandsRegistered = isMac
+ ? totalTestCommands
+ : totalTestCommands - totalMacOnlyCommands;
+
+ // Confirm the keysets have been added to both windows.
+ let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`;
+ let keyset = win1.document.getElementById(keysetID);
+ Assert.notEqual(keyset, null, "Expected keyset to exist");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ "Expected keyset to have the correct number of children"
+ );
+
+ keyset = win2.document.getElementById(keysetID);
+ Assert.notEqual(keyset, null, "Expected keyset to exist");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ "Expected keyset to have the correct number of children"
+ );
+
+ // Confirm that the commands are registered to both windows.
+ await focusWindow(win1);
+ await runTest(win1);
+
+ await focusWindow(win2);
+ await runTest(win2);
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ BrowserTestUtils.startLoadingURIString(
+ privateWin.gBrowser.selectedBrowser,
+ "about:robots"
+ );
+ await BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);
+
+ // See comment above.
+ privateWin.gBrowser.selectedBrowser.focus();
+
+ keyset = privateWin.document.getElementById(keysetID);
+ is(keyset, null, "Expected keyset is not added to private windows");
+
+ await extension.unload();
+
+ // Confirm that the keysets have been removed from both windows after the extension is unloaded.
+ keyset = win1.document.getElementById(keysetID);
+ is(keyset, null, "Expected keyset to be removed from the window");
+
+ keyset = win2.document.getElementById(keysetID);
+ is(keyset, null, "Expected keyset to be removed from the window");
+
+ // Test that given permission the keyset is added to the private window.
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: commands,
+ },
+ incognitoOverride: "spanning",
+ background,
+ });
+
+ // unrecognized_property in manifest triggers warning.
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ await extension.awaitMessage("ready");
+ keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`;
+
+ keyset = win1.document.getElementById(keysetID);
+ Assert.notEqual(keyset, null, "Expected keyset to exist on win1");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ "Expected keyset to have the correct number of children"
+ );
+
+ keyset = win2.document.getElementById(keysetID);
+ Assert.notEqual(keyset, null, "Expected keyset to exist on win2");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ "Expected keyset to have the correct number of children"
+ );
+
+ keyset = privateWin.document.getElementById(keysetID);
+ Assert.notEqual(keyset, null, "Expected keyset was added to private windows");
+ is(
+ keyset.children.length,
+ expectedCommandsRegistered,
+ "Expected keyset to have the correct number of children"
+ );
+
+ await focusWindow(privateWin);
+ await runTest(privateWin);
+
+ await extension.unload();
+
+ keyset = privateWin.document.getElementById(keysetID);
+ is(keyset, null, "Expected keyset to be removed from the private window");
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
+
+add_task(async function test_commands_event_page() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@commands" } },
+ background: { persistent: false },
+ commands: {
+ "toggle-feature": {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ },
+ background() {
+ browser.commands.onCommand.addListener(name => {
+ browser.test.assertEq(name, "toggle-feature", "command received");
+ browser.test.sendMessage("onCommand");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ assertPersistentListeners(extension, "commands", "onCommand", {
+ primed: false,
+ });
+
+ // test events waken background
+ await extension.terminateBackground();
+ assertPersistentListeners(extension, "commands", "onCommand", {
+ primed: true,
+ });
+
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onCommand");
+ ok(true, "persistent event woke background");
+ assertPersistentListeners(extension, "commands", "onCommand", {
+ primed: false,
+ });
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_commands_update.js b/browser/components/extensions/test/browser/browser_ext_commands_update.js
new file mode 100644
index 0000000000..5e1f05e346
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_update.js
@@ -0,0 +1,428 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+
+function enableAddon(addon) {
+ return new Promise(resolve => {
+ AddonManager.addAddonListener({
+ onEnabled(enabledAddon) {
+ if (enabledAddon.id == addon.id) {
+ resolve();
+ AddonManager.removeAddonListener(this);
+ }
+ },
+ });
+ addon.enable();
+ });
+}
+
+function disableAddon(addon) {
+ return new Promise(resolve => {
+ AddonManager.addAddonListener({
+ onDisabled(disabledAddon) {
+ if (disabledAddon.id == addon.id) {
+ resolve();
+ AddonManager.removeAddonListener(this);
+ }
+ },
+ });
+ addon.disable();
+ });
+}
+
+add_task(async function test_update_defined_command() {
+ let extension;
+ let updatedExtension;
+
+ registerCleanupFunction(async () => {
+ await extension.unload();
+
+ // updatedExtension might not have started up if we didn't make it that far.
+ if (updatedExtension) {
+ await updatedExtension.unload();
+ }
+
+ // Check that ESS is cleaned up on uninstall.
+ let storedCommands = ExtensionSettingsStore.getAllForExtension(
+ extension.id,
+ "commands"
+ );
+ is(storedCommands.length, 0, "There are no stored commands after unload");
+ });
+
+ extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "commands@mochi.test" } },
+ commands: {
+ foo: {
+ suggested_key: {
+ default: "Ctrl+Shift+I",
+ },
+ description: "The foo command",
+ },
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ if (msg == "update") {
+ await browser.commands.update(data);
+ return browser.test.sendMessage("updateDone");
+ } else if (msg == "reset") {
+ await browser.commands.reset(data);
+ return browser.test.sendMessage("resetDone");
+ } else if (msg != "run") {
+ return;
+ }
+ // Test initial manifest command.
+ let commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is 1 command");
+ let command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is right");
+ browser.test.assertEq(
+ "The foo command",
+ command.description,
+ "The description is right"
+ );
+ browser.test.assertEq(
+ "Ctrl+Shift+I",
+ command.shortcut,
+ "The shortcut is right"
+ );
+
+ // Update the shortcut.
+ await browser.commands.update({
+ name: "foo",
+ shortcut: "Ctrl+Shift+L",
+ });
+
+ // Test the updated shortcut.
+ commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is still 1 command");
+ command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is unchanged");
+ browser.test.assertEq(
+ "The foo command",
+ command.description,
+ "The description is unchanged"
+ );
+ browser.test.assertEq(
+ "Ctrl+Shift+L",
+ command.shortcut,
+ "The shortcut is updated"
+ );
+
+ // Update the description.
+ await browser.commands.update({
+ name: "foo",
+ description: "The only command",
+ });
+
+ // Test the updated shortcut.
+ commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is still 1 command");
+ command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is unchanged");
+ browser.test.assertEq(
+ "The only command",
+ command.description,
+ "The description is updated"
+ );
+ browser.test.assertEq(
+ "Ctrl+Shift+L",
+ command.shortcut,
+ "The shortcut is unchanged"
+ );
+
+ // Clear the shortcut.
+ await browser.commands.update({
+ name: "foo",
+ shortcut: "",
+ });
+ commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is still 1 command");
+ command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is unchanged");
+ browser.test.assertEq(
+ "The only command",
+ command.description,
+ "The description is unchanged"
+ );
+ browser.test.assertEq("", command.shortcut, "The shortcut is empty");
+
+ // Update the description and shortcut.
+ await browser.commands.update({
+ name: "foo",
+ description: "The new command",
+ shortcut: " Alt+ Shift +9",
+ });
+
+ // Test the updated shortcut.
+ commands = await browser.commands.getAll();
+ browser.test.assertEq(1, commands.length, "There is still 1 command");
+ command = commands[0];
+ browser.test.assertEq("foo", command.name, "The name is unchanged");
+ browser.test.assertEq(
+ "The new command",
+ command.description,
+ "The description is updated"
+ );
+ browser.test.assertEq(
+ "Alt+Shift+9",
+ command.shortcut,
+ "The shortcut is updated"
+ );
+
+ // Test a bad shortcut update.
+ browser.test.assertThrows(
+ () =>
+ browser.commands.update({ name: "foo", shortcut: "Ctl+Shift+L" }),
+ /Type error for parameter detail .+ primary modifier and a key/,
+ "It rejects for a bad shortcut"
+ );
+
+ // Try to update a command that doesn't exist.
+ await browser.test.assertRejects(
+ browser.commands.update({ name: "bar", shortcut: "Ctrl+Shift+L" }),
+ 'Unknown command "bar"',
+ "It rejects for an unknown command"
+ );
+
+ browser.test.notifyPass("commands");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+
+ function extensionKeyset(extensionId) {
+ return document.getElementById(
+ makeWidgetId(`ext-keyset-id-${extensionId}`)
+ );
+ }
+
+ function checkKey(extensionId, shortcutKey, modifiers) {
+ let keyset = extensionKeyset(extensionId);
+ is(keyset.children.length, 1, "There is 1 key in the keyset");
+ let key = keyset.children[0];
+ is(key.getAttribute("key"), shortcutKey, "The key is correct");
+ is(key.getAttribute("modifiers"), modifiers, "The modifiers are correct");
+ }
+
+ function checkNumericKey(extensionId, key, modifiers) {
+ let keyset = extensionKeyset(extensionId);
+ is(
+ keyset.children.length,
+ 2,
+ "There are 2 keys in the keyset now, 1 of which contains a keycode."
+ );
+ let numpadKey = keyset.children[0];
+ is(
+ numpadKey.getAttribute("keycode"),
+ `VK_NUMPAD${key}`,
+ "The numpad keycode is correct."
+ );
+ is(
+ numpadKey.getAttribute("modifiers"),
+ modifiers,
+ "The modifiers are correct"
+ );
+
+ let originalNumericKey = keyset.children[1];
+ is(
+ originalNumericKey.getAttribute("keycode"),
+ `VK_${key}`,
+ "The original key is correct."
+ );
+ is(
+ originalNumericKey.getAttribute("modifiers"),
+ modifiers,
+ "The modifiers are correct"
+ );
+ }
+
+ // Check that the is set for the original shortcut.
+ checkKey(extension.id, "I", "accel,shift");
+
+ await extension.awaitMessage("ready");
+ extension.sendMessage("run");
+ await extension.awaitFinish("commands");
+
+ // Check that the has been updated.
+ checkNumericKey(extension.id, "9", "alt,shift");
+
+ // Check that the updated command is stored in ExtensionSettingsStore.
+ let storedCommands = ExtensionSettingsStore.getAllForExtension(
+ extension.id,
+ "commands"
+ );
+ is(storedCommands.length, 1, "There is only one stored command");
+ let command = ExtensionSettingsStore.getSetting(
+ "commands",
+ "foo",
+ extension.id
+ ).value;
+ is(command.description, "The new command", "The description is stored");
+ is(command.shortcut, "Alt+Shift+9", "The shortcut is stored");
+
+ // Check that the key is updated immediately.
+ extension.sendMessage("update", { name: "foo", shortcut: "Ctrl+Shift+M" });
+ await extension.awaitMessage("updateDone");
+ checkKey(extension.id, "M", "accel,shift");
+
+ // Ensure all successive updates are stored.
+ // Force the command to only have a description saved.
+ await ExtensionSettingsStore.addSetting(extension.id, "commands", "foo", {
+ description: "description only",
+ });
+ // This command now only has a description set in storage, also update the shortcut.
+ extension.sendMessage("update", { name: "foo", shortcut: "Alt+Shift+9" });
+ await extension.awaitMessage("updateDone");
+ let storedCommand = await ExtensionSettingsStore.getSetting(
+ "commands",
+ "foo",
+ extension.id
+ );
+ is(
+ storedCommand.value.shortcut,
+ "Alt+Shift+9",
+ "The shortcut is saved correctly"
+ );
+ is(
+ storedCommand.value.description,
+ "description only",
+ "The description is saved correctly"
+ );
+
+ // Calling browser.commands.reset("foo") should reset to manifest version.
+ extension.sendMessage("reset", "foo");
+ await extension.awaitMessage("resetDone");
+
+ checkKey(extension.id, "I", "accel,shift");
+
+ // Check that enable/disable removes the keyset and reloads the saved command.
+ let addon = await AddonManager.getAddonByID(extension.id);
+ await disableAddon(addon);
+ let keyset = extensionKeyset(extension.id);
+ is(keyset, null, "The extension keyset is removed when disabled");
+ // Add some commands to storage, only "foo" should get loaded.
+ await ExtensionSettingsStore.addSetting(extension.id, "commands", "foo", {
+ shortcut: "Alt+Shift+9",
+ });
+ await ExtensionSettingsStore.addSetting(extension.id, "commands", "unknown", {
+ shortcut: "Ctrl+Shift+P",
+ });
+ storedCommands = ExtensionSettingsStore.getAllForExtension(
+ extension.id,
+ "commands"
+ );
+ is(storedCommands.length, 2, "There are now 2 commands stored");
+ await enableAddon(addon);
+ // Wait for the keyset to appear (it's async on enable).
+ await TestUtils.waitForCondition(() => extensionKeyset(extension.id));
+ // The keyset is back with the value from ExtensionSettingsStore.
+ checkNumericKey(extension.id, "9", "alt,shift");
+
+ // Check that an update to a shortcut in the manifest is mapped correctly.
+ updatedExtension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "commands@mochi.test" } },
+ commands: {
+ foo: {
+ suggested_key: {
+ default: "Ctrl+Shift+L",
+ },
+ description: "The foo command",
+ },
+ },
+ },
+ });
+ await updatedExtension.startup();
+
+ await TestUtils.waitForCondition(() => extensionKeyset(extension.id));
+ // Shortcut is unchanged since it was previously updated.
+ checkNumericKey(extension.id, "9", "alt,shift");
+});
+
+add_task(async function updateSidebarCommand() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ commands: {
+ _execute_sidebar_action: {
+ suggested_key: {
+ default: "Ctrl+Shift+E",
+ },
+ },
+ },
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ if (msg == "updateShortcut") {
+ await browser.commands.update(data);
+ return browser.test.sendMessage("done");
+ }
+ throw new Error("Unknown message");
+ });
+ },
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("sidebar");
+
+ // Show and hide the switcher panel to generate the initial shortcuts.
+ let switcherShown = promisePopupShown(SidebarUI._switcherPanel);
+ SidebarUI.showSwitcherPanel();
+ await switcherShown;
+ let switcherHidden = promisePopupHidden(SidebarUI._switcherPanel);
+ SidebarUI.hideSwitcherPanel();
+ await switcherHidden;
+
+ let menuitemId = `sidebarswitcher_menu_${makeWidgetId(
+ extension.id
+ )}-sidebar-action`;
+ let menuitem = document.getElementById(menuitemId);
+ let acceltext = menuitem.getAttribute("acceltext");
+ ok(acceltext.endsWith("E"), "The menuitem has the accel text set");
+
+ extension.sendMessage("updateShortcut", {
+ name: "_execute_sidebar_action",
+ shortcut: "Ctrl+Shift+M",
+ });
+ await extension.awaitMessage("done");
+
+ acceltext = menuitem.getAttribute("acceltext");
+ ok(acceltext.endsWith("M"), "The menuitem accel text has been updated");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_connect_and_move_tabs.js b/browser/components/extensions/test/browser/browser_ext_connect_and_move_tabs.js
new file mode 100644
index 0000000000..50f5380ce6
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_connect_and_move_tabs.js
@@ -0,0 +1,104 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Tests that the Port object created by browser.runtime.connect is not
+// prematurely disconnected as the underlying message managers change when a
+// tab is moved between windows.
+
+function loadExtension() {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["http://mochi.test/?discoTest"],
+ },
+ ],
+ },
+ background() {
+ browser.runtime.onConnect.addListener(port => {
+ port.onDisconnect.addListener(() => {
+ browser.test.fail(
+ "onDisconnect should not fire because the port is to be closed from this side"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ port.onMessage.addListener(async msg => {
+ browser.test.assertEq("connect_from_script", msg, "expected message");
+ // Move a tab to a new window and back. Regression test for bugzil.la/1448674
+ let { windowId, id: tabId, index } = port.sender.tab;
+ await browser.windows.create({ tabId });
+ await browser.tabs.move(tabId, { index, windowId });
+ await browser.windows.create({ tabId });
+ await browser.tabs.move(tabId, { index, windowId });
+ try {
+ // When the port is unexpectedly disconnected, postMessage will throw an error.
+ port.postMessage("ping");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.sendMessage("port_ping_ponged_before_disconnect");
+ }
+ });
+
+ browser.runtime.onMessage.addListener(async (msg, sender) => {
+ if (msg == "disconnect-me") {
+ port.disconnect();
+ // Now port.onDisconnect should fire in the content script.
+ } else if (msg == "close-tab") {
+ await browser.tabs.remove(sender.tab.id);
+ browser.test.sendMessage("closed_tab");
+ }
+ });
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("open_extension_tab", msg, "expected message");
+ browser.tabs.create({ url: "tab.html" });
+ });
+ },
+
+ files: {
+ "tab.html": `
+
+
+ `,
+ "script.js": function () {
+ let port = browser.runtime.connect();
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq("ping", msg, "expected message");
+ browser.test.sendMessage("port_ping_ponged_before_disconnect");
+ port.onDisconnect.addListener(() => {
+ browser.test.sendMessage("port_disconnected");
+ browser.runtime.sendMessage("close-tab");
+ });
+ browser.runtime.sendMessage("disconnect-me");
+ });
+ port.postMessage("connect_from_script");
+ },
+ },
+ });
+}
+
+add_task(async function contentscript_connect_and_move_tabs() {
+ let extension = loadExtension();
+ await extension.startup();
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/?discoTest"
+ );
+ await extension.awaitMessage("port_ping_ponged_before_disconnect");
+ await extension.awaitMessage("port_disconnected");
+ await extension.awaitMessage("closed_tab");
+ await extension.unload();
+});
+
+add_task(async function extension_tab_connect_and_move_tabs() {
+ let extension = loadExtension();
+ await extension.startup();
+ extension.sendMessage("open_extension_tab");
+ await extension.awaitMessage("port_ping_ponged_before_disconnect");
+ await extension.awaitMessage("port_disconnected");
+ await extension.awaitMessage("closed_tab");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_animate.js b/browser/components/extensions/test/browser/browser_ext_contentscript_animate.js
new file mode 100644
index 0000000000..bbe90207a4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_animate.js
@@ -0,0 +1,135 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_animate() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/*"],
+ js: ["content-script.js"],
+ },
+ ],
+ },
+
+ files: {
+ "content-script.js": function () {
+ let elem = document.getElementsByTagName("body")[0];
+ elem.style.border = "2px solid red";
+
+ let anim = elem.animate({ opacity: [1, 0] }, 2000);
+ let frames = anim.effect.getKeyframes();
+ browser.test.assertEq(
+ frames.length,
+ 2,
+ "frames for Element.animate should be non-zero"
+ );
+ browser.test.assertEq(
+ frames[0].opacity,
+ "1",
+ "first frame opacity for Element.animate should be specified value"
+ );
+ browser.test.assertEq(
+ frames[0].computedOffset,
+ 0,
+ "first frame offset for Element.animate should be 0"
+ );
+ browser.test.assertEq(
+ frames[1].opacity,
+ "0",
+ "last frame opacity for Element.animate should be specified value"
+ );
+ browser.test.assertEq(
+ frames[1].computedOffset,
+ 1,
+ "last frame offset for Element.animate should be 1"
+ );
+
+ browser.test.notifyPass("contentScriptAnimate");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("contentScriptAnimate");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_KeyframeEffect() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/*"],
+ js: ["content-script.js"],
+ },
+ ],
+ },
+
+ files: {
+ "content-script.js": function () {
+ let elem = document.getElementsByTagName("body")[0];
+ elem.style.border = "2px solid red";
+
+ let effect = new KeyframeEffect(
+ elem,
+ [
+ { opacity: 1, offset: 0 },
+ { opacity: 0, offset: 1 },
+ ],
+ { duration: 1000, fill: "forwards" }
+ );
+ let frames = effect.getKeyframes();
+ browser.test.assertEq(
+ frames.length,
+ 2,
+ "frames for KeyframeEffect ctor should be non-zero"
+ );
+ browser.test.assertEq(
+ frames[0].opacity,
+ "1",
+ "first frame opacity for KeyframeEffect ctor should be specified value"
+ );
+ browser.test.assertEq(
+ frames[0].computedOffset,
+ 0,
+ "first frame offset for KeyframeEffect ctor should be 0"
+ );
+ browser.test.assertEq(
+ frames[1].opacity,
+ "0",
+ "last frame opacity for KeyframeEffect ctor should be specified value"
+ );
+ browser.test.assertEq(
+ frames[1].computedOffset,
+ 1,
+ "last frame offset for KeyframeEffect ctor should be 1"
+ );
+
+ let animation = new Animation(effect, document.timeline);
+ animation.play();
+
+ browser.test.notifyPass("contentScriptKeyframeEffect");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("contentScriptKeyframeEffect");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_connect.js b/browser/components/extensions/test/browser/browser_ext_contentscript_connect.js
new file mode 100644
index 0000000000..11dbe88240
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_connect.js
@@ -0,0 +1,94 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+
+ background: function () {
+ let ports_received = 0;
+ let port_messages_received = 0;
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertTrue(!!port, "port1 received");
+
+ ports_received++;
+ browser.test.assertEq(1, ports_received, "1 port received");
+
+ port.onMessage.addListener((msg, msgPort) => {
+ browser.test.assertEq(
+ "port message",
+ msg,
+ "listener1 port message received"
+ );
+ browser.test.assertEq(
+ port,
+ msgPort,
+ "onMessage should receive port as second argument"
+ );
+
+ port_messages_received++;
+ browser.test.assertEq(
+ 1,
+ port_messages_received,
+ "1 port message received"
+ );
+ });
+ });
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertTrue(!!port, "port2 received");
+
+ ports_received++;
+ browser.test.assertEq(2, ports_received, "2 ports received");
+
+ port.onMessage.addListener((msg, msgPort) => {
+ browser.test.assertEq(
+ "port message",
+ msg,
+ "listener2 port message received"
+ );
+ browser.test.assertEq(
+ port,
+ msgPort,
+ "onMessage should receive port as second argument"
+ );
+
+ port_messages_received++;
+ browser.test.assertEq(
+ 2,
+ port_messages_received,
+ "2 port messages received"
+ );
+
+ browser.test.notifyPass("contentscript_connect.pass");
+ });
+ });
+
+ browser.tabs.executeScript({ file: "script.js" }).catch(e => {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("contentscript_connect.pass");
+ });
+ },
+
+ files: {
+ "script.js": function () {
+ let port = browser.runtime.connect();
+ port.postMessage("port message");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("contentscript_connect.pass");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_cross_docGroup_adoption.js b/browser/components/extensions/test/browser/browser_ext_contentscript_cross_docGroup_adoption.js
new file mode 100644
index 0000000000..a0d367ec93
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_cross_docGroup_adoption.js
@@ -0,0 +1,63 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_cross_docGroup_adoption() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.content_web_accessible.enabled", true]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://example.com/"],
+ js: ["content-script.js"],
+ },
+ ],
+ web_accessible_resources: ["current.html"],
+ },
+
+ files: {
+ "current.html": "data",
+ "content-script.js": function () {
+ let iframe = document.createElement("iframe");
+ iframe.src = browser.runtime.getURL("current.html");
+ document.body.appendChild(iframe);
+
+ iframe.addEventListener(
+ "load",
+ () => {
+ let parser = new DOMParser();
+ let bold = parser.parseFromString(
+ "NodeAdopted ",
+ "text/html"
+ );
+ let doc = iframe.contentDocument;
+
+ let node = document.adoptNode(bold.documentElement);
+ doc.replaceChild(node, doc.documentElement);
+
+ const expected =
+ "NodeAdopted ";
+ browser.test.assertEq(expected, doc.documentElement.outerHTML);
+
+ browser.test.notifyPass("nodeAdopted");
+ },
+ { once: true }
+ );
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("nodeAdopted");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_cross_docGroup_adoption_xhr.js b/browser/components/extensions/test/browser/browser_ext_contentscript_cross_docGroup_adoption_xhr.js
new file mode 100644
index 0000000000..274efb71f3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_cross_docGroup_adoption_xhr.js
@@ -0,0 +1,56 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_cross_docGroup_adoption() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.content_web_accessible.enabled", true]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://example.com/"],
+ js: ["content-script.js"],
+ },
+ ],
+ web_accessible_resources: ["blank.html"],
+ },
+
+ files: {
+ "blank.html": "data",
+ "content-script.js": function () {
+ let xhr = new XMLHttpRequest();
+ xhr.responseType = "document";
+ xhr.open("GET", browser.runtime.getURL("blank.html"));
+
+ xhr.onload = function () {
+ let doc = xhr.response;
+ try {
+ let node = doc.body.cloneNode(true);
+ document.body.appendChild(node);
+ browser.test.notifyPass("nodeAdopted");
+ } catch (SecurityError) {
+ browser.test.assertTrue(
+ false,
+ "The above node adoption should not fail"
+ );
+ }
+ };
+ xhr.send();
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("nodeAdopted");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_dataTransfer_files.js b/browser/components/extensions/test/browser/browser_ext_contentscript_dataTransfer_files.js
new file mode 100644
index 0000000000..4819f98475
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_dataTransfer_files.js
@@ -0,0 +1,104 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const TEST_ORIGIN = "http://mochi.test:8888";
+const TEST_BASEURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_ORIGIN
+);
+
+const TEST_URL = `${TEST_BASEURL}file_dataTransfer_files.html`;
+
+// This test ensure that we don't cache the DataTransfer files instances when
+// they are being accessed by an extension content or user script (regression
+// test related to Bug 1707214).
+add_task(async function test_contentAndUserScripts_dataTransfer_files() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ user_scripts: {},
+ },
+
+ background: async function () {
+ await browser.contentScripts.register({
+ js: [{ file: "content_script.js" }],
+ matches: ["http://mochi.test/*"],
+ runAt: "document_start",
+ });
+
+ await browser.userScripts.register({
+ js: [{ file: "user_script.js" }],
+ matches: ["http://mochi.test/*"],
+ runAt: "document_start",
+ });
+
+ browser.test.sendMessage("scripts-registered");
+ },
+
+ files: {
+ "content_script.js": function () {
+ document.addEventListener(
+ "drop",
+ function (e) {
+ const files = e.dataTransfer.files || [];
+ document.querySelector("#result-content-script").textContent =
+ files[0]?.name;
+ },
+ { once: true, capture: true }
+ );
+
+ // Export a function that will be called by the drop event listener subscribed
+ // by the test page itself, which is the last one to be registered and then
+ // executed. This function retrieve the test results and send them to be
+ // asserted for the expected filenames.
+ this.exportFunction(
+ () => {
+ const results = {
+ contentScript: document.querySelector("#result-content-script")
+ .textContent,
+ userScript: document.querySelector("#result-user-script")
+ .textContent,
+ pageScript: document.querySelector("#result-page-script")
+ .textContent,
+ };
+ browser.test.sendMessage("test-done", results);
+ },
+ window,
+ { defineAs: "testDone" }
+ );
+ },
+ "user_script.js": function () {
+ document.addEventListener(
+ "drop",
+ function (e) {
+ const files = e.dataTransfer.files || [];
+ document.querySelector("#result-user-script").textContent =
+ files[0]?.name;
+ },
+ { once: true, capture: true }
+ );
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("scripts-registered");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ const results = await extension.awaitMessage("test-done");
+ const expectedFilename = "testfile.html";
+ Assert.deepEqual(
+ results,
+ {
+ contentScript: expectedFilename,
+ userScript: expectedFilename,
+ pageScript: expectedFilename,
+ },
+ "Got the expected drag and drop filenames"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_in_parent.js b/browser/components/extensions/test/browser/browser_ext_contentscript_in_parent.js
new file mode 100644
index 0000000000..5a8bbcb589
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_in_parent.js
@@ -0,0 +1,101 @@
+"use strict";
+
+const TELEMETRY_EVENT = "security#javascriptLoad#parentProcess";
+
+add_task(async function test_contentscript_telemetry() {
+ // Turn on telemetry and reset it to the previous state once the test is completed.
+ // const telemetryCanRecordBase = Services.telemetry.canRecordBase;
+ // Services.telemetry.canRecordBase = true;
+ // SimpleTest.registerCleanupFunction(() => {
+ // Services.telemetry.canRecordBase = telemetryCanRecordBase;
+ // });
+
+ function background() {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "execute") {
+ return;
+ }
+ browser.tabs.executeScript({
+ file: "execute_script.js",
+ allFrames: true,
+ matchAboutBlank: true,
+ runAt: "document_start",
+ });
+
+ await browser.userScripts.register({
+ js: [{ file: "user_script.js" }],
+ matches: [""],
+ matchAboutBlank: true,
+ allFrames: true,
+ runAt: "document_start",
+ });
+
+ await browser.contentScripts.register({
+ js: [{ file: "content_script.js" }],
+ matches: [""],
+ matchAboutBlank: true,
+ allFrames: true,
+ runAt: "document_start",
+ });
+
+ browser.test.sendMessage("executed");
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["tabs", ""],
+ user_scripts: {},
+ },
+ background,
+ files: {
+ // Fail if this ever executes.
+ "execute_script.js": 'browser.test.fail("content-script-run");',
+ "user_script.js": 'browser.test.fail("content-script-run");',
+ "content_script.js": 'browser.test.fail("content-script-run");',
+ },
+ };
+
+ function getSecurityEventCount() {
+ let snap = Services.telemetry.getSnapshotForKeyedScalars();
+ return snap.parent["telemetry.event_counts"][TELEMETRY_EVENT] || 0;
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences"
+ );
+ is(
+ getSecurityEventCount(),
+ 0,
+ `No events recorded before startup: ${TELEMETRY_EVENT}.`
+ );
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+ is(
+ getSecurityEventCount(),
+ 0,
+ `No events recorded after startup: ${TELEMETRY_EVENT}.`
+ );
+
+ extension.sendMessage("execute");
+ await extension.awaitMessage("executed");
+
+ // Do another load.
+ BrowserTestUtils.removeTab(tab);
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+
+ is(
+ getSecurityEventCount(),
+ 0,
+ `No events recorded after executeScript: ${TELEMETRY_EVENT}.`
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_incognito.js b/browser/components/extensions/test/browser/browser_ext_contentscript_incognito.js
new file mode 100644
index 0000000000..fc365a2a4a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_incognito.js
@@ -0,0 +1,42 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Test that calling window.open from a content script running in a private
+// window does not trigger a crash (regression test introduced in Bug 1653530
+// to cover the issue introduced in Bug 1616353 and fixed by Bug 1638793).
+add_task(async function test_contentscript_window_open_doesnot_crash() {
+ const extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["https://example.com/*"],
+ js: ["test_window_open.js"],
+ },
+ ],
+ },
+ files: {
+ "test_window_open.js": function () {
+ const newWin = window.open();
+ browser.test.log("calling window.open did not triggered a crash");
+ browser.test.sendMessage("window-open-called", !!newWin);
+ },
+ },
+ });
+ await extension.startup();
+
+ const winPrivate = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await BrowserTestUtils.openNewForegroundTab(
+ winPrivate.gBrowser,
+ "https://example.com"
+ );
+ const newWinOpened = await extension.awaitMessage("window-open-called");
+ ok(newWinOpened, "Content script successfully open a new window");
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(winPrivate);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js b/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js
new file mode 100644
index 0000000000..548c35399f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js
@@ -0,0 +1,116 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// This script is loaded in a non-tab extension context, and starts the test by
+// loading an iframe that runs contentScript as a content script.
+function extensionScript() {
+ let FRAME_URL = browser.runtime.getManifest().content_scripts[0].matches[0];
+ // Cannot use :8888 in the manifest because of bug 1468162.
+ FRAME_URL = FRAME_URL.replace("mochi.test", "mochi.test:8888");
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq(port.sender.tab, undefined, "Sender is not a tab");
+ browser.test.assertEq(port.sender.frameId, undefined, "frameId unset");
+ browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL");
+
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq("pong", msg, "Reply from content script");
+ port.disconnect();
+ });
+ port.postMessage("ping");
+ });
+
+ browser.test.log(`Going to open ${FRAME_URL} at ${location.pathname}`);
+ let f = document.createElement("iframe");
+ f.src = FRAME_URL;
+ document.body.appendChild(f);
+}
+
+function contentScript() {
+ browser.test.log(`Running content script at ${document.URL}`);
+
+ let port = browser.runtime.connect();
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq("ping", msg, "Expected message to content script");
+ port.postMessage("pong");
+ });
+ port.onDisconnect.addListener(() => {
+ browser.test.sendMessage("disconnected_in_content_script");
+ });
+}
+
+add_task(async function connect_from_background_frame() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/?background"],
+ js: ["contentscript.js"],
+ all_frames: true,
+ },
+ ],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ },
+ background: extensionScript,
+ });
+ await extension.startup();
+ await extension.awaitMessage("disconnected_in_content_script");
+ await extension.unload();
+});
+
+add_task(async function connect_from_sidebar_panel() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/?sidebar"],
+ js: ["contentscript.js"],
+ all_frames: true,
+ },
+ ],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ files: {
+ "contentscript.js": contentScript,
+ "sidebar.html": ` `,
+ "sidebar.js": extensionScript,
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("disconnected_in_content_script");
+ await extension.unload();
+});
+
+add_task(async function connect_from_browser_action_popup() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/?browser_action_popup"],
+ js: ["contentscript.js"],
+ all_frames: true,
+ },
+ ],
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "contentscript.js": contentScript,
+ "popup.html": ` `,
+ "popup.js": extensionScript,
+ },
+ });
+ await extension.startup();
+ await clickBrowserAction(extension);
+ await extension.awaitMessage("disconnected_in_content_script");
+ await closeBrowserAction(extension);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js b/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js
new file mode 100644
index 0000000000..f751c3c202
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js
@@ -0,0 +1,67 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_sender_url() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/*"],
+ run_at: "document_start",
+ js: ["script.js"],
+ },
+ ],
+ },
+
+ background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.log("Message received.");
+ browser.test.sendMessage("sender.url", sender.url);
+ });
+ },
+
+ files: {
+ "script.js"() {
+ browser.test.log("Content script loaded.");
+ browser.runtime.sendMessage(0);
+ },
+ },
+ });
+
+ const image =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/ctxmenu-image.png";
+
+ // Bug is only visible and test only works without Fission,
+ // or with Fission but without BFcache in parent.
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", false]],
+ });
+
+ function awaitNewTab() {
+ return BrowserTestUtils.waitForLocationChange(gBrowser, "about:newtab");
+ }
+
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
+ let newTab = awaitNewTab();
+ BrowserTestUtils.startLoadingURIString(browser, "about:newtab");
+ await newTab;
+
+ BrowserTestUtils.startLoadingURIString(browser, image);
+ let url = await extension.awaitMessage("sender.url");
+ is(url, image, `Correct sender.url: ${url}`);
+
+ let wentBack = awaitNewTab();
+ await browser.goBack();
+ await wentBack;
+
+ await browser.goForward();
+ url = await extension.awaitMessage("sender.url");
+ is(url, image, `Correct sender.url: ${url}`);
+ });
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus.js b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
new file mode 100644
index 0000000000..c816c89f82
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -0,0 +1,854 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/places/tests/browser/head.js",
+ this
+);
+/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell, promiseLibrary, promiseLibraryClosed
+ */
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+
+ background: function () {
+ browser.test.assertEq(
+ browser.contextMenus.ContextType.TAB,
+ "tab",
+ "ContextType is available"
+ );
+ browser.contextMenus.create({
+ id: "clickme-image",
+ title: "Click me!",
+ contexts: ["image"],
+ });
+ browser.contextMenus.create(
+ {
+ id: "clickme-page",
+ title: "Click me!",
+ contexts: ["page"],
+ },
+ () => {
+ browser.test.sendMessage("ready");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let contentAreaContextMenu = await openContextMenu("#img1");
+ let item = contentAreaContextMenu.getElementsByAttribute(
+ "label",
+ "Click me!"
+ );
+ is(item.length, 1, "contextMenu item for image was found");
+ await closeContextMenu();
+
+ contentAreaContextMenu = await openContextMenu("body");
+ item = contentAreaContextMenu.getElementsByAttribute("label", "Click me!");
+ is(item.length, 1, "contextMenu item for page was found");
+ await closeContextMenu();
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+
+ background: async function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "removeall") {
+ browser.contextMenus.removeAll();
+ browser.test.sendMessage("removed");
+ }
+ });
+
+ // A generic onclick callback function.
+ function genericOnClick(info, tab) {
+ browser.test.sendMessage("onclick", { info, tab });
+ }
+
+ browser.contextMenus.onClicked.addListener((info, tab) => {
+ browser.test.sendMessage("browser.contextMenus.onClicked", {
+ info,
+ tab,
+ });
+ });
+
+ browser.contextMenus.create({
+ contexts: ["all"],
+ type: "separator",
+ });
+
+ let contexts = [
+ "page",
+ "link",
+ "selection",
+ "image",
+ "editable",
+ "password",
+ ];
+ for (let i = 0; i < contexts.length; i++) {
+ let context = contexts[i];
+ let title = context;
+ browser.contextMenus.create({
+ title: title,
+ contexts: [context],
+ id: "ext-" + context,
+ onclick: genericOnClick,
+ });
+ if (context == "selection") {
+ browser.contextMenus.update("ext-selection", {
+ title: "selection is: '%s'",
+ onclick: genericOnClick,
+ });
+ }
+ }
+
+ let parent = browser.contextMenus.create({
+ title: "parent",
+ });
+ browser.contextMenus.create({
+ title: "child1",
+ parentId: parent,
+ onclick: genericOnClick,
+ });
+ let child2 = browser.contextMenus.create({
+ title: "child2",
+ parentId: parent,
+ onclick: genericOnClick,
+ });
+
+ let parentToDel = browser.contextMenus.create({
+ title: "parentToDel",
+ });
+ browser.contextMenus.create({
+ title: "child1",
+ parentId: parentToDel,
+ onclick: genericOnClick,
+ });
+ browser.contextMenus.create({
+ title: "child2",
+ parentId: parentToDel,
+ onclick: genericOnClick,
+ });
+ browser.contextMenus.remove(parentToDel);
+
+ browser.contextMenus.create({
+ title: "Without onclick property",
+ id: "ext-without-onclick",
+ });
+
+ await browser.test.assertRejects(
+ browser.contextMenus.update(parent, { parentId: child2 }),
+ /cannot be an ancestor/,
+ "Should not be able to reparent an item as descendent of itself"
+ );
+
+ browser.test.sendMessage("contextmenus");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("contextmenus");
+
+ let expectedClickInfo = {
+ menuItemId: "ext-image",
+ mediaType: "image",
+ srcUrl:
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/ctxmenu-image.png",
+ pageUrl: PAGE,
+ editable: false,
+ };
+
+ function checkClickInfo(result) {
+ for (let i of Object.keys(expectedClickInfo)) {
+ is(
+ result.info[i],
+ expectedClickInfo[i],
+ "click info " +
+ i +
+ " expected to be: " +
+ expectedClickInfo[i] +
+ " but was: " +
+ result.info[i]
+ );
+ }
+ is(
+ expectedClickInfo.pageSrc,
+ result.tab.url,
+ "click info page source is the right tab"
+ );
+ }
+
+ let extensionMenuRoot = await openExtensionContextMenu();
+
+ // Check some menu items
+ let items = extensionMenuRoot.getElementsByAttribute("label", "image");
+ is(items.length, 1, "contextMenu item for image was found (context=image)");
+ let image = items[0];
+
+ items = extensionMenuRoot.getElementsByAttribute("label", "selection-edited");
+ is(
+ items.length,
+ 0,
+ "contextMenu item for selection was not found (context=image)"
+ );
+
+ items = extensionMenuRoot.getElementsByAttribute("label", "parentToDel");
+ is(
+ items.length,
+ 0,
+ "contextMenu item for removed parent was not found (context=image)"
+ );
+
+ items = extensionMenuRoot.getElementsByAttribute("label", "parent");
+ is(items.length, 1, "contextMenu item for parent was found (context=image)");
+
+ is(
+ items[0].menupopup.children.length,
+ 2,
+ "child items for parent were found (context=image)"
+ );
+
+ // Click on ext-image item and check the click results
+ await closeExtensionContextMenu(image);
+
+ let result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ // Test "link" context and OnClick data property.
+ extensionMenuRoot = await openExtensionContextMenu("[href=some-link]");
+
+ // Click on ext-link and check the click results
+ items = extensionMenuRoot.getElementsByAttribute("label", "link");
+ is(items.length, 1, "contextMenu item for parent was found (context=link)");
+ let link = items[0];
+
+ expectedClickInfo = {
+ menuItemId: "ext-link",
+ linkUrl:
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/some-link",
+ linkText: "Some link",
+ pageUrl: PAGE,
+ editable: false,
+ };
+
+ await closeExtensionContextMenu(link);
+
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ // Test "editable" context and OnClick data property.
+ extensionMenuRoot = await openExtensionContextMenu("#edit-me");
+
+ // Check some menu items.
+ items = extensionMenuRoot.getElementsByAttribute("label", "editable");
+ is(
+ items.length,
+ 1,
+ "contextMenu item for text input element was found (context=editable)"
+ );
+ let editable = items[0];
+
+ // Click on ext-editable item and check the click results.
+ await closeExtensionContextMenu(editable);
+
+ expectedClickInfo = {
+ menuItemId: "ext-editable",
+ pageUrl: PAGE,
+ editable: true,
+ };
+
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ extensionMenuRoot = await openExtensionContextMenu("#readonly-text");
+
+ // Check some menu items.
+ items = extensionMenuRoot.getElementsByAttribute("label", "editable");
+ is(
+ items.length,
+ 0,
+ "contextMenu item for text input element was not found (context=editable fails for readonly items)"
+ );
+
+ // Hide the popup "manually" because there's nothing to click.
+ await closeContextMenu();
+
+ // Test "editable" context on type=tel and type=number items, and OnClick data property.
+ extensionMenuRoot = await openExtensionContextMenu("#call-me-maybe");
+
+ // Check some menu items.
+ items = extensionMenuRoot.getElementsByAttribute("label", "editable");
+ is(
+ items.length,
+ 1,
+ "contextMenu item for text input element was found (context=editable)"
+ );
+ editable = items[0];
+
+ // Click on ext-editable item and check the click results.
+ await closeExtensionContextMenu(editable);
+
+ expectedClickInfo = {
+ menuItemId: "ext-editable",
+ pageUrl: PAGE,
+ editable: true,
+ };
+
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ extensionMenuRoot = await openExtensionContextMenu("#number-input");
+
+ // Check some menu items.
+ items = extensionMenuRoot.getElementsByAttribute("label", "editable");
+ is(
+ items.length,
+ 1,
+ "contextMenu item for text input element was found (context=editable)"
+ );
+ editable = items[0];
+
+ // Click on ext-editable item and check the click results.
+ await closeExtensionContextMenu(editable);
+
+ expectedClickInfo = {
+ menuItemId: "ext-editable",
+ pageUrl: PAGE,
+ editable: true,
+ };
+
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ extensionMenuRoot = await openExtensionContextMenu("#password");
+ items = extensionMenuRoot.getElementsByAttribute("label", "password");
+ is(
+ items.length,
+ 1,
+ "contextMenu item for password input element was found (context=password)"
+ );
+ let password = items[0];
+ await closeExtensionContextMenu(password);
+ expectedClickInfo = {
+ menuItemId: "ext-password",
+ pageUrl: PAGE,
+ editable: true,
+ };
+
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ extensionMenuRoot = await openExtensionContextMenu("#noneditablepassword");
+ items = extensionMenuRoot.getElementsByAttribute("label", "password");
+ is(
+ items.length,
+ 1,
+ "contextMenu item for password input element was found (context=password)"
+ );
+ password = items[0];
+ await closeExtensionContextMenu(password);
+ expectedClickInfo.editable = false;
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ // Select some text
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function (arg) {
+ let doc = content.document;
+ let range = doc.createRange();
+ let selection = content.getSelection();
+ selection.removeAllRanges();
+ let textNode = doc.getElementById("img1").previousSibling;
+ range.setStart(textNode, 0);
+ range.setEnd(textNode, 100);
+ selection.addRange(range);
+ });
+
+ // Bring up context menu again
+ extensionMenuRoot = await openExtensionContextMenu();
+
+ // Check some menu items
+ items = extensionMenuRoot.getElementsByAttribute(
+ "label",
+ "Without onclick property"
+ );
+ is(items.length, 1, "contextMenu item was found (context=page)");
+
+ await closeExtensionContextMenu(items[0]);
+
+ expectedClickInfo = {
+ menuItemId: "ext-without-onclick",
+ pageUrl: PAGE,
+ };
+
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ // Bring up context menu again
+ extensionMenuRoot = await openExtensionContextMenu();
+
+ // Check some menu items
+ items = extensionMenuRoot.getElementsByAttribute(
+ "label",
+ "selection is: 'just some text 12345678901234567890123456789012\u2026'"
+ );
+ is(
+ items.length,
+ 1,
+ "contextMenu item for selection was found (context=selection)"
+ );
+ let selectionItem = items[0];
+
+ items = extensionMenuRoot.getElementsByAttribute("label", "selection");
+ is(
+ items.length,
+ 0,
+ "contextMenu item label update worked (context=selection)"
+ );
+
+ await closeExtensionContextMenu(selectionItem);
+
+ expectedClickInfo = {
+ menuItemId: "ext-selection",
+ pageUrl: PAGE,
+ selectionText:
+ " just some text 1234567890123456789012345678901234567890123456789012345678901234567890123456789012",
+ };
+
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+
+ // Select a lot of text
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function (arg) {
+ let doc = content.document;
+ let range = doc.createRange();
+ let selection = content.getSelection();
+ selection.removeAllRanges();
+ let textNode = doc.getElementById("longtext").firstChild;
+ range.setStart(textNode, 0);
+ range.setEnd(textNode, textNode.length);
+ selection.addRange(range);
+ });
+
+ // Bring up context menu again
+ extensionMenuRoot = await openExtensionContextMenu("#longtext");
+
+ // Check some menu items
+ items = extensionMenuRoot.getElementsByAttribute(
+ "label",
+ "selection is: 'Sed ut perspiciatis unde omnis iste natus error\u2026'"
+ );
+ is(
+ items.length,
+ 1,
+ `contextMenu item for longtext selection was found (context=selection)`
+ );
+ await closeExtensionContextMenu(items[0]);
+
+ expectedClickInfo = {
+ menuItemId: "ext-selection",
+ pageUrl: PAGE,
+ };
+
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+ ok(
+ result.info.selectionText.endsWith("quo voluptas nulla pariatur?"),
+ "long text selection worked"
+ );
+
+ // Select a lot of text, excercise the editable element code path in
+ // the Browser:GetSelection handler.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function (arg) {
+ let doc = content.document;
+ let node = doc.getElementById("editabletext");
+ // content.js handleContentContextMenu fails intermittently without focus.
+ node.focus();
+ node.selectionStart = 0;
+ node.selectionEnd = 844;
+ });
+
+ // Bring up context menu again
+ extensionMenuRoot = await openExtensionContextMenu("#editabletext");
+
+ // Check some menu items
+ items = extensionMenuRoot.getElementsByAttribute("label", "editable");
+ is(
+ items.length,
+ 1,
+ "contextMenu item for text input element was found (context=editable)"
+ );
+ await closeExtensionContextMenu(items[0]);
+
+ expectedClickInfo = {
+ menuItemId: "ext-editable",
+ editable: true,
+ pageUrl: PAGE,
+ };
+
+ result = await extension.awaitMessage("onclick");
+ checkClickInfo(result);
+ result = await extension.awaitMessage("browser.contextMenus.onClicked");
+ checkClickInfo(result);
+ ok(
+ result.info.selectionText.endsWith(
+ "perferendis doloribus asperiores repellat."
+ ),
+ "long text selection worked"
+ );
+
+ extension.sendMessage("removeall");
+ await extension.awaitMessage("removed");
+
+ let contentAreaContextMenu = await openContextMenu("#img1");
+ items = contentAreaContextMenu.getElementsByAttribute(
+ "ext-type",
+ "top-level-menu"
+ );
+ is(items.length, 0, "top level item was not found (after removeAll()");
+ await closeContextMenu();
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function testRemoveAllWithTwoExtensions() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ const manifest = { permissions: ["contextMenus"] };
+
+ const first = ExtensionTestUtils.loadExtension({
+ manifest,
+ background() {
+ browser.contextMenus.create({ title: "alpha", contexts: ["all"] });
+
+ browser.contextMenus.onClicked.addListener(() => {
+ browser.contextMenus.removeAll();
+ });
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "ping") {
+ browser.test.sendMessage("pong-alpha");
+ return;
+ }
+ browser.contextMenus.create({ title: "gamma", contexts: ["all"] });
+ });
+ },
+ });
+
+ const second = ExtensionTestUtils.loadExtension({
+ manifest,
+ background() {
+ browser.contextMenus.create({ title: "beta", contexts: ["all"] });
+
+ browser.contextMenus.onClicked.addListener(() => {
+ browser.contextMenus.removeAll();
+ });
+
+ browser.test.onMessage.addListener(() => {
+ browser.test.sendMessage("pong-beta");
+ });
+ },
+ });
+
+ await first.startup();
+ await second.startup();
+
+ async function confirmMenuItems(...items) {
+ // Round-trip to extension to make sure that the context menu state has been
+ // updated by the async contextMenus.create / contextMenus.removeAll calls.
+ first.sendMessage("ping");
+ second.sendMessage("ping");
+ await first.awaitMessage("pong-alpha");
+ await second.awaitMessage("pong-beta");
+
+ const menu = await openContextMenu();
+ for (const id of ["alpha", "beta", "gamma"]) {
+ const expected = items.includes(id);
+ const found = menu.getElementsByAttribute("label", id);
+ is(
+ !!found.length,
+ expected,
+ `menu item ${id} ${expected ? "" : "not "}found`
+ );
+ }
+ // Return the first menu item, we need to click it.
+ return menu.getElementsByAttribute("label", items[0])[0];
+ }
+
+ // Confirm alpha, beta exist; click alpha to remove it.
+ const alpha = await confirmMenuItems("alpha", "beta");
+ await closeExtensionContextMenu(alpha);
+
+ // Confirm only beta exists.
+ await confirmMenuItems("beta");
+ await closeContextMenu();
+
+ // Create gamma, confirm, click.
+ first.sendMessage("create");
+ const beta = await confirmMenuItems("beta", "gamma");
+ await closeExtensionContextMenu(beta);
+
+ // Confirm only gamma is left.
+ await confirmMenuItems("gamma");
+ await closeContextMenu();
+
+ await first.unload();
+ await second.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+function bookmarkContextMenuExtension() {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus", "bookmarks", "activeTab"],
+ },
+ async background() {
+ const url = "https://example.com/";
+ const title = "Example";
+ let newBookmark = await browser.bookmarks.create({
+ url,
+ title,
+ parentId: "toolbar_____",
+ });
+ browser.contextMenus.onClicked.addListener(async (info, tab) => {
+ browser.test.assertEq(
+ undefined,
+ tab,
+ "click event in bookmarks menu is not associated with any tab"
+ );
+ browser.test.assertEq(
+ newBookmark.id,
+ info.bookmarkId,
+ "Bookmark ID matches"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript({ code: "'some code';" }),
+ /Missing host permission for the tab/,
+ "Content script should not run, activeTab should not be granted to bookmark menu events"
+ );
+
+ let [bookmark] = await browser.bookmarks.get(info.bookmarkId);
+ browser.test.assertEq(title, bookmark.title, "Bookmark title matches");
+ browser.test.assertEq(url, bookmark.url, "Bookmark url matches");
+ browser.test.assertFalse(
+ info.hasOwnProperty("pageUrl"),
+ "Context menu does not expose pageUrl"
+ );
+ await browser.bookmarks.remove(info.bookmarkId);
+ browser.test.sendMessage("test-finish");
+ });
+ browser.contextMenus.create(
+ {
+ title: "Get bookmark",
+ contexts: ["bookmark"],
+ },
+ () => {
+ browser.test.sendMessage("bookmark-created", newBookmark.id);
+ }
+ );
+ },
+ });
+}
+
+add_task(async function test_bookmark_contextmenu() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ await toggleBookmarksToolbar(true);
+
+ const extension = bookmarkContextMenuExtension();
+
+ await extension.startup();
+ await extension.awaitMessage("bookmark-created");
+ let menu = await openChromeContextMenu(
+ "placesContext",
+ "#PersonalToolbar .bookmark-item:last-child"
+ );
+
+ let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
+ closeChromeContextMenu("placesContext", menuItem);
+
+ await extension.awaitMessage("test-finish");
+ await extension.unload();
+ await toggleBookmarksToolbar(false);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_bookmark_sidebar_contextmenu() {
+ await withSidebarTree("bookmarks", async tree => {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ let extension = bookmarkContextMenuExtension();
+ await extension.startup();
+ let bookmarkGuid = await extension.awaitMessage("bookmark-created");
+
+ let sidebar = window.SidebarUI.browser;
+ let menu = sidebar.contentDocument.getElementById("placesContext");
+ tree.selectItems([bookmarkGuid]);
+ let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await shown;
+
+ let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
+ closeChromeContextMenu("placesContext", menuItem, sidebar.contentWindow);
+ await extension.awaitMessage("test-finish");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+function bookmarkFolderContextMenuExtension() {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus", "bookmarks"],
+ },
+ async background() {
+ const title = "Example";
+ let newBookmark = await browser.bookmarks.create({
+ title,
+ parentId: "toolbar_____",
+ });
+ await new Promise(resolve =>
+ browser.contextMenus.create(
+ {
+ title: "Get bookmark",
+ contexts: ["bookmark"],
+ },
+ resolve
+ )
+ );
+ browser.contextMenus.onClicked.addListener(async info => {
+ browser.test.assertEq(
+ newBookmark.id,
+ info.bookmarkId,
+ "Bookmark ID matches"
+ );
+
+ let [bookmark] = await browser.bookmarks.get(info.bookmarkId);
+ browser.test.assertEq(title, bookmark.title, "Bookmark title matches");
+ browser.test.assertFalse(
+ info.hasOwnProperty("pageUrl"),
+ "Context menu does not expose pageUrl"
+ );
+ await browser.bookmarks.remove(info.bookmarkId);
+ browser.test.sendMessage("test-finish");
+ });
+ browser.test.sendMessage("bookmark-created", newBookmark.id);
+ },
+ });
+}
+
+add_task(async function test_organizer_contextmenu() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ let library = await promiseLibrary("BookmarksToolbar");
+
+ let menu = library.document.getElementById("placesContext");
+ let mainTree = library.document.getElementById("placeContent");
+ let leftTree = library.document.getElementById("placesList");
+
+ let tests = [
+ [mainTree, bookmarkContextMenuExtension],
+ [mainTree, bookmarkFolderContextMenuExtension],
+ [leftTree, bookmarkFolderContextMenuExtension],
+ ];
+
+ for (let [tree, makeExtension] of tests) {
+ let extension = makeExtension();
+ await extension.startup();
+ let bookmarkGuid = await extension.awaitMessage("bookmark-created");
+
+ tree.selectItems([bookmarkGuid]);
+ let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await shown;
+
+ let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
+ closeChromeContextMenu("placesContext", menuItem, library);
+ await extension.awaitMessage("test-finish");
+ await extension.unload();
+ }
+
+ await promiseLibraryClosed(library);
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_bookmark_context_requires_permission() {
+ await toggleBookmarksToolbar(true);
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create(
+ {
+ title: "Get bookmark",
+ contexts: ["bookmark"],
+ },
+ () => {
+ browser.test.sendMessage("bookmark-created");
+ }
+ );
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("bookmark-created");
+ let menu = await openChromeContextMenu(
+ "placesContext",
+ "#PersonalToolbar .bookmark-item:last-child"
+ );
+
+ Assert.equal(
+ menu.getElementsByAttribute("label", "Get bookmark").length,
+ 0,
+ "bookmark context menu not created with `bookmarks` permission."
+ );
+
+ closeChromeContextMenu("placesContext");
+
+ await extension.unload();
+ await toggleBookmarksToolbar(false);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js
new file mode 100644
index 0000000000..1e95899513
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js
@@ -0,0 +1,115 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/places/tests/browser/head.js",
+ this
+);
+/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell, promiseLibrary, promiseLibraryClosed */
+
+function bookmarkContextMenuExtension() {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus", "bookmarks"],
+ },
+ async background() {
+ const CONTEXT_ENTRY_LABEL = "Test Context Entry ";
+
+ browser.contextMenus.create(
+ {
+ title: CONTEXT_ENTRY_LABEL,
+ contexts: ["bookmark"],
+ onclick: (info, tab) => {
+ browser.test.sendMessage(`clicked`, info.bookmarkId);
+ },
+ },
+ () => {
+ browser.test.assertEq(
+ browser.runtime.lastError,
+ null,
+ "Created context menu"
+ );
+ browser.test.sendMessage("created", CONTEXT_ENTRY_LABEL);
+ }
+ );
+ },
+ });
+}
+
+add_task(async function test_bookmark_sidebar_contextmenu() {
+ await withSidebarTree("bookmarks", async tree => {
+ let extension = bookmarkContextMenuExtension();
+ await extension.startup();
+ let context_entry_label = await extension.awaitMessage("created");
+
+ const expected_bookmarkID_2_virtualID = new Map([
+ ["toolbar_____", "toolbar____v"], // Bookmarks Toolbar
+ ["menu________", "menu_______v"], // Bookmarks Menu
+ ["unfiled_____", "unfiled____v"], // Other Bookmarks
+ ]);
+
+ for (let [
+ expectedBookmarkID,
+ expectedVirtualID,
+ ] of expected_bookmarkID_2_virtualID) {
+ info(`Testing context menu for Bookmark ID "${expectedBookmarkID}"`);
+ let sidebar = window.SidebarUI.browser;
+ let menu = sidebar.contentDocument.getElementById("placesContext");
+ tree.selectItems([expectedBookmarkID]);
+
+ let min = {},
+ max = {};
+ tree.view.selection.getRangeAt(0, min, max);
+ let node = tree.view.nodeForTreeIndex(min.value);
+ const actualVirtualID = node.bookmarkGuid;
+ Assert.equal(actualVirtualID, expectedVirtualID, "virtualIDs match");
+
+ let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await shown;
+
+ let menuItem = menu.getElementsByAttribute(
+ "label",
+ context_entry_label
+ )[0];
+ closeChromeContextMenu("placesContext", menuItem, sidebar.contentWindow);
+
+ const actualBookmarkID = await extension.awaitMessage(`clicked`);
+ Assert.equal(actualBookmarkID, expectedBookmarkID, "bookmarkIDs match");
+ }
+ await extension.unload();
+ });
+});
+
+add_task(async function test_bookmark_library_contextmenu() {
+ let extension = bookmarkContextMenuExtension();
+ await extension.startup();
+ let context_entry_label = await extension.awaitMessage("created");
+
+ let library = await promiseLibrary("BookmarksToolbar");
+ let menu = library.document.getElementById("placesContext");
+ let leftTree = library.document.getElementById("placesList");
+
+ const treeIDs = [
+ "allbms_____v",
+ "history____v",
+ "downloads__v",
+ "tags_______v",
+ ];
+
+ for (let treeID of treeIDs) {
+ info(`Testing context menu for TreeID "${treeID}"`);
+ leftTree.selectItems([treeID]);
+
+ let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ synthesizeClickOnSelectedTreeCell(leftTree, { type: "contextmenu" });
+ await shown;
+
+ let items = menu.getElementsByAttribute("label", context_entry_label);
+ Assert.equal(items.length, 0, "no extension context entry");
+ closeChromeContextMenu("placesContext", null, library);
+ }
+ await extension.unload();
+ await promiseLibraryClosed(library);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_checkboxes.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_checkboxes.js
new file mode 100644
index 0000000000..a471ae7e58
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_checkboxes.js
@@ -0,0 +1,157 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+
+ background: function () {
+ // Report onClickData info back.
+ browser.contextMenus.onClicked.addListener(info => {
+ browser.test.sendMessage("contextmenus-click", info);
+ });
+
+ browser.contextMenus.create({
+ title: "Checkbox",
+ type: "checkbox",
+ });
+
+ browser.test.sendMessage("single-contextmenu-item-added");
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "add-additional-menu-items") {
+ return;
+ }
+
+ browser.contextMenus.create({
+ type: "separator",
+ });
+
+ browser.contextMenus.create({
+ title: "Checkbox",
+ type: "checkbox",
+ checked: true,
+ });
+
+ browser.contextMenus.create({
+ title: "Checkbox",
+ type: "checkbox",
+ });
+
+ browser.test.notifyPass("contextmenus-checkboxes");
+ });
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("single-contextmenu-item-added");
+
+ async function testSingleCheckboxItem() {
+ let extensionMenuRoot = await openExtensionContextMenu();
+
+ // On Linux, the single menu item should be contained in a submenu.
+ if (AppConstants.platform === "linux") {
+ let items = extensionMenuRoot.getElementsByAttribute("type", "checkbox");
+ is(items.length, 1, "single checkbox should be in the submenu on Linux");
+ await closeContextMenu();
+ } else {
+ is(
+ extensionMenuRoot,
+ null,
+ "there should be no submenu for a single checkbox item"
+ );
+ await closeContextMenu();
+ }
+ }
+
+ await testSingleCheckboxItem();
+
+ extension.sendMessage("add-additional-menu-items");
+ await extension.awaitFinish("contextmenus-checkboxes");
+
+ function confirmCheckboxStates(extensionMenuRoot, expectedStates) {
+ let checkboxItems = extensionMenuRoot.getElementsByAttribute(
+ "type",
+ "checkbox"
+ );
+
+ is(
+ checkboxItems.length,
+ 3,
+ "there should be 3 checkbox items in the context menu"
+ );
+
+ is(
+ checkboxItems[0].hasAttribute("checked"),
+ expectedStates[0],
+ `checkbox item 1 has state (checked=${expectedStates[0]})`
+ );
+ is(
+ checkboxItems[1].hasAttribute("checked"),
+ expectedStates[1],
+ `checkbox item 2 has state (checked=${expectedStates[1]})`
+ );
+ is(
+ checkboxItems[2].hasAttribute("checked"),
+ expectedStates[2],
+ `checkbox item 3 has state (checked=${expectedStates[2]})`
+ );
+
+ return extensionMenuRoot.getElementsByAttribute("type", "checkbox");
+ }
+
+ function confirmOnClickData(onClickData, id, was, checked) {
+ is(
+ onClickData.wasChecked,
+ was,
+ `checkbox item ${id} was ${was ? "" : "not "}checked before the click`
+ );
+ is(
+ onClickData.checked,
+ checked,
+ `checkbox item ${id} is ${checked ? "" : "not "}checked after the click`
+ );
+ }
+
+ let extensionMenuRoot = await openExtensionContextMenu();
+ let items = confirmCheckboxStates(extensionMenuRoot, [false, true, false]);
+ await closeExtensionContextMenu(items[0]);
+
+ let result = await extension.awaitMessage("contextmenus-click");
+ confirmOnClickData(result, 1, false, true);
+
+ extensionMenuRoot = await openExtensionContextMenu();
+ items = confirmCheckboxStates(extensionMenuRoot, [true, true, false]);
+ await closeExtensionContextMenu(items[2]);
+
+ result = await extension.awaitMessage("contextmenus-click");
+ confirmOnClickData(result, 3, false, true);
+
+ extensionMenuRoot = await openExtensionContextMenu();
+ items = confirmCheckboxStates(extensionMenuRoot, [true, true, true]);
+ await closeExtensionContextMenu(items[0]);
+
+ result = await extension.awaitMessage("contextmenus-click");
+ confirmOnClickData(result, 1, true, false);
+
+ extensionMenuRoot = await openExtensionContextMenu();
+ items = confirmCheckboxStates(extensionMenuRoot, [false, true, true]);
+ await closeExtensionContextMenu(items[2]);
+
+ result = await extension.awaitMessage("contextmenus-click");
+ confirmOnClickData(result, 3, true, false);
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_commands.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_commands.js
new file mode 100644
index 0000000000..5a8f1db208
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_commands.js
@@ -0,0 +1,158 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testTabSwitchActionContext() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_actions_context_menu() {
+ function background() {
+ browser.contextMenus.create({
+ title: "open_browser_action",
+ contexts: ["all"],
+ command: "_execute_browser_action",
+ });
+ browser.contextMenus.create({
+ title: "open_page_action",
+ contexts: ["all"],
+ command: "_execute_page_action",
+ });
+ browser.contextMenus.create({
+ title: "open_sidebar_action",
+ contexts: ["all"],
+ command: "_execute_sidebar_action",
+ });
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ browser.pageAction.show(tabId);
+ });
+ browser.contextMenus.onClicked.addListener(() => {
+ browser.test.fail(`menu onClicked should not have been received`);
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ function testScript() {
+ window.onload = () => {
+ browser.test.sendMessage("test-opened", true);
+ };
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "contextMenus commands",
+ permissions: ["contextMenus", "activeTab", "tabs"],
+ browser_action: {
+ default_title: "Test BrowserAction",
+ default_popup: "test.html",
+ browser_style: true,
+ },
+ page_action: {
+ default_title: "Test PageAction",
+ default_popup: "test.html",
+ browser_style: true,
+ },
+ sidebar_action: {
+ default_title: "Test Sidebar",
+ default_panel: "test.html",
+ },
+ },
+ background,
+ files: {
+ "test.html": ` `,
+ "test.js": testScript,
+ },
+ });
+
+ async function testContext(id) {
+ const menu = await openExtensionContextMenu();
+ const items = menu.getElementsByAttribute("label", id);
+ is(items.length, 1, `exactly one menu item found`);
+ await closeExtensionContextMenu(items[0]);
+ return extension.awaitMessage("test-opened");
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // open a page so page action works
+ const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html?test=commands";
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ ok(
+ await testContext("open_sidebar_action"),
+ "_execute_sidebar_action worked"
+ );
+ ok(
+ await testContext("open_browser_action"),
+ "_execute_browser_action worked"
+ );
+ ok(await testContext("open_page_action"), "_execute_page_action worked");
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_v3_action_context_menu() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "contextMenus commands",
+ manifest_version: 3,
+ permissions: ["contextMenus"],
+ action: {
+ default_title: "Test Action",
+ default_popup: "test.html",
+ // TODO bug 1830712: Remove this. Probably not even needed for the test.
+ browser_style: true,
+ },
+ },
+ background() {
+ browser.contextMenus.onClicked.addListener(() => {
+ browser.test.fail(`menu onClicked should not have been received`);
+ });
+
+ browser.contextMenus.create(
+ {
+ id: "open_action",
+ title: "open_action",
+ contexts: ["all"],
+ command: "_execute_action",
+ },
+ () => {
+ browser.test.sendMessage("ready");
+ }
+ );
+ },
+ files: {
+ "test.html": ` `,
+ "test.js": () => {
+ window.onload = () => {
+ browser.test.sendMessage("test-opened", true);
+ };
+ },
+ },
+ });
+
+ async function testContext(id) {
+ const menu = await openContextMenu();
+ const items = menu.getElementsByAttribute("label", id);
+ is(items.length, 1, `exactly one menu item found`);
+ await closeExtensionContextMenu(items[0]);
+ return extension.awaitMessage("test-opened");
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html?test=commands";
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ ok(await testContext("open_action"), "_execute_action worked");
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
new file mode 100644
index 0000000000..30d4d528b2
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
@@ -0,0 +1,493 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html?test=icons";
+
+add_task(async function test_root_icon() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ let encodedImageData =
+ "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+ const IMAGE_ARRAYBUFFER = imageBufferFromDataURI(encodedImageData);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "contextMenus icons",
+ permissions: ["contextMenus"],
+ icons: {
+ 18: "extension.png",
+ },
+ },
+
+ files: {
+ "extension.png": IMAGE_ARRAYBUFFER,
+ },
+
+ background: function () {
+ let menuitemId = browser.contextMenus.create({
+ title: "child-to-delete",
+ onclick: () => {
+ browser.contextMenus.remove(menuitemId);
+ browser.test.sendMessage("child-deleted");
+ },
+ });
+
+ browser.contextMenus.create(
+ {
+ title: "child",
+ },
+ () => {
+ browser.test.sendMessage("contextmenus-icons");
+ }
+ );
+ },
+ });
+
+ let confirmContextMenuIcon = rootElements => {
+ let expectedURL = new RegExp(
+ String.raw`^moz-extension://[^/]+/extension\.png$`
+ );
+ is(rootElements.length, 1, "Found exactly one menu item");
+ let imageUrl = rootElements[0].getAttribute("image");
+ ok(
+ expectedURL.test(imageUrl),
+ "The context menu should display the extension icon next to the root element"
+ );
+ };
+
+ await extension.startup();
+ await extension.awaitMessage("contextmenus-icons");
+
+ let extensionMenu = await openExtensionContextMenu();
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let topLevelMenuItem = contextMenu.getElementsByAttribute(
+ "ext-type",
+ "top-level-menu"
+ );
+ confirmContextMenuIcon(topLevelMenuItem);
+
+ let childToDelete = extensionMenu.getElementsByAttribute(
+ "label",
+ "child-to-delete"
+ );
+ is(childToDelete.length, 1, "Found exactly one child to delete");
+ await closeExtensionContextMenu(childToDelete[0]);
+ await extension.awaitMessage("child-deleted");
+
+ await openExtensionContextMenu();
+
+ contextMenu = document.getElementById("contentAreaContextMenu");
+ topLevelMenuItem = contextMenu.getElementsByAttribute("label", "child");
+
+ confirmContextMenuIcon(topLevelMenuItem);
+ await closeContextMenu();
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_child_icon() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ let blackIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEhkO2P07+gAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAARSURBVCjPY2AYBaNgFAxPAAAD3gABo0ohTgAAAABJRU5ErkJggg==";
+ const IMAGE_ARRAYBUFFER_BLACK = imageBufferFromDataURI(blackIconData);
+
+ let redIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEgw1XkM0ygAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAYSURBVCjPY/zPQA5gYhjVNqptVNsg1wYAItkBI/GNR3YAAAAASUVORK5CYII=";
+ const IMAGE_ARRAYBUFFER_RED = imageBufferFromDataURI(redIconData);
+
+ let blueIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEg0QDFzRzAAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAbSURBVCjPY2SQ+89AOmBiIAuMahvVNqqNftoAlKMBQZXKX9kAAAAASUVORK5CYII=";
+ const IMAGE_ARRAYBUFFER_BLUE = imageBufferFromDataURI(blueIconData);
+
+ let greenIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEg0rvVc46AAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAaSURBVCjPY+Q8xkAGYGJgGNU2qm1U2+DWBgBolADz1beTnwAAAABJRU5ErkJggg==";
+ const IMAGE_ARRAYBUFFER_GREEN = imageBufferFromDataURI(greenIconData);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ icons: {
+ 18: "black_icon.png",
+ },
+ },
+
+ files: {
+ "black_icon.png": IMAGE_ARRAYBUFFER_BLACK,
+ "red_icon.png": IMAGE_ARRAYBUFFER_RED,
+ "blue_icon.png": IMAGE_ARRAYBUFFER_BLUE,
+ "green_icon.png": IMAGE_ARRAYBUFFER_GREEN,
+ },
+
+ background: function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "add-additional-contextmenu-items") {
+ return;
+ }
+
+ browser.contextMenus.create({
+ title: "child2",
+ id: "contextmenu-child2",
+ icons: {
+ 18: "blue_icon.png",
+ },
+ });
+
+ browser.contextMenus.create(
+ {
+ title: "child3",
+ id: "contextmenu-child3",
+ icons: {
+ 18: "green_icon.png",
+ },
+ },
+ () => {
+ browser.test.sendMessage("extra-contextmenu-items-added");
+ }
+ );
+ });
+
+ browser.contextMenus.create(
+ {
+ title: "child1",
+ id: "contextmenu-child1",
+ icons: {
+ 18: "red_icon.png",
+ },
+ },
+ () => {
+ browser.test.sendMessage("single-contextmenu-item-added");
+ }
+ );
+ },
+ });
+
+ let confirmContextMenuIcon = (element, imageName) => {
+ let imageURL = element.getAttribute("image");
+ ok(
+ imageURL.endsWith(imageName),
+ "The context menu should display the extension icon next to the child element"
+ );
+ };
+
+ await extension.startup();
+
+ await extension.awaitMessage("single-contextmenu-item-added");
+
+ let contextMenu = await openContextMenu();
+ let contextMenuChild1 = contextMenu.getElementsByAttribute(
+ "label",
+ "child1"
+ )[0];
+ confirmContextMenuIcon(contextMenuChild1, "black_icon.png");
+
+ await closeContextMenu();
+
+ extension.sendMessage("add-additional-contextmenu-items");
+ await extension.awaitMessage("extra-contextmenu-items-added");
+
+ contextMenu = await openExtensionContextMenu();
+
+ contextMenuChild1 = contextMenu.getElementsByAttribute("label", "child1")[0];
+ confirmContextMenuIcon(contextMenuChild1, "red_icon.png");
+
+ let contextMenuChild2 = contextMenu.getElementsByAttribute(
+ "label",
+ "child2"
+ )[0];
+ confirmContextMenuIcon(contextMenuChild2, "blue_icon.png");
+
+ let contextMenuChild3 = contextMenu.getElementsByAttribute(
+ "label",
+ "child3"
+ )[0];
+ confirmContextMenuIcon(contextMenuChild3, "green_icon.png");
+
+ await closeContextMenu();
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_manifest_without_icons() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ let redIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEgw1XkM0ygAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAYSURBVCjPY/zPQA5gYhjVNqptVNsg1wYAItkBI/GNR3YAAAAASUVORK5CYII=";
+ const IMAGE_ARRAYBUFFER_RED = imageBufferFromDataURI(redIconData);
+
+ let greenIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEg0rvVc46AAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAaSURBVCjPY+Q8xkAGYGJgGNU2qm1U2+DWBgBolADz1beTnwAAAABJRU5ErkJggg==";
+ const IMAGE_ARRAYBUFFER_GREEN = imageBufferFromDataURI(greenIconData);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "contextMenus icons",
+ permissions: ["contextMenus"],
+ },
+ files: {
+ "red.png": IMAGE_ARRAYBUFFER_RED,
+ "green.png": IMAGE_ARRAYBUFFER_GREEN,
+ },
+
+ background() {
+ browser.contextMenus.create(
+ {
+ title: "first item",
+ icons: {
+ 18: "red.png",
+ },
+ onclick() {
+ browser.contextMenus.create(
+ {
+ title: "second item",
+ icons: {
+ 18: "green.png",
+ },
+ },
+ () => {
+ browser.test.sendMessage("added-second-item");
+ }
+ );
+ },
+ },
+ () => {
+ browser.test.sendMessage("contextmenus-icons");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("contextmenus-icons");
+
+ let menu = await openContextMenu();
+ let items = menu.getElementsByAttribute("label", "first item");
+ is(items.length, 1, "Found first item");
+ // manifest.json does not declare icons, so the root menu item shouldn't have an icon either.
+ is(items[0].getAttribute("image"), "", "Root menu must not have an icon");
+
+ await closeExtensionContextMenu(items[0]);
+ await extension.awaitMessage("added-second-item");
+
+ menu = await openExtensionContextMenu();
+ items = document.querySelectorAll(
+ "#contentAreaContextMenu [ext-type='top-level-menu']"
+ );
+ is(items.length, 1, "Auto-generated root item exists");
+ is(
+ items[0].getAttribute("image"),
+ "",
+ "Auto-generated menu root must not have an icon"
+ );
+
+ items = menu.getElementsByAttribute("label", "first item");
+ is(items.length, 1, "First child item should exist");
+ is(
+ items[0].getAttribute("image").split("/").pop(),
+ "red.png",
+ "First item should have an icon"
+ );
+
+ items = menu.getElementsByAttribute("label", "second item");
+ is(items.length, 1, "Secobnd child item should exist");
+ is(
+ items[0].getAttribute("image").split("/").pop(),
+ "green.png",
+ "Second item should have an icon"
+ );
+
+ await closeExtensionContextMenu();
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_child_icon_update() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ let blackIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEhkO2P07+gAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAARSURBVCjPY2AYBaNgFAxPAAAD3gABo0ohTgAAAABJRU5ErkJggg==";
+ const IMAGE_ARRAYBUFFER_BLACK = imageBufferFromDataURI(blackIconData);
+
+ let redIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEgw1XkM0ygAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAYSURBVCjPY/zPQA5gYhjVNqptVNsg1wYAItkBI/GNR3YAAAAASUVORK5CYII=";
+ const IMAGE_ARRAYBUFFER_RED = imageBufferFromDataURI(redIconData);
+
+ let blueIconData =
+ "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEg0QDFzRzAAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAbSURBVCjPY2SQ+89AOmBiIAuMahvVNqqNftoAlKMBQZXKX9kAAAAASUVORK5CYII=";
+ const IMAGE_ARRAYBUFFER_BLUE = imageBufferFromDataURI(blueIconData);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ icons: {
+ 18: "black_icon.png",
+ },
+ },
+
+ files: {
+ "black_icon.png": IMAGE_ARRAYBUFFER_BLACK,
+ "red_icon.png": IMAGE_ARRAYBUFFER_RED,
+ "blue_icon.png": IMAGE_ARRAYBUFFER_BLUE,
+ },
+
+ background: function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "update-contextmenu-item") {
+ browser.contextMenus.update(
+ "contextmenu-child2",
+ {
+ icons: {
+ 18: "blue_icon.png",
+ },
+ },
+ () => {
+ browser.test.sendMessage("contextmenu-item-updated");
+ }
+ );
+ } else if (msg === "update-contextmenu-item-without-icons") {
+ browser.contextMenus.update("contextmenu-child2", {}, () => {
+ browser.test.sendMessage("contextmenu-item-updated-without-icons");
+ });
+ } else if (msg === "update-contextmenu-item-with-icons-as-null") {
+ browser.contextMenus.update(
+ "contextmenu-child2",
+ {
+ icons: null,
+ },
+ () => {
+ browser.test.sendMessage(
+ "contextmenu-item-updated-with-icons-as-null"
+ );
+ }
+ );
+ } else if (msg === "update-contextmenu-item-when-its-the-only-child") {
+ browser.contextMenus.update(
+ "contextmenu-child1",
+ {
+ icons: {
+ 18: "blue_icon.png",
+ },
+ },
+ () => {
+ browser.test.sendMessage(
+ "contextmenu-item-updated-when-its-only-child"
+ );
+ }
+ );
+ }
+ });
+
+ browser.contextMenus.create({
+ title: "child1",
+ id: "contextmenu-child1",
+ icons: {
+ 18: "blue_icon.png",
+ },
+ });
+
+ let menuitemId = browser.contextMenus.create(
+ {
+ title: "child2",
+ id: "contextmenu-child2",
+ icons: {
+ 18: "red_icon.png",
+ },
+ onclick: async () => {
+ await browser.contextMenus.remove(menuitemId);
+ browser.test.sendMessage("child-deleted");
+ },
+ },
+ () => {
+ browser.test.sendMessage("contextmenu-items-added");
+ }
+ );
+ },
+ });
+
+ let confirmContextMenuIcon = (element, imageName) => {
+ let imageURL = element.getAttribute("image");
+ ok(
+ imageURL.endsWith(imageName),
+ "The context menu should display the extension icon next to the child element"
+ );
+ };
+
+ await extension.startup();
+
+ await extension.awaitMessage("contextmenu-items-added");
+ let contextMenu = await openExtensionContextMenu();
+
+ let contextMenuChild1 = contextMenu.getElementsByAttribute(
+ "label",
+ "child1"
+ )[0];
+ confirmContextMenuIcon(contextMenuChild1, "blue_icon.png");
+
+ let contextMenuChild2 = contextMenu.getElementsByAttribute(
+ "label",
+ "child2"
+ )[0];
+ confirmContextMenuIcon(contextMenuChild2, "red_icon.png");
+
+ await closeContextMenu();
+
+ extension.sendMessage("update-contextmenu-item");
+ await extension.awaitMessage("contextmenu-item-updated");
+
+ contextMenu = await openExtensionContextMenu();
+
+ contextMenuChild2 = contextMenu.getElementsByAttribute("label", "child2")[0];
+ confirmContextMenuIcon(contextMenuChild2, "blue_icon.png");
+
+ await closeContextMenu();
+
+ extension.sendMessage("update-contextmenu-item-without-icons");
+ await extension.awaitMessage("contextmenu-item-updated-without-icons");
+
+ contextMenu = await openExtensionContextMenu();
+
+ contextMenuChild2 = contextMenu.getElementsByAttribute("label", "child2")[0];
+ confirmContextMenuIcon(contextMenuChild2, "blue_icon.png");
+
+ await closeContextMenu();
+
+ extension.sendMessage("update-contextmenu-item-with-icons-as-null");
+ await extension.awaitMessage("contextmenu-item-updated-with-icons-as-null");
+
+ contextMenu = await openExtensionContextMenu();
+
+ contextMenuChild2 = contextMenu.getElementsByAttribute("label", "child2")[0];
+ is(
+ contextMenuChild2.getAttribute("image"),
+ "",
+ "Second child should not have an icon"
+ );
+
+ await closeExtensionContextMenu(contextMenuChild2);
+ await extension.awaitMessage("child-deleted");
+
+ contextMenu = await openContextMenu();
+
+ contextMenuChild1 = contextMenu.getElementsByAttribute("label", "child1")[0];
+ confirmContextMenuIcon(contextMenuChild1, "black_icon.png");
+
+ await closeContextMenu();
+
+ extension.sendMessage("update-contextmenu-item-when-its-the-only-child");
+ await extension.awaitMessage("contextmenu-item-updated-when-its-only-child");
+
+ contextMenu = await openContextMenu();
+
+ contextMenuChild1 = contextMenu.getElementsByAttribute("label", "child1")[0];
+ confirmContextMenuIcon(contextMenuChild1, "black_icon.png");
+
+ await closeContextMenu();
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js
new file mode 100644
index 0000000000..72c365f7d7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js
@@ -0,0 +1,297 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+// Loaded both as a background script and a tab page.
+function testScript() {
+ let page = location.pathname.includes("tab.html") ? "tab" : "background";
+ let clickCounts = {
+ old: 0,
+ new: 0,
+ };
+ browser.contextMenus.onClicked.addListener(() => {
+ // Async to give other onclick handlers a chance to fire.
+ setTimeout(() => {
+ browser.test.sendMessage("onClicked-fired", page);
+ });
+ });
+ browser.test.onMessage.addListener((toPage, msg) => {
+ if (toPage !== page) {
+ return;
+ }
+ browser.test.log(`Received ${msg} for ${toPage}`);
+ if (msg == "get-click-counts") {
+ browser.test.sendMessage("click-counts", clickCounts);
+ } else if (msg == "clear-click-counts") {
+ clickCounts.old = clickCounts.new = 0;
+ browser.test.sendMessage("next");
+ } else if (msg == "create-with-onclick") {
+ browser.contextMenus.create(
+ {
+ id: "iden",
+ title: "tifier",
+ onclick() {
+ ++clickCounts.old;
+ browser.test.log(
+ `onclick fired for original onclick property in ${page}`
+ );
+ },
+ },
+ () => browser.test.sendMessage("next")
+ );
+ } else if (msg == "create-without-onclick") {
+ browser.contextMenus.create(
+ {
+ id: "iden",
+ title: "tifier",
+ },
+ () => browser.test.sendMessage("next")
+ );
+ } else if (msg == "update-without-onclick") {
+ browser.contextMenus.update(
+ "iden",
+ {
+ enabled: true, // Already enabled, so this does nothing.
+ },
+ () => browser.test.sendMessage("next")
+ );
+ } else if (msg == "update-with-onclick") {
+ browser.contextMenus.update(
+ "iden",
+ {
+ onclick() {
+ ++clickCounts.new;
+ browser.test.log(
+ `onclick fired for updated onclick property in ${page}`
+ );
+ },
+ },
+ () => browser.test.sendMessage("next")
+ );
+ } else if (msg == "remove") {
+ browser.contextMenus.remove("iden", () =>
+ browser.test.sendMessage("next")
+ );
+ } else if (msg == "removeAll") {
+ browser.contextMenus.removeAll(() => browser.test.sendMessage("next"));
+ }
+ });
+
+ if (page == "background") {
+ browser.test.log("Opening tab.html");
+ browser.tabs.create({
+ url: "tab.html",
+ active: false, // To not interfere with the context menu tests.
+ });
+ } else {
+ // Sanity check - the pages must be in the same process.
+ let pages = browser.extension.getViews();
+ browser.test.assertTrue(
+ pages.includes(window),
+ "Expected this tab to be an extension view"
+ );
+ pages = pages.filter(w => w !== window);
+ browser.test.assertEq(
+ pages[0],
+ browser.extension.getBackgroundPage(),
+ "Expected the other page to be a background page"
+ );
+ browser.test.sendMessage("tab.html ready");
+ }
+}
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ background: testScript,
+ files: {
+ "tab.html": ` `,
+ "tab.js": testScript,
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("tab.html ready");
+
+ async function clickContextMenu() {
+ // Using openContextMenu instead of openExtensionContextMenu because the
+ // test extension has only one context menu item.
+ let extensionMenuRoot = await openContextMenu();
+ let items = extensionMenuRoot.getElementsByAttribute("label", "tifier");
+ is(items.length, 1, "Expected one context menu item");
+ await closeExtensionContextMenu(items[0]);
+ // One of them is "tab", the other is "background".
+ info(`onClicked from: ${await extension.awaitMessage("onClicked-fired")}`);
+ info(`onClicked from: ${await extension.awaitMessage("onClicked-fired")}`);
+ }
+
+ function getCounts(page) {
+ extension.sendMessage(page, "get-click-counts");
+ return extension.awaitMessage("click-counts");
+ }
+ async function resetCounts() {
+ extension.sendMessage("tab", "clear-click-counts");
+ extension.sendMessage("background", "clear-click-counts");
+ await extension.awaitMessage("next");
+ await extension.awaitMessage("next");
+ }
+
+ // During this test, at most one "onclick" attribute is expected at any time.
+ for (let pageOne of ["background", "tab"]) {
+ for (let pageTwo of ["background", "tab"]) {
+ info(`Testing with menu created by ${pageOne} and updated by ${pageTwo}`);
+ extension.sendMessage(pageOne, "create-with-onclick");
+ await extension.awaitMessage("next");
+
+ // Test that update without onclick attribute does not clear the existing
+ // onclick handler.
+ extension.sendMessage(pageTwo, "update-without-onclick");
+ await extension.awaitMessage("next");
+ await clickContextMenu();
+ let clickCounts = await getCounts(pageOne);
+ is(
+ clickCounts.old,
+ 1,
+ `Original onclick should still be present in ${pageOne}`
+ );
+ is(clickCounts.new, 0, `Not expecting any new handlers in ${pageOne}`);
+ if (pageOne !== pageTwo) {
+ clickCounts = await getCounts(pageTwo);
+ is(clickCounts.old, 0, `Not expecting any handlers in ${pageTwo}`);
+ is(clickCounts.new, 0, `Not expecting any new handlers in ${pageTwo}`);
+ }
+ await resetCounts();
+
+ // Test that update with onclick handler in a different page clears the
+ // existing handler and activates the new onclick handler.
+ extension.sendMessage(pageTwo, "update-with-onclick");
+ await extension.awaitMessage("next");
+ await clickContextMenu();
+ clickCounts = await getCounts(pageOne);
+ is(clickCounts.old, 0, `Original onclick should be gone from ${pageOne}`);
+ if (pageOne !== pageTwo) {
+ is(
+ clickCounts.new,
+ 0,
+ `Still not expecting new handlers in ${pageOne}`
+ );
+ }
+ clickCounts = await getCounts(pageTwo);
+ if (pageOne !== pageTwo) {
+ is(clickCounts.old, 0, `Not expecting an old onclick in ${pageTwo}`);
+ }
+ is(clickCounts.new, 1, `New onclick should be triggered in ${pageTwo}`);
+ await resetCounts();
+
+ // Test that updating the handler (different again from the last `update`
+ // call, but the same as the `create` call) clears the existing handler
+ // and activates the new onclick handler.
+ extension.sendMessage(pageOne, "update-with-onclick");
+ await extension.awaitMessage("next");
+ await clickContextMenu();
+ clickCounts = await getCounts(pageOne);
+ is(clickCounts.new, 1, `onclick should be triggered in ${pageOne}`);
+ if (pageOne !== pageTwo) {
+ clickCounts = await getCounts(pageTwo);
+ is(clickCounts.new, 0, `onclick should be gone from ${pageTwo}`);
+ }
+ await resetCounts();
+
+ // Test that removing the context menu and recreating it with the same ID
+ // (in a different context) does not leave behind any onclick handlers.
+ extension.sendMessage(pageTwo, "remove");
+ await extension.awaitMessage("next");
+ extension.sendMessage(pageTwo, "create-without-onclick");
+ await extension.awaitMessage("next");
+ await clickContextMenu();
+ clickCounts = await getCounts(pageOne);
+ is(clickCounts.new, 0, `Did not expect any click handlers in ${pageOne}`);
+ if (pageOne !== pageTwo) {
+ clickCounts = await getCounts(pageTwo);
+ is(
+ clickCounts.new,
+ 0,
+ `Did not expect any click handlers in ${pageTwo}`
+ );
+ }
+ await resetCounts();
+
+ // Remove context menu for the next iteration of the test. And just to get
+ // more coverage, let's use removeAll instead of remove.
+ extension.sendMessage(pageOne, "removeAll");
+ await extension.awaitMessage("next");
+ }
+ }
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function test_onclick_modifiers() {
+ const manifest = {
+ permissions: ["contextMenus"],
+ };
+
+ function background() {
+ function onclick(info) {
+ browser.test.sendMessage("click", info);
+ }
+ browser.contextMenus.create(
+ { contexts: ["all"], title: "modify", onclick },
+ () => {
+ browser.test.sendMessage("ready");
+ }
+ );
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({ manifest, background });
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ async function click(modifiers = {}) {
+ const menu = await openContextMenu();
+ const items = menu.getElementsByAttribute("label", "modify");
+ is(items.length, 1, "Got exactly one context menu item");
+ await closeExtensionContextMenu(items[0], modifiers);
+ return extension.awaitMessage("click");
+ }
+
+ const plain = await click();
+ is(plain.modifiers.length, 0, "modifiers array empty with a plain click");
+
+ const shift = await click({ shiftKey: true });
+ is(shift.modifiers.join(), "Shift", "Correct modifier: Shift");
+
+ const ctrl = await click({ ctrlKey: true });
+ if (AppConstants.platform !== "macosx") {
+ is(ctrl.modifiers.join(), "Ctrl", "Correct modifier: Ctrl");
+ } else {
+ is(
+ ctrl.modifiers.sort().join(),
+ "Ctrl,MacCtrl",
+ "Correct modifier: Ctrl (and MacCtrl)"
+ );
+
+ const meta = await click({ metaKey: true });
+ is(meta.modifiers.join(), "Command", "Correct modifier: Command");
+ }
+
+ const altShift = await click({ altKey: true, shiftKey: true });
+ is(
+ altShift.modifiers.sort().join(),
+ "Alt,Shift",
+ "Correct modifiers: Shift+Alt"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_radioGroups.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_radioGroups.js
new file mode 100644
index 0000000000..9ca09df242
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_radioGroups.js
@@ -0,0 +1,140 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+
+ background: function () {
+ // Report onClickData info back.
+ browser.contextMenus.onClicked.addListener(info => {
+ browser.test.sendMessage("contextmenus-click", info);
+ });
+
+ browser.contextMenus.create({
+ title: "radio-group-1",
+ type: "radio",
+ checked: true,
+ });
+
+ browser.contextMenus.create({
+ type: "separator",
+ });
+
+ browser.contextMenus.create({
+ title: "radio-group-2",
+ type: "radio",
+ });
+
+ browser.contextMenus.create({
+ title: "radio-group-2",
+ type: "radio",
+ });
+
+ browser.test.notifyPass("contextmenus-radio-groups");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("contextmenus-radio-groups");
+
+ function confirmRadioGroupStates(extensionMenuRoot, expectedStates) {
+ let radioItems = extensionMenuRoot.getElementsByAttribute("type", "radio");
+ let radioGroup1 = extensionMenuRoot.getElementsByAttribute(
+ "label",
+ "radio-group-1"
+ );
+ let radioGroup2 = extensionMenuRoot.getElementsByAttribute(
+ "label",
+ "radio-group-2"
+ );
+
+ is(
+ radioItems.length,
+ 3,
+ "there should be 3 radio items in the context menu"
+ );
+ is(
+ radioGroup1.length,
+ 1,
+ "the first radio group should only have 1 radio item"
+ );
+ is(
+ radioGroup2.length,
+ 2,
+ "the second radio group should only have 2 radio items"
+ );
+
+ is(
+ radioGroup1[0].hasAttribute("checked"),
+ expectedStates[0],
+ `radio item 1 has state (checked=${expectedStates[0]})`
+ );
+ is(
+ radioGroup2[0].hasAttribute("checked"),
+ expectedStates[1],
+ `radio item 2 has state (checked=${expectedStates[1]})`
+ );
+ is(
+ radioGroup2[1].hasAttribute("checked"),
+ expectedStates[2],
+ `radio item 3 has state (checked=${expectedStates[2]})`
+ );
+
+ return extensionMenuRoot.getElementsByAttribute("type", "radio");
+ }
+
+ function confirmOnClickData(onClickData, id, was, checked) {
+ is(
+ onClickData.wasChecked,
+ was,
+ `radio item ${id} was ${was ? "" : "not "}checked before the click`
+ );
+ is(
+ onClickData.checked,
+ checked,
+ `radio item ${id} is ${checked ? "" : "not "}checked after the click`
+ );
+ }
+
+ let extensionMenuRoot = await openExtensionContextMenu();
+ let items = confirmRadioGroupStates(extensionMenuRoot, [true, false, false]);
+ await closeExtensionContextMenu(items[1]);
+
+ let result = await extension.awaitMessage("contextmenus-click");
+ confirmOnClickData(result, 2, false, true);
+
+ extensionMenuRoot = await openExtensionContextMenu();
+ items = confirmRadioGroupStates(extensionMenuRoot, [true, true, false]);
+ await closeExtensionContextMenu(items[2]);
+
+ result = await extension.awaitMessage("contextmenus-click");
+ confirmOnClickData(result, 3, false, true);
+
+ extensionMenuRoot = await openExtensionContextMenu();
+ items = confirmRadioGroupStates(extensionMenuRoot, [true, false, true]);
+ await closeExtensionContextMenu(items[0]);
+
+ result = await extension.awaitMessage("contextmenus-click");
+ confirmOnClickData(result, 1, true, true);
+
+ extensionMenuRoot = await openExtensionContextMenu();
+ items = confirmRadioGroupStates(extensionMenuRoot, [true, false, true]);
+ await closeExtensionContextMenu(items[0]);
+
+ result = await extension.awaitMessage("contextmenus-click");
+ confirmOnClickData(result, 1, true, true);
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_srcUrl_redirect.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_srcUrl_redirect.js
new file mode 100644
index 0000000000..2d00ca356a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_srcUrl_redirect.js
@@ -0,0 +1,69 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_srcUrl_of_redirected_image() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.contextMenus.onClicked.addListener(info => {
+ browser.test.assertEq(
+ "before_redir",
+ info.menuItemId,
+ "Expected menu item matched for pre-redirect URL"
+ );
+ browser.test.assertEq("image", info.mediaType, "Expected mediaType");
+ browser.test.assertEq(
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/redirect_to.sjs?ctxmenu-image.png",
+ info.srcUrl,
+ "Expected srcUrl"
+ );
+ browser.test.sendMessage("contextMenus_onClicked");
+ });
+ browser.contextMenus.create({
+ id: "before_redir",
+ title: "MyMenu",
+ targetUrlPatterns: ["*://*/*redirect_to.sjs*"],
+ });
+ browser.contextMenus.create(
+ {
+ id: "after_redir",
+ title: "MyMenu",
+ targetUrlPatterns: ["*://*/*/ctxmenu-image.png*"],
+ },
+ () => {
+ browser.test.sendMessage("menus_setup");
+ }
+ );
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("menus_setup");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_with_redirect.html",
+ },
+ async browser => {
+ // Verify that the image has been loaded, which implies that the redirect has
+ // been followed.
+ let imgWidth = await SpecialPowers.spawn(browser, [], () => {
+ let img = content.document.getElementById("img_that_redirects");
+ return img.naturalWidth;
+ });
+ is(imgWidth, 100, "Image has been loaded");
+
+ let menu = await openContextMenu("#img_that_redirects");
+ let items = menu.getElementsByAttribute("label", "MyMenu");
+ is(items.length, 1, "Only one menu item should have been matched");
+
+ await closeExtensionContextMenu(items[0]);
+ await extension.awaitMessage("contextMenus_onClicked");
+ }
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_targetUrlPatterns.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_targetUrlPatterns.js
new file mode 100644
index 0000000000..b28487064f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_targetUrlPatterns.js
@@ -0,0 +1,317 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function unsupportedSchemes() {
+ const testcases = [
+ {
+ // Link to URL with query string parameters only.
+ testUrl: "magnet:?xt=urn:btih:somesha1hash&dn=displayname.txt",
+ matchingPatterns: [
+ "magnet:*",
+ "magnet:?xt=*",
+ "magnet:?xt=*txt",
+ "magnet:*?xt=*txt",
+ ],
+ nonmatchingPatterns: [
+ // Although matches unsupported schemes in Chromium,
+ // we have specified that only matches all supported
+ // schemes. To match any scheme, an extension should not set the
+ // targetUrlPatterns field - this is checked below in subtest
+ // unsupportedSchemeWithoutTargetUrlPatterns.
+ "",
+ "agnet:*",
+ "magne:*",
+ ],
+ },
+ {
+ // Link to bookmarklet.
+ testUrl: "javascript:-URL",
+ matchingPatterns: ["javascript:*", "javascript:*URL", "javascript:-URL"],
+ nonmatchingPatterns: [
+ "",
+ "javascript://-URL",
+ "javascript:javascript:-URL",
+ ],
+ },
+ {
+ // Link to bookmarklet with comment.
+ testUrl: "javascript://-URL",
+ matchingPatterns: [
+ "javascript:*",
+ "javascript://-URL",
+ "javascript:*URL",
+ ],
+ nonmatchingPatterns: ["", "javascript:-URL"],
+ },
+ {
+ // Link to data-URI.
+ testUrl: "data:application/foo,bar",
+ matchingPatterns: [
+ "",
+ "data:application/foo,bar",
+ "data:*,*",
+ "data:*",
+ ],
+ nonmatchingPatterns: ["data:,bar", "data:application/foo,"],
+ },
+ {
+ // Extension page.
+ testUrl: "moz-extension://uuid/manifest.json",
+ matchingPatterns: ["moz-extension://*/*"],
+ nonmatchingPatterns: [
+ "",
+ "moz-extension://uuid/not/manifest.json*",
+ ],
+ },
+ {
+ // While the scheme is supported, the URL is invalid.
+ testUrl: "http://",
+ matchingPatterns: [],
+ nonmatchingPatterns: ["http://*/*", ""],
+ },
+ ];
+
+ async function testScript(testcases) {
+ let testcase;
+
+ browser.contextMenus.onShown.addListener(({ menuIds, linkUrl }) => {
+ browser.test.assertEq(testcase.testUrl, linkUrl, "Expected linkUrl");
+ for (let pattern of testcase.matchingPatterns) {
+ browser.test.assertTrue(
+ menuIds.includes(pattern),
+ `Menu item with targetUrlPattern="${pattern}" should be shown at ${testcase.testUrl}`
+ );
+ }
+ for (let pattern of testcase.nonmatchingPatterns) {
+ browser.test.assertFalse(
+ menuIds.includes(pattern),
+ `Menu item with targetUrlPattern="${pattern}" should not be shown at ${testcase.testUrl}`
+ );
+ }
+ testcase = null;
+ browser.test.sendMessage("onShown_checked");
+ });
+
+ browser.test.onMessage.addListener(async (msg, params) => {
+ browser.test.assertEq("setupTest", msg, "Expected message");
+
+ // Save test case in global variable for use in the onShown event.
+ testcase = params;
+ browser.test.log(`Running test for link with URL: ${testcase.testUrl}`);
+ document.getElementById("test_link_element").href = testcase.testUrl;
+ await browser.contextMenus.removeAll();
+ for (let targetUrlPattern of [
+ ...testcase.matchingPatterns,
+ ...testcase.nonmatchingPatterns,
+ ]) {
+ await new Promise(resolve => {
+ browser.test.log(`Creating menu with "${targetUrlPattern}"`);
+ browser.contextMenus.create(
+ {
+ id: targetUrlPattern,
+ contexts: ["link"],
+ title: "Some menu item",
+ targetUrlPatterns: [targetUrlPattern],
+ },
+ resolve
+ );
+ });
+ }
+ browser.test.sendMessage("setupTest_ready");
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.tabs.create({ url: "testrunner.html" });
+ },
+ files: {
+ "testrunner.js": `(${testScript})()`,
+ "testrunner.html": `
+
+
+ Test link
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let testcase of testcases) {
+ extension.sendMessage("setupTest", testcase);
+ await extension.awaitMessage("setupTest_ready");
+
+ await openExtensionContextMenu("#test_link_element");
+ await extension.awaitMessage("onShown_checked");
+ await closeContextMenu();
+ }
+ await extension.unload();
+});
+
+async function testLinkMenuWithoutTargetUrlPatterns(linkUrl) {
+ function background(expectedLinkUrl) {
+ let menuId;
+ browser.contextMenus.onShown.addListener(({ menuIds, linkUrl }) => {
+ browser.test.assertEq(1, menuIds.length, "Expected number of menus");
+ browser.test.assertEq(menuId, menuIds[0], "Expected menu ID");
+ browser.test.assertEq(expectedLinkUrl, linkUrl, "Expected linkUrl");
+ browser.test.sendMessage("done");
+ });
+ menuId = browser.contextMenus.create(
+ {
+ contexts: ["link"],
+ title: "Test menu item without targetUrlPattern",
+ },
+ () => {
+ browser.tabs.create({ url: "testpage.html" });
+ }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ background: `(${background})("${linkUrl}")`,
+ files: {
+ "testpage.js": `browser.test.sendMessage("ready")`,
+ "testpage.html": `
+
+ Test link
+
+ `,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ // Wait for the browser window chrome document to be flushed before
+ // trying to trigger the context menu in the newly created tab,
+ // to prevent intermittent failures (e.g. Bug 1775558).
+ await gBrowser.ownerGlobal.promiseDocumentFlushed(() => {});
+ await openExtensionContextMenu("#test_link_element");
+ await extension.awaitMessage("done");
+ await closeContextMenu();
+
+ await extension.unload();
+}
+
+// Tests that a menu item is shown on links with an unsupported scheme if
+// targetUrlPatterns is not set.
+add_task(async function unsupportedSchemeWithoutPattern() {
+ await testLinkMenuWithoutTargetUrlPatterns("unsupported-scheme:data");
+});
+
+// Tests that a menu item is shown on links with an invalid http:-URL if
+// targetUrlPatterns is not set.
+add_task(async function invalidHttpUrlWithoutPattern() {
+ await testLinkMenuWithoutTargetUrlPatterns("http://");
+});
+
+add_task(async function privileged_are_allowed_to_use_restrictedSchemes() {
+ let privilegedExtension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: ["tabs", "contextMenus", "mozillaAddons"],
+ },
+ async background() {
+ browser.contextMenus.create({
+ id: "privileged-extension",
+ title: "Privileged Extension",
+ contexts: ["page"],
+ documentUrlPatterns: ["about:reader*"],
+ });
+
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
+ if (
+ changeInfo.status === "complete" &&
+ tab.url.startsWith("about:reader")
+ ) {
+ browser.test.sendMessage("readerModeEntered");
+ }
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "enterReaderMode") {
+ browser.test.fail(`Received unexpected test message: ${msg}`);
+ return;
+ }
+
+ browser.tabs.toggleReaderMode();
+ });
+ },
+ });
+
+ let nonPrivilegedExtension = ExtensionTestUtils.loadExtension({
+ isPrivileged: false,
+ manifest: {
+ permissions: ["contextMenus", "mozillaAddons"],
+ },
+ async background() {
+ browser.contextMenus.create({
+ id: "non-privileged-extension",
+ title: "Non Privileged Extension",
+ contexts: ["page"],
+ documentUrlPatterns: ["about:reader*"],
+ });
+ },
+ });
+
+ const baseUrl = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ );
+ const url = `${baseUrl}/readerModeArticle.html`;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ true,
+ true
+ );
+
+ await Promise.all([
+ privilegedExtension.startup(),
+ nonPrivilegedExtension.startup(),
+ ]);
+
+ privilegedExtension.sendMessage("enterReaderMode");
+ await privilegedExtension.awaitMessage("readerModeEntered");
+
+ const contextMenu = await openContextMenu("body > h1");
+
+ let item = contextMenu.getElementsByAttribute(
+ "label",
+ "Privileged Extension"
+ );
+ is(
+ item.length,
+ 1,
+ "Privileged extension's contextMenu item found as expected"
+ );
+
+ item = contextMenu.getElementsByAttribute(
+ "label",
+ "Non Privileged Extension"
+ );
+ is(
+ item.length,
+ 0,
+ "Non privileged extension's contextMenu not found as expected"
+ );
+
+ await closeContextMenu();
+
+ BrowserTestUtils.removeTab(tab);
+
+ await Promise.all([
+ privilegedExtension.unload(),
+ nonPrivilegedExtension.unload(),
+ ]);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_uninstall.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_uninstall.js
new file mode 100644
index 0000000000..0d0b7cac7b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_uninstall.js
@@ -0,0 +1,114 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"
+ );
+
+ // Install an extension.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+
+ background: function () {
+ browser.contextMenus.create({ title: "a" });
+ browser.contextMenus.create({ title: "b" });
+ browser.test.notifyPass("contextmenus-icons");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("contextmenus-icons");
+
+ // Open the context menu.
+ let contextMenu = await openContextMenu("#img1");
+
+ // Confirm that the extension menu item exists.
+ let topLevelExtensionMenuItems = contextMenu.getElementsByAttribute(
+ "ext-type",
+ "top-level-menu"
+ );
+ is(
+ topLevelExtensionMenuItems.length,
+ 1,
+ "the top level extension menu item exists"
+ );
+
+ await closeContextMenu();
+
+ // Uninstall the extension.
+ await extension.unload();
+
+ // Open the context menu.
+ contextMenu = await openContextMenu("#img1");
+
+ // Confirm that the extension menu item has been removed.
+ topLevelExtensionMenuItems = contextMenu.getElementsByAttribute(
+ "ext-type",
+ "top-level-menu"
+ );
+ is(
+ topLevelExtensionMenuItems.length,
+ 0,
+ "no top level extension menu items should exist"
+ );
+
+ await closeContextMenu();
+
+ // Install a new extension.
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ background: function () {
+ browser.contextMenus.create({ title: "c" });
+ browser.contextMenus.create({ title: "d" });
+ browser.test.notifyPass("contextmenus-uninstall-second-extension");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("contextmenus-uninstall-second-extension");
+
+ // Open the context menu.
+ contextMenu = await openContextMenu("#img1");
+
+ // Confirm that only the new extension menu item is in the context menu.
+ topLevelExtensionMenuItems = contextMenu.getElementsByAttribute(
+ "ext-type",
+ "top-level-menu"
+ );
+ is(
+ topLevelExtensionMenuItems.length,
+ 1,
+ "only one top level extension menu item should exist"
+ );
+
+ // Close the context menu.
+ await closeContextMenu();
+
+ // Uninstall the extension.
+ await extension.unload();
+
+ // Open the context menu.
+ contextMenu = await openContextMenu("#img1");
+
+ // Confirm that no extension menu items exist.
+ topLevelExtensionMenuItems = contextMenu.getElementsByAttribute(
+ "ext-type",
+ "top-level-menu"
+ );
+ is(
+ topLevelExtensionMenuItems.length,
+ 0,
+ "no top level extension menu items should exist"
+ );
+
+ await closeContextMenu();
+
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js
new file mode 100644
index 0000000000..fc8bc28523
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js
@@ -0,0 +1,337 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+
+ background: function () {
+ // Test menu items using targetUrlPatterns.
+ browser.contextMenus.create({
+ title: "targetUrlPatterns-patternMatches-contextAll",
+ targetUrlPatterns: ["*://*/*ctxmenu-image.png", "*://*/*some-link"],
+ contexts: ["all"],
+ });
+
+ browser.contextMenus.create({
+ title: "targetUrlPatterns-patternMatches-contextImage",
+ targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+ contexts: ["image"],
+ });
+
+ browser.contextMenus.create({
+ title: "targetUrlPatterns-patternMatches-contextLink",
+ targetUrlPatterns: ["*://*/*some-link"],
+ contexts: ["link"],
+ });
+
+ browser.contextMenus.create({
+ title: "targetUrlPatterns-patternDoesNotMatch-contextAll",
+ targetUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["all"],
+ });
+
+ browser.contextMenus.create({
+ title: "targetUrlPatterns-patternDoesNotMatch-contextImage",
+ targetUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["image"],
+ });
+
+ browser.contextMenus.create({
+ title: "targetUrlPatterns-patternDoesNotMatch-contextLink",
+ targetUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["link"],
+ });
+
+ // Test menu items using documentUrlPatterns.
+ browser.contextMenus.create({
+ title: "documentUrlPatterns-patternMatches-contextAll",
+ documentUrlPatterns: ["*://*/*context*.html"],
+ contexts: ["all"],
+ });
+
+ browser.contextMenus.create({
+ title: "documentUrlPatterns-patternMatches-contextFrame",
+ documentUrlPatterns: ["*://*/*context_frame.html"],
+ contexts: ["frame"],
+ });
+
+ browser.contextMenus.create({
+ title: "documentUrlPatterns-patternMatches-contextImage",
+ documentUrlPatterns: [
+ "*://*/*context.html",
+ "http://*/url-that-does-not-match",
+ ],
+ contexts: ["image"],
+ });
+
+ browser.contextMenus.create({
+ title: "documentUrlPatterns-patternMatches-contextLink",
+ documentUrlPatterns: ["*://*/*context.html", "*://*/does-not-match"],
+ contexts: ["link"],
+ });
+
+ browser.contextMenus.create({
+ title: "documentUrlPatterns-patternDoesNotMatch-contextAll",
+ documentUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["all"],
+ });
+
+ browser.contextMenus.create({
+ title: "documentUrlPatterns-patternDoesNotMatch-contextImage",
+ documentUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["image"],
+ });
+
+ browser.contextMenus.create({
+ title: "documentUrlPatterns-patternDoesNotMatch-contextLink",
+ documentUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["link"],
+ });
+
+ // Test menu items using both targetUrlPatterns and documentUrlPatterns.
+ browser.contextMenus.create({
+ title:
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll",
+ documentUrlPatterns: ["*://*/*context.html"],
+ targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+ contexts: ["all"],
+ });
+
+ browser.contextMenus.create({
+ title:
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll",
+ documentUrlPatterns: ["*://*/does-not-match"],
+ targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+ contexts: ["all"],
+ });
+
+ browser.contextMenus.create({
+ title:
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll",
+ documentUrlPatterns: ["*://*/*context.html"],
+ targetUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["all"],
+ });
+
+ browser.contextMenus.create({
+ title:
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll",
+ documentUrlPatterns: ["*://*/does-not-match"],
+ targetUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["all"],
+ });
+
+ browser.contextMenus.create({
+ title:
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage",
+ documentUrlPatterns: ["*://*/*context.html"],
+ targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+ contexts: ["image"],
+ });
+
+ browser.contextMenus.create({
+ title:
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage",
+ documentUrlPatterns: ["*://*/does-not-match"],
+ targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+ contexts: ["image"],
+ });
+
+ browser.contextMenus.create({
+ title:
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage",
+ documentUrlPatterns: ["*://*/*context.html"],
+ targetUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["image"],
+ });
+
+ browser.contextMenus.create({
+ title:
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage",
+ documentUrlPatterns: ["*://*/does-not-match/"],
+ targetUrlPatterns: ["*://*/does-not-match"],
+ contexts: ["image"],
+ });
+
+ browser.test.notifyPass("contextmenus-urlPatterns");
+ },
+ });
+
+ function confirmContextMenuItems(menu, expected) {
+ for (let [label, shouldShow] of expected) {
+ let items = menu.getElementsByAttribute("label", label);
+ if (shouldShow) {
+ is(
+ items.length,
+ 1,
+ `The menu item for label ${label} was correctly shown`
+ );
+ } else {
+ is(
+ items.length,
+ 0,
+ `The menu item for label ${label} was correctly not shown`
+ );
+ }
+ }
+ }
+
+ await extension.startup();
+ await extension.awaitFinish("contextmenus-urlPatterns");
+
+ let extensionContextMenu = await openExtensionContextMenu("#img1");
+ let expected = [
+ ["targetUrlPatterns-patternMatches-contextAll", true],
+ ["targetUrlPatterns-patternMatches-contextImage", true],
+ ["targetUrlPatterns-patternMatches-contextLink", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextLink", false],
+ ["documentUrlPatterns-patternMatches-contextAll", true],
+ ["documentUrlPatterns-patternMatches-contextImage", true],
+ ["documentUrlPatterns-patternMatches-contextLink", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextAll", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextImage", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextLink", false],
+ [
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll",
+ true,
+ ],
+ [
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage",
+ true,
+ ],
+ [
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage",
+ false,
+ ],
+ ];
+ await confirmContextMenuItems(extensionContextMenu, expected);
+ await closeContextMenu();
+
+ let contextMenu = await openContextMenu("body");
+ expected = [
+ ["targetUrlPatterns-patternMatches-contextAll", false],
+ ["targetUrlPatterns-patternMatches-contextImage", false],
+ ["targetUrlPatterns-patternMatches-contextLink", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextLink", false],
+ ["documentUrlPatterns-patternMatches-contextAll", true],
+ ["documentUrlPatterns-patternMatches-contextImage", false],
+ ["documentUrlPatterns-patternMatches-contextLink", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextAll", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextImage", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextLink", false],
+ [
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage",
+ false,
+ ],
+ [
+ "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage",
+ false,
+ ],
+ ];
+ await confirmContextMenuItems(contextMenu, expected);
+ await closeContextMenu();
+
+ contextMenu = await openContextMenu("#link1");
+ expected = [
+ ["targetUrlPatterns-patternMatches-contextAll", true],
+ ["targetUrlPatterns-patternMatches-contextImage", false],
+ ["targetUrlPatterns-patternMatches-contextLink", true],
+ ["targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextLink", false],
+ ["documentUrlPatterns-patternMatches-contextAll", true],
+ ["documentUrlPatterns-patternMatches-contextImage", false],
+ ["documentUrlPatterns-patternMatches-contextLink", true],
+ ["documentUrlPatterns-patternDoesNotMatch-contextAll", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextImage", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextLink", false],
+ ];
+ await confirmContextMenuItems(contextMenu, expected);
+ await closeContextMenu();
+
+ contextMenu = await openContextMenu("#img-wrapped-in-link");
+ expected = [
+ ["targetUrlPatterns-patternMatches-contextAll", true],
+ ["targetUrlPatterns-patternMatches-contextImage", true],
+ ["targetUrlPatterns-patternMatches-contextLink", true],
+ ["targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+ ["targetUrlPatterns-patternDoesNotMatch-contextLink", false],
+ ["documentUrlPatterns-patternMatches-contextAll", true],
+ ["documentUrlPatterns-patternMatches-contextImage", true],
+ ["documentUrlPatterns-patternMatches-contextLink", true],
+ ["documentUrlPatterns-patternDoesNotMatch-contextAll", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextImage", false],
+ ["documentUrlPatterns-patternDoesNotMatch-contextLink", false],
+ ];
+ await confirmContextMenuItems(contextMenu, expected);
+ await closeContextMenu();
+
+ contextMenu = await openContextMenuInFrame();
+ expected = [
+ ["documentUrlPatterns-patternMatches-contextAll", true],
+ ["documentUrlPatterns-patternMatches-contextFrame", true],
+ ];
+ await confirmContextMenuItems(contextMenu, expected);
+ await closeContextMenu();
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_currentWindow.js b/browser/components/extensions/test/browser/browser_ext_currentWindow.js
new file mode 100644
index 0000000000..86f55e445f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_currentWindow.js
@@ -0,0 +1,183 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function genericChecker() {
+ let kind = "background";
+ let path = window.location.pathname;
+ if (path.includes("/popup.html")) {
+ kind = "popup";
+ } else if (path.includes("/page.html")) {
+ kind = "page";
+ }
+
+ browser.test.onMessage.addListener((msg, ...args) => {
+ if (msg == kind + "-check-current1") {
+ browser.tabs.query(
+ {
+ currentWindow: true,
+ },
+ function (tabs) {
+ browser.test.sendMessage("result", tabs[0].windowId);
+ }
+ );
+ } else if (msg == kind + "-check-current2") {
+ browser.tabs.query(
+ {
+ windowId: browser.windows.WINDOW_ID_CURRENT,
+ },
+ function (tabs) {
+ browser.test.sendMessage("result", tabs[0].windowId);
+ }
+ );
+ } else if (msg == kind + "-check-current3") {
+ browser.windows.getCurrent(function (window) {
+ browser.test.sendMessage("result", window.id);
+ });
+ } else if (msg == kind + "-open-page") {
+ browser.tabs.create({
+ windowId: args[0],
+ url: browser.runtime.getURL("page.html"),
+ });
+ } else if (msg == kind + "-close-page") {
+ browser.tabs.query(
+ {
+ windowId: args[0],
+ },
+ tabs => {
+ let tab = tabs.find(tab => tab.url.includes("/page.html"));
+ browser.tabs.remove(tab.id, () => {
+ browser.test.sendMessage("closed");
+ });
+ }
+ );
+ }
+ });
+ browser.test.sendMessage(kind + "-ready");
+}
+
+add_task(async function () {
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await focusWindow(win2);
+
+ BrowserTestUtils.startLoadingURIString(
+ win1.gBrowser.selectedBrowser,
+ "about:robots"
+ );
+ await BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
+
+ BrowserTestUtils.startLoadingURIString(
+ win2.gBrowser.selectedBrowser,
+ "about:config"
+ );
+ await BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+
+ files: {
+ "page.html": `
+
+
+
+
+ `,
+
+ "page.js": genericChecker,
+
+ "popup.html": `
+
+
+
+
+ `,
+
+ "popup.js": genericChecker,
+ },
+
+ background: genericChecker,
+ });
+
+ await Promise.all([
+ extension.startup(),
+ extension.awaitMessage("background-ready"),
+ ]);
+
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let winId1 = windowTracker.getId(win1);
+ let winId2 = windowTracker.getId(win2);
+
+ async function checkWindow(kind, winId, name) {
+ extension.sendMessage(kind + "-check-current1");
+ is(
+ await extension.awaitMessage("result"),
+ winId,
+ `${name} is on top (check 1) [${kind}]`
+ );
+ extension.sendMessage(kind + "-check-current2");
+ is(
+ await extension.awaitMessage("result"),
+ winId,
+ `${name} is on top (check 2) [${kind}]`
+ );
+ extension.sendMessage(kind + "-check-current3");
+ is(
+ await extension.awaitMessage("result"),
+ winId,
+ `${name} is on top (check 3) [${kind}]`
+ );
+ }
+
+ async function triggerPopup(win, callback) {
+ await clickBrowserAction(extension, win);
+ await awaitExtensionPanel(extension, win);
+
+ await extension.awaitMessage("popup-ready");
+
+ await callback();
+
+ closeBrowserAction(extension, win);
+ }
+
+ await focusWindow(win1);
+ await checkWindow("background", winId1, "win1");
+ await triggerPopup(win1, async function () {
+ await checkWindow("popup", winId1, "win1");
+ });
+
+ await focusWindow(win2);
+ await checkWindow("background", winId2, "win2");
+ await triggerPopup(win2, async function () {
+ await checkWindow("popup", winId2, "win2");
+ });
+
+ async function triggerPage(winId, name) {
+ extension.sendMessage("background-open-page", winId);
+ await extension.awaitMessage("page-ready");
+ await checkWindow("page", winId, name);
+ extension.sendMessage("background-close-page", winId);
+ await extension.awaitMessage("closed");
+ }
+
+ await triggerPage(winId1, "win1");
+ await triggerPage(winId2, "win2");
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js
new file mode 100644
index 0000000000..0cfcb33ab3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js
@@ -0,0 +1,540 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+/**
+ * Helper that returns the id of the last additional/extension tool for a provided
+ * toolbox.
+ *
+ * @param {object} toolbox
+ * The DevTools toolbox object.
+ * @param {string} label
+ * The expected label for the additional tool.
+ * @returns {string} the id of the last additional panel.
+ */
+function getAdditionalPanelId(toolbox, label) {
+ // Copy the tools array and pop the last element from it.
+ const panelDef = toolbox.getAdditionalTools().slice().pop();
+ is(panelDef.label, label, "Additional panel label is the expected label");
+ return panelDef.id;
+}
+
+/**
+ * Helper that returns the number of existing target actors for the content browserId
+ *
+ * @param {Tab} tab
+ * @returns {Integer} the number of targets
+ */
+function getTargetActorsCount(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const { TargetActorRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs"
+ );
+
+ // Retrieve the target actor instances
+ return TargetActorRegistry.getTargetActorsCountForBrowserElement(
+ content.browsingContext.browserId
+ );
+ });
+}
+
+/**
+ * this test file ensures that:
+ *
+ * - the devtools page gets only a subset of the runtime API namespace.
+ * - devtools.inspectedWindow.tabId is the same tabId that we can retrieve
+ * in the background page using the tabs API namespace.
+ * - devtools API is available in the devtools page sub-frames when a valid
+ * extension URL has been loaded.
+ * - devtools.inspectedWindow.eval:
+ * - returns a serialized version of the evaluation result.
+ * - returns the expected error object when the return value serialization raises a
+ * "TypeError: cyclic object value" exception.
+ * - returns the expected exception when an exception has been raised from the evaluated
+ * javascript code.
+ */
+add_task(async function test_devtools_inspectedWindow_tabId() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ async function background() {
+ browser.test.assertEq(
+ undefined,
+ browser.devtools,
+ "No devtools APIs should be available in the background page"
+ );
+
+ const tabs = await browser.tabs.query({
+ active: true,
+ lastFocusedWindow: true,
+ });
+ browser.test.sendMessage("current-tab-id", tabs[0].id);
+ }
+
+ function devtools_page() {
+ browser.test.assertEq(
+ undefined,
+ browser.runtime.getBackgroundPage,
+ "The `runtime.getBackgroundPage` API method should be missing in a devtools_page context"
+ );
+
+ try {
+ let tabId = browser.devtools.inspectedWindow.tabId;
+ browser.test.sendMessage("inspectedWindow-tab-id", tabId);
+ } catch (err) {
+ browser.test.sendMessage("inspectedWindow-tab-id", undefined);
+ throw err;
+ }
+ }
+
+ function devtools_page_iframe() {
+ try {
+ let tabId = browser.devtools.inspectedWindow.tabId;
+ browser.test.sendMessage(
+ "devtools_page_iframe.inspectedWindow-tab-id",
+ tabId
+ );
+ } catch (err) {
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ browser.test.sendMessage(
+ "devtools_page_iframe.inspectedWindow-tab-id",
+ undefined
+ );
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ "devtools_page_iframe.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page_iframe.js": devtools_page_iframe,
+ },
+ });
+
+ await extension.startup();
+
+ let backgroundPageCurrentTabId = await extension.awaitMessage(
+ "current-tab-id"
+ );
+
+ await openToolboxForTab(tab);
+
+ let devtoolsInspectedWindowTabId = await extension.awaitMessage(
+ "inspectedWindow-tab-id"
+ );
+
+ is(
+ devtoolsInspectedWindowTabId,
+ backgroundPageCurrentTabId,
+ "Got the expected tabId from devtool.inspectedWindow.tabId"
+ );
+
+ let devtoolsPageIframeTabId = await extension.awaitMessage(
+ "devtools_page_iframe.inspectedWindow-tab-id"
+ );
+
+ is(
+ devtoolsPageIframeTabId,
+ backgroundPageCurrentTabId,
+ "Got the expected tabId from devtool.inspectedWindow.tabId called in a devtool_page iframe"
+ );
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_devtools_inspectedWindow_eval() {
+ const TEST_TARGET_URL = "http://mochi.test:8888/";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TARGET_URL
+ );
+
+ function devtools_page() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg !== "inspectedWindow-eval-request") {
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ return;
+ }
+
+ try {
+ const [evalResult, errorResult] =
+ await browser.devtools.inspectedWindow.eval(...args);
+ browser.test.sendMessage("inspectedWindow-eval-result", {
+ evalResult,
+ errorResult,
+ });
+ } catch (err) {
+ browser.test.sendMessage("inspectedWindow-eval-result");
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ }
+ });
+ browser.test.sendMessage("devtools-page-loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+
+ await extension.startup();
+
+ await openToolboxForTab(tab);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools-page-loaded");
+
+ const evalTestCases = [
+ // Successful evaluation results.
+ {
+ args: ["window.location.href"],
+ expectedResults: { evalResult: TEST_TARGET_URL, errorResult: undefined },
+ },
+
+ // Error evaluation results.
+ {
+ args: ["window"],
+ expectedResults: {
+ evalResult: undefined,
+ errorResult: {
+ isError: true,
+ code: "E_PROTOCOLERROR",
+ description: "Inspector protocol error: %s",
+ details: ["TypeError: cyclic object value"],
+ },
+ },
+ },
+
+ // Exception evaluation results.
+ {
+ args: ["throw new Error('fake eval exception');"],
+ expectedResults: {
+ evalResult: undefined,
+ errorResult: {
+ isException: true,
+ value: /Error: fake eval exception\n.*moz-extension:\/\//,
+ },
+ },
+ },
+ ];
+
+ for (let testCase of evalTestCases) {
+ info(`test inspectedWindow.eval with ${JSON.stringify(testCase)}`);
+
+ const { args, expectedResults } = testCase;
+
+ extension.sendMessage(`inspectedWindow-eval-request`, ...args);
+
+ const { evalResult, errorResult } = await extension.awaitMessage(
+ `inspectedWindow-eval-result`
+ );
+
+ Assert.deepEqual(
+ evalResult,
+ expectedResults.evalResult,
+ "Got the expected eval result"
+ );
+
+ if (errorResult) {
+ for (const errorPropName of Object.keys(expectedResults.errorResult)) {
+ const expected = expectedResults.errorResult[errorPropName];
+ const actual = errorResult[errorPropName];
+
+ if (expected instanceof RegExp) {
+ ok(
+ expected.test(actual),
+ `Got exceptionInfo.${errorPropName} value ${actual} matches ${expected}`
+ );
+ } else {
+ Assert.deepEqual(
+ actual,
+ expected,
+ `Got the expected exceptionInfo.${errorPropName} value`
+ );
+ }
+ }
+ }
+ }
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * This test asserts that both the page and the panel can use devtools.inspectedWindow.
+ * See regression in Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1392531
+ */
+add_task(async function test_devtools_inspectedWindow_eval_in_page_and_panel() {
+ const TEST_TARGET_URL = "http://mochi.test:8888/";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TARGET_URL
+ );
+
+ async function devtools_page() {
+ await browser.devtools.panels.create(
+ "test-eval",
+ "fake-icon.png",
+ "devtools_panel.html"
+ );
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "inspectedWindow-page-eval-request": {
+ const [evalResult, errorResult] =
+ await browser.devtools.inspectedWindow.eval(...args);
+ browser.test.sendMessage("inspectedWindow-page-eval-result", {
+ evalResult,
+ errorResult,
+ });
+ break;
+ }
+ case "inspectedWindow-panel-eval-request":
+ // Ignore the test message expected by the devtools panel.
+ break;
+ default:
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ }
+ });
+
+ browser.test.sendMessage("devtools_panel_created");
+ }
+
+ function devtools_panel() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "inspectedWindow-panel-eval-request": {
+ const [evalResult, errorResult] =
+ await browser.devtools.inspectedWindow.eval(...args);
+ browser.test.sendMessage("inspectedWindow-panel-eval-result", {
+ evalResult,
+ errorResult,
+ });
+ break;
+ }
+ case "inspectedWindow-page-eval-request":
+ // Ignore the test message expected by the devtools page.
+ break;
+ default:
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ }
+ });
+ browser.test.sendMessage("devtools_panel_initialized");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ "devtools_panel.html": `
+
+
+
+
+
+ DEVTOOLS PANEL
+
+
+ `,
+ "devtools_panel.js": devtools_panel,
+ },
+ });
+
+ await extension.startup();
+
+ const toolbox = await openToolboxForTab(tab);
+
+ info("Wait for devtools_panel_created event");
+ await extension.awaitMessage("devtools_panel_created");
+
+ info("Switch to the extension test panel");
+ await openToolboxForTab(tab, getAdditionalPanelId(toolbox, "test-eval"));
+
+ info("Wait for devtools_panel_initialized event");
+ await extension.awaitMessage("devtools_panel_initialized");
+
+ info(
+ `test inspectedWindow.eval with eval(window.location.href) from the devtools page`
+ );
+ extension.sendMessage(
+ `inspectedWindow-page-eval-request`,
+ "window.location.href"
+ );
+
+ info("Wait for response from the page");
+ let { evalResult } = await extension.awaitMessage(
+ `inspectedWindow-page-eval-result`
+ );
+ Assert.deepEqual(
+ evalResult,
+ TEST_TARGET_URL,
+ "Got the expected eval result in the page"
+ );
+
+ info(
+ `test inspectedWindow.eval with eval(window.location.href) from the devtools panel`
+ );
+ extension.sendMessage(
+ `inspectedWindow-panel-eval-request`,
+ "window.location.href"
+ );
+
+ info("Wait for response from the panel");
+ ({ evalResult } = await extension.awaitMessage(
+ `inspectedWindow-panel-eval-result`
+ ));
+ Assert.deepEqual(
+ evalResult,
+ TEST_TARGET_URL,
+ "Got the expected eval result in the panel"
+ );
+
+ // Cleanup
+ await closeToolboxForTab(tab);
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * This test asserts that there's only one target created by the extension, and that
+ * closing the DevTools toolbox destroys it.
+ * See Bug 1652016
+ */
+add_task(async function test_devtools_inspectedWindow_eval_target_lifecycle() {
+ const TEST_TARGET_URL = "http://mochi.test:8888/";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TARGET_URL
+ );
+
+ function devtools_page() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg !== "inspectedWindow-eval-requests") {
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ return;
+ }
+
+ const promises = [];
+ for (let i = 0; i < 10; i++) {
+ promises.push(browser.devtools.inspectedWindow.eval(`${i * 2}`));
+ }
+
+ await Promise.all(promises);
+ browser.test.sendMessage("inspectedWindow-eval-requests-done");
+ });
+ browser.test.sendMessage("devtools-page-loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+
+ await extension.startup();
+
+ await openToolboxForTab(tab);
+ await extension.awaitMessage("devtools-page-loaded");
+
+ let targetsCount = await getTargetActorsCount(tab);
+ is(
+ targetsCount,
+ 1,
+ "There's only one target for the content page, the one for DevTools Toolbox"
+ );
+
+ info("Check that evaluating multiple times doesn't create multiple targets");
+ const onEvalRequestsDone = extension.awaitMessage(
+ `inspectedWindow-eval-requests-done`
+ );
+ extension.sendMessage(`inspectedWindow-eval-requests`);
+
+ info("Wait for response from the panel");
+ await onEvalRequestsDone;
+
+ targetsCount = await getTargetActorsCount(tab);
+ is(
+ targetsCount,
+ 2,
+ "Only 1 additional target was created when calling inspectedWindow.eval"
+ );
+
+ info(
+ "Close the toolbox and make sure the extension gets unloaded, and the target destroyed"
+ );
+ await closeToolboxForTab(tab);
+
+ targetsCount = await getTargetActorsCount(tab);
+ is(targetsCount, 0, "All targets were removed as toolbox was closed");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js
new file mode 100644
index 0000000000..a5f910e698
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js
@@ -0,0 +1,270 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+const BASE_URL =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+
+/**
+ * this test file ensures that:
+ *
+ * - devtools.inspectedWindow.eval provides the expected $0 and inspect bindings
+ */
+add_task(async function test_devtools_inspectedWindow_eval_bindings() {
+ const TEST_TARGET_URL = `${BASE_URL}/file_inspectedwindow_eval.html`;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TARGET_URL
+ );
+
+ function devtools_page() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg !== "inspectedWindow-eval-request") {
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ return;
+ }
+
+ try {
+ const [evalResult, errorResult] =
+ await browser.devtools.inspectedWindow.eval(...args);
+ browser.test.sendMessage("inspectedWindow-eval-result", {
+ evalResult,
+ errorResult,
+ });
+ } catch (err) {
+ browser.test.sendMessage("inspectedWindow-eval-result");
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ }
+ });
+ browser.test.sendMessage("devtools-page-loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+
+ await extension.startup();
+
+ const toolbox = await openToolboxForTab(tab);
+ await extension.awaitMessage("devtools-page-loaded");
+
+ // Test $0 binding with no selected node
+ info("Test inspectedWindow.eval $0 binding with no selected node");
+
+ const evalNoSelectedNodePromise = extension.awaitMessage(
+ `inspectedWindow-eval-result`
+ );
+ extension.sendMessage(`inspectedWindow-eval-request`, "$0");
+ const evalNoSelectedNodeResult = await evalNoSelectedNodePromise;
+
+ Assert.deepEqual(
+ evalNoSelectedNodeResult,
+ { evalResult: undefined, errorResult: undefined },
+ "Got the expected eval result"
+ );
+
+ // Test $0 binding with a selected node in the inspector.
+
+ await openToolboxForTab(tab, "inspector");
+
+ info(
+ "Test inspectedWindow.eval $0 binding with a selected node in the inspector"
+ );
+
+ const evalSelectedNodePromise = extension.awaitMessage(
+ `inspectedWindow-eval-result`
+ );
+ extension.sendMessage(`inspectedWindow-eval-request`, "$0 && $0.tagName");
+ const evalSelectedNodeResult = await evalSelectedNodePromise;
+
+ Assert.deepEqual(
+ evalSelectedNodeResult,
+ { evalResult: "BODY", errorResult: undefined },
+ "Got the expected eval result"
+ );
+
+ // Test that inspect($0) switch the developer toolbox to the inspector.
+
+ await openToolboxForTab(tab, TOOLBOX_BLANK_PANEL_ID);
+
+ const inspectorPanelSelectedPromise = (async () => {
+ const toolId = await toolbox.once("select");
+
+ if (toolId === "inspector") {
+ info("Toolbox has been switched to the inspector as expected");
+ const selectedNodeName =
+ toolbox.selection.nodeFront &&
+ toolbox.selection.nodeFront._form.nodeName;
+ is(
+ selectedNodeName,
+ "A",
+ "The expected DOM node has been selected in the inspector"
+ );
+ } else {
+ throw new Error(
+ `inspector panel expected, ${toolId} has been selected instead`
+ );
+ }
+ })();
+
+ info("Test inspectedWindow.eval inspect() binding called for a DOM element");
+ extension.sendMessage(
+ `inspectedWindow-eval-request`,
+ "inspect(document.querySelector('a#link-to-inspect'))"
+ );
+ await extension.awaitMessage(`inspectedWindow-eval-result`);
+
+ info(
+ "Wait for the toolbox to switch to the inspector and the expected node has been selected"
+ );
+ await inspectorPanelSelectedPromise;
+
+ function expectedSourceSelected(sourceFilename, sourceLine) {
+ return () => {
+ const dbg = toolbox.getPanel("jsdebugger");
+ const selectedLocation = dbg._selectors.getSelectedLocation(
+ dbg._getState()
+ );
+
+ if (!selectedLocation) {
+ return false;
+ }
+
+ return (
+ selectedLocation.source.id.includes(sourceFilename) &&
+ selectedLocation.line == sourceLine
+ );
+ };
+ }
+
+ info("Test inspectedWindow.eval inspect() binding called for a function");
+
+ const debuggerPanelSelectedPromise = (async () => {
+ const toolId = await toolbox.once("select");
+
+ if (toolId === "jsdebugger") {
+ info("Toolbox has been switched to the jsdebugger as expected");
+ } else {
+ throw new Error(
+ `jsdebugger panel expected, ${toolId} has been selected instead`
+ );
+ }
+ })();
+
+ extension.sendMessage(
+ `inspectedWindow-eval-request`,
+ "inspect(test_inspect_function)"
+ );
+ await extension.awaitMessage(`inspectedWindow-eval-result`);
+ await debuggerPanelSelectedPromise;
+
+ await BrowserTestUtils.waitForCondition(
+ expectedSourceSelected("file_inspectedwindow_eval.html", 9),
+ "Wait the expected function to be selected in the jsdebugger panel"
+ );
+
+ info("Test inspectedWindow.eval inspect() bound function");
+
+ extension.sendMessage(
+ `inspectedWindow-eval-request`,
+ "inspect(test_bound_function)"
+ );
+ await extension.awaitMessage(`inspectedWindow-eval-result`);
+
+ await BrowserTestUtils.waitForCondition(
+ expectedSourceSelected("file_inspectedwindow_eval.html", 15),
+ "Wait the expected function to be selected in the jsdebugger panel"
+ );
+
+ info("Test inspectedWindow.eval inspect() binding called for a JS object");
+
+ const splitPanelOpenedPromise = (async () => {
+ await toolbox.once("split-console");
+ const { hud } = toolbox.getPanel("webconsole");
+
+ // Wait for the message to appear on the console.
+ const messageNode = await new Promise(resolve => {
+ hud.ui.on("new-messages", function onThisMessage(messages) {
+ for (let m of messages) {
+ resolve(m.node);
+ hud.ui.off("new-messages", onThisMessage);
+ return;
+ }
+ });
+ });
+ let objectInspectors = [...messageNode.querySelectorAll(".tree")];
+ is(
+ objectInspectors.length,
+ 1,
+ "There is the expected number of object inspectors"
+ );
+
+ // We need to wait for the object to be expanded so we don't call the server on a closed connection.
+ const [oi] = objectInspectors;
+ let nodes = oi.querySelectorAll(".node");
+
+ Assert.greaterOrEqual(
+ nodes.length,
+ 1,
+ "The object preview is rendered as expected"
+ );
+
+ // The tree can still be collapsed since the properties are fetched asynchronously.
+ if (nodes.length === 1) {
+ info("Waiting for the object properties to be displayed");
+ // If this is the case, we wait for the properties to be fetched and displayed.
+ await new Promise(resolve => {
+ const observer = new MutationObserver(mutations => {
+ resolve();
+ observer.disconnect();
+ });
+ observer.observe(oi, { childList: true });
+ });
+
+ // Retrieve the new nodes.
+ nodes = oi.querySelectorAll(".node");
+ }
+
+ // We should have 3 nodes :
+ // ▼ Object { testkey: "testvalue" }
+ // | testkey: "testvalue"
+ // | ▶︎ __proto__: Object { … }
+ is(nodes.length, 3, "The object preview has the expected number of nodes");
+ })();
+
+ const inspectJSObjectPromise = extension.awaitMessage(
+ `inspectedWindow-eval-result`
+ );
+ extension.sendMessage(
+ `inspectedWindow-eval-request`,
+ "inspect({testkey: 'testvalue'})"
+ );
+ await inspectJSObjectPromise;
+
+ info("Wait for the split console to be opened and the JS object inspected");
+ await splitPanelOpenedPromise;
+ info("Split console has been opened as expected");
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_file.js b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_file.js
new file mode 100644
index 0000000000..419fc964e7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_file.js
@@ -0,0 +1,54 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+const FILE_URL = Services.io.newFileURI(
+ new FileUtils.File(getTestFilePath("file_dummy.html"))
+).spec;
+
+add_task(async function test_devtools_inspectedWindow_eval_in_file_url() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE_URL);
+
+ async function devtools_page() {
+ try {
+ const [evalResult, errorResult] =
+ await browser.devtools.inspectedWindow.eval("location.protocol");
+ browser.test.assertEq(undefined, errorResult, "eval should not fail");
+ browser.test.assertEq("file:", evalResult, "eval should succeed");
+ browser.test.notifyPass("inspectedWindow-eval-file");
+ } catch (err) {
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ browser.test.notifyFail("inspectedWindow-eval-file");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+
+ await extension.startup();
+
+ await openToolboxForTab(tab);
+
+ await extension.awaitFinish("inspectedWindow-eval-file");
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_reload.js b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_reload.js
new file mode 100644
index 0000000000..9f7d452a2c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_reload.js
@@ -0,0 +1,481 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Like most of the mochitest-browser devtools test,
+// on debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+loadTestSubscript("head_devtools.js");
+
+// Allow rejections related to closing the devtools toolbox too soon after the test
+// has already verified the details that were relevant for that test case
+// (e.g. this was triggering an intermittent failure in shippable optimized
+// builds, tracked Bug 1707644).
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Connection closed, pending request to/
+);
+
+const TEST_ORIGIN = "http://mochi.test:8888";
+const TEST_BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ ""
+);
+const TEST_PATH = `${TEST_BASE}file_inspectedwindow_reload_target.sjs`;
+
+// Small helper which provides the common steps to the following reload test cases.
+async function runReloadTestCase({
+ urlParams,
+ background,
+ devtoolsPage,
+ testCase,
+ closeToolbox = true,
+}) {
+ const TEST_TARGET_URL = `${TEST_ORIGIN}/${TEST_PATH}?${urlParams}`;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TARGET_URL
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ devtools_page: "devtools_page.html",
+ permissions: ["webNavigation", ""],
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtoolsPage,
+ },
+ });
+
+ await extension.startup();
+
+ const toolbox = await openToolboxForTab(tab);
+
+ // Wait the test extension to be ready.
+ await extension.awaitMessage("devtools_inspected_window_reload.ready");
+
+ info("devtools page ready");
+
+ // Run the test case.
+ await testCase(extension, tab, toolbox);
+
+ if (closeToolbox) {
+ await closeToolboxForTab(tab);
+ }
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+}
+
+add_task(async function test_devtools_inspectedWindow_reload_ignore_cache() {
+ function background() {
+ // Wait until the devtools page is ready to run the test.
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg !== "devtools_page.ready") {
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ return;
+ }
+
+ const tabs = await browser.tabs.query({ active: true });
+ const activeTabId = tabs[0].id;
+ let reloads = 0;
+
+ browser.webNavigation.onCompleted.addListener(async details => {
+ if (details.tabId == activeTabId && details.frameId == 0) {
+ reloads++;
+
+ // This test expects two `devtools.inspectedWindow.reload` calls:
+ // the first one without any options and the second one with
+ // `ignoreCache=true`.
+ let expectedContent;
+ let enabled;
+
+ switch (reloads) {
+ case 1:
+ enabled = false;
+ expectedContent = "empty cache headers";
+ break;
+ case 2:
+ enabled = true;
+ expectedContent = "no-cache:no-cache";
+ break;
+ }
+
+ if (!expectedContent) {
+ browser.test.fail(`Unexpected number of tab reloads: ${reloads}`);
+ } else {
+ try {
+ const code = `document.body.textContent`;
+ const [text] = await browser.tabs.executeScript(activeTabId, {
+ code,
+ });
+
+ browser.test.assertEq(
+ text,
+ expectedContent,
+ `Got the expected cache headers with ignoreCache=${enabled}`
+ );
+ } catch (err) {
+ browser.test.fail(`Error: ${err.message} - ${err.stack}`);
+ }
+ }
+
+ browser.test.sendMessage(
+ "devtools_inspectedWindow_reload_checkIgnoreCache.done"
+ );
+ }
+ });
+
+ browser.test.sendMessage("devtools_inspected_window_reload.ready");
+ });
+ }
+
+ async function devtoolsPage() {
+ browser.test.onMessage.addListener(msg => {
+ switch (msg) {
+ case "no-ignore-cache":
+ browser.devtools.inspectedWindow.reload();
+ break;
+ case "ignore-cache":
+ browser.devtools.inspectedWindow.reload({ ignoreCache: true });
+ break;
+ default:
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ }
+ });
+
+ browser.runtime.sendMessage("devtools_page.ready");
+ }
+
+ await runReloadTestCase({
+ urlParams: "test=cache",
+ background,
+ devtoolsPage,
+ testCase: async function (extension) {
+ for (const testMessage of ["no-ignore-cache", "ignore-cache"]) {
+ extension.sendMessage(testMessage);
+ await extension.awaitMessage(
+ "devtools_inspectedWindow_reload_checkIgnoreCache.done"
+ );
+ }
+ },
+ });
+});
+
+add_task(
+ async function test_devtools_inspectedWindow_reload_custom_user_agent() {
+ const CUSTOM_USER_AGENT = "CustomizedUserAgent";
+
+ function background() {
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg !== "devtools_page.ready") {
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ return;
+ }
+
+ browser.test.sendMessage("devtools_inspected_window_reload.ready");
+ });
+ }
+
+ function devtoolsPage() {
+ browser.test.onMessage.addListener(async msg => {
+ switch (msg) {
+ case "no-custom-user-agent":
+ await browser.devtools.inspectedWindow.reload({});
+ break;
+ case "custom-user-agent":
+ await browser.devtools.inspectedWindow.reload({
+ userAgent: "CustomizedUserAgent",
+ });
+ break;
+ default:
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ }
+ });
+
+ browser.runtime.sendMessage("devtools_page.ready");
+ }
+
+ async function checkUserAgent(expectedUA) {
+ const contexts =
+ gBrowser.selectedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
+
+ const uniqueRemoteTypes = new Set();
+ for (const context of contexts) {
+ uniqueRemoteTypes.add(context.currentRemoteType);
+ }
+
+ info(
+ `Got ${contexts.length} with remoteTypes: ${Array.from(
+ uniqueRemoteTypes
+ )}`
+ );
+ Assert.greaterOrEqual(
+ contexts.length,
+ 2,
+ "There should be at least 2 browsing contexts"
+ );
+
+ if (Services.appinfo.fissionAutostart) {
+ Assert.greaterOrEqual(
+ uniqueRemoteTypes.size,
+ 2,
+ "Expect at least one cross origin sub frame"
+ );
+ }
+
+ for (const context of contexts) {
+ const url = context.currentURI?.spec?.replace(
+ context.currentURI?.query,
+ "…"
+ );
+ info(
+ `Checking user agent on ${url} (remoteType: ${context.currentRemoteType})`
+ );
+ await SpecialPowers.spawn(context, [expectedUA], async _expectedUA => {
+ is(
+ content.navigator.userAgent,
+ _expectedUA,
+ `expected navigator.userAgent value`
+ );
+ is(
+ content.wrappedJSObject.initialUserAgent,
+ _expectedUA,
+ `expected navigator.userAgent value at startup`
+ );
+ if (content.wrappedJSObject.userAgentHeader) {
+ is(
+ content.wrappedJSObject.userAgentHeader,
+ _expectedUA,
+ `user agent header has expected value`
+ );
+ }
+ });
+ }
+ }
+
+ await runReloadTestCase({
+ urlParams: "test=user-agent",
+ background,
+ devtoolsPage,
+ closeToolbox: false,
+ testCase: async function (extension, tab, toolbox) {
+ info("Get the initial user agent");
+ const initialUserAgent = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.navigator.userAgent;
+ }
+ );
+
+ info(
+ "Check that calling inspectedWindow.reload without userAgent does not change the user agent of the page"
+ );
+ let onPageLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ true
+ );
+ extension.sendMessage("no-custom-user-agent");
+ await onPageLoaded;
+
+ await checkUserAgent(initialUserAgent);
+
+ info(
+ "Check that calling inspectedWindow.reload with userAgent does change the user agent of the page"
+ );
+ onPageLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ true
+ );
+ extension.sendMessage("custom-user-agent");
+ await onPageLoaded;
+
+ await checkUserAgent(CUSTOM_USER_AGENT);
+
+ info("Check that the user agent persists after a reload");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+ await checkUserAgent(CUSTOM_USER_AGENT);
+
+ info(
+ "Check that the user agent persists after navigating to a new browsing context"
+ );
+ const previousBrowsingContextId =
+ gBrowser.selectedBrowser.browsingContext.id;
+
+ // Navigate to a different origin
+ await navigateToWithDevToolsOpen(
+ tab,
+ `https://example.com/${TEST_PATH}?test=user-agent&crossOriginIsolated=true`
+ );
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ await checkUserAgent(CUSTOM_USER_AGENT);
+
+ info(
+ "Check that closing DevTools resets the user agent of the page to its initial value"
+ );
+
+ await closeToolboxForTab(tab);
+
+ // XXX: This is needed at the moment since Navigator.cpp retrieves the UserAgent from the
+ // headers (when there's no custom user agent). And here, since we reloaded the page once
+ // we set the custom user agent, the header was set accordingly and still holds the custom
+ // user agent value. This should be fixed by Bug 1705326.
+ is(
+ gBrowser.selectedBrowser.browsingContext.customUserAgent,
+ "",
+ "The flag on the browsing context was reset"
+ );
+ await checkUserAgent(CUSTOM_USER_AGENT);
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+ await checkUserAgent(initialUserAgent);
+ },
+ });
+ }
+);
+
+add_task(async function test_devtools_inspectedWindow_reload_injected_script() {
+ function background() {
+ function getIframesTextContent() {
+ let docs = [];
+ for (
+ let iframe, doc = document;
+ doc;
+ doc = iframe && iframe.contentDocument
+ ) {
+ docs.push(doc);
+ iframe = doc.querySelector("iframe");
+ }
+
+ return docs.map(doc => doc.querySelector("pre").textContent);
+ }
+
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg !== "devtools_page.ready") {
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ return;
+ }
+
+ const tabs = await browser.tabs.query({ active: true });
+ const activeTabId = tabs[0].id;
+ let reloads = 0;
+
+ browser.webNavigation.onCompleted.addListener(async details => {
+ if (details.tabId == activeTabId && details.frameId == 0) {
+ reloads++;
+
+ let expectedContent;
+ let enabled;
+
+ switch (reloads) {
+ case 1:
+ enabled = false;
+ expectedContent = "injected script NOT executed";
+ break;
+ case 2:
+ enabled = true;
+ expectedContent = "injected script executed first";
+ break;
+ default:
+ browser.test.fail(`Unexpected number of tab reloads: ${reloads}`);
+ }
+
+ if (!expectedContent) {
+ browser.test.fail(`Unexpected number of tab reloads: ${reloads}`);
+ } else {
+ let expectedResults = new Array(4).fill(expectedContent);
+ let code = `(${getIframesTextContent})()`;
+
+ try {
+ let [results] = await browser.tabs.executeScript(activeTabId, {
+ code,
+ });
+
+ browser.test.assertEq(
+ JSON.stringify(expectedResults),
+ JSON.stringify(results),
+ `Got the expected result with injectScript=${enabled}`
+ );
+ } catch (err) {
+ browser.test.fail(`Error: ${err.message} - ${err.stack}`);
+ }
+ }
+
+ browser.test.sendMessage(
+ `devtools_inspectedWindow_reload_injectedScript.done`
+ );
+ }
+ });
+
+ browser.test.sendMessage("devtools_inspected_window_reload.ready");
+ });
+ }
+
+ function devtoolsPage() {
+ function injectedScript() {
+ if (!window.pageScriptExecutedFirst) {
+ window.addEventListener(
+ "DOMContentLoaded",
+ function listener() {
+ document.querySelector("pre").textContent =
+ "injected script executed first";
+ },
+ { once: true }
+ );
+ }
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ switch (msg) {
+ case "no-injected-script":
+ browser.devtools.inspectedWindow.reload({});
+ break;
+ case "injected-script":
+ browser.devtools.inspectedWindow.reload({
+ injectedScript: `new ${injectedScript}`,
+ });
+ break;
+ default:
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ }
+ });
+
+ browser.runtime.sendMessage("devtools_page.ready");
+ }
+
+ await runReloadTestCase({
+ urlParams: "test=injected-script&frames=3",
+ background,
+ devtoolsPage,
+ testCase: async function (extension) {
+ extension.sendMessage("no-injected-script");
+
+ await extension.awaitMessage(
+ "devtools_inspectedWindow_reload_injectedScript.done"
+ );
+
+ extension.sendMessage("injected-script");
+
+ await extension.awaitMessage(
+ "devtools_inspectedWindow_reload_injectedScript.done"
+ );
+ },
+ });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_targetSwitch.js b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_targetSwitch.js
new file mode 100644
index 0000000000..514ee399b5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_targetSwitch.js
@@ -0,0 +1,128 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Like most of the mochitest-browser devtools test,
+// on debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+loadTestSubscript("head_devtools.js");
+
+const MAIN_PROCESS_PAGE = "about:robots";
+const CONTENT_PROCESS_PAGE =
+ "data:text/html,content process page ";
+const CONTENT_PROCESS_PAGE2 = "http://example.com/";
+
+async function assertInspectedWindow(extension, tab) {
+ const onReloaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ extension.sendMessage("inspectedWindow-reload-request");
+ await onReloaded;
+ ok(true, "inspectedWindow works correctly");
+}
+
+async function getCurrentTabId(extension) {
+ extension.sendMessage("inspectedWindow-tabId-request");
+ return extension.awaitMessage("inspectedWindow-tabId-response");
+}
+
+async function navigateTo(uri, tab, toolbox, extension) {
+ const originalTabId = await getCurrentTabId(extension);
+
+ const promiseBrowserLoaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser
+ );
+ const onSwitched = toolbox.commands.targetCommand.once("switched-target");
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, uri);
+ info("Wait for the tab to be loaded");
+ await promiseBrowserLoaded;
+ info("Wait for the toolbox target to have been switched");
+ await onSwitched;
+
+ const currentTabId = await getCurrentTabId(extension);
+ is(
+ originalTabId,
+ currentTabId,
+ "inspectWindow.tabId is not changed even when navigating to a page running on another process."
+ );
+}
+
+/**
+ * This test checks whether inspectedWindow works well even target-switching happens.
+ */
+add_task(async () => {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ CONTENT_PROCESS_PAGE
+ );
+
+ async function devtools_page() {
+ browser.test.onMessage.addListener(async message => {
+ if (message === "inspectedWindow-reload-request") {
+ browser.devtools.inspectedWindow.reload();
+ } else if (message === "inspectedWindow-tabId-request") {
+ browser.test.sendMessage(
+ "inspectedWindow-tabId-response",
+ browser.devtools.inspectedWindow.tabId
+ );
+ } else {
+ browser.test.fail(`Unexpected test message received: ${message}`);
+ }
+ });
+
+ browser.test.sendMessage("devtools-page-loaded");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+ await extension.startup();
+
+ info("Open the developer toolbox");
+ const toolbox = await openToolboxForTab(tab);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools-page-loaded");
+
+ info("Check whether inspectedWindow works on content process page");
+ await assertInspectedWindow(extension, tab);
+
+ info("Navigate to a page running on main process");
+ await navigateTo(MAIN_PROCESS_PAGE, tab, toolbox, extension);
+
+ info("Check whether inspectedWindow works on main process page");
+ await assertInspectedWindow(extension, tab);
+
+ info("Return to a page running on content process again");
+ await navigateTo(CONTENT_PROCESS_PAGE, tab, toolbox, extension);
+
+ info("Check whether inspectedWindow works again");
+ await assertInspectedWindow(extension, tab);
+
+ // Navigate to an url that should switch to another target
+ // when fission is enabled.
+ if (SpecialPowers.useRemoteSubframes) {
+ info("Navigate to another page running on content process");
+ await navigateTo(CONTENT_PROCESS_PAGE2, tab, toolbox, extension);
+
+ info("Check whether inspectedWindow works again");
+ await assertInspectedWindow(extension, tab);
+ }
+
+ await extension.unload();
+ await closeToolboxForTab(tab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_network.js b/browser/components/extensions/test/browser/browser_ext_devtools_network.js
new file mode 100644
index 0000000000..fa6613357e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_network.js
@@ -0,0 +1,298 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+// Allow rejections related to closing the devtools toolbox too soon after the test
+// has already verified the details that were relevant for that test case.
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /can't be sent as the connection just closed/
+);
+
+function background() {
+ browser.test.onMessage.addListener(msg => {
+ let code;
+ if (msg === "navigate") {
+ code = "window.wrappedJSObject.location.href = 'http://example.com/';";
+ browser.tabs.executeScript({ code });
+ } else if (msg === "reload") {
+ code = "window.wrappedJSObject.location.reload(true);";
+ browser.tabs.executeScript({ code });
+ }
+ });
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.status === "complete" && tab.url === "http://example.com/") {
+ browser.test.sendMessage("tabUpdated");
+ }
+ });
+ browser.test.sendMessage("ready");
+}
+
+function devtools_page() {
+ let eventCount = 0;
+ let listener = url => {
+ eventCount++;
+ browser.test.assertEq(
+ "http://example.com/",
+ url,
+ "onNavigated received the expected url."
+ );
+ browser.test.sendMessage("onNavigatedFired", eventCount);
+
+ if (eventCount === 2) {
+ eventCount = 0;
+ browser.devtools.network.onNavigated.removeListener(listener);
+ }
+ };
+ browser.devtools.network.onNavigated.addListener(listener);
+
+ let harLogCount = 0;
+ let harListener = async msg => {
+ if (msg !== "getHAR") {
+ return;
+ }
+
+ harLogCount++;
+
+ const harLog = await browser.devtools.network.getHAR();
+ browser.test.sendMessage("getHAR-result", harLog);
+
+ if (harLogCount === 3) {
+ harLogCount = 0;
+ browser.test.onMessage.removeListener(harListener);
+ }
+ };
+ browser.test.onMessage.addListener(harListener);
+
+ let requestFinishedListener = async request => {
+ browser.test.assertTrue(request.request, "Request entry must exist");
+ browser.test.assertTrue(request.response, "Response entry must exist");
+
+ browser.test.sendMessage("onRequestFinished");
+
+ // Get response content using callback
+ request.getContent((content, encoding) => {
+ browser.test.sendMessage("onRequestFinished-callbackExecuted", [
+ content,
+ encoding,
+ ]);
+ });
+
+ // Get response content using returned promise
+ request.getContent().then(([content, encoding]) => {
+ browser.test.sendMessage("onRequestFinished-promiseResolved", [
+ content,
+ encoding,
+ ]);
+ });
+
+ browser.devtools.network.onRequestFinished.removeListener(
+ requestFinishedListener
+ );
+ };
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "addOnRequestFinishedListener") {
+ browser.devtools.network.onRequestFinished.addListener(
+ requestFinishedListener
+ );
+ }
+ });
+ browser.test.sendMessage("devtools-page-loaded");
+}
+
+let extData = {
+ background,
+ manifest: {
+ permissions: ["tabs", "http://mochi.test/", "http://example.com/"],
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+};
+
+async function waitForRequestAdded(toolbox) {
+ let netPanel = await toolbox.getNetMonitorAPI();
+ return new Promise(resolve => {
+ netPanel.once("NetMonitor:RequestAdded", () => {
+ resolve();
+ });
+ });
+}
+
+async function navigateToolboxTarget(extension, toolbox) {
+ extension.sendMessage("navigate");
+
+ // Wait till the navigation is complete.
+ await Promise.all([
+ extension.awaitMessage("tabUpdated"),
+ extension.awaitMessage("onNavigatedFired"),
+ waitForRequestAdded(toolbox),
+ ]);
+}
+
+/**
+ * Test for `chrome.devtools.network.onNavigate()` API
+ */
+add_task(async function test_devtools_network_on_navigated() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ let extension = ExtensionTestUtils.loadExtension(extData);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await openToolboxForTab(tab);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools-page-loaded");
+
+ extension.sendMessage("navigate");
+ await extension.awaitMessage("tabUpdated");
+ let eventCount = await extension.awaitMessage("onNavigatedFired");
+ is(eventCount, 1, "The expected number of events were fired.");
+
+ extension.sendMessage("reload");
+ await extension.awaitMessage("tabUpdated");
+ eventCount = await extension.awaitMessage("onNavigatedFired");
+ is(eventCount, 2, "The expected number of events were fired.");
+
+ // do a reload after the listener has been removed, do not expect a message to be sent
+ extension.sendMessage("reload");
+ await extension.awaitMessage("tabUpdated");
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Test for `chrome.devtools.network.getHAR()` API
+ */
+add_task(async function test_devtools_network_get_har() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ let extension = ExtensionTestUtils.loadExtension(extData);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // Open the Toolbox
+ const toolbox = await openToolboxForTab(tab);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools-page-loaded");
+
+ // Get HAR, it should be empty since no data collected yet.
+ const getHAREmptyPromise = extension.awaitMessage("getHAR-result");
+ extension.sendMessage("getHAR");
+ const getHAREmptyResult = await getHAREmptyPromise;
+ is(getHAREmptyResult.entries.length, 0, "HAR log should be empty");
+
+ // Reload the page to collect some HTTP requests.
+ await navigateToolboxTarget(extension, toolbox);
+
+ // Get HAR, it should not be empty now.
+ const getHARPromise = extension.awaitMessage("getHAR-result");
+ extension.sendMessage("getHAR");
+ const getHARResult = await getHARPromise;
+ is(getHARResult.entries.length, 1, "HAR log should not be empty");
+
+ // Select the Net panel and reload page again.
+ await toolbox.selectTool("netmonitor");
+ await navigateToolboxTarget(extension, toolbox);
+
+ // Get HAR again, it should not be empty even if
+ // the Network panel is selected now.
+ const getHAREmptyPromiseWithPanel = extension.awaitMessage("getHAR-result");
+ extension.sendMessage("getHAR");
+ const emptyResultWithPanel = await getHAREmptyPromiseWithPanel;
+ is(emptyResultWithPanel.entries.length, 1, "HAR log should not be empty");
+
+ // Shutdown
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Test for `chrome.devtools.network.onRequestFinished()` API
+ */
+add_task(async function test_devtools_network_on_request_finished() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ let extension = ExtensionTestUtils.loadExtension(extData);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // Open the Toolbox
+ const toolbox = await openToolboxForTab(tab);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools-page-loaded");
+
+ // Wait the extension to subscribe the onRequestFinished listener.
+ await extension.sendMessage("addOnRequestFinishedListener");
+
+ // Reload the page
+ await navigateToolboxTarget(extension, toolbox);
+
+ info("Wait for an onRequestFinished event");
+ await extension.awaitMessage("onRequestFinished");
+
+ // Wait for response content being fetched.
+ info("Wait for request.getBody results");
+ let [callbackRes, promiseRes] = await Promise.all([
+ extension.awaitMessage("onRequestFinished-callbackExecuted"),
+ extension.awaitMessage("onRequestFinished-promiseResolved"),
+ ]);
+
+ ok(
+ callbackRes[0].startsWith(""),
+ "The expected content has been retrieved."
+ );
+ is(
+ callbackRes[1],
+ "text/html; charset=utf-8",
+ "The expected content has been retrieved."
+ );
+ is(
+ promiseRes[0],
+ callbackRes[0],
+ "The resolved value is equal to the one received in the callback API mode"
+ );
+ is(
+ promiseRes[1],
+ callbackRes[1],
+ "The resolved value is equal to the one received in the callback API mode"
+ );
+
+ // Shutdown
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_network_targetSwitch.js b/browser/components/extensions/test/browser/browser_ext_devtools_network_targetSwitch.js
new file mode 100644
index 0000000000..229067fd45
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_network_targetSwitch.js
@@ -0,0 +1,74 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Like most of the mochitest-browser devtools test,
+// on debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+loadTestSubscript("head_devtools.js");
+
+const MAIN_PROCESS_PAGE = "about:robots";
+const CONTENT_PROCESS_PAGE = "http://example.com/";
+
+async function testOnNavigatedEvent(uri, tab, toolbox, extension) {
+ const onNavigated = extension.awaitMessage("network-onNavigated");
+ const onSwitched = toolbox.commands.targetCommand.once("switched-target");
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, uri);
+ await onSwitched;
+ const result = await onNavigated;
+ is(result, uri, "devtools.network.onNavigated works correctly");
+}
+
+/**
+ * This test checks whether network works well even target-switching happens.
+ */
+add_task(async () => {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ CONTENT_PROCESS_PAGE
+ );
+
+ async function devtools_page() {
+ browser.devtools.network.onNavigated.addListener(url => {
+ browser.test.sendMessage("network-onNavigated", url);
+ });
+
+ browser.test.sendMessage("devtools-page-loaded");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+ await extension.startup();
+
+ info("Open the developer toolbox");
+ const toolbox = await openToolboxForTab(tab);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools-page-loaded");
+
+ info("Navigate to a page running on main process");
+ await testOnNavigatedEvent(MAIN_PROCESS_PAGE, tab, toolbox, extension);
+
+ info("Return to a page running on content process again");
+ await testOnNavigatedEvent(CONTENT_PROCESS_PAGE, tab, toolbox, extension);
+
+ await extension.unload();
+ await closeToolboxForTab(tab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_optional.js b/browser/components/extensions/test/browser/browser_ext_devtools_optional.js
new file mode 100644
index 0000000000..54ca061c67
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_optional.js
@@ -0,0 +1,169 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+/**
+ * This test file ensures that:
+ *
+ * - "devtools" permission can be used as an optional permission
+ * - the extension devtools page and panels are not disabled/enabled on changes
+ * to unrelated optional permissions.
+ */
+add_task(async function test_devtools_optional_permission() {
+ Services.prefs.setBoolPref(
+ "extensions.webextOptionalPermissionPrompts",
+ false
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webextOptionalPermissionPrompts");
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ function background() {
+ browser.test.onMessage.addListener(async (msg, perm) => {
+ if (msg === "request") {
+ let granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(browser.permissions.request(perm));
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+ browser.test.sendMessage("done");
+ } else if (msg === "revoke") {
+ await browser.permissions.remove(perm);
+ browser.test.sendMessage("done");
+ }
+ });
+ }
+
+ function devtools_page() {
+ browser.test.sendMessage("devtools_page_loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ optional_permissions: ["devtools", "*://mochi.test/*"],
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+
+ await extension.startup();
+
+ function checkEnabled(expect = false, { expectIsUserSet = true } = {}) {
+ const prefName = `devtools.webextensions.${extension.id}.enabled`;
+ Assert.equal(
+ expect,
+ Services.prefs.getBoolPref(prefName, false),
+ `Got the expected value set on pref ${prefName}`
+ );
+
+ Assert.equal(
+ expectIsUserSet,
+ Services.prefs.prefHasUserValue(prefName),
+ `pref "${prefName}" ${
+ expectIsUserSet ? "should" : "should not"
+ } be user set`
+ );
+ }
+
+ checkEnabled(false, { expectIsUserSet: false });
+
+ // Open the devtools first, then request permission
+ info("Open the developer toolbox");
+ await openToolboxForTab(tab);
+ assertDevToolsExtensionEnabled(extension.uuid, false);
+
+ info(
+ "Request unrelated permission, expect devtools page and panel to be disabled"
+ );
+ extension.sendMessage("request", {
+ permissions: [],
+ origins: ["*://mochi.test/*"],
+ });
+ await extension.awaitMessage("done");
+ checkEnabled(false, { expectIsUserSet: false });
+
+ info(
+ "Request devtools permission, expect devtools page and panel to be enabled"
+ );
+ extension.sendMessage("request", {
+ permissions: ["devtools"],
+ origins: [],
+ });
+ await extension.awaitMessage("done");
+ checkEnabled(true);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools_page_loaded");
+ assertDevToolsExtensionEnabled(extension.uuid, true);
+
+ info(
+ "Revoke unrelated permission, expect devtools page and panel to stay enabled"
+ );
+ extension.sendMessage("revoke", {
+ permissions: [],
+ origins: ["*://mochi.test/*"],
+ });
+ await extension.awaitMessage("done");
+ checkEnabled(true);
+
+ info(
+ "Revoke devtools permission, expect devtools page and panel to be destroyed"
+ );
+ let policy = WebExtensionPolicy.getByID(extension.id);
+ let closed = new Promise(resolve => {
+ // eslint-disable-next-line mozilla/balanced-listeners
+ policy.extension.on("devtools-page-shutdown", resolve);
+ });
+
+ extension.sendMessage("revoke", {
+ permissions: ["devtools"],
+ origins: [],
+ });
+ await extension.awaitMessage("done");
+
+ await closed;
+ checkEnabled(false);
+ assertDevToolsExtensionEnabled(extension.uuid, false);
+
+ info("Close the developer toolbox");
+ await closeToolboxForTab(tab);
+
+ extension.sendMessage("request", {
+ permissions: ["devtools"],
+ origins: [],
+ });
+ await extension.awaitMessage("done");
+
+ info("Open the developer toolbox");
+ openToolboxForTab(tab);
+
+ checkEnabled(true);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools_page_loaded");
+ assertDevToolsExtensionEnabled(extension.uuid, true);
+ await extension.unload();
+
+ await closeToolboxForTab(tab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_page.js b/browser/components/extensions/test/browser/browser_ext_devtools_page.js
new file mode 100644
index 0000000000..8f2700c657
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_page.js
@@ -0,0 +1,304 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+/**
+ * This test file ensures that:
+ *
+ * - the devtools_page property creates a new WebExtensions context
+ * - the devtools_page can exchange messages with the background page
+ */
+add_task(async function test_devtools_page_runtime_api_messaging() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (sender.tab) {
+ browser.test.sendMessage("content_script_message_received");
+ }
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ if (port.sender.tab) {
+ browser.test.sendMessage("content_script_port_received");
+ return;
+ }
+
+ let portMessageReceived = false;
+
+ port.onDisconnect.addListener(() => {
+ browser.test.assertTrue(
+ portMessageReceived,
+ "Got a port message before the port disconnect event"
+ );
+ browser.test.sendMessage("devtools_page_connect.done");
+ });
+
+ port.onMessage.addListener(msg => {
+ portMessageReceived = true;
+ browser.test.assertEq(
+ "devtools -> background port message",
+ msg,
+ "Got the expected message from the devtools page"
+ );
+ port.postMessage("background -> devtools port message");
+ });
+ });
+ }
+
+ function devtools_page() {
+ browser.runtime.onConnect.addListener(port => {
+ // Fail if a content script port has been received by the devtools page (Bug 1383310).
+ if (port.sender.tab) {
+ browser.test.fail(
+ `A DevTools page should not receive ports from content scripts`
+ );
+ }
+ });
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ // Fail if a content script message has been received by the devtools page (Bug 1383310).
+ if (sender.tab) {
+ browser.test.fail(
+ `A DevTools page should not receive messages from content scripts`
+ );
+ }
+ });
+
+ const port = browser.runtime.connect();
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(
+ "background -> devtools port message",
+ msg,
+ "Got the expected message from the background page"
+ );
+ port.disconnect();
+ });
+ port.postMessage("devtools -> background port message");
+
+ browser.test.sendMessage("devtools_page_loaded");
+ }
+
+ function content_script() {
+ browser.test.onMessage.addListener(msg => {
+ switch (msg) {
+ case "content_script.send_message":
+ browser.runtime.sendMessage("content_script_message");
+ break;
+ case "content_script.connect_port":
+ const port = browser.runtime.connect();
+ port.disconnect();
+ break;
+ default:
+ browser.test.fail(
+ `Unexpected message ${msg} received by content script`
+ );
+ }
+ });
+
+ browser.test.sendMessage("content_script_loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ devtools_page: "devtools_page.html",
+ content_scripts: [
+ {
+ js: ["content_script.js"],
+ matches: ["http://mochi.test/*"],
+ },
+ ],
+ },
+ files: {
+ "content_script.js": content_script,
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+
+ await extension.startup();
+
+ info("Wait the content script load");
+ await extension.awaitMessage("content_script_loaded");
+
+ info("Open the developer toolbox");
+ await openToolboxForTab(tab);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools_page_loaded");
+
+ info("Wait the connection 'devtools_page -> background' to complete");
+ await extension.awaitMessage("devtools_page_connect.done");
+
+ // Send a message from the content script and expect it to be received from
+ // the background page (repeated twice to be sure that the devtools_page had
+ // the chance to receive the message and fail as expected).
+ info(
+ "Wait for 2 content script messages to be received from the background page"
+ );
+ extension.sendMessage("content_script.send_message");
+ await extension.awaitMessage("content_script_message_received");
+ extension.sendMessage("content_script.send_message");
+ await extension.awaitMessage("content_script_message_received");
+
+ // Create a port from the content script and expect a port to be received from
+ // the background page (repeated twice to be sure that the devtools_page had
+ // the chance to receive the message and fail as expected).
+ info(
+ "Wait for 2 content script ports to be received from the background page"
+ );
+ extension.sendMessage("content_script.connect_port");
+ await extension.awaitMessage("content_script_port_received");
+ extension.sendMessage("content_script.connect_port");
+ await extension.awaitMessage("content_script_port_received");
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * This test file ensures that:
+ *
+ * - the devtools_page can exchange messages with an extension tab page
+ */
+
+add_task(async function test_devtools_page_and_extension_tab_messaging() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (sender.tab) {
+ browser.test.sendMessage("extension_tab_message_received");
+ }
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ if (port.sender.tab) {
+ browser.test.sendMessage("extension_tab_port_received");
+ }
+ });
+
+ browser.tabs.create({ url: browser.runtime.getURL("extension_tab.html") });
+ }
+
+ function devtools_page() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.sendMessage("devtools_page_onconnect");
+ });
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.sendMessage("devtools_page_onmessage");
+ });
+
+ browser.test.sendMessage("devtools_page_loaded");
+ }
+
+ function extension_tab() {
+ browser.test.onMessage.addListener(msg => {
+ switch (msg) {
+ case "extension_tab.send_message":
+ browser.runtime.sendMessage("content_script_message");
+ break;
+ case "extension_tab.connect_port":
+ const port = browser.runtime.connect();
+ port.disconnect();
+ break;
+ default:
+ browser.test.fail(
+ `Unexpected message ${msg} received by content script`
+ );
+ }
+ });
+
+ browser.test.sendMessage("extension_tab_loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "extension_tab.html": `
+
+
+
+
+
+ Extension Tab Page
+
+
+ `,
+ "extension_tab.js": extension_tab,
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+
+ await extension.startup();
+
+ info("Wait the extension tab page load");
+ await extension.awaitMessage("extension_tab_loaded");
+
+ info("Open the developer toolbox");
+ await openToolboxForTab(tab);
+
+ info("Wait the devtools page load");
+ await extension.awaitMessage("devtools_page_loaded");
+
+ extension.sendMessage("extension_tab.send_message");
+
+ info(
+ "Wait for an extension tab message to be received from the devtools page"
+ );
+ await extension.awaitMessage("devtools_page_onmessage");
+
+ info(
+ "Wait for an extension tab message to be received from the background page"
+ );
+ await extension.awaitMessage("extension_tab_message_received");
+
+ extension.sendMessage("extension_tab.connect_port");
+
+ info("Wait for an extension tab port to be received from the devtools page");
+ await extension.awaitMessage("devtools_page_onconnect");
+
+ info(
+ "Wait for an extension tab port to be received from the background page"
+ );
+ await extension.awaitMessage("extension_tab_port_received");
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_page_incognito.js b/browser/components/extensions/test/browser/browser_ext_devtools_page_incognito.js
new file mode 100644
index 0000000000..fe87913563
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_page_incognito.js
@@ -0,0 +1,92 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+async function testIncognito(incognitoOverride) {
+ let privateAllowed = incognitoOverride == "spanning";
+
+ function devtools_page(privateAllowed) {
+ if (!privateAllowed) {
+ browser.test.fail(
+ "Extension devtools_page should not be created on private tabs if not allowed"
+ );
+ }
+
+ browser.test.sendMessage("devtools_page:loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ incognitoOverride,
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": `(${devtools_page})(${privateAllowed})`,
+ },
+ });
+
+ let existingPrivateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await extension.startup();
+
+ await openToolboxForTab(existingPrivateWindow.gBrowser.selectedTab);
+
+ if (privateAllowed) {
+ // Wait the devtools_page to be loaded if it is allowed.
+ await extension.awaitMessage("devtools_page:loaded");
+ }
+
+ // If the devtools_page is created for a not allowed extension, the devtools page will
+ // trigger a test failure, but let's make an explicit assertion otherwise mochitest will
+ // complain because there was no assertion in the test.
+ ok(
+ true,
+ `Opened toolbox on an existing private window (extension ${incognitoOverride})`
+ );
+
+ let newPrivateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await openToolboxForTab(newPrivateWindow.gBrowser.selectedTab);
+
+ if (privateAllowed) {
+ await extension.awaitMessage("devtools_page:loaded");
+ }
+
+ // If the devtools_page is created for a not allowed extension, the devtools page will
+ // trigger a test failure.
+ ok(
+ true,
+ `Opened toolbox on a newprivate window (extension ${incognitoOverride})`
+ );
+
+ // Close opened toolboxes and private windows.
+ await closeToolboxForTab(existingPrivateWindow.gBrowser.selectedTab);
+ await closeToolboxForTab(newPrivateWindow.gBrowser.selectedTab);
+ await BrowserTestUtils.closeWindow(existingPrivateWindow);
+ await BrowserTestUtils.closeWindow(newPrivateWindow);
+
+ await extension.unload();
+}
+
+add_task(async function test_devtools_page_not_allowed() {
+ await testIncognito();
+});
+
+add_task(async function test_devtools_page_allowed() {
+ await testIncognito("spanning");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_panel.js b/browser/components/extensions/test/browser/browser_ext_devtools_panel.js
new file mode 100644
index 0000000000..c9bf12b9fd
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_panel.js
@@ -0,0 +1,812 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Like most of the mochitest-browser devtools test,
+// on debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+loadTestSubscript("head_devtools.js");
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+const DEVTOOLS_THEME_PREF = "devtools.theme";
+
+/**
+ * This test file ensures that:
+ *
+ * - devtools.panels.themeName returns the correct value,
+ * both from a page and a panel.
+ * - devtools.panels.onThemeChanged fires for theme changes,
+ * both from a page and a panel.
+ * - devtools.panels.create is able to create a devtools panel.
+ */
+
+function createPage(jsScript, bodyText = "") {
+ return `
+
+
+
+
+
+ ${bodyText}
+
+
+ `;
+}
+
+async function test_theme_name(testWithPanel = false) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ function switchTheme(theme) {
+ const waitforThemeChanged = gDevTools.once("theme-changed");
+ Preferences.set(DEVTOOLS_THEME_PREF, theme);
+ return waitforThemeChanged;
+ }
+
+ async function testThemeSwitching(extension, locations = ["page"]) {
+ for (let newTheme of ["dark", "light"]) {
+ await switchTheme(newTheme);
+ for (let location of locations) {
+ is(
+ await extension.awaitMessage(`devtools_theme_changed_${location}`),
+ newTheme,
+ `The onThemeChanged event listener fired for the ${location}.`
+ );
+ is(
+ await extension.awaitMessage(`current_theme_${location}`),
+ newTheme,
+ `The current theme is reported as expected for the ${location}.`
+ );
+ }
+ }
+ }
+
+ async function devtools_page(createPanel) {
+ if (createPanel) {
+ await browser.devtools.panels.create(
+ "Test Panel Theme",
+ "fake-icon.png",
+ "devtools_panel.html"
+ );
+ }
+
+ browser.devtools.panels.onThemeChanged.addListener(themeName => {
+ browser.test.sendMessage("devtools_theme_changed_page", themeName);
+ browser.test.sendMessage(
+ "current_theme_page",
+ browser.devtools.panels.themeName
+ );
+ });
+
+ browser.test.sendMessage(
+ "initial_theme_page",
+ browser.devtools.panels.themeName
+ );
+ }
+
+ async function devtools_panel() {
+ browser.devtools.panels.onThemeChanged.addListener(themeName => {
+ browser.test.sendMessage("devtools_theme_changed_panel", themeName);
+ browser.test.sendMessage(
+ "current_theme_panel",
+ browser.devtools.panels.themeName
+ );
+ });
+
+ browser.test.sendMessage(
+ "initial_theme_panel",
+ browser.devtools.panels.themeName
+ );
+ }
+
+ let files = {
+ "devtools_page.html": createPage("devtools_page.js"),
+ "devtools_page.js": `(${devtools_page})(${testWithPanel})`,
+ };
+
+ if (testWithPanel) {
+ files["devtools_panel.js"] = devtools_panel;
+ files["devtools_panel.html"] = createPage(
+ "devtools_panel.js",
+ "Test Panel Theme"
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files,
+ });
+
+ // Ensure that the initial value of the devtools theme is "light".
+ await SpecialPowers.pushPrefEnv({ set: [[DEVTOOLS_THEME_PREF, "light"]] });
+ registerCleanupFunction(async function () {
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await extension.startup();
+
+ const toolbox = await openToolboxForTab(tab);
+
+ info("Waiting initial theme from devtools_page");
+ is(
+ await extension.awaitMessage("initial_theme_page"),
+ "light",
+ "The initial theme is reported as expected."
+ );
+
+ if (testWithPanel) {
+ let toolboxAdditionalTools = toolbox.getAdditionalTools();
+ is(
+ toolboxAdditionalTools.length,
+ 1,
+ "Got the expected number of toolbox specific panel registered."
+ );
+
+ let panelId = toolboxAdditionalTools[0].id;
+
+ await gDevTools.showToolboxForTab(tab, { toolId: panelId });
+ is(
+ await extension.awaitMessage("initial_theme_panel"),
+ "light",
+ "The initial theme is reported as expected from a devtools panel."
+ );
+
+ await testThemeSwitching(extension, ["page", "panel"]);
+ } else {
+ await testThemeSwitching(extension);
+ }
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_devtools_page_theme() {
+ await test_theme_name(false);
+});
+
+add_task(async function test_devtools_panel_theme() {
+ await test_theme_name(true);
+});
+
+add_task(async function test_devtools_page_panels_create() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ async function devtools_page() {
+ const result = {
+ devtoolsPageTabId: browser.devtools.inspectedWindow.tabId,
+ panelCreated: 0,
+ panelShown: 0,
+ panelHidden: 0,
+ };
+
+ try {
+ const panel = await browser.devtools.panels.create(
+ "Test Panel Create",
+ "fake-icon.png",
+ "devtools_panel.html"
+ );
+
+ result.panelCreated++;
+
+ panel.onShown.addListener(contentWindow => {
+ result.panelShown++;
+ browser.test.assertEq(
+ "complete",
+ contentWindow.document.readyState,
+ "Got the expected 'complete' panel document readyState"
+ );
+ browser.test.assertEq(
+ "test_panel_global",
+ contentWindow.TEST_PANEL_GLOBAL,
+ "Got the expected global in the panel contentWindow"
+ );
+ browser.test.sendMessage("devtools_panel_shown", result);
+ });
+
+ panel.onHidden.addListener(() => {
+ result.panelHidden++;
+
+ browser.test.sendMessage("devtools_panel_hidden", result);
+ });
+
+ browser.test.sendMessage("devtools_panel_created");
+ } catch (err) {
+ // Make the test able to fail fast when it is going to be a failure.
+ browser.test.sendMessage("devtools_panel_created");
+ throw err;
+ }
+ }
+
+ function devtools_panel() {
+ // Set a property in the global and check that it is defined
+ // and accessible from the devtools_page when the panel.onShown
+ // event has been received.
+ window.TEST_PANEL_GLOBAL = "test_panel_global";
+
+ browser.test.sendMessage(
+ "devtools_panel_inspectedWindow_tabId",
+ browser.devtools.inspectedWindow.tabId
+ );
+ }
+
+ const longPrefix = new Array(80).fill("x").join("");
+ // Extension ID includes "inspector" to verify Bug 1474379 doesn't regress.
+ const EXTENSION_ID = `${longPrefix}-inspector@create-devtools-panel.test`;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ devtools_page: "devtools_page.html",
+ browser_specific_settings: {
+ gecko: { id: EXTENSION_ID },
+ },
+ },
+ files: {
+ "devtools_page.html": createPage("devtools_page.js"),
+ "devtools_page.js": devtools_page,
+ "devtools_panel.html": createPage(
+ "devtools_panel.js",
+ "Test Panel Create"
+ ),
+ "devtools_panel.js": devtools_panel,
+ },
+ });
+
+ await extension.startup();
+
+ const extensionPrefBranch = `devtools.webextensions.${EXTENSION_ID}.`;
+ const extensionPrefName = `${extensionPrefBranch}enabled`;
+
+ let prefBranch = Services.prefs.getBranch(extensionPrefBranch);
+ ok(
+ prefBranch,
+ "The preference branch for the extension should have been created"
+ );
+ is(
+ prefBranch.getBoolPref("enabled", false),
+ true,
+ "The 'enabled' bool preference for the extension should be initially true"
+ );
+
+ // Get the devtools panel info for the first item in the toolbox additional tools array.
+ const getPanelInfo = toolbox => {
+ let toolboxAdditionalTools = toolbox.getAdditionalTools();
+ is(
+ toolboxAdditionalTools.length,
+ 1,
+ "Got the expected number of toolbox specific panel registered."
+ );
+ return toolboxAdditionalTools[0];
+ };
+
+ // Test the devtools panel shown and hide events.
+ const testPanelShowAndHide = async ({
+ tab,
+ panelId,
+ isFirstPanelLoad,
+ expectedResults,
+ }) => {
+ info("Wait Addon Devtools Panel to be shown");
+
+ await gDevTools.showToolboxForTab(tab, { toolId: panelId });
+ const { devtoolsPageTabId } = await extension.awaitMessage(
+ "devtools_panel_shown"
+ );
+
+ // If the panel is loaded for the first time, we expect to also
+ // receive the test messages and assert that both the page and the panel
+ // have the same devtools.inspectedWindow.tabId value.
+ if (isFirstPanelLoad) {
+ const devtoolsPanelTabId = await extension.awaitMessage(
+ "devtools_panel_inspectedWindow_tabId"
+ );
+ is(
+ devtoolsPanelTabId,
+ devtoolsPageTabId,
+ "Got the same devtools.inspectedWindow.tabId from devtools page and panel"
+ );
+ }
+
+ info("Wait Addon Devtools Panel to be shown");
+
+ await gDevTools.showToolboxForTab(tab, { toolId: "testBlankPanel" });
+ const results = await extension.awaitMessage("devtools_panel_hidden");
+
+ // We already checked the tabId, remove it from the results, so that we can check
+ // the remaining properties using a single Assert.deepEqual.
+ delete results.devtoolsPageTabId;
+
+ Assert.deepEqual(
+ results,
+ expectedResults,
+ "Got the expected number of created panels and shown/hidden events"
+ );
+ };
+
+ // Test the extension devtools_page enabling/disabling through the related
+ // about:config preference.
+ const testExtensionDevToolsPref = async ({
+ prefValue,
+ toolbox,
+ oldPanelId,
+ }) => {
+ if (!prefValue) {
+ // Test that the extension devtools_page is shutting down when the related
+ // about:config preference has been set to false, and the panel on its left
+ // is being selected.
+ info(
+ "Turning off the extension devtools page from its about:config preference"
+ );
+ let waitToolSelected = toolbox.once("select");
+ Services.prefs.setBoolPref(extensionPrefName, false);
+ const selectedTool = await waitToolSelected;
+ isnot(
+ selectedTool,
+ oldPanelId,
+ "Expect a different panel to be selected"
+ );
+
+ let toolboxAdditionalTools = toolbox.getAdditionalTools();
+ is(
+ toolboxAdditionalTools.length,
+ 0,
+ "Extension devtools panel unregistered"
+ );
+ is(
+ toolbox.visibleAdditionalTools.filter(toolId => toolId == oldPanelId)
+ .length,
+ 0,
+ "Removed panel should not be listed in the visible additional tools"
+ );
+ } else {
+ // Test that the extension devtools_page and panel are being created again when
+ // the related about:config preference has been set to true.
+ info(
+ "Turning on the extension devtools page from its about:config preference"
+ );
+ Services.prefs.setBoolPref(extensionPrefName, true);
+ await extension.awaitMessage("devtools_panel_created");
+
+ let toolboxAdditionalTools = toolbox.getAdditionalTools();
+ is(
+ toolboxAdditionalTools.length,
+ 1,
+ "Got one extension devtools panel registered"
+ );
+
+ let newPanelId = getPanelInfo(toolbox).id;
+ is(
+ toolbox.visibleAdditionalTools.filter(toolId => toolId == newPanelId)
+ .length,
+ 1,
+ "Extension panel is listed in the visible additional tools"
+ );
+ }
+ };
+
+ // Wait that the devtools_page has created its devtools panel and retrieve its
+ // panel id.
+ let toolbox = await openToolboxForTab(tab);
+ await extension.awaitMessage("devtools_panel_created");
+ let panelId = getPanelInfo(toolbox).id;
+
+ info("Test panel show and hide - first cycle");
+ await testPanelShowAndHide({
+ tab,
+ panelId,
+ isFirstPanelLoad: true,
+ expectedResults: {
+ panelCreated: 1,
+ panelShown: 1,
+ panelHidden: 1,
+ },
+ });
+
+ info("Test panel show and hide - second cycle");
+ await testPanelShowAndHide({
+ tab,
+ panelId,
+ isFirstPanelLoad: false,
+ expectedResults: {
+ panelCreated: 1,
+ panelShown: 2,
+ panelHidden: 2,
+ },
+ });
+
+ // Go back to the extension devtools panel.
+ await gDevTools.showToolboxForTab(tab, { toolId: panelId });
+ await extension.awaitMessage("devtools_panel_shown");
+
+ // Check that the aria-label has been set on the devtools panel.
+ const panelFrame = toolbox.doc.getElementById(
+ `toolbox-panel-iframe-${panelId}`
+ );
+ const panelInfo = getPanelInfo(toolbox);
+ ok(
+ panelInfo.panelLabel && !!panelInfo.panelLabel.length,
+ "Expect the registered panel to include a non empty panelLabel property"
+ );
+ is(
+ panelFrame && panelFrame.getAttribute("aria-label"),
+ panelInfo.panelLabel,
+ "Got the expected aria-label on the extension panel frame"
+ );
+
+ // Turn off the extension devtools page using the preference that enable/disable the
+ // devtools page for a given installed WebExtension.
+ await testExtensionDevToolsPref({
+ toolbox,
+ prefValue: false,
+ oldPanelId: panelId,
+ });
+
+ // Close and Re-open the toolbox to verify that the toolbox doesn't load the
+ // devtools_page and the devtools panel.
+ info("Re-open the toolbox and expect no extension devtools panel");
+ await closeToolboxForTab(tab);
+ toolbox = await openToolboxForTab(tab);
+
+ let toolboxAdditionalTools = toolbox.getAdditionalTools();
+ is(
+ toolboxAdditionalTools.length,
+ 0,
+ "Got no extension devtools panel on the opened toolbox as expected."
+ );
+
+ // Close and Re-open the toolbox to verify that the toolbox does load the
+ // devtools_page and the devtools panel again.
+ info("Restart the toolbox and enable the extension devtools panel");
+ await closeToolboxForTab(tab);
+ toolbox = await openToolboxForTab(tab);
+
+ // Turn the addon devtools panel back on using the preference that enable/disable the
+ // devtools page for a given installed WebExtension.
+ await testExtensionDevToolsPref({
+ toolbox,
+ prefValue: true,
+ });
+
+ // Test devtools panel is loaded correctly after being toggled and
+ // devtools panel events has been fired as expected.
+ panelId = getPanelInfo(toolbox).id;
+
+ info("Test panel show and hide - after disabling/enabling devtools_page");
+ await testPanelShowAndHide({
+ tab,
+ panelId,
+ isFirstPanelLoad: true,
+ expectedResults: {
+ panelCreated: 1,
+ panelShown: 1,
+ panelHidden: 1,
+ },
+ });
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ // Verify that the extension preference branch has been removed once the extension
+ // has been uninstalled.
+ prefBranch = Services.prefs.getBranch(extensionPrefBranch);
+ is(
+ prefBranch.getPrefType("enabled"),
+ prefBranch.PREF_INVALID,
+ "The preference branch for the extension should have been removed"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_devtools_page_panels_switch_toolbox_host() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ function devtools_panel() {
+ const hasDevToolsAPINamespace = "devtools" in browser;
+
+ browser.test.sendMessage("devtools_panel_loaded", {
+ hasDevToolsAPINamespace,
+ panelLoadedURL: window.location.href,
+ });
+ }
+
+ async function devtools_page() {
+ const panel = await browser.devtools.panels.create(
+ "Test Panel Switch Host",
+ "fake-icon.png",
+ "devtools_panel.html"
+ );
+
+ panel.onShown.addListener(panelWindow => {
+ browser.test.sendMessage(
+ "devtools_panel_shown",
+ panelWindow.location.href
+ );
+ });
+
+ panel.onHidden.addListener(() => {
+ browser.test.sendMessage("devtools_panel_hidden");
+ });
+
+ browser.test.sendMessage("devtools_panel_created");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": createPage("devtools_page.js"),
+ "devtools_page.js": devtools_page,
+ "devtools_panel.html": createPage("devtools_panel.js", "DEVTOOLS PANEL"),
+ "devtools_panel.js": devtools_panel,
+ },
+ });
+
+ await extension.startup();
+
+ let toolbox = await openToolboxForTab(tab);
+ await extension.awaitMessage("devtools_panel_created");
+
+ const toolboxAdditionalTools = toolbox.getAdditionalTools();
+
+ is(
+ toolboxAdditionalTools.length,
+ 1,
+ "Got the expected number of toolbox specific panel registered."
+ );
+
+ const panelDef = toolboxAdditionalTools[0];
+ const panelId = panelDef.id;
+
+ info("Selecting the addon devtools panel");
+ await gDevTools.showToolboxForTab(tab, { toolId: panelId });
+
+ info("Wait for the panel to show and load for the first time");
+ const panelShownURL = await extension.awaitMessage("devtools_panel_shown");
+
+ const { panelLoadedURL, hasDevToolsAPINamespace } =
+ await extension.awaitMessage("devtools_panel_loaded");
+
+ is(
+ panelShownURL,
+ panelLoadedURL,
+ "Got the expected panel URL on the first load"
+ );
+ ok(
+ hasDevToolsAPINamespace,
+ "The devtools panel has the devtools API on the first load"
+ );
+
+ const originalToolboxHostType = toolbox.hostType;
+
+ info("Switch the toolbox from docked on bottom to docked on right");
+ toolbox.switchHost("right");
+
+ info(
+ "Wait for the panel to emit hide, show and load messages once docked on side"
+ );
+ await extension.awaitMessage("devtools_panel_hidden");
+ const dockedOnSideShownURL = await extension.awaitMessage(
+ "devtools_panel_shown"
+ );
+
+ is(
+ dockedOnSideShownURL,
+ panelShownURL,
+ "Got the expected panel url once the panel shown event has been emitted on toolbox host changed"
+ );
+
+ const dockedOnSideLoaded = await extension.awaitMessage(
+ "devtools_panel_loaded"
+ );
+
+ is(
+ dockedOnSideLoaded.panelLoadedURL,
+ panelShownURL,
+ "Got the expected panel url once the panel has been reloaded on toolbox host changed"
+ );
+ ok(
+ dockedOnSideLoaded.hasDevToolsAPINamespace,
+ "The devtools panel has the devtools API once the toolbox host has been changed"
+ );
+
+ info("Switch the toolbox from docked on bottom to the original dock mode");
+ toolbox.switchHost(originalToolboxHostType);
+
+ info(
+ "Wait for the panel test messages once toolbox dock mode has been restored"
+ );
+ await extension.awaitMessage("devtools_panel_hidden");
+ await extension.awaitMessage("devtools_panel_shown");
+ await extension.awaitMessage("devtools_panel_loaded");
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_devtools_page_invalid_panel_urls() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ async function devtools_page() {
+ const matchInvalidPanelURL = /must be a relative URL/;
+ const matchInvalidIconURL =
+ /be one of \[""\], or match the format "strictRelativeUrl"/;
+
+ // Invalid panel urls (validated by the schema wrappers, throws on invalid urls).
+ const invalid_panels = [
+ {
+ panel: "about:about",
+ icon: "icon.png",
+ expectError: matchInvalidPanelURL,
+ },
+ {
+ panel: "about:addons",
+ icon: "icon.png",
+ expectError: matchInvalidPanelURL,
+ },
+ {
+ panel: "http://mochi.test:8888",
+ icon: "icon.png",
+ expectError: matchInvalidPanelURL,
+ },
+ // Invalid icon urls (validated inside the API method because of the empty icon string
+ // which have to be resolved to the default icon, reject the returned promise).
+ {
+ panel: "panel.html",
+ icon: "about:about",
+ expectError: matchInvalidIconURL,
+ },
+ {
+ panel: "panel.html",
+ icon: "http://mochi.test:8888",
+ expectError: matchInvalidIconURL,
+ },
+ ];
+
+ const valid_panels = [
+ { panel: "panel.html", icon: "icon.png" },
+ { panel: "./panel.html", icon: "icon.png" },
+ { panel: "/panel.html", icon: "icon.png" },
+ { panel: "/panel.html", icon: "" },
+ ];
+
+ let valid_panels_length = valid_panels.length;
+
+ const test_cases = [].concat(invalid_panels, valid_panels);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "start_test_panel_create") {
+ return;
+ }
+
+ for (let { panel, icon, expectError } of test_cases) {
+ browser.test.log(
+ `Testing devtools.panels.create for ${JSON.stringify({
+ panel,
+ icon,
+ })}`
+ );
+
+ if (expectError) {
+ // Verify that invalid panel urls throw.
+ browser.test.assertThrows(
+ () => browser.devtools.panels.create("Test Panel", icon, panel),
+ expectError,
+ "Got the expected rejection on creating a devtools panel with " +
+ `panel url ${panel} and icon ${icon}`
+ );
+ } else {
+ // Verify that with valid panel and icon urls the panel is created and loaded
+ // as expected.
+ try {
+ const pane = await browser.devtools.panels.create(
+ "Test Panel",
+ icon,
+ panel
+ );
+
+ valid_panels_length--;
+
+ // Wait the panel to be loaded.
+ const oncePanelLoaded = new Promise(resolve => {
+ pane.onShown.addListener(paneWin => {
+ browser.test.assertTrue(
+ paneWin.location.href.endsWith("/panel.html"),
+ `The panel has loaded the expected extension URL with ${panel}`
+ );
+ resolve();
+ });
+ });
+
+ // Ask the privileged code to select the last created panel.
+ const done = valid_panels_length === 0;
+ browser.test.sendMessage("select-devtools-panel", done);
+ await oncePanelLoaded;
+ } catch (err) {
+ browser.test.fail(
+ "Unexpected failure on creating a devtools panel with " +
+ `panel url ${panel} and icon ${icon}`
+ );
+ throw err;
+ }
+ }
+ }
+
+ browser.test.sendMessage("test_invalid_devtools_panel_urls_done");
+ });
+
+ browser.test.sendMessage("devtools_page_ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ icons: {
+ 32: "icon.png",
+ },
+ },
+ files: {
+ "devtools_page.html": createPage("devtools_page.js"),
+ "devtools_page.js": devtools_page,
+ "panel.html": createPage("panel.js", "DEVTOOLS PANEL"),
+ "panel.js": "",
+ "icon.png": imageBuffer,
+ "default-icon.png": imageBuffer,
+ },
+ });
+
+ await extension.startup();
+
+ let toolbox = await openToolboxForTab(tab);
+ info("developer toolbox opened");
+
+ await extension.awaitMessage("devtools_page_ready");
+
+ extension.sendMessage("start_test_panel_create");
+
+ let done = false;
+
+ while (!done) {
+ info("Waiting test extension request to select the last created panel");
+ done = await extension.awaitMessage("select-devtools-panel");
+
+ const toolboxAdditionalTools = toolbox.getAdditionalTools();
+ const lastTool = toolboxAdditionalTools[toolboxAdditionalTools.length - 1];
+
+ gDevTools.showToolboxForTab(tab, { toolId: lastTool.id });
+ info("Last created panel selected");
+ }
+
+ await extension.awaitMessage("test_invalid_devtools_panel_urls_done");
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_panels_elements.js b/browser/components/extensions/test/browser/browser_ext_devtools_panels_elements.js
new file mode 100644
index 0000000000..37aa44cfc1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_panels_elements.js
@@ -0,0 +1,124 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+add_task(async function test_devtools_panels_elements_onSelectionChanged() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ function devtools_page() {
+ browser.devtools.panels.elements.onSelectionChanged.addListener(
+ async () => {
+ const [evalResult, exceptionInfo] =
+ await browser.devtools.inspectedWindow.eval("$0 && $0.tagName");
+
+ if (exceptionInfo) {
+ browser.test.fail(
+ "Unexpected exceptionInfo on inspectedWindow.eval: " +
+ JSON.stringify(exceptionInfo)
+ );
+ }
+
+ browser.test.sendMessage("devtools_eval_result", evalResult);
+ }
+ );
+
+ browser.test.onMessage.addListener(msg => {
+ switch (msg) {
+ case "inspectedWindow_reload": {
+ // Force a reload to test that the expected onSelectionChanged events are sent
+ // while the page is navigating and once it has been fully reloaded.
+ browser.devtools.inspectedWindow.eval("window.location.reload();");
+ break;
+ }
+
+ default: {
+ browser.test.fail(`Received unexpected test.onMesssage: ${msg}`);
+ }
+ }
+ });
+
+ browser.test.sendMessage("devtools_page_loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ },
+ });
+
+ await extension.startup();
+
+ const toolbox = await openToolboxForTab(tab);
+
+ await extension.awaitMessage("devtools_page_loaded");
+
+ await toolbox.selectTool("inspector");
+
+ const inspector = toolbox.getPanel("inspector");
+
+ info(
+ "Waiting for the first onSelectionChanged event to be fired once the inspector is open"
+ );
+
+ const evalResult = await extension.awaitMessage("devtools_eval_result");
+ is(
+ evalResult,
+ "BODY",
+ "Got the expected onSelectionChanged once the inspector is selected"
+ );
+
+ // Reload the inspected tab and wait for the inspector markup view to have been
+ // fully reloaded.
+ const onceMarkupReloaded = inspector.once("markuploaded");
+ extension.sendMessage("inspectedWindow_reload");
+ await onceMarkupReloaded;
+
+ info(
+ "Waiting for the two onSelectionChanged events fired before and after the navigation"
+ );
+
+ // Expect the eval result to be undefined on the first onSelectionChanged event
+ // (fired when the page is navigating away, and so the current selection is undefined).
+ const evalResultNavigating = await extension.awaitMessage(
+ "devtools_eval_result"
+ );
+ is(
+ evalResultNavigating,
+ undefined,
+ "Got the expected onSelectionChanged once the tab is navigating"
+ );
+
+ // Expect the eval result to be related to the body element on the second onSelectionChanged
+ // event (fired when the page have been navigated to the new page).
+ const evalResultOnceMarkupReloaded = await extension.awaitMessage(
+ "devtools_eval_result"
+ );
+ is(
+ evalResultOnceMarkupReloaded,
+ "BODY",
+ "Got the expected onSelectionChanged once the tab has been completely reloaded"
+ );
+
+ await closeToolboxForTab(tab);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js b/browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js
new file mode 100644
index 0000000000..0abc8c44e7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js
@@ -0,0 +1,323 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals getExtensionSidebarActors, expectNoSuchActorIDs, testSetExpressionSidebarPanel */
+
+// Import the shared test helpers from the related devtools tests.
+loadTestSubscript("head_devtools.js");
+loadTestSubscript("head_devtools_inspector_sidebar.js");
+
+function isActiveSidebarTabTitle(inspector, expectedTabTitle, message) {
+ const actualTabTitle = inspector.panelDoc.querySelector(
+ "#inspector-sidebar .tabs-menu-item.is-active"
+ ).innerText;
+ is(actualTabTitle, expectedTabTitle, message);
+}
+
+function testSetObjectSidebarPanel(panel, expectedCellType, expectedTitle) {
+ is(
+ panel.querySelectorAll("table.treeTable").length,
+ 1,
+ "The sidebar panel contains a rendered TreeView component"
+ );
+
+ is(
+ panel.querySelectorAll(`table.treeTable .${expectedCellType}Cell`).length,
+ 1,
+ `The TreeView component contains the expected a cell of type ${expectedCellType}`
+ );
+
+ if (expectedTitle) {
+ const panelTree = panel.querySelector("table.treeTable");
+ ok(
+ panelTree.innerText.includes(expectedTitle),
+ "The optional root object title has been included in the object tree"
+ );
+ }
+}
+
+async function testSidebarPanelSelect(extension, inspector, tabId, expected) {
+ const { sidebarShown, sidebarHidden, activeSidebarTabTitle } = expected;
+
+ inspector.sidebar.show(tabId);
+
+ const shown = await extension.awaitMessage("devtools_sidebar_shown");
+ is(
+ shown,
+ sidebarShown,
+ "Got the shown event on the second extension sidebar"
+ );
+
+ if (sidebarHidden) {
+ const hidden = await extension.awaitMessage("devtools_sidebar_hidden");
+ is(
+ hidden,
+ sidebarHidden,
+ "Got the hidden event on the first extension sidebar"
+ );
+ }
+
+ isActiveSidebarTabTitle(
+ inspector,
+ activeSidebarTabTitle,
+ "Got the expected title on the active sidebar tab"
+ );
+}
+
+add_task(async function test_devtools_panels_elements_sidebar() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ async function devtools_page() {
+ const sidebar1 = await browser.devtools.panels.elements.createSidebarPane(
+ "Test Sidebar 1"
+ );
+ const sidebar2 = await browser.devtools.panels.elements.createSidebarPane(
+ "Test Sidebar 2"
+ );
+ const sidebar3 = await browser.devtools.panels.elements.createSidebarPane(
+ "Test Sidebar 3"
+ );
+ const sidebar4 = await browser.devtools.panels.elements.createSidebarPane(
+ "Test Sidebar 4"
+ );
+
+ const onShownListener = (event, sidebarInstance) => {
+ browser.test.sendMessage(`devtools_sidebar_${event}`, sidebarInstance);
+ };
+
+ sidebar1.onShown.addListener(() => onShownListener("shown", "sidebar1"));
+ sidebar2.onShown.addListener(() => onShownListener("shown", "sidebar2"));
+ sidebar3.onShown.addListener(() => onShownListener("shown", "sidebar3"));
+ sidebar4.onShown.addListener(() => onShownListener("shown", "sidebar4"));
+
+ sidebar1.onHidden.addListener(() => onShownListener("hidden", "sidebar1"));
+ sidebar2.onHidden.addListener(() => onShownListener("hidden", "sidebar2"));
+ sidebar3.onHidden.addListener(() => onShownListener("hidden", "sidebar3"));
+ sidebar4.onHidden.addListener(() => onShownListener("hidden", "sidebar4"));
+
+ // Refresh the sidebar content on every inspector selection.
+ browser.devtools.panels.elements.onSelectionChanged.addListener(() => {
+ const expression = `
+ var obj = Object.create(null);
+ obj.prop1 = 123;
+ obj[Symbol('sym1')] = 456;
+ obj.cyclic = obj;
+ obj;
+ `;
+ sidebar1.setExpression(expression, "sidebar.setExpression rootTitle");
+ });
+
+ sidebar2.setObject({ anotherPropertyName: 123 });
+ sidebar3.setObject(
+ { propertyName: "propertyValue" },
+ "Optional Root Object Title"
+ );
+
+ sidebar4.setPage("sidebar.html");
+
+ browser.test.sendMessage("devtools_page_loaded");
+ }
+
+ function sidebar() {
+ browser.test.sendMessage("sidebar-loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `
+
+
+
+
+
+
+
+ `,
+ "devtools_page.js": devtools_page,
+ "sidebar.html": `
+
+
+
+
+
+ sidebar panel
+
+
+ `,
+ "sidebar.js": sidebar,
+ },
+ });
+
+ await extension.startup();
+
+ const toolbox = await openToolboxForTab(tab);
+
+ await extension.awaitMessage("devtools_page_loaded");
+
+ const waitInspector = toolbox.once("inspector-selected");
+ toolbox.selectTool("inspector");
+ await waitInspector;
+
+ const sidebarIds = Array.from(toolbox._inspectorExtensionSidebars.keys());
+
+ const inspector = await toolbox.getPanel("inspector");
+
+ info("Test extension inspector sidebar 1 (sidebar.setExpression)");
+
+ inspector.sidebar.show(sidebarIds[0]);
+
+ const shownSidebarInstance = await extension.awaitMessage(
+ "devtools_sidebar_shown"
+ );
+
+ is(
+ shownSidebarInstance,
+ "sidebar1",
+ "Got the shown event on the first extension sidebar"
+ );
+
+ isActiveSidebarTabTitle(
+ inspector,
+ "Test Sidebar 1",
+ "Got the expected title on the active sidebar tab"
+ );
+
+ const sidebarPanel1 = inspector.sidebar.getTabPanel(sidebarIds[0]);
+
+ ok(
+ sidebarPanel1,
+ "Got a rendered sidebar panel for the first registered extension sidebar"
+ );
+
+ info("Waiting for the first panel to be rendered");
+
+ // Verify that the panel contains an ObjectInspector, with the expected number of nodes
+ // and with the expected property names.
+ await testSetExpressionSidebarPanel(sidebarPanel1, {
+ nodesLength: 4,
+ propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"],
+ rootTitle: "sidebar.setExpression rootTitle",
+ });
+
+ // Retrieve the actors currently rendered into the extension sidebars.
+ const actors = getExtensionSidebarActors(inspector);
+
+ info(
+ "Test extension inspector sidebar 2 (sidebar.setObject without a root title)"
+ );
+
+ await testSidebarPanelSelect(extension, inspector, sidebarIds[1], {
+ sidebarShown: "sidebar2",
+ sidebarHidden: "sidebar1",
+ activeSidebarTabTitle: "Test Sidebar 2",
+ });
+
+ const sidebarPanel2 = inspector.sidebar.getTabPanel(sidebarIds[1]);
+
+ ok(
+ sidebarPanel2,
+ "Got a rendered sidebar panel for the second registered extension sidebar"
+ );
+
+ testSetObjectSidebarPanel(sidebarPanel2, "number");
+
+ info(
+ "Test extension inspector sidebar 3 (sidebar.setObject with a root title)"
+ );
+
+ await testSidebarPanelSelect(extension, inspector, sidebarIds[2], {
+ sidebarShown: "sidebar3",
+ sidebarHidden: "sidebar2",
+ activeSidebarTabTitle: "Test Sidebar 3",
+ });
+
+ const sidebarPanel3 = inspector.sidebar.getTabPanel(sidebarIds[2]);
+
+ ok(
+ sidebarPanel3,
+ "Got a rendered sidebar panel for the third registered extension sidebar"
+ );
+
+ testSetObjectSidebarPanel(
+ sidebarPanel3,
+ "string",
+ "Optional Root Object Title"
+ );
+
+ info(
+ "Unloading the extension and check that all the sidebar have been removed"
+ );
+
+ inspector.sidebar.show(sidebarIds[3]);
+
+ const shownSidebarInstance4 = await extension.awaitMessage(
+ "devtools_sidebar_shown"
+ );
+ const hiddenSidebarInstance3 = await extension.awaitMessage(
+ "devtools_sidebar_hidden"
+ );
+
+ is(
+ shownSidebarInstance4,
+ "sidebar4",
+ "Got the shown event on the third extension sidebar"
+ );
+ is(
+ hiddenSidebarInstance3,
+ "sidebar3",
+ "Got the hidden event on the second extension sidebar"
+ );
+
+ isActiveSidebarTabTitle(
+ inspector,
+ "Test Sidebar 4",
+ "Got the expected title on the active sidebar tab"
+ );
+
+ await extension.awaitMessage("sidebar-loaded");
+
+ await extension.unload();
+
+ is(
+ Array.from(toolbox._inspectorExtensionSidebars.keys()).length,
+ 0,
+ "All the registered sidebars have been unregistered on extension unload"
+ );
+
+ is(
+ inspector.sidebar.getTabPanel(sidebarIds[0]),
+ null,
+ "The first registered sidebar has been removed"
+ );
+
+ is(
+ inspector.sidebar.getTabPanel(sidebarIds[1]),
+ null,
+ "The second registered sidebar has been removed"
+ );
+
+ is(
+ inspector.sidebar.getTabPanel(sidebarIds[2]),
+ null,
+ "The third registered sidebar has been removed"
+ );
+
+ is(
+ inspector.sidebar.getTabPanel(sidebarIds[3]),
+ null,
+ "The fourth registered sidebar has been removed"
+ );
+
+ await expectNoSuchActorIDs(toolbox.target.client, actors);
+
+ await closeToolboxForTab(tab);
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_find.js b/browser/components/extensions/test/browser/browser_ext_find.js
new file mode 100644
index 0000000000..c8a4c10bd8
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_find.js
@@ -0,0 +1,468 @@
+/* global browser */
+"use strict";
+
+function frameScript() {
+ let docShell = content.docShell;
+ let controller = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+ let selection = controller.getSelection(controller.SELECTION_FIND);
+ if (!selection.rangeCount) {
+ return {
+ text: "",
+ };
+ }
+
+ let range = selection.getRangeAt(0);
+ const { FindContent } = ChromeUtils.importESModule(
+ "resource://gre/modules/FindContent.sys.mjs"
+ );
+ let highlighter = new FindContent(docShell).highlighter;
+ let r1 = content.parent.frameElement.getBoundingClientRect();
+ let f1 = highlighter._getFrameElementOffsets(content.parent);
+ let r2 = content.frameElement.getBoundingClientRect();
+ let f2 = highlighter._getFrameElementOffsets(content);
+ let r3 = range.getBoundingClientRect();
+ let rect = {
+ top: r1.top + r2.top + r3.top + f1.y + f2.y,
+ left: r1.left + r2.left + r3.left + f1.x + f2.x,
+ };
+ return {
+ text: selection.toString(),
+ rect,
+ };
+}
+
+add_task(async function testFind() {
+ async function background() {
+ function awaitLoad(tabId, url) {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId_,
+ changed,
+ tab
+ ) {
+ if (
+ tabId == tabId_ &&
+ changed.status == "complete" &&
+ tab.url == url
+ ) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ }
+
+ let url =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_find_frames.html";
+ let tab = await browser.tabs.update({ url });
+ await awaitLoad(tab.id, url);
+
+ let data = await browser.find.find("banana", { includeRangeData: true });
+ let rangeData = data.rangeData;
+
+ browser.test.log("Test that `data.count` is the expected value.");
+ browser.test.assertEq(
+ 6,
+ data.count,
+ "The value returned from `data.count`"
+ );
+
+ browser.test.log("Test that `rangeData` has the proper number of values.");
+ browser.test.assertEq(
+ 6,
+ rangeData.length,
+ "The number of values held in `rangeData`"
+ );
+
+ browser.test.log(
+ "Test that the text found in the top window and nested frames corresponds to the proper position."
+ );
+ let terms = ["Bánana", "bAnana", "baNana", "banAna", "banaNa", "bananA"];
+ for (let i = 0; i < terms.length; i++) {
+ browser.test.assertEq(
+ terms[i],
+ rangeData[i].text,
+ `The text at range position ${i}:`
+ );
+ }
+
+ browser.test.log("Test that case sensitive match works properly.");
+ data = await browser.find.find("baNana", {
+ caseSensitive: true,
+ includeRangeData: true,
+ });
+ browser.test.assertEq(1, data.count, "The number of matches found:");
+ browser.test.assertEq("baNana", data.rangeData[0].text, "The text found:");
+
+ browser.test.log("Test that diacritic sensitive match works properly.");
+ data = await browser.find.find("bánana", {
+ matchDiacritics: true,
+ includeRangeData: true,
+ });
+ browser.test.assertEq(1, data.count, "The number of matches found:");
+ browser.test.assertEq("Bánana", data.rangeData[0].text, "The text found:");
+
+ browser.test.log("Test that case insensitive match works properly.");
+ data = await browser.find.find("banana", { caseSensitive: false });
+ browser.test.assertEq(6, data.count, "The number of matches found:");
+
+ browser.test.log("Test that entire word match works properly.");
+ data = await browser.find.find("banana", { entireWord: true });
+ browser.test.assertEq(
+ 4,
+ data.count,
+ 'The number of matches found, should skip 2 matches, "banaNaland" and "bananAland":'
+ );
+
+ let expectedRangeData = [
+ {
+ framePos: 0,
+ text: "example",
+ startTextNodePos: 16,
+ startOffset: 11,
+ endTextNodePos: 16,
+ endOffset: 18,
+ },
+ {
+ framePos: 0,
+ text: "example",
+ startTextNodePos: 16,
+ startOffset: 25,
+ endTextNodePos: 16,
+ endOffset: 32,
+ },
+ {
+ framePos: 0,
+ text: "example",
+ startTextNodePos: 19,
+ startOffset: 6,
+ endTextNodePos: 19,
+ endOffset: 13,
+ },
+ {
+ framePos: 0,
+ text: "example",
+ startTextNodePos: 21,
+ startOffset: 3,
+ endTextNodePos: 21,
+ endOffset: 10,
+ },
+ {
+ framePos: 1,
+ text: "example",
+ startTextNodePos: 0,
+ startOffset: 0,
+ endTextNodePos: 0,
+ endOffset: 7,
+ },
+ {
+ framePos: 2,
+ text: "example",
+ startTextNodePos: 0,
+ startOffset: 0,
+ endTextNodePos: 0,
+ endOffset: 7,
+ },
+ ];
+
+ browser.test.log(
+ "Test that word found in the same node, different nodes and different frames returns the correct rangeData results."
+ );
+ data = await browser.find.find("example", { includeRangeData: true });
+ for (let i = 0; i < data.rangeData.length; i++) {
+ for (let name in data.rangeData[i]) {
+ browser.test.assertEq(
+ expectedRangeData[i][name],
+ data.rangeData[i][name],
+ `rangeData[${i}].${name}:`
+ );
+ }
+ }
+
+ browser.test.log(
+ "Test that `rangeData` is not returned if `includeRangeData` is false."
+ );
+ data = await browser.find.find("banana", {
+ caseSensitive: false,
+ includeRangeData: false,
+ });
+ browser.test.assertEq(
+ false,
+ !!data.rangeData,
+ "The boolean cast value of `rangeData`:"
+ );
+
+ browser.test.log(
+ "Test that `rectData` is not returned if `includeRectData` is false."
+ );
+ data = await browser.find.find("banana", {
+ caseSensitive: false,
+ includeRectData: false,
+ });
+ browser.test.assertEq(
+ false,
+ !!data.rectData,
+ "The boolean cast value of `rectData`:"
+ );
+
+ browser.test.log(
+ "Test that text spanning multiple inline elements is found."
+ );
+ data = await browser.find.find("fruitcake");
+ browser.test.assertEq(1, data.count, "The number of matches found:");
+
+ browser.test.log(
+ "Test that text spanning multiple block elements is not found."
+ );
+ data = await browser.find.find("angelfood");
+ browser.test.assertEq(0, data.count, "The number of matches found:");
+
+ browser.test.log(
+ "Test that `highlightResults` returns proper status code."
+ );
+ await browser.find.find("banana");
+
+ await browser.test.assertRejects(
+ browser.find.highlightResults({ rangeIndex: 6 }),
+ /index supplied was out of range/,
+ "rejected Promise should pass the expected error"
+ );
+
+ data = await browser.find.find("xyz");
+ await browser.test.assertRejects(
+ browser.find.highlightResults({ rangeIndex: 0 }),
+ /no search results to highlight/,
+ "rejected Promise should pass the expected error"
+ );
+
+ // Test highlightResults without any arguments, especially `rangeIndex`.
+ data = await browser.find.find("example");
+ browser.test.assertEq(6, data.count, "The number of matches found:");
+ await browser.find.highlightResults();
+
+ await browser.find.removeHighlighting();
+
+ data = await browser.find.find("banana", { includeRectData: true });
+ await browser.find.highlightResults({ rangeIndex: 5 });
+
+ browser.test.sendMessage("test:find:WebExtensionFinished", data.rectData);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["find", "tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ let rectData = await extension.awaitMessage("test:find:WebExtensionFinished");
+ let { top, left } = rectData[5].rectsAndTexts.rectList[0];
+ await extension.unload();
+
+ let subFrameBrowsingContext =
+ gBrowser.selectedBrowser.browsingContext.children[0].children[1];
+ let result = await SpecialPowers.spawn(
+ subFrameBrowsingContext,
+ [],
+ frameScript
+ );
+
+ info("Test that text was highlighted properly.");
+ is(
+ result.text,
+ "bananA",
+ `The text that was highlighted: - Expected: bananA, Actual: ${result.text}`
+ );
+
+ info(
+ "Test that rectangle data returned from the search matches the highlighted result."
+ );
+ is(
+ result.rect.top,
+ top,
+ `rect.top: - Expected: ${result.rect.top}, Actual: ${top}`
+ );
+ is(
+ result.rect.left,
+ left,
+ `rect.left: - Expected: ${result.rect.left}, Actual: ${left}`
+ );
+});
+
+add_task(async function testRemoveHighlighting() {
+ async function background() {
+ function awaitLoad(tabId, url) {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId_,
+ changed,
+ tab
+ ) {
+ if (
+ tabId == tabId_ &&
+ changed.status == "complete" &&
+ tab.url == url
+ ) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ }
+
+ let url =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_find_frames.html";
+ let tab = await browser.tabs.update({ url });
+ await awaitLoad(tab.id, url);
+
+ let data = await browser.find.find("banana", { includeRangeData: true });
+
+ browser.test.log("Test that `data.count` is the expected value.");
+ browser.test.assertEq(
+ 6,
+ data.count,
+ "The value returned from `data.count`"
+ );
+
+ await browser.find.highlightResults({ rangeIndex: 5 });
+
+ browser.find.removeHighlighting();
+
+ browser.test.sendMessage("test:find:WebExtensionFinished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["find", "tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("test:find:WebExtensionFinished");
+ await extension.unload();
+
+ let subFrameBrowsingContext =
+ gBrowser.selectedBrowser.browsingContext.children[0].children[1];
+ let result = await SpecialPowers.spawn(
+ subFrameBrowsingContext,
+ [],
+ frameScript
+ );
+
+ info("Test that highlight was cleared properly.");
+ is(
+ result.text,
+ "",
+ `The text that was highlighted: - Expected: '', Actual: ${result.text}`
+ );
+});
+
+add_task(async function testAboutFind() {
+ async function background() {
+ await browser.test.assertRejects(
+ browser.find.find("banana"),
+ /Unable to search:/,
+ "Should not be able to search about tabs"
+ );
+
+ browser.test.sendMessage("done");
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["find", "tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testIncognitoFind() {
+ async function background() {
+ await browser.test.assertRejects(
+ browser.find.find("banana"),
+ /Unable to search:/,
+ "Should not be able to search private window"
+ );
+ await browser.test.assertRejects(
+ browser.find.highlightResults(),
+ /Unable to search:/,
+ "Should not be able to highlight in private window"
+ );
+ await browser.test.assertRejects(
+ browser.find.removeHighlighting(),
+ /Invalid tab ID:/,
+ "Should not be able to remove highlight in private window"
+ );
+
+ browser.test.sendMessage("done");
+ }
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ BrowserTestUtils.startLoadingURIString(
+ privateWin.gBrowser.selectedBrowser,
+ "http://example.com"
+ );
+ await BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["find", "tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function testIncognitoFindAllowed() {
+ // We're only testing we can make the calls in a private window,
+ // testFind above tests full functionality.
+ async function background() {
+ await browser.find.find("banana");
+ await browser.find.highlightResults({ rangeIndex: 0 });
+ await browser.find.removeHighlighting();
+
+ browser.test.sendMessage("done");
+ }
+
+ let url =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_find_frames.html";
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ BrowserTestUtils.startLoadingURIString(
+ privateWin.gBrowser.selectedBrowser,
+ url
+ );
+ await BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["find", "tabs"],
+ },
+ background,
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_getViews.js b/browser/components/extensions/test/browser/browser_ext_getViews.js
new file mode 100644
index 0000000000..1af190e753
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_getViews.js
@@ -0,0 +1,439 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function genericChecker() {
+ let kind = window.location.search.slice(1) || "background";
+ window.kind = kind;
+
+ let bcGroupId = SpecialPowers.wrap(window).browsingContext.group.id;
+
+ browser.test.onMessage.addListener((msg, ...args) => {
+ if (msg == kind + "-check-views") {
+ let counts = {
+ background: 0,
+ tab: 0,
+ popup: 0,
+ kind: 0,
+ sidebar: 0,
+ };
+ if (kind !== "background") {
+ counts.kind = browser.extension.getViews({ type: kind }).length;
+ }
+ let views = browser.extension.getViews();
+ let background;
+ for (let i = 0; i < views.length; i++) {
+ let view = views[i];
+ browser.test.assertTrue(view.kind in counts, "view type is valid");
+ counts[view.kind]++;
+ if (view.kind == "background") {
+ browser.test.assertTrue(
+ view === browser.extension.getBackgroundPage(),
+ "background page is correct"
+ );
+ background = view;
+ }
+
+ browser.test.assertEq(
+ bcGroupId,
+ SpecialPowers.wrap(view).browsingContext.group.id,
+ "browsing context group is correct"
+ );
+ }
+ if (background) {
+ browser.runtime.getBackgroundPage().then(view => {
+ browser.test.assertEq(
+ background,
+ view,
+ "runtime.getBackgroundPage() is correct"
+ );
+ browser.test.sendMessage("counts", counts);
+ });
+ } else {
+ browser.test.sendMessage("counts", counts);
+ }
+ } else if (msg == kind + "-getViews-with-filter") {
+ let filter = args[0];
+ let count = browser.extension.getViews(filter).length;
+ browser.test.sendMessage("getViews-count", count);
+ } else if (msg == kind + "-open-tab") {
+ let url = browser.runtime.getURL("page.html?tab");
+ browser.tabs
+ .create({ windowId: args[0], url })
+ .then(tab => browser.test.sendMessage("opened-tab", tab.id));
+ } else if (msg == kind + "-close-tab") {
+ browser.tabs.query(
+ {
+ windowId: args[0],
+ },
+ tabs => {
+ let tab = tabs.find(tab => tab.url.includes("page.html?tab"));
+ browser.tabs.remove(tab.id, () => {
+ browser.test.sendMessage("closed");
+ });
+ }
+ );
+ }
+ });
+ browser.test.sendMessage(kind + "-ready");
+}
+
+async function promiseBrowserContentUnloaded(browser) {
+ // Wait until the content has unloaded before resuming the test, to avoid
+ // calling extension.getViews too early (and having intermittent failures).
+ const MSG_WINDOW_DESTROYED = "Test:BrowserContentDestroyed";
+ let unloadPromise = new Promise(resolve => {
+ Services.ppmm.addMessageListener(MSG_WINDOW_DESTROYED, function listener() {
+ Services.ppmm.removeMessageListener(MSG_WINDOW_DESTROYED, listener);
+ resolve();
+ });
+ });
+
+ await ContentTask.spawn(
+ browser,
+ MSG_WINDOW_DESTROYED,
+ MSG_WINDOW_DESTROYED => {
+ let innerWindowId = this.content.windowGlobalChild.innerWindowId;
+ let observer = subject => {
+ if (
+ innerWindowId === subject.QueryInterface(Ci.nsISupportsPRUint64).data
+ ) {
+ Services.obs.removeObserver(observer, "inner-window-destroyed");
+
+ // Use process message manager to ensure that the message is delivered
+ // even after the 's message manager is disconnected.
+ Services.cpmm.sendAsyncMessage(MSG_WINDOW_DESTROYED);
+ }
+ };
+ // Observe inner-window-destroyed, like ExtensionPageChild, to ensure that
+ // the ExtensionPageContextChild instance has been unloaded when we resolve
+ // the unloadPromise.
+ Services.obs.addObserver(observer, "inner-window-destroyed");
+ }
+ );
+
+ // Return an object so that callers can use "await".
+ return { unloadPromise };
+}
+
+add_task(async function () {
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ manifest: {
+ permissions: ["tabs"],
+
+ browser_action: {
+ default_popup: "page.html?popup",
+ default_area: "navbar",
+ },
+
+ sidebar_action: {
+ default_panel: "page.html?sidebar",
+ },
+ },
+
+ files: {
+ "page.html": `
+
+
+
+
+
+
+ `,
+
+ "page.js": genericChecker,
+ },
+
+ background: genericChecker,
+ });
+
+ await Promise.all([
+ extension.startup(),
+ extension.awaitMessage("background-ready"),
+ ]);
+
+ await extension.awaitMessage("sidebar-ready");
+ await extension.awaitMessage("sidebar-ready");
+ await extension.awaitMessage("sidebar-ready");
+
+ info("started");
+
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let winId1 = windowTracker.getId(win1);
+ let winId2 = windowTracker.getId(win2);
+
+ async function openTab(winId) {
+ extension.sendMessage("background-open-tab", winId);
+ await extension.awaitMessage("tab-ready");
+ return extension.awaitMessage("opened-tab");
+ }
+
+ async function checkViews(kind, tabCount, popupCount, kindCount) {
+ extension.sendMessage(kind + "-check-views");
+ let counts = await extension.awaitMessage("counts");
+ if (kind === "sidebar") {
+ // We have 3 sidebars thaat will answer.
+ await extension.awaitMessage("counts");
+ await extension.awaitMessage("counts");
+ }
+ is(counts.background, 1, "background count correct");
+ is(counts.tab, tabCount, "tab count correct");
+ is(counts.popup, popupCount, "popup count correct");
+ is(counts.kind, kindCount, "count for type correct");
+ is(counts.sidebar, 3, "sidebar count is constant");
+ }
+
+ async function checkViewsWithFilter(filter, expectedCount) {
+ extension.sendMessage("background-getViews-with-filter", filter);
+ let count = await extension.awaitMessage("getViews-count");
+ is(count, expectedCount, `count for ${JSON.stringify(filter)} correct`);
+ }
+
+ await checkViews("background", 0, 0, 0);
+ await checkViews("sidebar", 0, 0, 3);
+ await checkViewsWithFilter({ windowId: -1 }, 1);
+ await checkViewsWithFilter({ windowId: 0 }, 0);
+ await checkViewsWithFilter({ tabId: -1 }, 4);
+ await checkViewsWithFilter({ tabId: 0 }, 0);
+
+ let tabId1 = await openTab(winId1);
+
+ await checkViews("background", 1, 0, 0);
+ await checkViews("sidebar", 1, 0, 3);
+ await checkViews("tab", 1, 0, 1);
+ await checkViewsWithFilter({ windowId: winId1 }, 2);
+ await checkViewsWithFilter({ tabId: tabId1 }, 1);
+
+ let tabId2 = await openTab(winId2);
+
+ await checkViews("background", 2, 0, 0);
+ await checkViews("sidebar", 2, 0, 3);
+ await checkViewsWithFilter({ windowId: winId2 }, 2);
+ await checkViewsWithFilter({ tabId: tabId2 }, 1);
+
+ async function triggerPopup(win, callback) {
+ // Window needs focus to open popups.
+ await focusWindow(win);
+ await clickBrowserAction(extension, win);
+ let browser = await awaitExtensionPanel(extension, win);
+
+ await extension.awaitMessage("popup-ready");
+
+ await callback();
+
+ let { unloadPromise } = await promiseBrowserContentUnloaded(browser);
+ closeBrowserAction(extension, win);
+ await unloadPromise;
+ }
+
+ await triggerPopup(win1, async function () {
+ await checkViews("background", 2, 1, 0);
+ await checkViews("sidebar", 2, 1, 3);
+ await checkViews("popup", 2, 1, 1);
+ await checkViewsWithFilter({ windowId: winId1 }, 3);
+ await checkViewsWithFilter({ type: "popup", tabId: -1 }, 1);
+ });
+
+ await triggerPopup(win2, async function () {
+ await checkViews("background", 2, 1, 0);
+ await checkViews("sidebar", 2, 1, 3);
+ await checkViews("popup", 2, 1, 1);
+ await checkViewsWithFilter({ windowId: winId2 }, 3);
+ await checkViewsWithFilter({ type: "popup", tabId: -1 }, 1);
+ });
+
+ info("checking counts after popups");
+
+ await checkViews("background", 2, 0, 0);
+ await checkViews("sidebar", 2, 0, 3);
+ await checkViewsWithFilter({ windowId: winId1 }, 2);
+ await checkViewsWithFilter({ tabId: -1 }, 4);
+
+ info("closing one tab");
+
+ let { unloadPromise } = await promiseBrowserContentUnloaded(
+ win1.gBrowser.selectedBrowser
+ );
+ extension.sendMessage("background-close-tab", winId1);
+ await extension.awaitMessage("closed");
+ await unloadPromise;
+
+ info("one tab closed, one remains");
+
+ await checkViews("background", 1, 0, 0);
+ await checkViews("sidebar", 1, 0, 3);
+
+ info("opening win1 popup");
+
+ await triggerPopup(win1, async function () {
+ await checkViews("background", 1, 1, 0);
+ await checkViews("sidebar", 1, 1, 3);
+ await checkViews("tab", 1, 1, 1);
+ await checkViews("popup", 1, 1, 1);
+ });
+
+ info("opening win2 popup");
+
+ await triggerPopup(win2, async function () {
+ await checkViews("background", 1, 1, 0);
+ await checkViews("sidebar", 1, 1, 3);
+ await checkViews("tab", 1, 1, 1);
+ await checkViews("popup", 1, 1, 1);
+ });
+
+ await checkViews("sidebar", 1, 0, 3);
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
+
+add_task(async function test_getViews_excludes_blocked_parsing_documents() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "popup.html": `
+
+ ExtensionPopup
+ `,
+ "popup.js": function () {
+ browser.test.sendMessage(
+ "browserActionPopup:loaded",
+ window.location.href
+ );
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("getViews", msg, "Got the expected test message");
+ const views = browser.extension
+ .getViews()
+ .map(win => win.location?.href);
+
+ browser.test.sendMessage("getViews:done", views);
+ });
+ browser.test.sendMessage("bgpage:loaded", window.location.href);
+ },
+ });
+
+ await extension.startup();
+ const bgpageURL = await extension.awaitMessage("bgpage:loaded");
+ extension.sendMessage("getViews");
+ Assert.deepEqual(
+ await extension.awaitMessage("getViews:done"),
+ [bgpageURL],
+ "Expect only the background page to be initially listed in getViews"
+ );
+
+ const {
+ Management: {
+ global: { browserActionFor },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let ext = WebExtensionPolicy.getByID(extension.id)?.extension;
+ let browserAction = browserActionFor(ext);
+
+ // Ensure the mouse is not initially hovering the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ window.gURLBar.textbox,
+ { type: "mouseover" },
+ window
+ );
+
+ let widget = await TestUtils.waitForCondition(
+ () => getBrowserActionWidget(extension).forWindow(window),
+ "Wait for browserAction widget"
+ );
+
+ await TestUtils.waitForCondition(
+ () => !browserAction.pendingPopup,
+ "Wait for no pending preloaded popup"
+ );
+
+ await TestUtils.waitForCondition(async () => {
+ // Trigger preload browserAction popup (by directly dispatching a MouseEvent
+ // to prevent intermittent failures that where often triggered in macos
+ // PGO builds when this was using EventUtils.synthesizeMouseAtCenter).
+ let mouseOverEvent = new MouseEvent("mouseover");
+ widget.node.firstElementChild.dispatchEvent(mouseOverEvent);
+
+ await TestUtils.waitForCondition(
+ () => browserAction.pendingPopup?.browser,
+ "Wait for pending preloaded popup browser"
+ );
+
+ return SpecialPowers.spawn(
+ browserAction.pendingPopup.browser,
+ [],
+ async () => {
+ const policy = this.content.WebExtensionPolicy.getByHostname(
+ this.content.location.hostname
+ );
+ return policy?.weakExtension
+ ?.get()
+ ?.blockedParsingDocuments.has(this.content.document);
+ }
+ ).catch(err => {
+ // Tolerate errors triggered by SpecialPowers.spawn
+ // being aborted before we got a result back.
+ if (err.name === "AbortError") {
+ return false;
+ }
+ throw err;
+ });
+ }, "Wait for preload browserAction document to be blocked on parsing");
+
+ extension.sendMessage("getViews");
+ Assert.deepEqual(
+ await extension.awaitMessage("getViews:done"),
+ [bgpageURL],
+ "Expect preloaded browserAction popup to not be listed in getViews"
+ );
+
+ // Test browserAction popup is listed in getViews once document parser is unblocked.
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mousedown", button: 0 },
+ window
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseup", button: 0 },
+ window
+ );
+
+ const popupURL = await extension.awaitMessage("browserActionPopup:loaded");
+
+ extension.sendMessage("getViews");
+ Assert.deepEqual(
+ (await extension.awaitMessage("getViews:done")).sort(),
+ [bgpageURL, popupURL].sort(),
+ "Expect loaded browserAction popup to be listed in getViews"
+ );
+
+ // Ensure the mouse is not hovering the browserAction widget anymore when exiting the test case.
+ EventUtils.synthesizeMouseAtCenter(
+ window.gURLBar.textbox,
+ { type: "mouseover", button: 0 },
+ window
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_history_redirect.js b/browser/components/extensions/test/browser/browser_ext_history_redirect.js
new file mode 100644
index 0000000000..bbce498887
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_history_redirect.js
@@ -0,0 +1,72 @@
+"use strict";
+
+const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser";
+const REDIRECT_URL = BASE + "/redirection.sjs";
+const REDIRECTED_URL = BASE + "/dummy_page.html";
+
+add_task(async function history_redirect() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, url) => {
+ switch (msg) {
+ case "delete-all": {
+ let results = await browser.history.deleteAll();
+ browser.test.sendMessage("delete-all-result", results);
+ break;
+ }
+ case "search": {
+ let results = await browser.history.search({
+ text: url,
+ startTime: new Date(0),
+ });
+ browser.test.sendMessage("search-result", results);
+ break;
+ }
+ case "get-visits": {
+ let results = await browser.history.getVisits({ url });
+ browser.test.sendMessage("get-visits-result", results);
+ break;
+ }
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["history"],
+ },
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ info("extension loaded");
+
+ extension.sendMessage("delete-all");
+ await extension.awaitMessage("delete-all-result");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: REDIRECT_URL },
+ async browser => {
+ is(
+ browser.currentURI.spec,
+ REDIRECTED_URL,
+ "redirected to the expected location"
+ );
+
+ extension.sendMessage("search", REDIRECT_URL);
+ let results = await extension.awaitMessage("search-result");
+ is(results.length, 1, "search returned expected length of results");
+
+ extension.sendMessage("get-visits", REDIRECT_URL);
+ let visits = await extension.awaitMessage("get-visits-result");
+ is(visits.length, 1, "getVisits returned expected length of visits");
+ }
+ );
+
+ await extension.unload();
+ info("extension unloaded");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_identity_indication.js b/browser/components/extensions/test/browser/browser_ext_identity_indication.js
new file mode 100644
index 0000000000..cae90fd2d0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_identity_indication.js
@@ -0,0 +1,141 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function confirmDefaults() {
+ if (gURLBar.searchButton) {
+ is(
+ getComputedStyle(document.getElementById("identity-box")).display,
+ "none",
+ "Identity box should be hidden"
+ );
+ } else {
+ is(
+ getComputedStyle(document.getElementById("identity-icon")).listStyleImage,
+ 'url("chrome://global/skin/icons/search-glass.svg")',
+ "Identity icon should be the search icon"
+ );
+ }
+
+ let label = document.getElementById("identity-icon-label");
+ ok(
+ BrowserTestUtils.isHidden(label),
+ "No label should be used before the extension is started"
+ );
+}
+
+async function waitForIndentityBoxMutation({ expectExtensionIcon }) {
+ const el = document.getElementById("identity-box");
+ await BrowserTestUtils.waitForMutationCondition(
+ el,
+ {
+ attributeFilter: ["class"],
+ },
+ () => el.classList.contains("extensionPage") == expectExtensionIcon
+ );
+}
+
+function confirmExtensionPage() {
+ let identityIconEl = document.getElementById("identity-icon");
+
+ is(
+ getComputedStyle(identityIconEl).listStyleImage,
+ 'url("chrome://mozapps/skin/extensions/extension.svg")',
+ "Identity icon should be the default extension icon"
+ );
+
+ is(
+ identityIconEl.tooltipText,
+ "Loaded by extension: Test Extension",
+ "The correct tooltip should be used"
+ );
+
+ let label = document.getElementById("identity-icon-label");
+ is(
+ label.value,
+ "Extension (Test Extension)",
+ "The correct label should be used"
+ );
+ ok(BrowserTestUtils.isVisible(label), "No label should be visible");
+}
+
+add_task(async function testIdentityIndication() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("url", browser.runtime.getURL("icon.png"));
+ },
+ manifest: {
+ name: "Test Extension",
+ },
+ files: {
+ "icon.png": "",
+ },
+ });
+
+ await extension.startup();
+
+ confirmDefaults();
+
+ let url = await extension.awaitMessage("url");
+
+ const promiseIdentityBoxExtension = waitForIndentityBoxMutation({
+ expectExtensionIcon: true,
+ });
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async function () {
+ await promiseIdentityBoxExtension;
+ confirmExtensionPage();
+ });
+
+ const promiseIdentityBoxDefault = waitForIndentityBoxMutation({
+ expectExtensionIcon: false,
+ });
+ await extension.unload();
+ await promiseIdentityBoxDefault;
+
+ confirmDefaults();
+});
+
+add_task(async function testIdentityIndicationNewTab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("url", browser.runtime.getURL("newtab.html"));
+ },
+ manifest: {
+ name: "Test Extension",
+ browser_specific_settings: {
+ gecko: {
+ id: "@newtab",
+ },
+ },
+ chrome_url_overrides: {
+ newtab: "newtab.html",
+ },
+ },
+ files: {
+ "newtab.html": "New tab! ",
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ confirmDefaults();
+
+ let url = await extension.awaitMessage("url");
+ const promiseIdentityBoxExtension = waitForIndentityBoxMutation({
+ expectExtensionIcon: true,
+ });
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async function () {
+ await promiseIdentityBoxExtension;
+ confirmExtensionPage();
+ is(gURLBar.value, "", "The URL bar is blank");
+ });
+
+ const promiseIdentityBoxDefault = waitForIndentityBoxMutation({
+ expectExtensionIcon: false,
+ });
+ await extension.unload();
+ await promiseIdentityBoxDefault;
+
+ confirmDefaults();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_incognito_popup.js b/browser/components/extensions/test/browser/browser_ext_incognito_popup.js
new file mode 100644
index 0000000000..cbe6e68cdc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_incognito_popup.js
@@ -0,0 +1,209 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testIncognitoPopup() {
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs"],
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ page_action: {
+ default_popup: "popup.html",
+ },
+ },
+
+ background: async function () {
+ let resolveMessage;
+ browser.runtime.onMessage.addListener(msg => {
+ if (resolveMessage && msg.message == "popup-details") {
+ resolveMessage(msg);
+ }
+ });
+
+ const awaitPopup = windowId => {
+ return new Promise(resolve => {
+ resolveMessage = resolve;
+ }).then(msg => {
+ browser.test.assertEq(
+ windowId,
+ msg.windowId,
+ "Got popup message from correct window"
+ );
+ return msg;
+ });
+ };
+
+ const testWindow = async window => {
+ const [tab] = await browser.tabs.query({
+ active: true,
+ windowId: window.id,
+ });
+
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("click-pageAction");
+
+ let msg = await awaitPopup(window.id);
+ browser.test.assertEq(
+ window.incognito,
+ msg.incognito,
+ "Correct incognito status in pageAction popup"
+ );
+
+ browser.test.sendMessage("click-browserAction");
+
+ msg = await awaitPopup(window.id);
+ browser.test.assertEq(
+ window.incognito,
+ msg.incognito,
+ "Correct incognito status in browserAction popup"
+ );
+ };
+
+ const testNonPrivateWindow = async () => {
+ const window = await browser.windows.getCurrent();
+ await testWindow(window);
+ };
+
+ const testPrivateWindow = async () => {
+ const URL = "https://example.com/incognito";
+ const windowReady = new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId,
+ changed,
+ tab
+ ) {
+ if (changed.status == "complete" && tab.url == URL) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+
+ const window = await browser.windows.create({
+ incognito: true,
+ url: URL,
+ });
+ await windowReady;
+
+ await testWindow(window);
+ };
+
+ browser.test.onMessage.addListener(async msg => {
+ switch (msg) {
+ case "test-nonprivate-window":
+ await testNonPrivateWindow();
+ break;
+ case "test-private-window":
+ await testPrivateWindow();
+ break;
+ default:
+ browser.test.fail(
+ `Unexpected test message: ${JSON.stringify(msg)}`
+ );
+ }
+
+ browser.test.sendMessage(`${msg}:done`);
+ });
+
+ browser.test.sendMessage("bgscript:ready");
+ },
+
+ files: {
+ "popup.html":
+ ' ',
+
+ "popup.js": async function () {
+ let win = await browser.windows.getCurrent();
+ browser.runtime.sendMessage({
+ message: "popup-details",
+ windowId: win.id,
+ incognito: browser.extension.inIncognitoContext,
+ });
+ window.close();
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bgscript:ready");
+
+ info("Run test on non private window");
+ extension.sendMessage("test-nonprivate-window");
+ await extension.awaitMessage("click-pageAction");
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ ok(!PrivateBrowsingUtils.isWindowPrivate(win), "Got a nonprivate window");
+ await clickPageAction(extension, win);
+
+ await extension.awaitMessage("click-browserAction");
+ await clickBrowserAction(extension, win);
+
+ await extension.awaitMessage("test-nonprivate-window:done");
+ await closeBrowserAction(extension, win);
+ await closePageAction(extension, win);
+
+ info("Run test on private window");
+ extension.sendMessage("test-private-window");
+ await extension.awaitMessage("click-pageAction");
+ const privateWin = Services.wm.getMostRecentWindow("navigator:browser");
+ ok(PrivateBrowsingUtils.isWindowPrivate(privateWin), "Got a private window");
+ await clickPageAction(extension, privateWin);
+
+ await extension.awaitMessage("click-browserAction");
+ await clickBrowserAction(extension, privateWin);
+
+ await extension.awaitMessage("test-private-window:done");
+ // Wait for the private window chrome document to be flushed before
+ // closing the browserACtion, pageAction and the entire private window,
+ // to prevent intermittent failures.
+ await privateWin.promiseDocumentFlushed(() => {});
+
+ await closeBrowserAction(extension, privateWin);
+ await closePageAction(extension, privateWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ await extension.unload();
+});
+
+add_task(async function test_pageAction_incognito_not_allowed() {
+ const URL = "https://example.com/";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["*://example.com/*"],
+ page_action: {
+ show_matches: [""],
+ pinned: true,
+ },
+ },
+ });
+
+ await extension.startup();
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ URL,
+ true,
+ true
+ );
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await BrowserTestUtils.openNewForegroundTab(
+ privateWindow.gBrowser,
+ URL,
+ true,
+ true
+ );
+
+ let elem = await getPageActionButton(extension, window);
+ ok(elem, "pageAction button state correct in non-PB");
+
+ elem = await getPageActionButton(extension, privateWindow);
+ ok(!elem, "pageAction button state correct in private window");
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_incognito_views.js b/browser/components/extensions/test/browser/browser_ext_incognito_views.js
new file mode 100644
index 0000000000..8a7c3219b3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_incognito_views.js
@@ -0,0 +1,269 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.openPopupWithoutUserGesture.enabled", true]],
+ });
+});
+
+add_task(async function testIncognitoViews() {
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mouseover" },
+ window
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs"],
+ description: JSON.stringify({
+ headless: Services.env.get("MOZ_HEADLESS"),
+ debug: AppConstants.DEBUG,
+ }),
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+
+ background: async function () {
+ window.isBackgroundPage = true;
+ const { headless, debug } = JSON.parse(
+ browser.runtime.getManifest().description
+ );
+
+ class ConnectedPopup {
+ #msgPromise;
+ #disconnectPromise;
+
+ static promiseNewPopup() {
+ return new Promise(resolvePort => {
+ browser.runtime.onConnect.addListener(function onConnect(port) {
+ browser.runtime.onConnect.removeListener(onConnect);
+ browser.test.assertEq("from-popup", port.name, "Port from popup");
+ resolvePort(new ConnectedPopup(port));
+ });
+ });
+ }
+
+ constructor(port) {
+ this.port = port;
+ this.#msgPromise = new Promise(resolveMessage => {
+ // popup.js sends one message with the popup's metadata.
+ port.onMessage.addListener(resolveMessage);
+ });
+ this.#disconnectPromise = new Promise(resolveDisconnect => {
+ port.onDisconnect.addListener(resolveDisconnect);
+ });
+ }
+
+ async getPromisedMessage() {
+ browser.test.log("Waiting for popup to send information");
+ let msg = await this.#msgPromise;
+ browser.test.assertEq("popup-details", msg.message, "Got port msg");
+ return msg;
+ }
+
+ async promisePopupClosed(desc) {
+ browser.test.log(`Waiting for popup to be closed (${desc})`);
+ // There is currently no great way for extension to detect a closed
+ // popup. Extensions can observe the port.onDisconnect event for now.
+ await this.#disconnectPromise;
+ browser.test.log(`Popup was closed (${desc})`);
+ }
+
+ async closePopup(desc) {
+ browser.test.log(`Closing popup (${desc})`);
+ this.port.postMessage("close_popup");
+ return this.promisePopupClosed(desc);
+ }
+ }
+
+ let testPopupForWindow = async window => {
+ let popupConnectionPromise = ConnectedPopup.promiseNewPopup();
+
+ await browser.browserAction.openPopup({
+ windowId: window.id,
+ });
+
+ let connectedPopup = await popupConnectionPromise;
+ let msg = await connectedPopup.getPromisedMessage();
+ browser.test.assertEq(
+ window.id,
+ msg.windowId,
+ "Got popup message from correct window"
+ );
+ browser.test.assertEq(
+ window.incognito,
+ msg.incognito,
+ "Correct incognito status in browserAction popup"
+ );
+
+ return connectedPopup;
+ };
+
+ async function createPrivateWindow() {
+ const URL = "https://example.com/?dummy-incognito-window";
+ let windowReady = new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function l(tabId, changed, tab) {
+ if (changed.status == "complete" && tab.url == URL) {
+ browser.tabs.onUpdated.removeListener(l);
+ resolve();
+ }
+ });
+ });
+ let window = await browser.windows.create({
+ incognito: true,
+ url: URL,
+ });
+ await windowReady;
+ return window;
+ }
+
+ function getNonPrivateViewCount() {
+ // The background context is in non-private browsing mode, so getViews()
+ // only returns the views that are not in private browsing mode.
+ return browser.extension.getViews({ type: "popup" }).length;
+ }
+
+ try {
+ let nonPrivatePopup;
+ {
+ let window = await browser.windows.getCurrent();
+
+ nonPrivatePopup = await testPopupForWindow(window);
+
+ browser.test.assertEq(1, getNonPrivateViewCount(), "popup is open");
+ // ^ The popup will close when a new window is opened below.
+ if (headless) {
+ // ... except when --headless is used. For some reason, the popup
+ // does not close when another window is opened. Close manually.
+ await nonPrivatePopup.closePopup("Work-around for --headless bug");
+ }
+ }
+
+ {
+ let window = await createPrivateWindow();
+
+ let privatePopup = await testPopupForWindow(window);
+
+ await nonPrivatePopup.promisePopupClosed("First popup closed by now");
+
+ browser.test.assertEq(
+ 0,
+ getNonPrivateViewCount(),
+ "First popup should have been closed when a new window was opened"
+ );
+
+ // TODO bug 1809000: On debug builds, a memory leak is reported when
+ // the popup is closed as part of closing a window. As a work-around,
+ // we explicitly close the popup here.
+ // TODO: Remove when bug 1809000 is fixed.
+ if (debug) {
+ await privatePopup.closePopup("Work-around for bug 1809000");
+ }
+
+ await browser.windows.remove(window.id);
+ // ^ This also closes the popup panel associated with the window. If
+ // it somehow does not close properly, errors may be reported, e.g.
+ // leakcheck failures in debug mode (like bug 1800100).
+
+ // This check is not strictly necessary, but we're doing this to
+ // confirm that the private popup has indeed been closed.
+ await privatePopup.promisePopupClosed("Window closed = popup gone");
+ }
+
+ browser.test.notifyPass("incognito-views");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("incognito-views");
+ }
+ },
+
+ files: {
+ "popup.html":
+ ' ',
+
+ "popup.js": async function () {
+ let views = browser.extension.getViews();
+
+ if (browser.extension.inIncognitoContext) {
+ let bgPage = browser.extension.getBackgroundPage();
+ browser.test.assertEq(
+ null,
+ bgPage,
+ "Should not be able to access background page in incognito context"
+ );
+
+ bgPage = await browser.runtime.getBackgroundPage();
+ browser.test.assertEq(
+ null,
+ bgPage,
+ "Should not be able to access background page in incognito context"
+ );
+
+ browser.test.assertEq(
+ 1,
+ views.length,
+ "Should only see one view in incognito popup"
+ );
+ browser.test.assertEq(
+ window,
+ views[0],
+ "This window should be the only view"
+ );
+ } else {
+ let bgPage = browser.extension.getBackgroundPage();
+ browser.test.assertEq(
+ true,
+ bgPage.isBackgroundPage,
+ "Should be able to access background page in non-incognito context"
+ );
+
+ bgPage = await browser.runtime.getBackgroundPage();
+ browser.test.assertEq(
+ true,
+ bgPage.isBackgroundPage,
+ "Should be able to access background page in non-incognito context"
+ );
+
+ browser.test.assertEq(
+ 2,
+ views.length,
+ "Should only two views in non-incognito popup"
+ );
+ browser.test.assertEq(
+ bgPage,
+ views[0],
+ "The background page should be the first view"
+ );
+ browser.test.assertEq(
+ window,
+ views[1],
+ "This window should be the second view"
+ );
+ }
+
+ let win = await browser.windows.getCurrent();
+ let port = browser.runtime.connect({ name: "from-popup" });
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq("close_popup", msg, "Close popup msg");
+ window.close();
+ });
+ port.postMessage({
+ message: "popup-details",
+ windowId: win.id,
+ incognito: browser.extension.inIncognitoContext,
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("incognito-views");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_lastError.js b/browser/components/extensions/test/browser/browser_ext_lastError.js
new file mode 100644
index 0000000000..f7015f131b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_lastError.js
@@ -0,0 +1,61 @@
+"use strict";
+
+async function sendMessage(options) {
+ function background(options) {
+ browser.runtime.sendMessage(result => {
+ browser.test.assertEq(undefined, result, "Argument value");
+ if (options.checkLastError) {
+ browser.test.assertEq(
+ "runtime.sendMessage's message argument is missing",
+ browser.runtime.lastError?.message,
+ "lastError value"
+ );
+ }
+ browser.test.sendMessage("done");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${background})(${JSON.stringify(options)})`,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+}
+
+add_task(async function testLastError() {
+ // Not necessary in browser-chrome tests, but monitorConsole gripes
+ // if we don't call it.
+ SimpleTest.waitForExplicitFinish();
+
+ // Check that we have no unexpected console messages when lastError is
+ // checked.
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ { message: /message argument is missing/, forbid: true },
+ ]);
+ });
+
+ await sendMessage({ checkLastError: true });
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+
+ // Check that we do have a console message when lastError is not checked.
+ waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ /Unchecked lastError value: Error: runtime.sendMessage's message argument is missing/,
+ },
+ ]);
+ });
+
+ await sendMessage({});
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_management.js b/browser/components/extensions/test/browser/browser_ext_management.js
new file mode 100644
index 0000000000..e4776583a8
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_management.js
@@ -0,0 +1,139 @@
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const testServer = AddonTestUtils.createHttpServer();
+
+add_task(async function test_management_install() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["xpinstall.signatures.required", false]],
+ });
+
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ browser_style: false,
+ default_area: "navbar",
+ },
+ permissions: ["management"],
+ },
+ background() {
+ let addons;
+ browser.test.onMessage.addListener((msg, init) => {
+ addons = init;
+ browser.test.sendMessage("ready");
+ });
+ browser.browserAction.onClicked.addListener(async () => {
+ try {
+ let { url, hash } = addons.shift();
+ browser.test.log(
+ `Installing XPI from ${url} with hash ${hash || "missing"}`
+ );
+ let { id } = await browser.management.install({ url, hash });
+ let { type } = await browser.management.get(id);
+ browser.test.sendMessage("installed", { id, type });
+ } catch (e) {
+ browser.test.log(`management.install() throws ${e}`);
+ browser.test.sendMessage("failed", e.message);
+ }
+ });
+ },
+ });
+
+ const themeXPIFile = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ manifest_version: 2,
+ name: "Tigers Matter",
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: "tiger@persona.beard",
+ },
+ },
+ theme: {
+ colors: {
+ frame: "orange",
+ },
+ },
+ },
+ });
+
+ let themeXPIFileHash = await IOUtils.computeHexDigest(
+ themeXPIFile.path,
+ "sha256"
+ );
+
+ const otherXPIFile = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ manifest_version: 2,
+ name: "Tigers Don't Matter",
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: "other@web.extension",
+ },
+ },
+ },
+ });
+
+ testServer.registerFile("/install_theme-1.0-fx.xpi", themeXPIFile);
+ testServer.registerFile("/install_other-1.0-fx.xpi", otherXPIFile);
+
+ const { primaryHost, primaryPort } = testServer.identity;
+ const baseURL = `http://${primaryHost}:${primaryPort}`;
+
+ let addons = [
+ {
+ url: `${baseURL}/install_theme-1.0-fx.xpi`,
+ hash: `sha256:${themeXPIFileHash}`,
+ },
+ {
+ url: `${baseURL}/install_other-1.0-fx.xpi`,
+ },
+ ];
+
+ await extension.startup();
+ extension.sendMessage("addons", addons);
+ await extension.awaitMessage("ready");
+
+ // Test installing a static WE theme.
+ clickBrowserAction(extension);
+
+ let { id, type } = await extension.awaitMessage("installed");
+ is(id, "tiger@persona.beard", "Static web extension theme installed");
+ is(type, "theme", "Extension type is correct");
+
+ is(
+ getToolboxBackgroundColor(),
+ "rgb(255, 165, 0)",
+ "Background is the new black"
+ );
+
+ let addon = await AddonManager.getAddonByID("tiger@persona.beard");
+
+ Assert.deepEqual(
+ addon.installTelemetryInfo,
+ {
+ source: "extension",
+ method: "management-webext-api",
+ },
+ "Got the expected telemetry info on the installed webext theme"
+ );
+
+ await addon.uninstall();
+
+ // Test installing a standard WE.
+ clickBrowserAction(extension);
+ let error = await extension.awaitMessage("failed");
+ is(error, "Incompatible addon", "Standard web extension rejected");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus.js b/browser/components/extensions/test/browser/browser_ext_menus.js
new file mode 100644
index 0000000000..76ac3cf045
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus.js
@@ -0,0 +1,458 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+add_task(async function test_permissions() {
+ function background() {
+ browser.test.sendMessage("apis", {
+ menus: typeof browser.menus,
+ contextMenus: typeof browser.contextMenus,
+ menusInternal: typeof browser.menusInternal,
+ });
+ }
+
+ const first = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["menus"] },
+ background,
+ });
+ const second = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["contextMenus"] },
+ background,
+ });
+
+ await first.startup();
+ await second.startup();
+
+ const apis1 = await first.awaitMessage("apis");
+ const apis2 = await second.awaitMessage("apis");
+
+ is(apis1.menus, "object", "browser.menus available with 'menus' permission");
+ is(
+ apis1.contextMenus,
+ "undefined",
+ "browser.contextMenus unavailable with 'menus' permission"
+ );
+ is(
+ apis1.menusInternal,
+ "undefined",
+ "browser.menusInternal is never available"
+ );
+
+ is(
+ apis2.menus,
+ "undefined",
+ "browser.menus unavailable with 'contextMenus' permission"
+ );
+ is(
+ apis2.contextMenus,
+ "object",
+ "browser.contextMenus unavailable with 'contextMenus' permission"
+ );
+ is(
+ apis2.menusInternal,
+ "undefined",
+ "browser.menusInternal is never available"
+ );
+
+ await first.unload();
+ await second.unload();
+});
+
+add_task(async function test_actionContextMenus() {
+ const manifest = {
+ page_action: {},
+ browser_action: {
+ default_area: "navbar",
+ },
+ permissions: ["menus"],
+ };
+
+ async function background() {
+ const contexts = ["page_action", "browser_action"];
+
+ const parentId = browser.menus.create({ contexts, title: "parent" });
+ browser.menus.create({ parentId, title: "click A" });
+ browser.menus.create({ parentId, title: "click B" });
+
+ for (let i = 1; i < 9; i++) {
+ browser.menus.create({ contexts, id: `${i}`, title: `click ${i}` });
+ }
+
+ browser.menus.onClicked.addListener((info, tab) => {
+ browser.test.sendMessage("click", { info, tab });
+ });
+
+ const [tab] = await browser.tabs.query({ active: true });
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("ready", tab.id);
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({ manifest, background });
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ await extension.startup();
+ const tabId = await extension.awaitMessage("ready");
+
+ for (const kind of ["page", "browser"]) {
+ const menu = await openActionContextMenu(extension, kind);
+ const [submenu, second, , , , last, separator] = menu.children;
+
+ is(submenu.tagName, "menu", "Correct submenu type");
+ is(submenu.label, "parent", "Correct submenu title");
+
+ const popup = await openSubmenu(submenu);
+ is(popup, submenu.menupopup, "Correct submenu opened");
+ is(popup.children.length, 2, "Correct number of submenu items");
+
+ let idPrefix = `${makeWidgetId(extension.id)}-menuitem-_`;
+
+ is(second.tagName, "menuitem", "Second menu item type is correct");
+ is(second.label, "click 1", "Second menu item title is correct");
+ is(second.id, `${idPrefix}1`, "Second menu item id is correct");
+
+ is(last.tagName, "menu", "Last menu item type is correct");
+ is(last.label, "Generated extension", "Last menu item title is correct");
+ is(
+ last.getAttribute("ext-type"),
+ "top-level-menu",
+ "Last menu ext-type is correct"
+ );
+ is(separator.tagName, "menuseparator", "Separator after last menu item");
+
+ // Verify that menu items exceeding ACTION_MENU_TOP_LEVEL_LIMIT are moved into a submenu.
+ let overflowPopup = await openSubmenu(last);
+ is(
+ overflowPopup.children.length,
+ 4,
+ "Excess items should be moved into a submenu"
+ );
+ is(
+ overflowPopup.firstElementChild.id,
+ `${idPrefix}5`,
+ "First submenu item ID is correct"
+ );
+ is(
+ overflowPopup.lastElementChild.id,
+ `${idPrefix}8`,
+ "Last submenu item ID is correct"
+ );
+
+ await closeActionContextMenu(overflowPopup.firstElementChild, kind);
+ const { info, tab } = await extension.awaitMessage("click");
+ is(info.pageUrl, "http://example.com/", "Click info pageUrl is correct");
+ is(tab.id, tabId, "Click event tab ID is correct");
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_bookmarkContextMenu() {
+ const ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "bookmarks"],
+ },
+ background() {
+ browser.menus.onShown.addListener(() => {
+ browser.test.sendMessage("hello");
+ });
+ browser.menus.create({ title: "blarg", contexts: ["bookmark"] }, () => {
+ browser.test.sendMessage("ready");
+ });
+ },
+ });
+
+ await ext.startup();
+ await ext.awaitMessage("ready");
+ await toggleBookmarksToolbar(true);
+
+ let menu = await openChromeContextMenu(
+ "placesContext",
+ "#PlacesToolbarItems .bookmark-item"
+ );
+ let children = Array.from(menu.children);
+ let item = children[children.length - 1];
+ is(item.label, "blarg", "Menu item label is correct");
+ await ext.awaitMessage("hello"); // onShown listener fired
+
+ closeChromeContextMenu("placesContext", item);
+ await ext.unload();
+ await toggleBookmarksToolbar(false);
+});
+
+add_task(async function test_tabContextMenu() {
+ const first = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ async background() {
+ browser.menus.create({
+ id: "alpha-beta-parent",
+ title: "alpha-beta parent",
+ contexts: ["tab"],
+ });
+
+ browser.menus.create({ parentId: "alpha-beta-parent", title: "alpha" });
+ browser.menus.create({ parentId: "alpha-beta-parent", title: "beta" });
+
+ browser.menus.create({ title: "dummy", contexts: ["page"] });
+
+ browser.menus.onClicked.addListener((info, tab) => {
+ browser.test.sendMessage("click", { info, tab });
+ });
+
+ const [tab] = await browser.tabs.query({ active: true });
+ browser.test.sendMessage("ready", tab.id);
+ },
+ });
+
+ const second = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background() {
+ browser.menus.create({
+ title: "invisible",
+ contexts: ["tab"],
+ documentUrlPatterns: ["http://does/not/match"],
+ });
+ browser.menus.create(
+ {
+ title: "gamma",
+ contexts: ["tab"],
+ documentUrlPatterns: ["http://example.com/"],
+ },
+ () => {
+ browser.test.sendMessage("ready");
+ }
+ );
+ },
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await first.startup();
+ await second.startup();
+
+ const tabId = await first.awaitMessage("ready");
+ await second.awaitMessage("ready");
+
+ const menu = await openTabContextMenu();
+ const [separator, submenu, gamma] = Array.from(menu.children).slice(-3);
+ is(
+ separator.tagName,
+ "menuseparator",
+ "Separator before first extension item"
+ );
+
+ is(submenu.tagName, "menu", "Correct submenu type");
+ is(submenu.label, "alpha-beta parent", "Correct submenu title");
+
+ isnot(
+ gamma.label,
+ "dummy",
+ "`page` context menu item should not appear here"
+ );
+
+ is(gamma.tagName, "menuitem", "Third menu item type is correct");
+ is(gamma.label, "gamma", "Third menu item label is correct");
+
+ const popup = await openSubmenu(submenu);
+ is(popup, submenu.menupopup, "Correct submenu opened");
+ is(popup.children.length, 2, "Correct number of submenu items");
+
+ const [alpha, beta] = popup.children;
+ is(alpha.tagName, "menuitem", "First menu item type is correct");
+ is(alpha.label, "alpha", "First menu item label is correct");
+ is(beta.tagName, "menuitem", "Second menu item type is correct");
+ is(beta.label, "beta", "Second menu item label is correct");
+
+ await closeTabContextMenu(beta);
+ const click = await first.awaitMessage("click");
+ is(
+ click.info.pageUrl,
+ "http://example.com/",
+ "Click info pageUrl is correct"
+ );
+ is(click.tab.id, tabId, "Click event tab ID is correct");
+ is(click.info.frameId, undefined, "no frameId on chrome");
+
+ BrowserTestUtils.removeTab(tab);
+ await first.unload();
+ await second.unload();
+});
+
+add_task(async function test_onclick_frameid() {
+ const manifest = {
+ permissions: ["menus"],
+ };
+
+ function background() {
+ function onclick(info) {
+ browser.test.sendMessage("click", info);
+ }
+ browser.menus.create(
+ { contexts: ["frame", "page"], title: "modify", onclick },
+ () => {
+ browser.test.sendMessage("ready");
+ }
+ );
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({ manifest, background });
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ async function click(menu) {
+ const items = menu.getElementsByAttribute("label", "modify");
+ is(items.length, 1, "found menu item");
+ await closeExtensionContextMenu(items[0]);
+ return extension.awaitMessage("click");
+ }
+
+ let info = await click(await openContextMenu("body"));
+ is(info.frameId, 0, "top level click");
+ info = await click(await openContextMenuInFrame());
+ isnot(info.frameId, undefined, "frame click, frameId is not undefined");
+ isnot(info.frameId, 0, "frame click, frameId probably okay");
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_multiple_contexts_init() {
+ const manifest = {
+ permissions: ["menus"],
+ };
+
+ function background() {
+ browser.menus.create({ id: "parent", title: "parent" }, () => {
+ browser.tabs.create({ url: "tab.html", active: false });
+ });
+ }
+
+ const files = {
+ "tab.html":
+ " ",
+ "tab.js": function () {
+ browser.menus.onClicked.addListener(info => {
+ browser.test.sendMessage("click", info);
+ });
+ browser.menus.create(
+ { parentId: "parent", id: "child", title: "child" },
+ () => {
+ browser.test.sendMessage("ready");
+ }
+ );
+ },
+ };
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ files,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ const menu = await openContextMenu();
+ const items = menu.getElementsByAttribute("label", "parent");
+
+ is(items.length, 1, "Found parent menu item");
+ is(items[0].tagName, "menu", "And it has children");
+
+ const popup = await openSubmenu(items[0]);
+ is(popup.firstElementChild.label, "child", "Correct child menu item");
+ await closeExtensionContextMenu(popup.firstElementChild);
+
+ const info = await extension.awaitMessage("click");
+ is(info.menuItemId, "child", "onClicked the correct item");
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_tools_menu() {
+ const first = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background() {
+ browser.menus.create({ title: "alpha", contexts: ["tools_menu"] });
+ browser.menus.create({ title: "beta", contexts: ["tools_menu"] }, () => {
+ browser.test.sendMessage("ready");
+ });
+ },
+ });
+
+ const second = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ async background() {
+ browser.menus.create({ title: "gamma", contexts: ["tools_menu"] });
+ browser.menus.onClicked.addListener((info, tab) => {
+ browser.test.sendMessage("click", { info, tab });
+ });
+
+ const [tab] = await browser.tabs.query({ active: true });
+ browser.test.sendMessage("ready", tab.id);
+ },
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await first.startup();
+ await second.startup();
+
+ await first.awaitMessage("ready");
+ const tabId = await second.awaitMessage("ready");
+ const menu = await openToolsMenu();
+
+ const [separator, submenu, gamma] = Array.from(menu.children).slice(-3);
+ is(
+ separator.tagName,
+ "menuseparator",
+ "Separator before first extension item"
+ );
+
+ is(submenu.tagName, "menu", "Correct submenu type");
+ is(
+ submenu.getAttribute("label"),
+ "Generated extension",
+ "Correct submenu title"
+ );
+ is(submenu.menupopup.children.length, 2, "Correct number of submenu items");
+
+ is(gamma.tagName, "menuitem", "Third menu item type is correct");
+ is(gamma.getAttribute("label"), "gamma", "Third menu item label is correct");
+
+ closeToolsMenu(gamma);
+
+ const click = await second.awaitMessage("click");
+ is(
+ click.info.pageUrl,
+ "http://example.com/",
+ "Click info pageUrl is correct"
+ );
+ is(click.tab.id, tabId, "Click event tab ID is correct");
+
+ BrowserTestUtils.removeTab(tab);
+ await first.unload();
+ await second.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_accesskey.js b/browser/components/extensions/test/browser/browser_ext_menus_accesskey.js
new file mode 100644
index 0000000000..88c89902ac
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_accesskey.js
@@ -0,0 +1,209 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+add_task(async function accesskeys() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ gBrowser.selectedTab = tab;
+
+ async function background() {
+ // description is informative.
+ // title is passed to menus.create.
+ // label and key are compared with the actual values.
+ const TESTCASES = [
+ {
+ description: "Amp at start",
+ title: "&accesskey",
+ label: "accesskey",
+ key: "a",
+ },
+ {
+ description: "amp in between",
+ title: "A& b",
+ label: "A b",
+ key: " ",
+ },
+ {
+ description: "lonely amp",
+ title: "&",
+ label: "",
+ key: "",
+ },
+ {
+ description: "amp at end",
+ title: "End &",
+ label: "End ",
+ key: "",
+ },
+ {
+ description: "escaped amp",
+ title: "A && B",
+ label: "A & B",
+ key: "",
+ },
+ {
+ description: "amp before escaped amp",
+ title: "A &T&& before",
+ label: "A T& before",
+ key: "T",
+ },
+ {
+ description: "amp after escaped amp",
+ title: "A &&&T after",
+ label: "A &T after",
+ key: "T",
+ },
+ {
+ // Only the first amp should be used as the access key.
+ description: "amp, escaped amp, amp to ignore",
+ title: "First &1 comes && first &2 serves",
+ label: "First 1 comes & first 2 serves",
+ key: "1",
+ },
+ {
+ description: "created with amp, updated without amp",
+ title: "temp with &X", // will be updated below.
+ label: "remove amp",
+ key: "",
+ },
+ {
+ description: "created without amp, update with amp",
+ title: "temp without access key", // will be updated below.
+ label: "add ampY",
+ key: "Y",
+ },
+ ];
+
+ let menuIds = TESTCASES.map(({ title }) => browser.menus.create({ title }));
+
+ // Should clear the access key:
+ await browser.menus.update(menuIds[menuIds.length - 2], {
+ title: "remove amp",
+ });
+
+ // Should add an access key:
+ await browser.menus.update(menuIds[menuIds.length - 1], {
+ title: "add amp&Y",
+ });
+ // Should not clear the access key because title is not set:
+ await browser.menus.update(menuIds[menuIds.length - 1], { enabled: true });
+
+ browser.test.sendMessage("testCases", TESTCASES);
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ const TESTCASES = await extension.awaitMessage("testCases");
+ let menu = await openExtensionContextMenu();
+ let items = menu.getElementsByTagName("menuitem");
+ is(items.length, TESTCASES.length, "Expected menu items for page");
+ TESTCASES.forEach(({ description, label, key }, i) => {
+ is(items[i].label, label, `Label for item ${i} (${description})`);
+ is(items[i].accessKey, key, `Accesskey for item ${i} (${description})`);
+ });
+
+ await closeExtensionContextMenu();
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function accesskeys_selection() {
+ const PAGE_WITH_AMPS = "data:text/plain;charset=utf-8,PageSelection&Amp";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ PAGE_WITH_AMPS
+ );
+ gBrowser.selectedTab = tab;
+
+ async function background() {
+ const TESTCASES = [
+ {
+ description: "Selection without amp",
+ title: "percent-s: %s.",
+ label: "percent-s: PageSelection&Amp.",
+ key: "",
+ },
+ {
+ description: "Selection with amp after %s",
+ title: "percent-s: %s &A.",
+ label: "percent-s: PageSelection&Amp A.",
+ key: "A",
+ },
+ {
+ description: "Selection with amp before %s",
+ title: "percent-s: &B %s.",
+ label: "percent-s: B PageSelection&Amp.",
+ key: "B",
+ },
+ {
+ description: "Amp-percent",
+ title: "Amp-percent: &%.",
+ label: "Amp-percent: %.",
+ key: "%",
+ },
+ {
+ // "&%s" should be treated as "%s", and "ignore this" with amps should be ignored.
+ description: "Selection with amp-percent-s",
+ title: "Amp-percent-s: &%s.&i&g&n&o&r&e& &t&h&i&s",
+ label: "Amp-percent-s: PageSelection&Amp.ignore this",
+ // Chrome uses the first character of the selection as access key.
+ // Let's not copy that behavior...
+ key: "",
+ },
+ {
+ description: "Selection with amp before amp-percent-s",
+ title: "Amp-percent-s: &_ &%s.",
+ label: "Amp-percent-s: _ PageSelection&Amp.",
+ key: "_",
+ },
+ ];
+
+ let lastMenuId;
+ for (let { title } of TESTCASES) {
+ lastMenuId = browser.menus.create({ contexts: ["selection"], title });
+ }
+ // Round-trip to ensure that the menus have been registered.
+ await browser.menus.update(lastMenuId, {});
+ browser.test.sendMessage("testCases", TESTCASES);
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ // Select all
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function (arg) {
+ let doc = content.document;
+ let range = doc.createRange();
+ let selection = content.getSelection();
+ selection.removeAllRanges();
+ range.selectNodeContents(doc.body);
+ selection.addRange(range);
+ });
+
+ const TESTCASES = await extension.awaitMessage("testCases");
+ let menu = await openExtensionContextMenu();
+ let items = menu.getElementsByTagName("menuitem");
+ is(items.length, TESTCASES.length, "Expected menu items for page");
+ TESTCASES.forEach(({ description, label, key }, i) => {
+ is(items[i].label, label, `Label for item ${i} (${description})`);
+ is(items[i].accessKey, key, `Accesskey for item ${i} (${description})`);
+ });
+
+ await closeExtensionContextMenu();
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_activeTab.js b/browser/components/extensions/test/browser/browser_ext_menus_activeTab.js
new file mode 100644
index 0000000000..3f3bfd6633
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_activeTab.js
@@ -0,0 +1,115 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Opens two tabs at the start of the tab strip and focuses the second tab.
+// Then an extension menu is registered for the "tab" context and a menu is
+// opened on the first tab and the extension menu item is clicked.
+// This triggers the onTabMenuClicked handler.
+async function openTwoTabsAndOpenTabMenu(onTabMenuClicked) {
+ const PAGE_URL =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+ const OTHER_URL =
+ "http://127.0.0.1:8888/browser/browser/components/extensions/test/browser/context.html";
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, OTHER_URL);
+ // Move the first tab to the start so that it can be found by the .tabbrowser-tab selector below.
+ gBrowser.moveTabTo(tab1, 0);
+ gBrowser.moveTabTo(tab2, 1);
+
+ async function background(onTabMenuClicked) {
+ browser.menus.onClicked.addListener(async (info, tab) => {
+ await onTabMenuClicked(info, tab);
+ browser.test.sendMessage("onCommand_on_tab_click");
+ });
+
+ browser.menus.create(
+ {
+ title: "menu item on tab",
+ contexts: ["tab"],
+ },
+ () => {
+ browser.test.sendMessage("ready");
+ }
+ );
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "activeTab"],
+ },
+ background: `(${background})(${onTabMenuClicked})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // Focus a selected tab to to make tabbrowser.js to load localization files,
+ // and thereby initialize document.l10n property.
+ gBrowser.selectedTab.focus();
+
+ // The .tabbrowser-tab selector matches the first tab (tab1).
+ let menu = await openChromeContextMenu(
+ "tabContextMenu",
+ ".tabbrowser-tab",
+ window
+ );
+ let menuItem = menu.getElementsByAttribute("label", "menu item on tab")[0];
+ await closeTabContextMenu(menuItem);
+ await extension.awaitMessage("onCommand_on_tab_click");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+}
+
+add_task(async function activeTabForTabMenu() {
+ await openTwoTabsAndOpenTabMenu(async function onTabMenuClicked(info, tab) {
+ browser.test.assertEq(0, tab.index, "Expected a menu on the first tab.");
+
+ try {
+ let [actualUrl] = await browser.tabs.executeScript(tab.id, {
+ code: "document.URL",
+ });
+ browser.test.assertEq(
+ tab.url,
+ actualUrl,
+ "Content script to execute in the first tab"
+ );
+ // (the activeTab permission should have been granted to the first tab.)
+ } catch (e) {
+ browser.test.fail(
+ `Unexpected error in executeScript: ${e} :: ${e.stack}`
+ );
+ }
+ });
+});
+
+add_task(async function noActiveTabForCurrentTab() {
+ await openTwoTabsAndOpenTabMenu(async function onTabMenuClicked(info, tab) {
+ const PAGE_URL =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+ browser.test.assertEq(0, tab.index, "Expected a menu on the first tab.");
+ browser.test.assertEq(
+ PAGE_URL,
+ tab.url,
+ "Expected tab.url to be available for the first tab"
+ );
+
+ let [tab2] = await browser.tabs.query({ windowId: tab.windowId, index: 1 });
+ browser.test.assertTrue(tab2.active, "The second tab should be focused.");
+ browser.test.assertEq(
+ undefined,
+ tab2.url,
+ "Expected tab.url to be unavailable for the second tab."
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab2.id, { code: "document.URL" }),
+ /Missing host permission for the tab/,
+ "Content script should not run in the second tab"
+ );
+ // (The activeTab permission was granted to the first tab, not tab2.)
+ });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_capture_secondary_click.js b/browser/components/extensions/test/browser/browser_ext_menus_capture_secondary_click.js
new file mode 100644
index 0000000000..1b251ca262
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_capture_secondary_click.js
@@ -0,0 +1,140 @@
+// /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+// /* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+add_task(async function test_buttons() {
+ const manifest = {
+ permissions: ["menus"],
+ };
+
+ function background() {
+ function onclick(info) {
+ browser.test.sendMessage("click", info);
+ }
+ browser.menus.create({ title: "modify", onclick }, () => {
+ browser.test.sendMessage("ready");
+ });
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({ manifest, background });
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ for (let i of [0, 1, 2]) {
+ const menu = await openContextMenu();
+ const items = menu.getElementsByAttribute("label", "modify");
+ await closeExtensionContextMenu(items[0], { button: i });
+ const info = await extension.awaitMessage("click");
+ is(info.button, i, `Button value should be ${i}`);
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_submenu() {
+ function background() {
+ browser.menus.onClicked.addListener(info => {
+ browser.test.assertEq("child", info.menuItemId, "expected menu item");
+ browser.test.sendMessage("clicked_button", info.button);
+ });
+ browser.menus.create({
+ id: "parent",
+ title: "parent",
+ });
+ browser.menus.create(
+ {
+ id: "child",
+ parentId: "parent",
+ title: "child",
+ },
+ () => browser.test.sendMessage("ready")
+ );
+ }
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background,
+ });
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ for (let button of [0, 1, 2]) {
+ const menu = await openContextMenu();
+ const parentItem = menu.getElementsByAttribute("label", "parent")[0];
+ const submenu = await openSubmenu(parentItem);
+ const childItem = submenu.firstElementChild;
+ // This should not trigger a click event, thus we intentionally turn off
+ // this a11y check as containers are not expected to be interactive.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ await EventUtils.synthesizeMouseAtCenter(parentItem, { button });
+ AccessibilityUtils.resetEnv();
+ await closeExtensionContextMenu(childItem, { button });
+ is(
+ await extension.awaitMessage("clicked_button"),
+ button,
+ "Expected button"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_disabled_item() {
+ function background() {
+ browser.menus.onHidden.addListener(() =>
+ browser.test.sendMessage("onHidden")
+ );
+ browser.menus.create(
+ {
+ title: "disabled_item",
+ enabled: false,
+ onclick(info) {
+ browser.test.fail(
+ `Unexpected click on disabled_item, button=${info.button}`
+ );
+ },
+ },
+ () => browser.test.sendMessage("ready")
+ );
+ }
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background,
+ });
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ for (let button of [0, 1, 2]) {
+ const menu = await openContextMenu();
+ const items = menu.getElementsByAttribute("label", "disabled_item");
+ // We intentionally turn off this a11y check, because the following click
+ // is targeting a disabled control to confirm the click event won't come through.
+ // It is not meant to be interactive and is not expected to be accessible:
+ AccessibilityUtils.setEnv({
+ mustBeEnabled: false,
+ });
+ await EventUtils.synthesizeMouseAtCenter(items[0], { button });
+ AccessibilityUtils.resetEnv();
+ await closeContextMenu();
+ await extension.awaitMessage("onHidden");
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_errors.js b/browser/components/extensions/test/browser/browser_ext_menus_errors.js
new file mode 100644
index 0000000000..1569644996
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_errors.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function test_create_error() {
+ // lastError is the only means to communicate errors in the menus.create API,
+ // so make sure that a warning is logged to the console if the error is not
+ // checked.
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.monitorConsole(resolve, [
+ // Callback exists, lastError is checked. Should not be logged.
+ {
+ message: /Unchecked lastError value: Error: ID already exists: some_id/,
+ forbid: true,
+ },
+ // No callback, lastError not checked. Should be logged.
+ {
+ message:
+ /Unchecked lastError value: Error: Could not find any MenuItem with id: noCb/,
+ },
+ // Callback exists, lastError not checked. Should be logged.
+ {
+ message:
+ /Unchecked lastError value: Error: Could not find any MenuItem with id: cbIgnoreError/,
+ },
+ ]);
+ });
+
+ async function background() {
+ // Note: browser.menus.create returns the menu ID instead of a promise, so
+ // we have to use callbacks.
+ await new Promise(resolve => {
+ browser.menus.create({ id: "some_id", title: "menu item" }, () => {
+ browser.test.assertEq(
+ null,
+ browser.runtime.lastError,
+ "Expected no error"
+ );
+ resolve();
+ });
+ });
+
+ // Callback exists, lastError is checked:
+ await new Promise(resolve => {
+ browser.menus.create({ id: "some_id", title: "menu item" }, () => {
+ browser.test.assertEq(
+ "ID already exists: some_id",
+ browser.runtime.lastError.message,
+ "Expected error"
+ );
+ resolve();
+ });
+ });
+
+ // No callback, lastError not checked:
+ browser.menus.create({ id: "noCb", parentId: "noCb", title: "menu item" });
+
+ // Callback exists, lastError not checked:
+ await new Promise(resolve => {
+ browser.menus.create(
+ { id: "cbIgnoreError", parentId: "cbIgnoreError", title: "menu item" },
+ () => {
+ resolve();
+ }
+ );
+ });
+
+ // Do another roundtrip with the menus API to ensure that any console
+ // error messages from the previous call are flushed.
+ await browser.menus.removeAll();
+
+ browser.test.sendMessage("done");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["menus"] },
+ background,
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
+
+add_task(async function test_update_error() {
+ async function background() {
+ const id = browser.menus.create({ title: "menu item" });
+
+ await browser.test.assertRejects(
+ browser.menus.update(id, { parentId: "bogus" }),
+ "Could not find any MenuItem with id: bogus",
+ "menus.update with invalid parentMenuId should fail"
+ );
+
+ await browser.test.assertRejects(
+ browser.menus.update(id, { parentId: id }),
+ "MenuItem cannot be an ancestor (or self) of its new parent.",
+ "menus.update cannot assign itself as the parent of a menu."
+ );
+
+ browser.test.sendMessage("done");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["menus"] },
+ background,
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_invalid_documentUrlPatterns() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ async background() {
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ title: "invalid url",
+ contexts: ["tab"],
+ documentUrlPatterns: ["test1"],
+ },
+ () => {
+ browser.test.assertEq(
+ "Invalid url pattern: test1",
+ browser.runtime.lastError.message,
+ "Expected invalid match pattern"
+ );
+ resolve();
+ }
+ );
+ });
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ title: "invalid url",
+ contexts: ["link"],
+ targetUrlPatterns: ["test2"],
+ },
+ () => {
+ browser.test.assertEq(
+ "Invalid url pattern: test2",
+ browser.runtime.lastError.message,
+ "Expected invalid match pattern"
+ );
+ resolve();
+ }
+ );
+ });
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_event_order.js b/browser/components/extensions/test/browser/browser_ext_menus_event_order.js
new file mode 100644
index 0000000000..4ec43c8cde
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_event_order.js
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+add_task(async function test_menus_click_event_sequence() {
+ async function background() {
+ let events = [];
+
+ browser.menus.onShown.addListener(() => {
+ events.push("onShown");
+ });
+ browser.menus.onHidden.addListener(() => {
+ events.push("onHidden");
+ browser.test.sendMessage("event_sequence", events);
+ events.length = 0;
+ });
+
+ browser.menus.create({
+ title: "item in page menu",
+ contexts: ["page"],
+ onclick() {
+ events.push("onclick parameter of page menu item");
+ },
+ });
+ browser.menus.create(
+ {
+ title: "item in tools menu",
+ contexts: ["tools_menu"],
+ onclick() {
+ events.push("onclick parameter of tools_menu menu item");
+ },
+ },
+ () => {
+ // The menus creation requests are expected to be handled in-order.
+ // So when the callback for the last menu creation request is called,
+ // we can assume that all menus have been registered.
+ browser.test.sendMessage("created menus");
+ }
+ );
+ }
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["menus"],
+ },
+ });
+ await extension.startup();
+ info("Waiting for events and menu items to be registered");
+ await extension.awaitMessage("created menus");
+
+ async function verifyResults(menuType) {
+ info("Getting menu event info...");
+ let events = await extension.awaitMessage("event_sequence");
+ Assert.deepEqual(
+ events,
+ ["onShown", `onclick parameter of ${menuType} menu item`, "onHidden"],
+ "Expected order of menus events"
+ );
+ }
+
+ {
+ info("Opening and closing page menu");
+ const menu = await openContextMenu("body");
+ const menuitem = menu.querySelector("menuitem[label='item in page menu']");
+ ok(menuitem, "Page menu item should exist");
+ await closeExtensionContextMenu(menuitem);
+ await verifyResults("page");
+ }
+
+ {
+ info("Opening and closing tools menu");
+ const menu = await openToolsMenu();
+ let menuitem = menu.querySelector("menuitem[label='item in tools menu']");
+ ok(menuitem, "Tools menu item should exist");
+ await closeToolsMenu(menuitem);
+ await verifyResults("tools_menu");
+ }
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_eventpage.js b/browser/components/extensions/test/browser/browser_ext_menus_eventpage.js
new file mode 100644
index 0000000000..a1f6d8c81f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_eventpage.js
@@ -0,0 +1,277 @@
+"use strict";
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+});
+
+function getExtension(background, useAddonManager) {
+ return ExtensionTestUtils.loadExtension({
+ useAddonManager,
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ permissions: ["menus"],
+ background: { persistent: false },
+ },
+ background,
+ });
+}
+
+add_task(async function test_menu_create_id() {
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.monitorConsole(resolve, [
+ // Callback exists, lastError is checked, so we should not see this logged.
+ {
+ message:
+ /Unchecked lastError value: Error: menus.create requires an id for non-persistent background scripts./,
+ forbid: true,
+ },
+ ]);
+ });
+
+ function background() {
+ // Event pages require ID
+ browser.menus.create(
+ { contexts: ["browser_action"], title: "parent" },
+ () => {
+ browser.test.assertEq(
+ "menus.create requires an id for non-persistent background scripts.",
+ browser.runtime.lastError?.message,
+ "lastError message for missing id"
+ );
+ browser.test.sendMessage("done");
+ }
+ );
+ }
+ const extension = getExtension(background);
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
+
+add_task(async function test_menu_onclick() {
+ async function background() {
+ const contexts = ["browser_action"];
+
+ const parentId = browser.menus.create({
+ contexts,
+ title: "parent",
+ id: "test-parent",
+ });
+ browser.menus.create({ parentId, title: "click A", id: "test-click" });
+
+ browser.menus.onClicked.addListener((info, tab) => {
+ browser.test.sendMessage("click", { info, tab });
+ });
+ browser.runtime.onSuspend.addListener(() => {
+ browser.test.sendMessage("suspended-test_menu_onclick");
+ });
+ }
+
+ const extension = getExtension(background);
+
+ await extension.startup();
+ await extension.terminateBackground(); // Simulated suspend on idle.
+ await extension.awaitMessage("suspended-test_menu_onclick");
+
+ // The background is now suspended, test that a menu click starts it.
+ const kind = "browser";
+
+ const menu = await openActionContextMenu(extension, kind);
+ const [submenu] = menu.children;
+ const popup = await openSubmenu(submenu);
+
+ await closeActionContextMenu(popup.firstElementChild, kind);
+ const clicked = await extension.awaitMessage("click");
+ is(clicked.info.pageUrl, "about:blank", "Click info pageUrl is correct");
+ Assert.greater(clicked.tab.id, -1, "Click event tab ID is correct");
+
+ await extension.unload();
+});
+
+add_task(async function test_menu_onshown() {
+ async function background() {
+ const contexts = ["browser_action"];
+
+ const parentId = browser.menus.create({
+ contexts,
+ title: "parent",
+ id: "test-parent",
+ });
+ browser.menus.create({ parentId, title: "click A", id: "test-click" });
+
+ browser.menus.onClicked.addListener((info, tab) => {
+ browser.test.sendMessage("click", { info, tab });
+ });
+ browser.menus.onShown.addListener((info, tab) => {
+ browser.test.sendMessage("shown", { info, tab });
+ });
+ browser.menus.onHidden.addListener((info, tab) => {
+ browser.test.sendMessage("hidden", { info, tab });
+ });
+ browser.runtime.onSuspend.addListener(() => {
+ browser.test.sendMessage("suspended-test_menu_onshown");
+ });
+ }
+
+ const extension = getExtension(background);
+
+ await extension.startup();
+ await extension.terminateBackground(); // Simulated suspend on idle.
+ await extension.awaitMessage("suspended-test_menu_onshown");
+
+ // The background is now suspended, test that showing a menu starts it.
+ const kind = "browser";
+
+ const menu = await openActionContextMenu(extension, kind);
+ const [submenu] = menu.children;
+ const popup = await openSubmenu(submenu);
+ await extension.awaitMessage("shown");
+
+ await closeActionContextMenu(popup.firstElementChild, kind);
+ await extension.awaitMessage("hidden");
+ // The click still should work after the background was restarted.
+ const clicked = await extension.awaitMessage("click");
+ is(clicked.info.pageUrl, "about:blank", "Click info pageUrl is correct");
+ Assert.greater(clicked.tab.id, -1, "Click event tab ID is correct");
+
+ await extension.unload();
+});
+
+add_task(async function test_actions_context_menu() {
+ function background() {
+ browser.contextMenus.create({
+ id: "my_browser_action",
+ title: "open_browser_action",
+ contexts: ["all"],
+ command: "_execute_browser_action",
+ });
+ browser.contextMenus.onClicked.addListener(() => {
+ browser.test.fail(`menu onClicked should not have been received`);
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ function testScript() {
+ window.onload = () => {
+ browser.test.sendMessage("test-opened", true);
+ };
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "contextMenus commands",
+ permissions: ["contextMenus"],
+ browser_action: {
+ default_title: "Test BrowserAction",
+ default_popup: "test.html",
+ default_area: "navbar",
+ browser_style: true,
+ },
+ background: { persistent: false },
+ },
+ background,
+ files: {
+ "test.html": ` `,
+ "test.js": testScript,
+ },
+ });
+
+ async function testContext(id) {
+ const menu = await openContextMenu();
+ const items = menu.getElementsByAttribute("label", id);
+ is(items.length, 1, `exactly one menu item found`);
+ await closeExtensionContextMenu(items[0]);
+ return extension.awaitMessage("test-opened");
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await extension.terminateBackground();
+
+ // open a page so context menu works
+ const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html?test=commands";
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ ok(
+ await testContext("open_browser_action"),
+ "_execute_browser_action worked"
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+ is(ext.backgroundState, "stopped", "background is not running");
+
+ await extension.unload();
+});
+
+add_task(async function test_menu_create_id_reuse() {
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.monitorConsole(resolve, [
+ // Callback exists, lastError is checked, so we should not see this logged.
+ {
+ message:
+ /Unchecked lastError value: Error: menus.create requires an id for non-persistent background scripts./,
+ forbid: true,
+ },
+ ]);
+ });
+
+ function background() {
+ browser.menus.create(
+ {
+ contexts: ["browser_action"],
+ title: "click A",
+ id: "test-click",
+ },
+ () => {
+ browser.test.sendMessage("create", browser.runtime.lastError?.message);
+ }
+ );
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("add-again", msg, "expected msg");
+ browser.menus.create(
+ {
+ contexts: ["browser_action"],
+ title: "click A",
+ id: "test-click",
+ },
+ () => {
+ browser.test.assertEq(
+ "The menu id test-click already exists in menus.create.",
+ browser.runtime.lastError?.message,
+ "lastError message for missing id"
+ );
+ browser.test.sendMessage("done");
+ }
+ );
+ });
+ }
+ const extension = getExtension(background, "temporary");
+ await extension.startup();
+ let lastError = await extension.awaitMessage("create");
+ Assert.equal(lastError, undefined, "no error creating menu");
+ extension.sendMessage("add-again");
+ await extension.awaitMessage("done");
+ await extension.terminateBackground();
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-click already exists in menus.create.",
+ "lastError using duplicate ID"
+ );
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_events.js b/browser/components/extensions/test/browser/browser_ext_menus_events.js
new file mode 100644
index 0000000000..0127c05007
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_events.js
@@ -0,0 +1,911 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+const PAGE_BASE = PAGE.replace("context.html", "");
+const PAGE_HOST_PATTERN = "http://mochi.test/*";
+
+const EXPECT_TARGET_ELEMENT = 13337;
+
+async function grantOptionalPermission(extension, permissions) {
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+ return ExtensionPermissions.add(extension.id, permissions, ext);
+}
+
+var someOtherTab, testTab;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+
+ // To help diagnose an intermittent later.
+ SimpleTest.requestCompleteLog();
+
+ // Setup the test tab now, rather than for each test
+ someOtherTab = gBrowser.selectedTab;
+ testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ registerCleanupFunction(() => BrowserTestUtils.removeTab(testTab));
+});
+
+// Registers a context menu using menus.create(menuCreateParams) and checks
+// whether the menus.onShown and menus.onHidden events are fired as expected.
+// doOpenMenu must open the menu and its returned promise must resolve after the
+// menu is shown. Similarly, doCloseMenu must hide the menu.
+async function testShowHideEvent({
+ menuCreateParams,
+ id,
+ doOpenMenu,
+ doCloseMenu,
+ expectedShownEvent,
+ expectedShownEventWithPermissions = null,
+ forceTabToBackground = false,
+ manifest_version = 2,
+}) {
+ async function background(menu_create_params) {
+ const [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ if (browser.pageAction) {
+ await browser.pageAction.show(tab.id);
+ }
+
+ let shownEvents = [];
+ let hiddenEvents = [];
+
+ browser.menus.onShown.addListener((...args) => {
+ browser.test.log(`==> onShown args ${JSON.stringify(args)}`);
+ let [info, shownTab] = args;
+ if (info.targetElementId) {
+ // In this test, we aren't interested in the exact value,
+ // only in whether it is set or not.
+ info.targetElementId = 13337; // = EXPECT_TARGET_ELEMENT
+ }
+ shownEvents.push(info);
+
+ if (menu_create_params.title.includes("TEST_EXPECT_NO_TAB")) {
+ browser.test.assertEq(undefined, shownTab, "expect no tab");
+ } else {
+ browser.test.assertEq(tab.id, shownTab?.id, "expected tab");
+ }
+ browser.test.assertEq(2, args.length, "expected number of onShown args");
+ });
+ browser.menus.onHidden.addListener(event => hiddenEvents.push(event));
+
+ browser.test.onMessage.addListener(async msg => {
+ switch (msg) {
+ case "register-menu":
+ let menuId;
+ await new Promise(resolve => {
+ menuId = browser.menus.create(menu_create_params, resolve);
+ });
+ browser.test.assertEq(
+ 0,
+ shownEvents.length,
+ "no onShown before menu"
+ );
+ browser.test.assertEq(
+ 0,
+ hiddenEvents.length,
+ "no onHidden before menu"
+ );
+ browser.test.sendMessage("menu-registered", menuId);
+ break;
+ case "assert-menu-shown":
+ browser.test.assertEq(1, shownEvents.length, "expected onShown");
+ browser.test.assertEq(
+ 0,
+ hiddenEvents.length,
+ "no onHidden before closing"
+ );
+ browser.test.sendMessage("onShown-event-data", shownEvents[0]);
+ break;
+ case "assert-menu-hidden":
+ browser.test.assertEq(
+ 1,
+ shownEvents.length,
+ "expected no more onShown"
+ );
+ browser.test.assertEq(1, hiddenEvents.length, "expected onHidden");
+ browser.test.sendMessage("onHidden-event-data", hiddenEvents[0]);
+ break;
+ case "optional-menu-shown-with-permissions":
+ browser.test.assertEq(
+ 2,
+ shownEvents.length,
+ "expected second onShown"
+ );
+ browser.test.sendMessage("onShown-event-data2", shownEvents[1]);
+ break;
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ // Tab must initially open as a foreground tab, because the test extension
+ // looks for the active tab.
+ if (gBrowser.selectedTab != testTab) {
+ await BrowserTestUtils.switchTab(gBrowser, testTab);
+ }
+
+ let useAddonManager, browser_specific_settings;
+ const action = manifest_version < 3 ? "browser_action" : "action";
+ // hook up AOM so event pages in MV3 work.
+ if (manifest_version > 2) {
+ browser_specific_settings = { gecko: { id } };
+ useAddonManager = "temporary";
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${background})(${JSON.stringify(menuCreateParams)})`,
+ useAddonManager,
+ manifest: {
+ manifest_version,
+ browser_specific_settings,
+ page_action: {},
+ [action]: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ permissions: ["menus"],
+ optional_permissions: [PAGE_HOST_PATTERN],
+ },
+ files: {
+ "popup.html": ` Popup body`,
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ extension.sendMessage("register-menu");
+ let menuId = await extension.awaitMessage("menu-registered");
+ info(`menu registered ${menuId}`);
+
+ if (forceTabToBackground && gBrowser.selectedTab != someOtherTab) {
+ await BrowserTestUtils.switchTab(gBrowser, someOtherTab);
+ }
+
+ await doOpenMenu(extension, testTab);
+ extension.sendMessage("assert-menu-shown");
+ let shownEvent = await extension.awaitMessage("onShown-event-data");
+
+ // menuCreateParams.id is not set, therefore a numeric ID is generated.
+ expectedShownEvent.menuIds = [menuId];
+ Assert.deepEqual(shownEvent, expectedShownEvent, "expected onShown info");
+
+ await doCloseMenu(extension);
+ extension.sendMessage("assert-menu-hidden");
+ let hiddenEvent = await extension.awaitMessage("onHidden-event-data");
+ is(hiddenEvent, undefined, "expected no event data for onHidden event");
+
+ if (expectedShownEventWithPermissions) {
+ expectedShownEventWithPermissions.menuIds = [menuId];
+ await grantOptionalPermission(extension, {
+ permissions: [],
+ origins: [PAGE_HOST_PATTERN],
+ });
+ await doOpenMenu(extension, testTab);
+ extension.sendMessage("optional-menu-shown-with-permissions");
+ let shownEvent2 = await extension.awaitMessage("onShown-event-data2");
+ Assert.deepEqual(
+ shownEvent2,
+ expectedShownEventWithPermissions,
+ "expected onShown info when host permissions are enabled"
+ );
+ await doCloseMenu(extension);
+ }
+
+ await extension.unload();
+}
+
+// Make sure that we won't trigger onShown when extensions cannot add menus.
+add_task(async function test_no_show_hide_for_unsupported_menu() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ let events = [];
+ browser.menus.onShown.addListener(data => events.push(data));
+ browser.menus.onHidden.addListener(() => events.push("onHidden"));
+ browser.test.onMessage.addListener(() => {
+ browser.test.assertEq(
+ "[]",
+ JSON.stringify(events),
+ "Should not have any events when the context is unsupported."
+ );
+ browser.test.notifyPass("done listening to menu events");
+ });
+ },
+ manifest: {
+ permissions: ["menus"],
+ },
+ });
+
+ await extension.startup();
+ // Open and close a menu for which the extension cannot add menu items.
+ await openChromeContextMenu("toolbar-context-menu", "#stop-reload-button");
+ await closeChromeContextMenu("toolbar-context-menu");
+
+ extension.sendMessage("check menu events");
+ await extension.awaitFinish("done listening to menu events");
+
+ await extension.unload();
+});
+
+add_task(async function test_show_hide_without_menu_item() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ let events = [];
+ browser.menus.onShown.addListener(data => events.push(data));
+ browser.menus.onHidden.addListener(() => events.push("onHidden"));
+ browser.test.onMessage.addListener(() => {
+ browser.test.sendMessage("events from menuless extension", events);
+ });
+
+ browser.menus.create({
+ title: "never shown",
+ documentUrlPatterns: ["*://url-pattern-that-never-matches/*"],
+ contexts: ["all"],
+ });
+ },
+ manifest: {
+ permissions: ["menus", PAGE_HOST_PATTERN],
+ },
+ });
+
+ await extension.startup();
+
+ // Run another context menu test where onShown/onHidden will fire.
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "any menu item",
+ contexts: ["all"],
+ },
+ expectedShownEvent: {
+ contexts: ["page", "all"],
+ viewType: "tab",
+ editable: false,
+ frameId: 0,
+ },
+ async doOpenMenu() {
+ await openContextMenu("body");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+
+ // Now the menu has been shown and hidden, and in another extension the
+ // onShown/onHidden events have been dispatched.
+ extension.sendMessage("check menu events");
+ let events = await extension.awaitMessage("events from menuless extension");
+ is(events.length, 2, "expect two events");
+ is(events[1], "onHidden", "last event should be onHidden");
+ ok(events[0].targetElementId, "info.targetElementId must be set in onShown");
+ delete events[0].targetElementId;
+ Assert.deepEqual(
+ events[0],
+ {
+ menuIds: [],
+ contexts: ["page", "all"],
+ viewType: "tab",
+ editable: false,
+ pageUrl: PAGE,
+ frameId: 0,
+ },
+ "expected onShown info from menuless extension"
+ );
+ await extension.unload();
+});
+
+add_task(async function test_show_hide_pageAction() {
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "pageAction item",
+ contexts: ["page_action"],
+ },
+ expectedShownEvent: {
+ contexts: ["page_action", "all"],
+ viewType: undefined,
+ editable: false,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["page_action", "all"],
+ viewType: undefined,
+ editable: false,
+ pageUrl: PAGE,
+ },
+ async doOpenMenu(extension) {
+ await openActionContextMenu(extension, "page");
+ },
+ async doCloseMenu() {
+ await closeActionContextMenu(null, "page");
+ },
+ });
+});
+
+add_task(async function test_show_hide_browserAction() {
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "browserAction item",
+ contexts: ["browser_action"],
+ },
+ expectedShownEvent: {
+ contexts: ["browser_action", "all"],
+ viewType: undefined,
+ editable: false,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["browser_action", "all"],
+ viewType: undefined,
+ editable: false,
+ pageUrl: PAGE,
+ },
+ async doOpenMenu(extension) {
+ await openActionContextMenu(extension, "browser");
+ },
+ async doCloseMenu() {
+ await closeActionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_browserAction_v3() {
+ await testShowHideEvent({
+ manifest_version: 3,
+ id: "browser-action@mochitest",
+ menuCreateParams: {
+ id: "action_item",
+ title: "Action item",
+ contexts: ["action"],
+ },
+ expectedShownEvent: {
+ contexts: ["action", "all"],
+ viewType: undefined,
+ editable: false,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["action", "all"],
+ viewType: undefined,
+ editable: false,
+ pageUrl: PAGE,
+ },
+ async doOpenMenu(extension) {
+ await openActionContextMenu(extension, "browser");
+ },
+ async doCloseMenu() {
+ await closeActionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_browserAction_popup() {
+ let popupUrl;
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "browserAction popup - TEST_EXPECT_NO_TAB",
+ contexts: ["all", "browser_action"],
+ },
+ expectedShownEvent: {
+ contexts: ["page", "all"],
+ viewType: "popup",
+ frameId: 0,
+ editable: false,
+ get pageUrl() {
+ return popupUrl;
+ },
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["page", "all"],
+ viewType: "popup",
+ frameId: 0,
+ editable: false,
+ get pageUrl() {
+ return popupUrl;
+ },
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu(extension) {
+ popupUrl = `moz-extension://${extension.uuid}/popup.html`;
+ await clickBrowserAction(extension);
+ await openContextMenuInPopup(extension);
+ },
+ async doCloseMenu(extension) {
+ await closeExtensionContextMenu();
+ await closeBrowserAction(extension);
+ },
+ });
+});
+
+add_task(async function test_show_hide_browserAction_popup_v3() {
+ let popupUrl;
+ await testShowHideEvent({
+ manifest_version: 3,
+ id: "browser-action-popup@mochitest",
+ menuCreateParams: {
+ id: "action_popup",
+ title: "Action popup - TEST_EXPECT_NO_TAB",
+ contexts: ["all", "action"],
+ },
+ expectedShownEvent: {
+ contexts: ["page", "all"],
+ viewType: "popup",
+ frameId: 0,
+ editable: false,
+ get pageUrl() {
+ return popupUrl;
+ },
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["page", "all"],
+ viewType: "popup",
+ frameId: 0,
+ editable: false,
+ get pageUrl() {
+ return popupUrl;
+ },
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu(extension) {
+ popupUrl = `moz-extension://${extension.uuid}/popup.html`;
+ await clickBrowserAction(extension);
+ await openContextMenuInPopup(extension);
+ },
+ async doCloseMenu(extension) {
+ await closeExtensionContextMenu();
+ await closeBrowserAction(extension);
+ },
+ });
+});
+
+// Common code used by test_show_hide_tab and test_show_hide_tab_via_tab_panel.
+async function testShowHideTabMenu({
+ doOpenTabContextMenu,
+ doCloseTabContextMenu,
+}) {
+ await testShowHideEvent({
+ // To verify that the event matches the contextmenu target, switch to
+ // an unrelated tab before opening a contextmenu on the desired tab.
+ forceTabToBackground: true,
+ menuCreateParams: {
+ title: "tab menu item",
+ contexts: ["tab"],
+ },
+ expectedShownEvent: {
+ contexts: ["tab"],
+ viewType: undefined,
+ editable: false,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["tab"],
+ viewType: undefined,
+ editable: false,
+ pageUrl: PAGE,
+ },
+ async doOpenMenu(extension, contextTab) {
+ await doOpenTabContextMenu(contextTab);
+ },
+ async doCloseMenu() {
+ await doCloseTabContextMenu();
+ },
+ });
+}
+
+add_task(async function test_show_hide_tab() {
+ await testShowHideTabMenu({
+ async doOpenTabContextMenu(contextTab) {
+ await openTabContextMenu(contextTab);
+ },
+ async doCloseTabContextMenu() {
+ await closeTabContextMenu();
+ },
+ });
+});
+
+// Checks that right-clicking on a tab in the tabs panel (the one that appears
+// when there are many tabs, or when browser.tabs.tabmanager.enabled = true)
+// results in an event that is associated with the expected tab.
+add_task(async function test_show_hide_tab_via_tab_panel() {
+ gTabsPanel.init();
+ const tabContainer = document.getElementById("tabbrowser-tabs");
+ let shouldAddOverflow = !tabContainer.hasAttribute("overflow");
+ const revertTabContainerAttribute = () => {
+ if (shouldAddOverflow) {
+ // Revert attribute if it was changed.
+ tabContainer.removeAttribute("overflow");
+ // The function is going to be called twice, but let's run the logic once.
+ shouldAddOverflow = false;
+ }
+ };
+ if (shouldAddOverflow) {
+ // Ensure the visibility of the "all tabs menu" button (#alltabs-button).
+ tabContainer.setAttribute("overflow", "true");
+ // Register cleanup function in case the test fails before we reach the end.
+ registerCleanupFunction(revertTabContainerAttribute);
+ }
+
+ const allTabsView = document.getElementById("allTabsMenu-allTabsView");
+
+ await testShowHideTabMenu({
+ async doOpenTabContextMenu(contextTab) {
+ // Show the tabs panel.
+ let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
+ allTabsView,
+ "ViewShown"
+ );
+ gTabsPanel.showAllTabsPanel();
+ await allTabsPopupShownPromise;
+
+ // Find the menu item that is associated with the given tab
+ let index = Array.prototype.findIndex.call(
+ gTabsPanel.allTabsViewTabs.children,
+ toolbaritem => toolbaritem.tab === contextTab
+ );
+ Assert.notStrictEqual(
+ index,
+ -1,
+ "sanity check: tabs panel has item for the tab"
+ );
+
+ // Finally, open the context menu on it.
+ await openChromeContextMenu(
+ "tabContextMenu",
+ `.all-tabs-item:nth-child(${index + 1})`
+ );
+ },
+ async doCloseTabContextMenu() {
+ await closeTabContextMenu();
+ let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(
+ allTabsView.panelMultiView,
+ "PanelMultiViewHidden"
+ );
+ gTabsPanel.hideAllTabsPanel();
+ await allTabsPopupHiddenPromise;
+ },
+ });
+
+ revertTabContainerAttribute();
+});
+
+add_task(async function test_show_hide_tools_menu() {
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "menu item",
+ contexts: ["tools_menu"],
+ },
+ expectedShownEvent: {
+ contexts: ["tools_menu"],
+ viewType: undefined,
+ editable: false,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["tools_menu"],
+ viewType: undefined,
+ editable: false,
+ pageUrl: PAGE,
+ },
+ async doOpenMenu() {
+ await openToolsMenu();
+ },
+ async doCloseMenu() {
+ await closeToolsMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_page() {
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "page menu item",
+ contexts: ["page"],
+ },
+ expectedShownEvent: {
+ contexts: ["page", "all"],
+ viewType: "tab",
+ editable: false,
+ frameId: 0,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["page", "all"],
+ viewType: "tab",
+ editable: false,
+ pageUrl: PAGE,
+ frameId: 0,
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu() {
+ await openContextMenu("body");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_frame() {
+ // frame info will be determined before opening the menu.
+ let frameId;
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "subframe menu item",
+ contexts: ["frame"],
+ },
+ expectedShownEvent: {
+ contexts: ["frame", "all"],
+ viewType: "tab",
+ editable: false,
+ get frameId() {
+ return frameId;
+ },
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["frame", "all"],
+ viewType: "tab",
+ editable: false,
+ get frameId() {
+ return frameId;
+ },
+ pageUrl: PAGE,
+ frameUrl: PAGE_BASE + "context_frame.html",
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu() {
+ frameId = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ const { WebNavigationFrames } = ChromeUtils.importESModule(
+ "resource://gre/modules/WebNavigationFrames.sys.mjs"
+ );
+
+ let { contentWindow } = content.document.getElementById("frame");
+ return WebNavigationFrames.getFrameId(contentWindow);
+ }
+ );
+ await openContextMenuInFrame();
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_password() {
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "password item",
+ contexts: ["password"],
+ },
+ expectedShownEvent: {
+ contexts: ["editable", "password", "all"],
+ viewType: "tab",
+ editable: true,
+ frameId: 0,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["editable", "password", "all"],
+ viewType: "tab",
+ editable: true,
+ frameId: 0,
+ pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu() {
+ await openContextMenu("#password");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_link() {
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "link item",
+ contexts: ["link"],
+ },
+ expectedShownEvent: {
+ contexts: ["link", "all"],
+ viewType: "tab",
+ editable: false,
+ frameId: 0,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["link", "all"],
+ viewType: "tab",
+ editable: false,
+ frameId: 0,
+ linkText: "Some link",
+ linkUrl: PAGE_BASE + "some-link",
+ pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu() {
+ await openContextMenu("#link1");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_image_link() {
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "image item",
+ contexts: ["image"],
+ },
+ expectedShownEvent: {
+ contexts: ["image", "link", "all"],
+ viewType: "tab",
+ mediaType: "image",
+ editable: false,
+ frameId: 0,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["image", "link", "all"],
+ viewType: "tab",
+ mediaType: "image",
+ editable: false,
+ frameId: 0,
+ // Apparently, when a link has no content, its href is used as linkText.
+ linkText: PAGE_BASE + "image-around-some-link",
+ linkUrl: PAGE_BASE + "image-around-some-link",
+ srcUrl: PAGE_BASE + "ctxmenu-image.png",
+ pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu() {
+ await openContextMenu("#img-wrapped-in-link");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_editable_selection() {
+ let selectionText;
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "editable item",
+ contexts: ["editable"],
+ },
+ expectedShownEvent: {
+ contexts: ["editable", "selection", "all"],
+ viewType: "tab",
+ editable: true,
+ frameId: 0,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["editable", "selection", "all"],
+ viewType: "tab",
+ editable: true,
+ frameId: 0,
+ pageUrl: PAGE,
+ get selectionText() {
+ return selectionText;
+ },
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu() {
+ // Select lots of text in the test page before opening the menu.
+ selectionText = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ let node = content.document.getElementById("editabletext");
+ node.scrollIntoView();
+ node.select();
+ node.focus();
+ return node.value;
+ }
+ );
+
+ await openContextMenu("#editabletext");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_video() {
+ const VIDEO_URL = "data:video/webm,xxx";
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "video item",
+ contexts: ["video"],
+ },
+ expectedShownEvent: {
+ contexts: ["video", "all"],
+ viewType: "tab",
+ mediaType: "video",
+ editable: false,
+ frameId: 0,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["video", "all"],
+ viewType: "tab",
+ mediaType: "video",
+ editable: false,
+ frameId: 0,
+ srcUrl: VIDEO_URL,
+ pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [VIDEO_URL],
+ function (VIDEO_URL) {
+ let video = content.document.createElement("video");
+ video.controls = true;
+ video.src = VIDEO_URL;
+ content.document.body.appendChild(video);
+ video.scrollIntoView();
+ video.focus();
+ }
+ );
+
+ await openContextMenu("video");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+});
+
+add_task(async function test_show_hide_audio() {
+ const AUDIO_URL = "data:audio/ogg,xxx";
+ await testShowHideEvent({
+ menuCreateParams: {
+ title: "audio item",
+ contexts: ["audio"],
+ },
+ expectedShownEvent: {
+ contexts: ["audio", "all"],
+ viewType: "tab",
+ mediaType: "audio",
+ editable: false,
+ frameId: 0,
+ },
+ expectedShownEventWithPermissions: {
+ contexts: ["audio", "all"],
+ viewType: "tab",
+ mediaType: "audio",
+ editable: false,
+ frameId: 0,
+ srcUrl: AUDIO_URL,
+ pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
+ },
+ async doOpenMenu() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [AUDIO_URL],
+ function (AUDIO_URL) {
+ let audio = content.document.createElement("audio");
+ audio.controls = true;
+ audio.src = AUDIO_URL;
+ content.document.body.appendChild(audio);
+ audio.scrollIntoView();
+ audio.focus();
+ }
+ );
+
+ await openContextMenu("audio");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_events_after_context_destroy.js b/browser/components/extensions/test/browser/browser_ext_menus_events_after_context_destroy.js
new file mode 100644
index 0000000000..317f9c4321
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_events_after_context_destroy.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This test does verify that the menus API events are still emitted when
+// there are extension context alive with subscribed listeners
+// (See Bug 1602384).
+add_task(async function test_subscribed_events_fired_after_context_destroy() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ files: {
+ "page.html": `
+
+ Extension Page
+ `,
+ "page.js": async function () {
+ browser.menus.onShown.addListener(() => {
+ browser.test.sendMessage("menu-onShown");
+ });
+ browser.menus.onHidden.addListener(() => {
+ browser.test.sendMessage("menu-onHidden");
+ });
+ // Call an API method implemented in the parent process
+ // to ensure the menu listeners are subscribed in the
+ // parent process.
+ await browser.runtime.getBrowserInfo();
+ browser.test.sendMessage("page-loaded");
+ },
+ },
+ });
+
+ await extension.startup();
+ const pageURL = `moz-extension://${extension.uuid}/page.html`;
+
+ info("Loading extension page in a tab");
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageURL);
+ await extension.awaitMessage("page-loaded");
+
+ info("Loading extension page in another tab");
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageURL);
+ await extension.awaitMessage("page-loaded");
+
+ info("Remove the first tab");
+ BrowserTestUtils.removeTab(tab1);
+
+ info("Open a context menu and expect menu.onShown to be fired");
+ await openContextMenu("body");
+
+ await extension.awaitMessage("menu-onShown");
+
+ info("Close context menu and expect menu.onHidden to be fired");
+ await closeExtensionContextMenu();
+ await extension.awaitMessage("menu-onHidden");
+
+ ok(true, "The expected menu events have been fired");
+
+ BrowserTestUtils.removeTab(tab2);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_incognito.js b/browser/components/extensions/test/browser/browser_ext_menus_incognito.js
new file mode 100644
index 0000000000..397b140ab6
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_incognito.js
@@ -0,0 +1,155 @@
+"use strict";
+
+// Make sure that we won't trigger events for a private window.
+add_task(async function test_no_show_hide_for_private_window() {
+ function background() {
+ let events = [];
+ browser.menus.onShown.addListener(data => events.push(data));
+ browser.menus.onHidden.addListener(() => events.push("onHidden"));
+ browser.test.onMessage.addListener(async (name, data) => {
+ if (name == "check-events") {
+ browser.test.sendMessage("events", events);
+ events = [];
+ }
+ if (name == "create-menu") {
+ let id = await new Promise(resolve => {
+ let mid = browser.menus.create(data, () => resolve(mid));
+ });
+ browser.test.sendMessage("menu-id", id);
+ }
+ });
+ }
+
+ let pb_extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "@private-allowed" },
+ },
+ permissions: ["menus", "tabs"],
+ },
+ incognitoOverride: "spanning",
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "@not-allowed" },
+ },
+ permissions: ["menus", "tabs"],
+ },
+ });
+
+ async function testEvents(ext, expected) {
+ ext.sendMessage("check-events");
+ let events = await ext.awaitMessage("events");
+ Assert.deepEqual(
+ expected,
+ events,
+ `expected events received for ${ext.id}.`
+ );
+ }
+
+ await pb_extension.startup();
+ await extension.startup();
+
+ extension.sendMessage("create-menu", {
+ title: "not_allowed",
+ contexts: ["all", "tools_menu"],
+ });
+ let id1 = await extension.awaitMessage("menu-id");
+ let extMenuId = `${makeWidgetId(extension.id)}-menuitem-${id1}`;
+ pb_extension.sendMessage("create-menu", {
+ title: "spanning_allowed",
+ contexts: ["all", "tools_menu"],
+ });
+ let id2 = await pb_extension.awaitMessage("menu-id");
+ let pb_extMenuId = `${makeWidgetId(pb_extension.id)}-menuitem-${id2}`;
+
+ // Expected menu events
+ let baseShownEvent = {
+ contexts: ["page", "all"],
+ viewType: "tab",
+ frameId: 0,
+ editable: false,
+ };
+ let publicShown = { menuIds: [id1], ...baseShownEvent };
+ let privateShown = { menuIds: [id2], ...baseShownEvent };
+
+ baseShownEvent = {
+ contexts: ["tools_menu"],
+ viewType: undefined,
+ editable: false,
+ };
+ let toolsShown = { menuIds: [id1], ...baseShownEvent };
+ let privateToolsShown = { menuIds: [id2], ...baseShownEvent };
+
+ // Run tests in non-private window
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+
+ // Open and close a menu on the public window.
+ await openContextMenu("body");
+
+ // We naturally expect both extensions here.
+ ok(document.getElementById(extMenuId), `menu exists ${extMenuId}`);
+ ok(document.getElementById(pb_extMenuId), `menu exists ${pb_extMenuId}`);
+ await closeContextMenu();
+
+ await testEvents(extension, [publicShown, "onHidden"]);
+ await testEvents(pb_extension, [privateShown, "onHidden"]);
+
+ await openToolsMenu();
+ ok(document.getElementById(extMenuId), `menu exists ${extMenuId}`);
+ ok(document.getElementById(pb_extMenuId), `menu exists ${pb_extMenuId}`);
+ await closeToolsMenu();
+
+ await testEvents(extension, [toolsShown, "onHidden"]);
+ await testEvents(pb_extension, [privateToolsShown, "onHidden"]);
+
+ // Run tests on private window
+
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open and close a menu on the private window.
+ let menu = await openContextMenu("body div", privateWindow);
+ // We should not see the "not_allowed" extension here.
+ ok(
+ !privateWindow.document.getElementById(extMenuId),
+ `menu does not exist ${extMenuId} in private window`
+ );
+ ok(
+ privateWindow.document.getElementById(pb_extMenuId),
+ `menu exists ${pb_extMenuId} in private window`
+ );
+ await closeContextMenu(menu);
+
+ await testEvents(extension, []);
+ await testEvents(pb_extension, [privateShown, "onHidden"]);
+
+ await openToolsMenu(privateWindow);
+ // We should not see the "not_allowed" extension here.
+ ok(
+ !privateWindow.document.getElementById(extMenuId),
+ `menu does not exist ${extMenuId} in private window`
+ );
+ ok(
+ privateWindow.document.getElementById(pb_extMenuId),
+ `menu exists ${pb_extMenuId} in private window`
+ );
+ await closeToolsMenu(undefined, privateWindow);
+
+ await testEvents(extension, []);
+ await testEvents(pb_extension, [privateToolsShown, "onHidden"]);
+
+ await extension.unload();
+ await pb_extension.unload();
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_refresh.js b/browser/components/extensions/test/browser/browser_ext_menus_refresh.js
new file mode 100644
index 0000000000..836f370039
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_refresh.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/. */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+// Load an extension that has the "menus" permission. The returned Extension
+// instance has a `callMenuApi` method to easily call a browser.menus method
+// and wait for its result. It also emits the "onShown fired" message whenever
+// the menus.onShown event is fired.
+// The `getXULElementByMenuId` method returns the XUL element that corresponds
+// to the menu item ID from the browser.menus API (if existent, null otherwise).
+function loadExtensionWithMenusApi() {
+ async function background() {
+ function shownHandler() {
+ browser.test.sendMessage("onShown fired");
+ }
+
+ browser.menus.onShown.addListener(shownHandler);
+ browser.test.onMessage.addListener((method, ...params) => {
+ let result;
+ if (method === "* remove onShown listener") {
+ browser.menus.onShown.removeListener(shownHandler);
+ result = Promise.resolve();
+ } else if (method === "create") {
+ result = new Promise(resolve => {
+ browser.menus.create(params[0], resolve);
+ });
+ } else {
+ result = browser.menus[method](...params);
+ }
+ result.then(() => {
+ browser.test.sendMessage(`${method}-result`);
+ });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ permissions: ["menus"],
+ },
+ });
+
+ extension.callMenuApi = async function (method, ...params) {
+ info(`Calling ${method}(${JSON.stringify(params)})`);
+ extension.sendMessage(method, ...params);
+ return extension.awaitMessage(`${method}-result`);
+ };
+
+ extension.removeOnShownListener = async function () {
+ extension.callMenuApi("* remove onShown listener");
+ };
+
+ extension.getXULElementByMenuId = id => {
+ // Same implementation as elementId getter in ext-menus.js
+ if (typeof id != "number") {
+ id = `_${id}`;
+ }
+ let xulId = `${makeWidgetId(extension.id)}-menuitem-${id}`;
+ return document.getElementById(xulId);
+ };
+
+ return extension;
+}
+
+// Tests whether browser.menus.refresh works as expected with respect to the
+// menu items that are added/updated/removed before/during/after opening a menu:
+// - browser.refresh before a menu is shown should not have any effect.
+// - browser.refresh while a menu is shown should update the menu.
+// - browser.refresh after a menu is hidden should not have any effect.
+async function testRefreshMenusWhileVisible({
+ contexts,
+ doOpenMenu,
+ doCloseMenu,
+}) {
+ let extension = loadExtensionWithMenusApi();
+ await extension.startup();
+ await extension.callMenuApi("create", {
+ id: "abc",
+ title: "first",
+ contexts,
+ });
+ let elem = extension.getXULElementByMenuId("abc");
+ is(elem, null, "Menu item should not be visible");
+
+ // Refresh before a menu is shown - should be noop.
+ await extension.callMenuApi("refresh");
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem, null, "Menu item should still not be visible");
+
+ // Open menu and expect menu to be rendered.
+ await doOpenMenu(extension);
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem.getAttribute("label"), "first", "expected label");
+
+ await extension.awaitMessage("onShown fired");
+
+ // Add new menus, but don't expect them to be rendered yet.
+ await extension.callMenuApi("update", "abc", { title: "updated first" });
+ await extension.callMenuApi("create", {
+ id: "def",
+ title: "second",
+ contexts,
+ });
+
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem.getAttribute("label"), "first", "expected unchanged label");
+ elem = extension.getXULElementByMenuId("def");
+ is(elem, null, "Second menu item should not be visible");
+
+ // Refresh while a menu is shown - should be updated.
+ await extension.callMenuApi("refresh");
+
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem.getAttribute("label"), "updated first", "expected updated label");
+ elem = extension.getXULElementByMenuId("def");
+ is(elem.getAttribute("label"), "second", "expected second label");
+
+ // Update the two menu items again.
+ await extension.callMenuApi("update", "abc", { enabled: false });
+ await extension.callMenuApi("update", "def", { enabled: false });
+ await extension.callMenuApi("refresh");
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem.getAttribute("disabled"), "true", "1st menu item should be disabled");
+ elem = extension.getXULElementByMenuId("def");
+ is(elem.getAttribute("disabled"), "true", "2nd menu item should be disabled");
+
+ // Remove one.
+ await extension.callMenuApi("remove", "abc");
+ await extension.callMenuApi("refresh");
+ elem = extension.getXULElementByMenuId("def");
+ is(elem.getAttribute("label"), "second", "other menu item should exist");
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem, null, "removed menu item should be gone");
+
+ // Remove the last one.
+ await extension.callMenuApi("removeAll");
+ await extension.callMenuApi("refresh");
+ elem = extension.getXULElementByMenuId("def");
+ is(elem, null, "all menu items should be gone");
+
+ // At this point all menu items have been removed. Create a new menu item so
+ // we can confirm that browser.menus.refresh() does not render the menu item
+ // after the menu has been hidden.
+ await extension.callMenuApi("create", {
+ // The menu item with ID "abc" was removed before, so re-using the ID should
+ // not cause any issues:
+ id: "abc",
+ title: "re-used",
+ contexts,
+ });
+ await extension.callMenuApi("refresh");
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem.getAttribute("label"), "re-used", "menu item should be created");
+
+ await doCloseMenu();
+
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem, null, "menu item must be gone");
+
+ // Refresh after menu was hidden - should be noop.
+ await extension.callMenuApi("refresh");
+ elem = extension.getXULElementByMenuId("abc");
+ is(elem, null, "menu item must still be gone");
+
+ await extension.unload();
+}
+
+// Check that one extension calling refresh() doesn't interfere with others.
+// When expectOtherItems == false, the other extension's menu items should not
+// show at all (e.g. for browserAction).
+async function testRefreshOther({
+ contexts,
+ doOpenMenu,
+ doCloseMenu,
+ expectOtherItems,
+}) {
+ let extension = loadExtensionWithMenusApi();
+ let other_extension = loadExtensionWithMenusApi();
+ await extension.startup();
+ await other_extension.startup();
+
+ await extension.callMenuApi("create", {
+ id: "action_item",
+ title: "visible menu item",
+ contexts: contexts,
+ });
+
+ await other_extension.callMenuApi("create", {
+ id: "action_item",
+ title: "other menu item",
+ contexts: contexts,
+ });
+
+ await doOpenMenu(extension);
+ await extension.awaitMessage("onShown fired");
+ if (expectOtherItems) {
+ await other_extension.awaitMessage("onShown fired");
+ }
+
+ let elem = extension.getXULElementByMenuId("action_item");
+ is(elem.getAttribute("label"), "visible menu item", "extension menu shown");
+ elem = other_extension.getXULElementByMenuId("action_item");
+ if (expectOtherItems) {
+ is(
+ elem.getAttribute("label"),
+ "other menu item",
+ "other extension's menu is also shown"
+ );
+ } else {
+ is(elem, null, "other extension's menu should be hidden");
+ }
+
+ await extension.callMenuApi("update", "action_item", { title: "changed" });
+ await other_extension.callMenuApi("update", "action_item", { title: "foo" });
+ await other_extension.callMenuApi("refresh");
+
+ // refreshing the menu of an unrelated extension should not affect the menu
+ // of another extension.
+ elem = extension.getXULElementByMenuId("action_item");
+ is(elem.getAttribute("label"), "visible menu item", "extension menu shown");
+ elem = other_extension.getXULElementByMenuId("action_item");
+ if (expectOtherItems) {
+ is(elem.getAttribute("label"), "foo", "other extension's item is updated");
+ } else {
+ is(elem, null, "other extension's menu should still be hidden");
+ }
+
+ await doCloseMenu();
+ await extension.unload();
+ await other_extension.unload();
+}
+
+add_task(async function refresh_menus_with_browser_action() {
+ const args = {
+ contexts: ["browser_action"],
+ async doOpenMenu(extension) {
+ await openActionContextMenu(extension, "browser");
+ },
+ async doCloseMenu() {
+ await closeActionContextMenu();
+ },
+ };
+ await testRefreshMenusWhileVisible(args);
+ args.expectOtherItems = false;
+ await testRefreshOther(args);
+});
+
+add_task(async function refresh_menus_with_tab() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ const args = {
+ contexts: ["tab"],
+ async doOpenMenu() {
+ await openTabContextMenu();
+ },
+ async doCloseMenu() {
+ await closeTabContextMenu();
+ },
+ };
+ await testRefreshMenusWhileVisible(args);
+ args.expectOtherItems = true;
+ await testRefreshOther(args);
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function refresh_menus_with_tools_menu() {
+ const args = {
+ contexts: ["tools_menu"],
+ async doOpenMenu() {
+ await openToolsMenu();
+ },
+ async doCloseMenu() {
+ await closeToolsMenu();
+ },
+ };
+ await testRefreshMenusWhileVisible(args);
+ args.expectOtherItems = true;
+ await testRefreshOther(args);
+});
+
+add_task(async function refresh_menus_with_page() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ const args = {
+ contexts: ["page"],
+ async doOpenMenu() {
+ await openContextMenu("body");
+ },
+ async doCloseMenu() {
+ await closeExtensionContextMenu();
+ },
+ };
+ await testRefreshMenusWhileVisible(args);
+ args.expectOtherItems = true;
+ await testRefreshOther(args);
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function refresh_without_menus_at_onShown() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ let extension = loadExtensionWithMenusApi();
+ await extension.startup();
+
+ const doOpenMenu = () => openContextMenu("body");
+ const doCloseMenu = () => closeExtensionContextMenu();
+
+ await doOpenMenu();
+ await extension.awaitMessage("onShown fired");
+ await extension.callMenuApi("create", {
+ id: "too late",
+ title: "created after shown",
+ });
+ await extension.callMenuApi("refresh");
+ let elem = extension.getXULElementByMenuId("too late");
+ is(
+ elem.getAttribute("label"),
+ "created after shown",
+ "extension without visible menu items can add new items"
+ );
+
+ await extension.callMenuApi("update", "too late", { title: "the menu item" });
+ await extension.callMenuApi("refresh");
+ elem = extension.getXULElementByMenuId("too late");
+ is(elem.getAttribute("label"), "the menu item", "label should change");
+
+ // The previously created menu item should be visible if the menu is closed
+ // and re-opened.
+ await doCloseMenu();
+ await doOpenMenu();
+ await extension.awaitMessage("onShown fired");
+ elem = extension.getXULElementByMenuId("too late");
+ is(elem.getAttribute("label"), "the menu item", "previously registered item");
+ await doCloseMenu();
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function refresh_without_onShown() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ let extension = loadExtensionWithMenusApi();
+ await extension.startup();
+ await extension.removeOnShownListener();
+
+ const doOpenMenu = () => openContextMenu("body");
+ const doCloseMenu = () => closeExtensionContextMenu();
+
+ await doOpenMenu();
+ await extension.callMenuApi("create", {
+ id: "too late",
+ title: "created after shown",
+ });
+
+ is(
+ extension.getXULElementByMenuId("too late"),
+ null,
+ "item created after shown is not visible before refresh"
+ );
+
+ await extension.callMenuApi("refresh");
+ let elem = extension.getXULElementByMenuId("too late");
+ is(
+ elem.getAttribute("label"),
+ "created after shown",
+ "refresh updates the menu even without onShown"
+ );
+
+ await doCloseMenu();
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function refresh_menus_during_navigation() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ PAGE + "?1"
+ );
+ let extension = loadExtensionWithMenusApi();
+ await extension.startup();
+
+ await extension.callMenuApi("create", {
+ id: "item1",
+ title: "item1",
+ contexts: ["browser_action"],
+ documentUrlPatterns: ["*://*/*?1*"],
+ });
+
+ await extension.callMenuApi("create", {
+ id: "item2",
+ title: "item2",
+ contexts: ["browser_action"],
+ documentUrlPatterns: ["*://*/*?2*"],
+ });
+
+ await openActionContextMenu(extension, "browser");
+ await extension.awaitMessage("onShown fired");
+
+ let elem = extension.getXULElementByMenuId("item1");
+ is(elem.getAttribute("label"), "item1", "menu item 1 should be shown");
+ elem = extension.getXULElementByMenuId("item2");
+ is(elem, null, "menu item 2 should be hidden");
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, PAGE + "?2");
+ await BrowserTestUtils.browserStopped(tab.linkedBrowser);
+
+ await extension.callMenuApi("refresh");
+
+ // The menu items in a context menu are based on the context at the time of
+ // opening the menu. Menus are not updated if the context changes, e.g. as a
+ // result of navigation events after the menu was shown.
+ // So when refresh() is called during the onShown event, then the original
+ // URL (before navigation) should be used to determine whether to show a
+ // URL-specific menu item, and NOT the current URL (after navigation).
+ elem = extension.getXULElementByMenuId("item1");
+ is(elem.getAttribute("label"), "item1", "menu item 1 should still be shown");
+ elem = extension.getXULElementByMenuId("item2");
+ is(elem, null, "menu item 2 should still be hidden");
+
+ await closeActionContextMenu();
+ await openActionContextMenu(extension, "browser");
+ await extension.awaitMessage("onShown fired");
+
+ // Now after closing and re-opening the menu, the latest contextual info
+ // should be used.
+ elem = extension.getXULElementByMenuId("item1");
+ is(elem, null, "menu item 1 should be hidden");
+ elem = extension.getXULElementByMenuId("item2");
+ is(elem.getAttribute("label"), "item2", "menu item 2 should be shown");
+
+ await closeActionContextMenu();
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
new file mode 100644
index 0000000000..c430b6ad71
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
@@ -0,0 +1,525 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+function getVisibleChildrenIds(menuElem) {
+ return Array.from(menuElem.children)
+ .filter(elem => !elem.hidden)
+ .map(elem => (elem.tagName == "menuseparator" ? elem.tagName : elem.id));
+}
+
+function checkIsLinkMenuItemVisible(visibleMenuItemIds) {
+ // In most of this test file, we open a menu on a link. Assume that all
+ // relevant menu items are shown if one link-specific menu item is shown.
+ ok(
+ visibleMenuItemIds.includes("context-openlink"),
+ `The default 'Open Link in New Tab' menu item should be in ${visibleMenuItemIds}.`
+ );
+}
+
+// Tests the following:
+// - Calling overrideContext({}) during oncontextmenu forces the menu to only
+// show an extension's own items.
+// - These menu items all appear in the root menu.
+// - The usual extension filtering behavior (e.g. documentUrlPatterns and
+// targetUrlPatterns) is still applied; some menu items are therefore hidden.
+// - Calling overrideContext({showDefaults:true}) causes the default menu items
+// to be shown, but only after the extension's.
+// - overrideContext expires after the menu is opened once.
+// - overrideContext can be called from shadow DOM.
+add_task(async function overrideContext_in_extension_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.allow_eval_with_system_principal", true]],
+ });
+
+ function extensionTabScript() {
+ document.addEventListener(
+ "contextmenu",
+ () => {
+ browser.menus.overrideContext({});
+ browser.test.sendMessage("oncontextmenu_in_dom_part_1");
+ },
+ { once: true }
+ );
+
+ let shadowRoot = document
+ .getElementById("shadowHost")
+ .attachShadow({ mode: "open" });
+ shadowRoot.innerHTML = `Link `;
+ shadowRoot.firstChild.addEventListener(
+ "contextmenu",
+ () => {
+ browser.menus.overrideContext({});
+ browser.test.sendMessage("oncontextmenu_in_shadow_dom");
+ },
+ { once: true }
+ );
+
+ document.querySelector("p").addEventListener(
+ "contextmenu",
+ () => {
+ browser.menus.overrideContext({ showDefaults: true });
+ },
+ { once: true }
+ );
+
+ browser.menus.create({
+ id: "tab_1",
+ title: "tab_1",
+ documentUrlPatterns: [document.URL],
+ onclick() {
+ document.addEventListener(
+ "contextmenu",
+ () => {
+ // Verifies that last call takes precedence.
+ browser.menus.overrideContext({ showDefaults: false });
+ browser.menus.overrideContext({ showDefaults: true });
+ browser.test.sendMessage("oncontextmenu_in_dom_part_2");
+ },
+ { once: true }
+ );
+ browser.test.sendMessage("onClicked_tab_1");
+ },
+ });
+ browser.menus.create(
+ {
+ id: "tab_2",
+ title: "tab_2",
+ onclick() {
+ browser.test.sendMessage("onClicked_tab_2");
+ },
+ },
+ () => {
+ browser.test.sendMessage("menu-registered");
+ }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "menus.overrideContext"],
+ },
+ files: {
+ "tab.html": `
+
+ Link
+ Some text
+
+
+ `,
+ "tab.js": extensionTabScript,
+ },
+ background() {
+ // Expected to match and thus be visible.
+ browser.menus.create({ id: "bg_1", title: "bg_1" });
+ browser.menus.create({
+ id: "bg_2",
+ title: "bg_2",
+ targetUrlPatterns: ["*://example.com/*"],
+ });
+
+ // Expected to not match and be hidden.
+ browser.menus.create({
+ id: "bg_3",
+ title: "bg_3",
+ targetUrlPatterns: ["*://nomatch/*"],
+ });
+ browser.menus.create({
+ id: "bg_4",
+ title: "bg_4",
+ documentUrlPatterns: [document.URL],
+ });
+
+ browser.menus.onShown.addListener(info => {
+ browser.test.assertEq("tab", info.viewType, "Expected viewType");
+ let sortedContexts = info.contexts.sort().join(",");
+ if (info.contexts.includes("link")) {
+ browser.test.assertEq(
+ "bg_1,bg_2,tab_1,tab_2",
+ info.menuIds.join(","),
+ "Expected menu items."
+ );
+ browser.test.assertEq(
+ "all,link",
+ sortedContexts,
+ "Expected menu contexts"
+ );
+ } else if (info.contexts.includes("page")) {
+ browser.test.assertEq(
+ "bg_1,tab_1,tab_2",
+ info.menuIds.join(","),
+ "Expected menu items."
+ );
+ browser.test.assertEq(
+ "all,page",
+ sortedContexts,
+ "Expected menu contexts"
+ );
+ } else {
+ browser.test.fail(`Unexpected menu context: ${sortedContexts}`);
+ }
+ browser.test.sendMessage("onShown");
+ });
+
+ browser.tabs.create({ url: "tab.html" });
+ },
+ });
+
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background() {
+ browser.menus.create(
+ { id: "other_extension_item", title: "other_extension_item" },
+ () => {
+ browser.test.sendMessage("other_extension_item_created");
+ }
+ );
+ },
+ });
+ await otherExtension.startup();
+ await otherExtension.awaitMessage("other_extension_item_created");
+
+ let extensionTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ null,
+ true
+ );
+ await extension.startup();
+ // Must wait for the tab to have loaded completely before calling openContextMenu.
+ await extensionTabPromise;
+ await extension.awaitMessage("menu-registered");
+
+ const EXPECTED_EXTENSION_MENU_IDS = [
+ `${makeWidgetId(extension.id)}-menuitem-_bg_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_bg_2`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_2`,
+ ];
+
+ const EXPECTED_EXTENSION_MENU_IDS_NOLINK = [
+ `${makeWidgetId(extension.id)}-menuitem-_bg_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_2`,
+ ];
+ const OTHER_EXTENSION_MENU_ID = `${makeWidgetId(
+ otherExtension.id
+ )}-menuitem-_other_extension_item`;
+
+ {
+ // Tests overrideContext({})
+ info("Expecting the menu to be replaced by overrideContext.");
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom_part_1");
+ await extension.awaitMessage("onShown");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected only extension menu items"
+ );
+
+ let menuItems = menu.getElementsByAttribute("label", "tab_1");
+ await closeExtensionContextMenu(menuItems[0]);
+ await extension.awaitMessage("onClicked_tab_1");
+ }
+
+ {
+ // Tests overrideContext({showDefaults:true}))
+ info(
+ "Expecting the menu to be replaced by overrideContext, including default menu items."
+ );
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom_part_2");
+ await extension.awaitMessage("onShown");
+
+ let visibleMenuItemIds = getVisibleChildrenIds(menu);
+ Assert.deepEqual(
+ visibleMenuItemIds.slice(0, EXPECTED_EXTENSION_MENU_IDS.length),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected extension menu items at the start."
+ );
+
+ checkIsLinkMenuItemVisible(visibleMenuItemIds);
+
+ is(
+ visibleMenuItemIds[visibleMenuItemIds.length - 1],
+ OTHER_EXTENSION_MENU_ID,
+ "Other extension menu item should be at the end."
+ );
+
+ let menuItems = menu.getElementsByAttribute("label", "tab_2");
+ await closeExtensionContextMenu(menuItems[0]);
+ await extension.awaitMessage("onClicked_tab_2");
+ }
+
+ {
+ // Tests that previous overrideContext call has been forgotten,
+ // so the default behavior should occur (=move items into submenu).
+ info(
+ "Expecting the default menu to be used when overrideContext is not called."
+ );
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("onShown");
+
+ checkIsLinkMenuItemVisible(getVisibleChildrenIds(menu));
+
+ let menuItems = menu.getElementsByAttribute("ext-type", "top-level-menu");
+ is(menuItems.length, 1, "Expected top-level menu element for extension.");
+ let topLevelExtensionMenuItem = menuItems[0];
+ is(
+ topLevelExtensionMenuItem.nextSibling,
+ null,
+ "Extension menu should be the last element."
+ );
+
+ const submenu = await openSubmenu(topLevelExtensionMenuItem);
+ is(submenu, topLevelExtensionMenuItem.menupopup, "Correct submenu opened");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(submenu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Extension menu items should be in the submenu by default."
+ );
+
+ await closeContextMenu();
+ }
+
+ {
+ // Tests that overrideContext({}) can be used from a listener inside shadow DOM.
+ let menu = await openContextMenu(
+ () => this.document.getElementById("shadowHost").shadowRoot.firstChild
+ );
+ await extension.awaitMessage("oncontextmenu_in_shadow_dom");
+ await extension.awaitMessage("onShown");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ EXPECTED_EXTENSION_MENU_IDS,
+ "Expected only extension menu items after overrideContext({}) in shadow DOM"
+ );
+
+ await closeContextMenu();
+ }
+
+ {
+ // Tests overrideContext({showDefaults:true}) on a non-link
+ info(
+ "Expecting overrideContext to insert items after the navigation group."
+ );
+ let menu = await openContextMenu("p");
+ await extension.awaitMessage("onShown");
+
+ let visibleMenuItemIds = getVisibleChildrenIds(menu);
+ if (AppConstants.platform == "macosx") {
+ // On mac, the items should be at the top:
+ Assert.deepEqual(
+ visibleMenuItemIds.slice(0, EXPECTED_EXTENSION_MENU_IDS_NOLINK.length),
+ EXPECTED_EXTENSION_MENU_IDS_NOLINK,
+ "Expected extension menu items at the start."
+ );
+ } else {
+ // Elsewhere, they should be immediately after the navigation group:
+ Assert.deepEqual(
+ visibleMenuItemIds.slice(
+ 0,
+ 2 + EXPECTED_EXTENSION_MENU_IDS_NOLINK.length
+ ),
+ [
+ "context-navigation",
+ "menuseparator",
+ ...EXPECTED_EXTENSION_MENU_IDS_NOLINK,
+ ],
+ "Expected extension menu items immmediately after navigation items."
+ );
+ }
+ ok(
+ visibleMenuItemIds.includes("context-savepage"),
+ "Default menu items should be there."
+ );
+
+ is(
+ visibleMenuItemIds[visibleMenuItemIds.length - 1],
+ OTHER_EXTENSION_MENU_ID,
+ "Other extension menu item should be at the end."
+ );
+
+ await closeContextMenu();
+ }
+
+ // Unloading the extension will automatically close the extension's tab.html
+ await extension.unload();
+ await otherExtension.unload();
+});
+
+// Tests some edge cases:
+// - overrideContext() is called without any menu registrations,
+// followed by a menu registration + menus.refresh..
+// - overrideContext() is called and event.preventDefault() is also
+// called to stop the menu from appearing.
+// - Open menu again and verify that the default menu behavior occurs.
+add_task(async function overrideContext_sidebar_edge_cases() {
+ function sidebarJs() {
+ const TIME_BEFORE_MENU_SHOWN = Date.now();
+ let count = 0;
+ // eslint-disable-next-line mozilla/balanced-listeners
+ document.addEventListener("contextmenu", event => {
+ ++count;
+ if (count === 1) {
+ browser.menus.overrideContext({});
+ } else if (count === 2) {
+ browser.menus.overrideContext({});
+ event.preventDefault(); // Prevent menu from being shown.
+
+ // We are not expecting a menu. Wait for the time it took to show and
+ // hide the previous menu, to check that no new menu appears.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ browser.test.sendMessage(
+ "stop_waiting_for_menu_shown",
+ "timer_reached"
+ );
+ }, Date.now() - TIME_BEFORE_MENU_SHOWN);
+ } else if (count === 3) {
+ // The overrideContext from the previous call should be forgotten.
+ // Use the default behavior, i.e. show the default menu.
+ } else {
+ browser.test.fail(`Unexpected menu count: ${count}`);
+ }
+
+ browser.test.sendMessage("oncontextmenu_in_dom");
+ });
+
+ browser.menus.onShown.addListener(info => {
+ browser.test.assertEq("sidebar", info.viewType, "Expected viewType");
+ if (count === 1) {
+ browser.test.assertEq("", info.menuIds.join(","), "Expected no items");
+ browser.menus.create({ id: "some_item", title: "some_item" }, () => {
+ browser.test.sendMessage("onShown_1_and_menu_item_created");
+ });
+ } else if (count === 2) {
+ browser.test.fail(
+ "onShown should not have fired when the menu is not shown."
+ );
+ } else if (count === 3) {
+ browser.test.assertEq(
+ "some_item",
+ info.menuIds.join(","),
+ "Expected menu item"
+ );
+ browser.test.sendMessage("onShown_3");
+ } else {
+ browser.test.fail(`Unexpected onShown at count: ${count}`);
+ }
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.assertEq("refresh_menus", msg, "Expected message");
+ browser.test.assertEq(1, count, "Expected at first menu test");
+ await browser.menus.refresh();
+ browser.test.sendMessage("menus_refreshed");
+ });
+
+ browser.menus.onHidden.addListener(() => {
+ browser.test.sendMessage("onHidden", count);
+ });
+
+ browser.test.sendMessage("sidebar_ready");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ manifest: {
+ permissions: ["menus", "menus.overrideContext"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ files: {
+ "sidebar.html": `
+
+ Link
+
+ `,
+ "sidebar.js": sidebarJs,
+ },
+ background() {
+ browser.test.assertThrows(
+ () => {
+ browser.menus.overrideContext({ someInvalidParameter: true });
+ },
+ /Unexpected property "someInvalidParameter"/,
+ "overrideContext should be available and the parameters be validated."
+ );
+ browser.test.sendMessage("bg_test_done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bg_test_done");
+ await extension.awaitMessage("sidebar_ready");
+
+ const EXPECTED_EXTENSION_MENU_ID = `${makeWidgetId(
+ extension.id
+ )}-menuitem-_some_item`;
+
+ {
+ // Checks that a menu can initially be empty and be updated.
+ info(
+ "Expecting menu without items to appear and be updated after menus.refresh()"
+ );
+ let menu = await openContextMenuInSidebar("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+ await extension.awaitMessage("onShown_1_and_menu_item_created");
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ [],
+ "Expected no items, initially"
+ );
+ extension.sendMessage("refresh_menus");
+ await extension.awaitMessage("menus_refreshed");
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ [EXPECTED_EXTENSION_MENU_ID],
+ "Expected updated menu"
+ );
+ await closeContextMenu(menu);
+ is(await extension.awaitMessage("onHidden"), 1, "Menu hidden");
+ }
+
+ {
+ // Trigger a context menu. The page has prevented the menu from being
+ // shown, so the promise should not resolve.
+ info("Expecting menu to not appear because of event.preventDefault()");
+ let popupShowingPromise = openContextMenuInSidebar("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+ is(
+ await Promise.race([
+ extension.awaitMessage("stop_waiting_for_menu_shown"),
+ popupShowingPromise.then(() => "popup_shown"),
+ ]),
+ "timer_reached",
+ "The menu should not be shown."
+ );
+ }
+
+ {
+ info(
+ "Expecting default menu to be shown when the menu is reopened after event.preventDefault()"
+ );
+ let menu = await openContextMenuInSidebar("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+ await extension.awaitMessage("onShown_3");
+ let visibleMenuItemIds = getVisibleChildrenIds(menu);
+ checkIsLinkMenuItemVisible(visibleMenuItemIds);
+ ok(
+ visibleMenuItemIds.includes(EXPECTED_EXTENSION_MENU_ID),
+ "Expected extension menu item"
+ );
+ await closeContextMenu(menu);
+ is(await extension.awaitMessage("onHidden"), 3, "Menu hidden");
+ }
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
new file mode 100644
index 0000000000..d3180ea487
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
@@ -0,0 +1,475 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+function getVisibleChildrenIds(menuElem) {
+ return Array.from(menuElem.children)
+ .filter(elem => !elem.hidden)
+ .map(elem => (elem.tagName != "menuseparator" ? elem.id : elem.tagName));
+}
+
+function checkIsDefaultMenuItemVisible(visibleMenuItemIds) {
+ // In this whole test file, we open a menu on a link. Assume that all
+ // default menu items are shown if one link-specific menu item is shown.
+ ok(
+ visibleMenuItemIds.includes("context-openlink"),
+ `The default 'Open Link in New Tab' menu item should be in ${visibleMenuItemIds}.`
+ );
+}
+
+// Tests that the context of an extension menu can be changed to:
+// - tab
+// - bookmark
+add_task(async function overrideContext_with_context() {
+ // Background script of the main test extension and the auxilary other extension.
+ function background() {
+ const HTTP_URL = "http://example.com/?SomeTab";
+ browser.test.onMessage.addListener(async (msg, tabId) => {
+ browser.test.assertEq(
+ "testTabAccess",
+ msg,
+ `Expected message in ${browser.runtime.id}`
+ );
+ let tab = await browser.tabs.get(tabId);
+ if (!tab.url) {
+ // tabs or activeTab not active.
+ browser.test.sendMessage("testTabAccessDone", "tab_no_url");
+ return;
+ }
+ try {
+ let [url] = await browser.tabs.executeScript(tabId, {
+ code: "document.URL",
+ });
+ browser.test.assertEq(
+ HTTP_URL,
+ url,
+ "Expected successful executeScript"
+ );
+ browser.test.sendMessage("testTabAccessDone", "executeScript_ok");
+ } catch (e) {
+ browser.test.assertEq(
+ "Missing host permission for the tab",
+ e.message,
+ "Expected error message"
+ );
+ browser.test.sendMessage("testTabAccessDone", "executeScript_failed");
+ }
+ });
+ browser.menus.onShown.addListener((info, tab) => {
+ browser.test.assertEq(
+ "tab",
+ info.viewType,
+ "Expected viewType at onShown"
+ );
+ browser.test.assertEq(
+ undefined,
+ info.linkUrl,
+ "Expected linkUrl at onShown"
+ );
+ browser.test.assertEq(
+ undefined,
+ info.srckUrl,
+ "Expected srcUrl at onShown"
+ );
+ browser.test.sendMessage("onShown", {
+ menuIds: info.menuIds.sort(),
+ contexts: info.contexts,
+ bookmarkId: info.bookmarkId,
+ pageUrl: info.pageUrl,
+ frameUrl: info.frameUrl,
+ tabId: tab && tab.id,
+ });
+ });
+ browser.menus.onClicked.addListener((info, tab) => {
+ browser.test.assertEq(
+ "tab",
+ info.viewType,
+ "Expected viewType at onClicked"
+ );
+ browser.test.assertEq(
+ undefined,
+ info.linkUrl,
+ "Expected linkUrl at onClicked"
+ );
+ browser.test.assertEq(
+ undefined,
+ info.srckUrl,
+ "Expected srcUrl at onClicked"
+ );
+ browser.test.sendMessage("onClicked", {
+ menuItemId: info.menuItemId,
+ bookmarkId: info.bookmarkId,
+ pageUrl: info.pageUrl,
+ frameUrl: info.frameUrl,
+ tabId: tab && tab.id,
+ });
+ });
+
+ // Minimal properties to define menu items for a specific context.
+ browser.menus.create({
+ id: "tab_context",
+ title: "tab_context",
+ contexts: ["tab"],
+ });
+ browser.menus.create({
+ id: "bookmark_context",
+ title: "bookmark_context",
+ contexts: ["bookmark"],
+ });
+
+ // documentUrlPatterns in the tab context applies to the tab's URL.
+ browser.menus.create({
+ id: "tab_context_http",
+ title: "tab_context_http",
+ contexts: ["tab"],
+ documentUrlPatterns: [HTTP_URL],
+ });
+ browser.menus.create({
+ id: "tab_context_moz_unexpected",
+ title: "tab_context_moz",
+ contexts: ["tab"],
+ documentUrlPatterns: ["moz-extension://*/tab.html"],
+ });
+ // When viewTypes is present, the document's URL is matched instead.
+ browser.menus.create({
+ id: "tab_context_viewType_http_unexpected",
+ title: "tab_context_viewType_http",
+ contexts: ["tab"],
+ viewTypes: ["tab"],
+ documentUrlPatterns: [HTTP_URL],
+ });
+ browser.menus.create({
+ id: "tab_context_viewType_moz",
+ title: "tab_context_viewType_moz",
+ contexts: ["tab"],
+ viewTypes: ["tab"],
+ documentUrlPatterns: ["moz-extension://*/tab.html"],
+ });
+
+ // documentUrlPatterns is not restricting bookmark menu items.
+ browser.menus.create({
+ id: "bookmark_context_http",
+ title: "bookmark_context_http",
+ contexts: ["bookmark"],
+ documentUrlPatterns: [HTTP_URL],
+ });
+ browser.menus.create({
+ id: "bookmark_context_moz",
+ title: "bookmark_context_moz",
+ contexts: ["bookmark"],
+ documentUrlPatterns: ["moz-extension://*/tab.html"],
+ });
+ // When viewTypes is present, the document's URL is matched instead.
+ browser.menus.create({
+ id: "bookmark_context_viewType_http_unexpected",
+ title: "bookmark_context_viewType_http",
+ contexts: ["bookmark"],
+ viewTypes: ["tab"],
+ documentUrlPatterns: [HTTP_URL],
+ });
+ browser.menus.create({
+ id: "bookmark_context_viewType_moz",
+ title: "bookmark_context_viewType_moz",
+ contexts: ["bookmark"],
+ viewTypes: ["tab"],
+ documentUrlPatterns: ["moz-extension://*/tab.html"],
+ });
+
+ browser.menus.create({ id: "link_context", title: "link_context" }, () => {
+ browser.test.sendMessage("menu_items_registered");
+ });
+
+ if (browser.runtime.id === "@menu-test-extension") {
+ browser.tabs.create({ url: "tab.html" });
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "@menu-test-extension" } },
+ permissions: ["menus", "menus.overrideContext", "tabs", "bookmarks"],
+ },
+ files: {
+ "tab.html": `
+
+ Link
+
+ `,
+ "tab.js": async () => {
+ let [tab] = await browser.tabs.query({
+ url: "http://example.com/?SomeTab",
+ });
+ let bookmark = await browser.bookmarks.create({
+ title: "Bookmark for menu test",
+ url: "http://example.com/bookmark",
+ });
+ let testCases = [
+ {
+ context: "tab",
+ tabId: tab.id,
+ },
+ {
+ context: "tab",
+ tabId: tab.id,
+ },
+ {
+ context: "bookmark",
+ bookmarkId: bookmark.id,
+ },
+ {
+ context: "tab",
+ tabId: 123456789, // Some invalid tabId.
+ },
+ ];
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ document.addEventListener("contextmenu", () => {
+ browser.menus.overrideContext(testCases.shift());
+ browser.test.sendMessage("oncontextmenu_in_dom");
+ });
+
+ browser.test.sendMessage("setup_ready", {
+ bookmarkId: bookmark.id,
+ tabId: tab.id,
+ httpUrl: tab.url,
+ extensionUrl: document.URL,
+ });
+ },
+ },
+ background,
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?SomeTab"
+ );
+
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "@other-test-extension" } },
+ permissions: ["menus", "bookmarks", "activeTab"],
+ },
+ background,
+ });
+ await otherExtension.startup();
+ await otherExtension.awaitMessage("menu_items_registered");
+
+ await extension.startup();
+ await extension.awaitMessage("menu_items_registered");
+
+ let { bookmarkId, tabId, httpUrl, extensionUrl } =
+ await extension.awaitMessage("setup_ready");
+ info(`Set up test with tabId=${tabId} and bookmarkId=${bookmarkId}.`);
+
+ {
+ // Test case 1: context=tab
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+ for (let ext of [extension, otherExtension]) {
+ info(`Testing menu from ${ext.id} after changing context to tab`);
+ Assert.deepEqual(
+ await ext.awaitMessage("onShown"),
+ {
+ menuIds: [
+ "tab_context",
+ "tab_context_http",
+ "tab_context_viewType_moz",
+ ],
+ contexts: ["tab"],
+ bookmarkId: undefined,
+ pageUrl: undefined, // because extension has no host permissions.
+ frameUrl: extensionUrl,
+ tabId,
+ },
+ "Expected onShown details after changing context to tab"
+ );
+ }
+ let topLevels = menu.getElementsByAttribute("ext-type", "top-level-menu");
+ is(topLevels.length, 1, "Expected top-level menu for otherExtension");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ [
+ `${makeWidgetId(extension.id)}-menuitem-_tab_context`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_context_http`,
+ `${makeWidgetId(extension.id)}-menuitem-_tab_context_viewType_moz`,
+ `menuseparator`,
+ topLevels[0].id,
+ ],
+ "Expected menu items after changing context to tab"
+ );
+
+ let submenu = await openSubmenu(topLevels[0]);
+ is(submenu, topLevels[0].menupopup, "Correct submenu opened");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(submenu),
+ [
+ `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context`,
+ `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context_http`,
+ `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context_viewType_moz`,
+ ],
+ "Expected menu items in submenu after changing context to tab"
+ );
+
+ extension.sendMessage("testTabAccess", tabId);
+ is(
+ await extension.awaitMessage("testTabAccessDone"),
+ "executeScript_failed",
+ "executeScript should fail due to the lack of permissions."
+ );
+
+ otherExtension.sendMessage("testTabAccess", tabId);
+ is(
+ await otherExtension.awaitMessage("testTabAccessDone"),
+ "tab_no_url",
+ "Other extension should not have activeTab permissions yet."
+ );
+
+ // Click on the menu item of the other extension to unlock host permissions.
+ let menuItems = menu.getElementsByAttribute("label", "tab_context");
+ is(
+ menuItems.length,
+ 2,
+ "There are two menu items with label 'tab_context'"
+ );
+ await closeExtensionContextMenu(menuItems[1]);
+
+ Assert.deepEqual(
+ await otherExtension.awaitMessage("onClicked"),
+ {
+ menuItemId: "tab_context",
+ bookmarkId: undefined,
+ pageUrl: httpUrl,
+ frameUrl: extensionUrl,
+ tabId,
+ },
+ "Expected onClicked details after changing context to tab"
+ );
+
+ extension.sendMessage("testTabAccess", tabId);
+ is(
+ await extension.awaitMessage("testTabAccessDone"),
+ "executeScript_failed",
+ "executeScript of extension that created the menu should still fail."
+ );
+
+ otherExtension.sendMessage("testTabAccess", tabId);
+ is(
+ await otherExtension.awaitMessage("testTabAccessDone"),
+ "executeScript_ok",
+ "Other extension should have activeTab permissions."
+ );
+ }
+
+ {
+ // Test case 2: context=tab, click on menu item of extension..
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+
+ // The previous test has already verified the visible menu items,
+ // so we skip checking the onShown result and only test clicking.
+ await extension.awaitMessage("onShown");
+ await otherExtension.awaitMessage("onShown");
+ let menuItems = menu.getElementsByAttribute("label", "tab_context");
+ is(
+ menuItems.length,
+ 2,
+ "There are two menu items with label 'tab_context'"
+ );
+ await closeExtensionContextMenu(menuItems[0]);
+
+ Assert.deepEqual(
+ await extension.awaitMessage("onClicked"),
+ {
+ menuItemId: "tab_context",
+ bookmarkId: undefined,
+ pageUrl: httpUrl,
+ frameUrl: extensionUrl,
+ tabId,
+ },
+ "Expected onClicked details after changing context to tab"
+ );
+
+ extension.sendMessage("testTabAccess", tabId);
+ is(
+ await extension.awaitMessage("testTabAccessDone"),
+ "executeScript_failed",
+ "activeTab permission should not be available to the extension that created the menu."
+ );
+ }
+
+ {
+ // Test case 3: context=bookmark
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+ for (let ext of [extension, otherExtension]) {
+ info(`Testing menu from ${ext.id} after changing context to bookmark`);
+ let shownInfo = await ext.awaitMessage("onShown");
+ Assert.deepEqual(
+ shownInfo,
+ {
+ menuIds: [
+ "bookmark_context",
+ "bookmark_context_http",
+ "bookmark_context_moz",
+ "bookmark_context_viewType_moz",
+ ],
+ contexts: ["bookmark"],
+ bookmarkId,
+ pageUrl: undefined,
+ frameUrl: extensionUrl,
+ tabId: undefined,
+ },
+ "Expected onShown details after changing context to bookmark"
+ );
+ }
+ let topLevels = menu.getElementsByAttribute("ext-type", "top-level-menu");
+ is(topLevels.length, 1, "Expected top-level menu for otherExtension");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(menu),
+ [
+ `${makeWidgetId(extension.id)}-menuitem-_bookmark_context`,
+ `${makeWidgetId(extension.id)}-menuitem-_bookmark_context_http`,
+ `${makeWidgetId(extension.id)}-menuitem-_bookmark_context_moz`,
+ `${makeWidgetId(extension.id)}-menuitem-_bookmark_context_viewType_moz`,
+ `menuseparator`,
+ topLevels[0].id,
+ ],
+ "Expected menu items after changing context to bookmark"
+ );
+
+ let submenu = await openSubmenu(topLevels[0]);
+ is(submenu, topLevels[0].menupopup, "Correct submenu opened");
+
+ Assert.deepEqual(
+ getVisibleChildrenIds(submenu),
+ [
+ `${makeWidgetId(otherExtension.id)}-menuitem-_bookmark_context`,
+ `${makeWidgetId(otherExtension.id)}-menuitem-_bookmark_context_http`,
+ `${makeWidgetId(otherExtension.id)}-menuitem-_bookmark_context_moz`,
+ `${makeWidgetId(
+ otherExtension.id
+ )}-menuitem-_bookmark_context_viewType_moz`,
+ ],
+ "Expected menu items in submenu after changing context to bookmark"
+ );
+ await closeContextMenu(menu);
+ }
+
+ {
+ // Test case 4: context=tab, invalid tabId.
+ let menu = await openContextMenu("a");
+ await extension.awaitMessage("oncontextmenu_in_dom");
+ // When an invalid tabId is used, all extension menu logic is skipped and
+ // the default menu is shown.
+ checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
+ await closeContextMenu(menu);
+ }
+
+ await extension.unload();
+ await otherExtension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js
new file mode 100644
index 0000000000..c9627c5ae9
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function auto_approve_optional_permissions() {
+ // Auto-approve optional permission requests, without UI.
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextOptionalPermissionPrompts", false]],
+ });
+ // TODO: Consider an observer for "webextension-optional-permission-prompt"
+ // once bug 1493396 is fixed.
+});
+
+add_task(async function overrideContext_permissions() {
+ function sidebarJs() {
+ // If the extension has the right permissions, calling
+ // menus.overrideContext with one of the following should not throw.
+ const CONTEXT_OPTIONS_TAB = { context: "tab", tabId: 1 };
+ const CONTEXT_OPTIONS_BOOKMARK = { context: "bookmark", bookmarkId: "x" };
+
+ const E_PERM_TAB = /The "tab" context requires the "tabs" permission/;
+ const E_PERM_BOOKMARK =
+ /The "bookmark" context requires the "bookmarks" permission/;
+
+ function assertAllowed(contextOptions) {
+ try {
+ let result = browser.menus.overrideContext(contextOptions);
+ browser.test.assertEq(
+ undefined,
+ result,
+ `Allowed menu for context=${contextOptions.context}`
+ );
+ } catch (e) {
+ browser.test.fail(
+ `Unexpected error for context=${contextOptions.context}: ${e}`
+ );
+ }
+ }
+
+ function assertNotAllowed(contextOptions, expectedError) {
+ browser.test.assertThrows(
+ () => {
+ browser.menus.overrideContext(contextOptions);
+ },
+ expectedError,
+ `Expected error for context=${contextOptions.context}`
+ );
+ }
+
+ async function requestPermissions(permissions) {
+ try {
+ let permPromise;
+ window.withHandlingUserInputForPermissionRequestTest(() => {
+ permPromise = browser.permissions.request(permissions);
+ });
+ browser.test.assertTrue(
+ await permPromise,
+ `Should have granted ${JSON.stringify(permissions)}`
+ );
+ } catch (e) {
+ browser.test.fail(
+ `Failed to use permissions.request(${JSON.stringify(
+ permissions
+ )}): ${e}`
+ );
+ }
+ }
+
+ // The menus.overrideContext method can only be called during a
+ // "contextmenu" event. So we use a generator to run tests, and yield
+ // before we call overrideContext after an asynchronous operation.
+ let testGenerator = (async function* () {
+ browser.test.assertEq(
+ undefined,
+ browser.menus.overrideContext,
+ "menus.overrideContext requires the 'menus.overrideContext' permission"
+ );
+ await requestPermissions({ permissions: ["menus.overrideContext"] });
+ yield;
+
+ // context without required property.
+ browser.test.assertThrows(
+ () => {
+ browser.menus.overrideContext({ context: "tab" });
+ },
+ /Property "tabId" is required for context "tab"/,
+ "Required property for context tab"
+ );
+ browser.test.assertThrows(
+ () => {
+ browser.menus.overrideContext({ context: "bookmark" });
+ },
+ /Property "bookmarkId" is required for context "bookmark"/,
+ "Required property for context bookmarks"
+ );
+
+ // context with too many properties.
+ browser.test.assertThrows(
+ () => {
+ browser.menus.overrideContext({
+ context: "bookmark",
+ bookmarkId: "x",
+ tabId: 1,
+ });
+ },
+ /Property "tabId" can only be used with context "tab"/,
+ "Invalid property for context bookmarks"
+ );
+ browser.test.assertThrows(
+ () => {
+ browser.menus.overrideContext({
+ context: "bookmark",
+ bookmarkId: "x",
+ showDefaults: true,
+ });
+ },
+ /Property "showDefaults" cannot be used with context "bookmark"/,
+ "showDefaults cannot be used with context bookmark"
+ );
+
+ // context with right properties, but missing permissions.
+ assertNotAllowed(CONTEXT_OPTIONS_BOOKMARK, E_PERM_BOOKMARK);
+ assertNotAllowed(CONTEXT_OPTIONS_TAB, E_PERM_TAB);
+
+ await requestPermissions({ permissions: ["bookmarks"] });
+ browser.test.log("Active permissions: bookmarks");
+ yield;
+
+ assertAllowed(CONTEXT_OPTIONS_BOOKMARK);
+ assertNotAllowed(CONTEXT_OPTIONS_TAB, E_PERM_TAB);
+
+ await requestPermissions({ permissions: ["tabs"] });
+ await browser.permissions.remove({ permissions: ["bookmarks"] });
+ browser.test.log("Active permissions: tabs");
+ yield;
+
+ assertNotAllowed(CONTEXT_OPTIONS_BOOKMARK, E_PERM_BOOKMARK);
+ assertAllowed(CONTEXT_OPTIONS_TAB);
+ await browser.permissions.remove({ permissions: ["tabs"] });
+ browser.test.log("Active permissions: none");
+ yield;
+
+ assertNotAllowed(CONTEXT_OPTIONS_TAB, E_PERM_TAB);
+
+ await browser.permissions.remove({
+ permissions: ["menus.overrideContext"],
+ });
+ browser.test.assertEq(
+ undefined,
+ browser.menus.overrideContext,
+ "menus.overrideContext is unavailable after revoking the permission"
+ );
+ })();
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ document.addEventListener("contextmenu", async event => {
+ event.preventDefault();
+ try {
+ let { done } = await testGenerator.next();
+ browser.test.sendMessage("continue_test", !done);
+ } catch (e) {
+ browser.test.fail(`Unexpected error: ${e} :: ${e.stack}`);
+ browser.test.sendMessage("continue_test", false);
+ }
+ });
+ browser.test.sendMessage("sidebar_ready");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ manifest: {
+ permissions: ["menus"],
+ optional_permissions: ["menus.overrideContext", "tabs", "bookmarks"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ files: {
+ "sidebar.html": `
+
+ Link
+
+ `,
+ "sidebar.js": sidebarJs,
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("sidebar_ready");
+
+ // permissions.request requires user input, export helper.
+ await SpecialPowers.spawn(
+ SidebarUI.browser.contentDocument.getElementById("webext-panels-browser"),
+ [],
+ () => {
+ const { ExtensionCommon } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionCommon.sys.mjs"
+ );
+ Cu.exportFunction(
+ fn => {
+ return ExtensionCommon.withHandlingUserInput(content, fn);
+ },
+ content,
+ {
+ defineAs: "withHandlingUserInputForPermissionRequestTest",
+ }
+ );
+ }
+ );
+
+ do {
+ info(`Going to trigger "contextmenu" event.`);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "a",
+ { type: "contextmenu" },
+ SidebarUI.browser.contentDocument.getElementById("webext-panels-browser")
+ );
+ } while (await extension.awaitMessage("continue_test"));
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_targetElement.js b/browser/components/extensions/test/browser/browser_ext_menus_targetElement.js
new file mode 100644
index 0000000000..abfbf26a05
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_targetElement.js
@@ -0,0 +1,326 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+// Loads an extension that records menu visibility events in the current tab.
+// The returned extension has two helper functions "openContextMenu" and
+// "checkIsValid" that are used to verify the behavior of targetElementId.
+async function loadExtensionAndTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ gBrowser.selectedTab = tab;
+
+ function contentScript() {
+ browser.test.onMessage.addListener(
+ (msg, targetElementId, expectedSelector, description) => {
+ browser.test.assertEq("checkIsValid", msg, "Expected message");
+
+ let expected = expectedSelector
+ ? document.querySelector(expectedSelector)
+ : null;
+ let elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(expected, elem, description);
+ browser.test.sendMessage("checkIsValidDone");
+ }
+ );
+ }
+
+ async function background() {
+ browser.menus.onShown.addListener(async (info, tab) => {
+ browser.test.sendMessage("onShownMenu", info.targetElementId);
+ });
+ await browser.tabs.executeScript({ file: "contentScript.js" });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "http://mochi.test/*"],
+ },
+ background,
+ files: {
+ "contentScript.js": contentScript,
+ },
+ });
+
+ extension.openAndCloseMenu = async selector => {
+ await openContextMenu(selector);
+ let targetElementId = await extension.awaitMessage("onShownMenu");
+ await closeContextMenu();
+ return targetElementId;
+ };
+
+ extension.checkIsValid = async (
+ targetElementId,
+ expectedSelector,
+ description
+ ) => {
+ extension.sendMessage(
+ "checkIsValid",
+ targetElementId,
+ expectedSelector,
+ description
+ );
+ await extension.awaitMessage("checkIsValidDone");
+ };
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ return { extension, tab };
+}
+
+// Tests that info.targetElementId is only available with the right permissions.
+add_task(async function required_permission() {
+ let { extension, tab } = await loadExtensionAndTab();
+
+ // Load another extension to verify that the permission from the first
+ // extension does not enable the "targetElementId" parameter.
+ function background() {
+ browser.contextMenus.onShown.addListener((info, tab) => {
+ browser.test.assertEq(
+ undefined,
+ info.targetElementId,
+ "targetElementId requires permission"
+ );
+ browser.test.sendMessage("onShown");
+ });
+ browser.contextMenus.onClicked.addListener(async info => {
+ browser.test.assertEq(
+ undefined,
+ info.targetElementId,
+ "targetElementId requires permission"
+ );
+ const code = `
+ browser.test.assertEq(undefined, browser.menus, "menus API requires permission in content script");
+ browser.test.assertEq(undefined, browser.contextMenus, "contextMenus API not available in content script.");
+ `;
+ await browser.tabs.executeScript({ code });
+ browser.test.sendMessage("onClicked");
+ });
+ browser.contextMenus.create({ title: "menu for page" }, () => {
+ browser.test.sendMessage("ready");
+ });
+ }
+ let extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus", "http://mochi.test/*"],
+ },
+ background,
+ });
+ await extension2.startup();
+ await extension2.awaitMessage("ready");
+
+ let menu = await openContextMenu();
+ await extension.awaitMessage("onShownMenu");
+ let menuItem = menu.getElementsByAttribute("label", "menu for page")[0];
+ await closeExtensionContextMenu(menuItem);
+
+ await extension2.awaitMessage("onShown");
+ await extension2.awaitMessage("onClicked");
+ await extension2.unload();
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Tests that the basic functionality works as expected.
+add_task(async function getTargetElement_in_page() {
+ let { extension, tab } = await loadExtensionAndTab();
+
+ for (let selector of ["#img1", "#link1", "#password"]) {
+ let targetElementId = await extension.openAndCloseMenu(selector);
+ ok(
+ Number.isInteger(targetElementId),
+ `targetElementId (${targetElementId}) should be an integer for ${selector}`
+ );
+
+ await extension.checkIsValid(
+ targetElementId,
+ selector,
+ `Expected target to match ${selector}`
+ );
+ }
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function getTargetElement_in_frame() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ gBrowser.selectedTab = tab;
+
+ async function background() {
+ let targetElementId;
+ browser.menus.onShown.addListener(async (info, tab) => {
+ browser.test.assertTrue(
+ info.frameUrl.endsWith("context_frame.html"),
+ `Expected frame ${info.frameUrl}`
+ );
+ targetElementId = info.targetElementId;
+ let elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(
+ null,
+ elem,
+ "should not find page element in extension's background"
+ );
+
+ await browser.tabs.executeScript(tab.id, {
+ code: `{
+ let elem = browser.menus.getTargetElement(${targetElementId});
+ browser.test.assertEq(null, elem, "should not find element from different frame");
+ }`,
+ });
+
+ await browser.tabs.executeScript(tab.id, {
+ frameId: info.frameId,
+ code: `{
+ let elem = browser.menus.getTargetElement(${targetElementId});
+ browser.test.assertEq(document.body, elem, "should find the target element in the frame");
+ }`,
+ });
+ browser.test.sendMessage("pageAndFrameChecked");
+ });
+
+ browser.menus.onClicked.addListener(info => {
+ browser.test.assertEq(
+ targetElementId,
+ info.targetElementId,
+ "targetElementId in onClicked must match onShown."
+ );
+ browser.test.sendMessage("onClickedChecked");
+ });
+
+ browser.menus.create(
+ { title: "menu for frame", contexts: ["frame"] },
+ () => {
+ browser.test.sendMessage("ready");
+ }
+ );
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "http://mochi.test/*"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let menu = await openContextMenuInFrame();
+ await extension.awaitMessage("pageAndFrameChecked");
+ let menuItem = menu.getElementsByAttribute("label", "menu for frame")[0];
+ await closeExtensionContextMenu(menuItem);
+ await extension.awaitMessage("onClickedChecked");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test that getTargetElement does not return a detached element.
+add_task(async function getTargetElement_after_removing_element() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ function background() {
+ function contentScript(targetElementId) {
+ let expectedElem = document.getElementById("edit-me");
+ let { nextElementSibling } = expectedElem;
+
+ let elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(
+ expectedElem,
+ elem,
+ "Expected target element before element removal"
+ );
+
+ expectedElem.remove();
+ elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(
+ null,
+ elem,
+ "Expected no target element after element removal."
+ );
+
+ nextElementSibling.insertAdjacentElement("beforebegin", expectedElem);
+ elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(
+ expectedElem,
+ elem,
+ "Expected target element after element restoration."
+ );
+ }
+ browser.menus.onClicked.addListener(async (info, tab) => {
+ const code = `(${contentScript})(${info.targetElementId})`;
+ browser.test.log(code);
+ await browser.tabs.executeScript(tab.id, { code });
+ browser.test.sendMessage("checkedRemovedElement");
+ });
+ browser.menus.create({ title: "some menu item" }, () => {
+ browser.test.sendMessage("ready");
+ });
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "http://mochi.test/*"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ let menu = await openContextMenu("#edit-me");
+ let menuItem = menu.getElementsByAttribute("label", "some menu item")[0];
+ await closeExtensionContextMenu(menuItem);
+ await extension.awaitMessage("checkedRemovedElement");
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Tests whether targetElementId expires after opening a new menu.
+add_task(async function expireTargetElement() {
+ let { extension, tab } = await loadExtensionAndTab();
+
+ // Open the menu once to get the first element ID.
+ let targetElementId = await extension.openAndCloseMenu("#longtext");
+
+ // Open another menu. The previous ID should expire.
+ await extension.openAndCloseMenu("#longtext");
+ await extension.checkIsValid(
+ targetElementId,
+ null,
+ `Expected initial target ID to expire after opening another menu`
+ );
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Tests whether targetElementId of different tabs are independent.
+add_task(async function independentMenusInDifferentTabs() {
+ let { extension, tab } = await loadExtensionAndTab();
+
+ let targetElementId = await extension.openAndCloseMenu("#longtext");
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE + "?");
+ gBrowser.selectedTab = tab2;
+
+ let targetElementId2 = await extension.openAndCloseMenu("#editabletext");
+
+ await extension.checkIsValid(
+ targetElementId2,
+ null,
+ "targetElementId from different tab should not resolve."
+ );
+ await extension.checkIsValid(
+ targetElementId,
+ "#longtext",
+ "Expected getTargetElement to work after closing a menu in another tab."
+ );
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_targetElement_extension.js b/browser/components/extensions/test/browser/browser_ext_menus_targetElement_extension.js
new file mode 100644
index 0000000000..4712ab7b1d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_targetElement_extension.js
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function getTargetElement_in_extension_tab() {
+ async function background() {
+ browser.menus.onShown.addListener(info => {
+ let elem = browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(
+ null,
+ elem,
+ "should not get element of tab content in background"
+ );
+
+ // By using getViews() here, we verify that the targetElementId can
+ // synchronously be mapped to a valid element in a different tab
+ // during the onShown event.
+ let [tabGlobal] = browser.extension.getViews({ type: "tab" });
+ elem = tabGlobal.browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(
+ "BUTTON",
+ elem.tagName,
+ "should get element in tab content"
+ );
+ browser.test.sendMessage("elementChecked");
+ });
+
+ browser.tabs.create({ url: "tab.html" });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ files: {
+ "tab.html": `Button in tab `,
+ },
+ background,
+ });
+
+ let extensionTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ null,
+ true
+ );
+ await extension.startup();
+ // Must wait for the tab to have loaded completely before calling openContextMenu.
+ await extensionTabPromise;
+ await openContextMenu("button");
+ await extension.awaitMessage("elementChecked");
+ await closeContextMenu();
+
+ // Unloading the extension will automatically close the extension's tab.html
+ await extension.unload();
+});
+
+add_task(async function getTargetElement_in_extension_tab_on_click() {
+ // Similar to getTargetElement_in_extension_tab, except we check whether
+ // calling getTargetElement in onClicked results in the expected behavior.
+ async function background() {
+ browser.menus.onClicked.addListener(info => {
+ let [tabGlobal] = browser.extension.getViews({ type: "tab" });
+ let elem = tabGlobal.browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(
+ "BUTTON",
+ elem.tagName,
+ "should get element in tab content on click"
+ );
+ browser.test.sendMessage("elementClicked");
+ });
+
+ browser.menus.create({ title: "click here" }, () => {
+ browser.tabs.create({ url: "tab.html" });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ files: {
+ "tab.html": `Button in tab `,
+ },
+ background,
+ });
+
+ let extensionTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ null,
+ true
+ );
+ await extension.startup();
+ await extensionTabPromise;
+ let menu = await openContextMenu("button");
+ let menuItem = menu.getElementsByAttribute("label", "click here")[0];
+ await closeExtensionContextMenu(menuItem);
+ await extension.awaitMessage("elementClicked");
+
+ await extension.unload();
+});
+
+add_task(async function getTargetElement_in_browserAction_popup() {
+ async function background() {
+ browser.menus.onShown.addListener(info => {
+ let elem = browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(
+ null,
+ elem,
+ "should not get element of popup content in background"
+ );
+
+ let [popupGlobal] = browser.extension.getViews({ type: "popup" });
+ elem = popupGlobal.browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(
+ "BUTTON",
+ elem.tagName,
+ "should get element in popup content"
+ );
+ browser.test.sendMessage("popupChecked");
+ });
+
+ // Ensure that onShown is registered (workaround for bug 1300234):
+ await browser.menus.removeAll();
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "popup.html": `Button in popup `,
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await clickBrowserAction(extension);
+ await openContextMenuInPopup(extension, "button");
+ await extension.awaitMessage("popupChecked");
+ await closeContextMenu();
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
+
+add_task(async function getTargetElement_in_sidebar_panel() {
+ async function sidebarJs() {
+ browser.menus.onShown.addListener(info => {
+ let expected = document.querySelector("button");
+ let elem = browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(
+ expected,
+ elem,
+ "should get element in sidebar content"
+ );
+ browser.test.sendMessage("done");
+ });
+
+ // Ensure that onShown is registered (workaround for bug 1300234):
+ await browser.menus.removeAll();
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ manifest: {
+ permissions: ["menus"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ files: {
+ "sidebar.html": `
+
+ Button in sidebar
+
+ `,
+ "sidebar.js": sidebarJs,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let sidebarMenu = await openContextMenuInSidebar("button");
+ await extension.awaitMessage("done");
+ await closeContextMenu(sidebarMenu);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_targetElement_shadow.js b/browser/components/extensions/test/browser/browser_ext_menus_targetElement_shadow.js
new file mode 100644
index 0000000000..115f4fe96a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_targetElement_shadow.js
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+add_task(async function menuInShadowDOM() {
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ gBrowser.selectedTab = tab;
+
+ async function background() {
+ browser.menus.onShown.addListener(async (info, tab) => {
+ browser.test.assertTrue(
+ Number.isInteger(info.targetElementId),
+ `${info.targetElementId} should be an integer`
+ );
+ browser.test.assertEq(
+ "all,link",
+ info.contexts.sort().join(","),
+ "Expected context"
+ );
+ browser.test.assertEq(
+ "http://example.com/?shadowlink",
+ info.linkUrl,
+ "Menu target should be a link in the shadow DOM"
+ );
+
+ let code = `{
+ try {
+ let elem = browser.menus.getTargetElement(${info.targetElementId});
+ browser.test.assertTrue(elem, "Shadow element must be found");
+ browser.test.assertEq("http://example.com/?shadowlink", elem.href, "Element is a link in shadow DOM " - elem.outerHTML);
+ } catch (e) {
+ browser.test.fail("Unexpected error in getTargetElement: " + e);
+ }
+ }`;
+ await browser.tabs.executeScript(tab.id, { code });
+ browser.test.sendMessage(
+ "onShownMenuAndCheckedInfo",
+ info.targetElementId
+ );
+ });
+
+ // Ensure that onShown is registered (workaround for bug 1300234):
+ await browser.menus.removeAll();
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "http://mochi.test/*"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ async function testShadowMenu(setupMenuTarget) {
+ await openContextMenu(setupMenuTarget);
+ await extension.awaitMessage("onShownMenuAndCheckedInfo");
+ await closeContextMenu();
+ }
+
+ info("Clicking in open shadow root");
+ await testShadowMenu(() => {
+ let doc = this.document;
+ doc.body.innerHTML = `
`;
+ let host = doc.body.firstElementChild.attachShadow({ mode: "open" });
+ host.innerHTML = `Test open `;
+ this.document.testTarget = host.firstElementChild;
+ return this.document.testTarget;
+ });
+
+ info("Clicking in closed shadow root");
+ await testShadowMenu(() => {
+ let doc = this.document;
+ doc.body.innerHTML = `
`;
+ let host = doc.body.firstElementChild.attachShadow({ mode: "closed" });
+ host.innerHTML = `Test closed `;
+ this.document.testTarget = host.firstElementChild;
+ return this.document.testTarget;
+ });
+
+ info("Clicking in nested shadow DOM");
+ await testShadowMenu(() => {
+ let doc = this.document;
+ let host;
+ for (let container = doc.body, i = 0; i < 10; ++i) {
+ container.innerHTML = `
`;
+ host = container.firstElementChild.attachShadow({ mode: "open" });
+ container = host;
+ }
+ host.innerHTML = `Test nested `;
+ this.document.testTarget = host.firstElementChild;
+ return this.document.testTarget;
+ });
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_viewType.js b/browser/components/extensions/test/browser/browser_ext_menus_viewType.js
new file mode 100644
index 0000000000..bb59e0fffd
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_viewType.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// browser_ext_menus_events.js provides some coverage for viewTypes in normal
+// tabs and extension popups.
+// This test provides coverage for extension tabs and sidebars, as well as
+// using the viewTypes property in menus.create and menus.update.
+
+add_task(async function extension_tab_viewType() {
+ async function background() {
+ browser.menus.onShown.addListener(info => {
+ browser.test.assertEq(
+ "tabonly",
+ info.menuIds.join(","),
+ "Expected menu items"
+ );
+ browser.test.sendMessage("shown");
+ });
+ browser.menus.onClicked.addListener(info => {
+ browser.test.assertEq("tab", info.viewType, "Expected viewType");
+ browser.test.sendMessage("clicked");
+ });
+
+ browser.menus.create({
+ id: "sidebaronly",
+ title: "sidebar-only",
+ viewTypes: ["sidebar"],
+ });
+ browser.menus.create(
+ { id: "tabonly", title: "click here", viewTypes: ["tab"] },
+ () => {
+ browser.tabs.create({ url: "tab.html" });
+ }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ files: {
+ "tab.html": ` `,
+ "tab.js": `browser.test.sendMessage("ready");`,
+ },
+ background,
+ });
+
+ let extensionTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ null,
+ true
+ );
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await extensionTabPromise;
+ let menu = await openContextMenu();
+ await extension.awaitMessage("shown");
+
+ let menuItem = menu.getElementsByAttribute("label", "click here")[0];
+ await closeExtensionContextMenu(menuItem);
+ await extension.awaitMessage("clicked");
+
+ // Unloading the extension will automatically close the extension's tab.html
+ await extension.unload();
+});
+
+add_task(async function sidebar_panel_viewType() {
+ async function sidebarJs() {
+ browser.menus.onShown.addListener(info => {
+ browser.test.assertEq(
+ "sidebaronly",
+ info.menuIds.join(","),
+ "Expected menu items"
+ );
+ browser.test.assertEq("sidebar", info.viewType, "Expected viewType");
+ browser.test.sendMessage("shown");
+ });
+
+ // Create menus and change their viewTypes using menus.update.
+ browser.menus.create({
+ id: "sidebaronly",
+ title: "sidebaronly",
+ viewTypes: ["tab"],
+ });
+ browser.menus.create({
+ id: "tabonly",
+ title: "tabonly",
+ viewTypes: ["sidebar"],
+ });
+ await browser.menus.update("sidebaronly", { viewTypes: ["sidebar"] });
+ await browser.menus.update("tabonly", { viewTypes: ["tab"] });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ manifest: {
+ permissions: ["menus"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ files: {
+ "sidebar.html": `
+
+
+ `,
+ "sidebar.js": sidebarJs,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let sidebarMenu = await openContextMenuInSidebar();
+ await extension.awaitMessage("shown");
+ await closeContextMenu(sidebarMenu);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_visible.js b/browser/components/extensions/test/browser/browser_ext_menus_visible.js
new file mode 100644
index 0000000000..cf9718fddc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_visible.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const PAGE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
+
+add_task(async function visible_false() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ async function background() {
+ browser.menus.onShown.addListener(info => {
+ browser.test.assertEq(
+ "[]",
+ JSON.stringify(info.menuIds),
+ "Expected no menu items"
+ );
+ browser.test.sendMessage("done");
+ });
+ browser.menus.create({
+ id: "create-visible-false",
+ title: "invisible menu item",
+ visible: false,
+ });
+ browser.menus.create({
+ id: "update-without-params",
+ title: "invisible menu item",
+ visible: false,
+ });
+ await browser.menus.update("update-without-params", {});
+ browser.menus.create({
+ id: "update-visible-to-false",
+ title: "initially visible menu item",
+ });
+ await browser.menus.update("update-visible-to-false", { visible: false });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await openContextMenu();
+ await extension.awaitMessage("done");
+ await closeContextMenu();
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function visible_true() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ async function background() {
+ browser.menus.onShown.addListener(info => {
+ browser.test.assertEq(
+ `["update-to-true"]`,
+ JSON.stringify(info.menuIds),
+ "Expected no menu items"
+ );
+ browser.test.sendMessage("done");
+ });
+ browser.menus.create({
+ id: "update-to-true",
+ title: "invisible menu item",
+ visible: false,
+ });
+ await browser.menus.update("update-to-true", { visible: true });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await openContextMenu();
+ await extension.awaitMessage("done");
+ await closeContextMenu();
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js b/browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js
new file mode 100644
index 0000000000..d558400a7e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js
@@ -0,0 +1,186 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Extensions can be loaded in 3 ways: as a sidebar, as a browser action,
+// or as a page action. We use these constants to alter the extension
+// manifest, content script, background script, and setup conventions.
+const TESTS = {
+ SIDEBAR: "sidebar",
+ BROWSER_ACTION: "browserAction",
+ PAGE_ACTION: "pageAction",
+};
+
+function promiseBrowserReflow(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ return new Promise(resolve => {
+ content.window.requestAnimationFrame(() => {
+ content.window.requestAnimationFrame(resolve);
+ });
+ });
+ });
+}
+
+async function promiseBrowserZoom(browser, extension) {
+ await promiseBrowserReflow(browser);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "html",
+ { type: "mousedown", button: 0 },
+ browser
+ );
+ return extension.awaitMessage("zoom");
+}
+
+async function test_mousewheel_zoom(test) {
+ info(`Starting test of ${test} extension.`);
+ let browser;
+
+ // Scroll on Ctrl + mousewheel
+ SpecialPowers.pushPrefEnv({ set: [["mousewheel.with_control.action", 3]] });
+
+ function contentScript() {
+ // eslint-disable-next-line mozilla/balanced-listeners
+ document.addEventListener("mousedown", e => {
+ // Send the zoom level back as a "zoom" message.
+ const zoom = SpecialPowers.getFullZoom(window).toFixed(2);
+ browser.test.sendMessage("zoom", zoom);
+ });
+ }
+
+ function sidebarContentScript() {
+ // eslint-disable-next-line mozilla/balanced-listeners
+ document.addEventListener("mousedown", e => {
+ // Send the zoom level back as a "zoom" message.
+ const zoom = SpecialPowers.getFullZoom(window).toFixed(2);
+ browser.test.sendMessage("zoom", zoom);
+ });
+ browser.test.sendMessage("content-loaded");
+ }
+
+ function pageActionBackgroundScript() {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ const tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("content-loaded");
+ });
+ });
+ }
+
+ let manifest;
+ if (test == TESTS.SIDEBAR) {
+ manifest = {
+ sidebar_action: {
+ default_panel: "panel.html",
+ },
+ };
+ } else if (test == TESTS.BROWSER_ACTION) {
+ manifest = {
+ browser_action: {
+ default_popup: "panel.html",
+ default_area: "navbar",
+ },
+ };
+ } else if (test == TESTS.PAGE_ACTION) {
+ manifest = {
+ page_action: {
+ default_popup: "panel.html",
+ },
+ };
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ useAddonManager: "temporary",
+ files: {
+ "panel.html": `
+
+
+
+
+
+
+
+ Please Zoom Me
+
+
+ `,
+ "panel.js": test == TESTS.SIDEBAR ? sidebarContentScript : contentScript,
+ },
+ background:
+ test == TESTS.PAGE_ACTION ? pageActionBackgroundScript : undefined,
+ });
+
+ await extension.startup();
+ info("Awaiting notification that extension has loaded.");
+
+ if (test == TESTS.SIDEBAR) {
+ await extension.awaitMessage("content-loaded");
+
+ const sidebar = document.getElementById("sidebar-box");
+ ok(!sidebar.hidden, "Sidebar box is visible");
+
+ browser = SidebarUI.browser.contentWindow.gBrowser.selectedBrowser;
+ } else if (test == TESTS.BROWSER_ACTION) {
+ browser = await openBrowserActionPanel(extension, undefined, true);
+ } else if (test == TESTS.PAGE_ACTION) {
+ await extension.awaitMessage("content-loaded");
+
+ clickPageAction(extension, window);
+
+ browser = await awaitExtensionPanel(extension);
+ }
+
+ info(`Requesting initial zoom from ${test} extension.`);
+ let initialZoom = await promiseBrowserZoom(browser, extension);
+ info(`Extension (${test}) initial zoom is ${initialZoom}.`);
+
+ // Attempt to change the zoom of the extension with a mousewheel event.
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "html",
+ {
+ wheel: true,
+ ctrlKey: true,
+ deltaY: -1,
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ },
+ browser
+ );
+
+ info(`Requesting changed zoom from ${test} extension.`);
+ let changedZoom = await promiseBrowserZoom(browser, extension);
+ info(`Extension (${test}) changed zoom is ${changedZoom}.`);
+ isnot(
+ changedZoom,
+ initialZoom,
+ `Extension (${test}) zoom was changed as expected.`
+ );
+
+ // Attempt to restore the zoom of the extension with a mousewheel event.
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "html",
+ {
+ wheel: true,
+ ctrlKey: true,
+ deltaY: 1,
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ },
+ browser
+ );
+
+ info(`Requesting changed zoom from ${test} extension.`);
+ let finalZoom = await promiseBrowserZoom(browser, extension);
+ is(
+ finalZoom,
+ initialZoom,
+ `Extension (${test}) zoom was restored as expected.`
+ );
+
+ await extension.unload();
+}
+
+// Actually trigger the tests. Bind test_mousewheel_zoom each time so we
+// capture the test type.
+for (const t in TESTS) {
+ add_task(test_mousewheel_zoom.bind(this, TESTS[t]));
+}
diff --git a/browser/components/extensions/test/browser/browser_ext_nontab_process_switch.js b/browser/components/extensions/test/browser/browser_ext_nontab_process_switch.js
new file mode 100644
index 0000000000..56511d1a7d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_nontab_process_switch.js
@@ -0,0 +1,154 @@
+"use strict";
+
+add_task(async function process_switch_in_sidebars_popups() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.content_web_accessible.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://example.com/*"],
+ js: ["cs.js"],
+ },
+ ],
+
+ sidebar_action: {
+ default_panel: "page.html?sidebar",
+ },
+ browser_action: {
+ default_popup: "page.html?popup",
+ default_area: "navbar",
+ },
+ web_accessible_resources: ["page.html"],
+ },
+ files: {
+ "page.html": ` `,
+ async "page.js"() {
+ browser.test.sendMessage("extension_page", {
+ place: location.search,
+ pid: await SpecialPowers.spawnChrome([], () => {
+ return windowGlobalParent.osPid;
+ }),
+ });
+ if (!location.search.endsWith("_back")) {
+ window.location.href = "http://example.com/" + location.search;
+ }
+ },
+
+ async "cs.js"() {
+ browser.test.sendMessage("content_script", {
+ url: location.href,
+ pid: await this.wrappedJSObject.SpecialPowers.spawnChrome([], () => {
+ return windowGlobalParent.osPid;
+ }),
+ });
+ if (location.search === "?popup") {
+ window.location.href =
+ browser.runtime.getURL("page.html") + "?popup_back";
+ }
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let sidebar = await extension.awaitMessage("extension_page");
+ is(sidebar.place, "?sidebar", "Message from the extension sidebar");
+
+ let cs1 = await extension.awaitMessage("content_script");
+ is(cs1.url, "http://example.com/?sidebar", "CS on example.com in sidebar");
+ isnot(sidebar.pid, cs1.pid, "Navigating to example.com changed process");
+
+ await clickBrowserAction(extension);
+ let popup = await extension.awaitMessage("extension_page");
+ is(popup.place, "?popup", "Message from the extension popup");
+
+ let cs2 = await extension.awaitMessage("content_script");
+ is(cs2.url, "http://example.com/?popup", "CS on example.com in popup");
+ isnot(popup.pid, cs2.pid, "Navigating to example.com changed process");
+
+ let popup2 = await extension.awaitMessage("extension_page");
+ is(popup2.place, "?popup_back", "Back at extension page in popup");
+ is(popup.pid, popup2.pid, "Same process as original popup page");
+
+ is(sidebar.pid, popup.pid, "Sidebar and popup pages from the same process");
+
+ // There's no guarantee that two (independent) pages from the same domain will
+ // end up in the same process.
+
+ await closeBrowserAction(extension);
+ await extension.unload();
+});
+
+// Test that navigating the browserAction popup between extension pages doesn't keep the
+// parser blocked (See Bug 1747813).
+add_task(
+ async function test_navigate_browserActionPopups_shouldnot_block_parser() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup-1.html",
+ default_area: "navbar",
+ },
+ },
+ files: {
+ "popup-1.html": `Popup 1 `,
+ "popup-2.html": `Popup 2 `,
+
+ "popup-1.js": function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "navigate-popup") {
+ browser.test.fail(`Unexpected test message "${msg}"`);
+ return;
+ }
+ location.href = "/popup-2.html";
+ });
+ window.onload = () => browser.test.sendMessage("popup-page-1");
+ },
+
+ "popup-2.js": function () {
+ window.onload = () => browser.test.sendMessage("popup-page-2");
+ },
+ },
+ });
+
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mouseover" },
+ window
+ );
+
+ await extension.startup();
+
+ // Triggers popup preload (otherwise we wouldn't be blocking the parser for the browserAction popup
+ // and the issue wouldn't be triggered, a real user on the contrary has a pretty high chance to trigger a
+ // preload while hovering the browserAction popup before opening the popup with a click).
+ let widget = getBrowserActionWidget(extension).forWindow(window);
+ EventUtils.synthesizeMouseAtCenter(
+ widget.node,
+ { type: "mouseover" },
+ window
+ );
+ await clickBrowserAction(extension);
+
+ await extension.awaitMessage("popup-page-1");
+
+ extension.sendMessage("navigate-popup");
+
+ await extension.awaitMessage("popup-page-2");
+ // If the bug is triggered (e.g. it did regress), the test will get stuck waiting for
+ // the test message "popup-page-2" (which will never be sent because the extension page
+ // script isn't executed while the parser is blocked).
+ ok(
+ true,
+ "Extension browserAction popup successfully navigated to popup-page-2.html"
+ );
+
+ await closeBrowserAction(extension);
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_omnibox.js b/browser/components/extensions/test/browser/browser_ext_omnibox.js
new file mode 100644
index 0000000000..f7c27af14d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_omnibox.js
@@ -0,0 +1,504 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+const keyword = "VeryUniqueKeywordThatDoesNeverMatchAnyTestUrl";
+
+// This test does a lot. To ease debugging, we'll sometimes print the lines.
+function getCallerLines() {
+ const lines = Array.from(
+ new Error().stack.split("\n").slice(1),
+ line => /browser_ext_omnibox.js:(\d+):\d+$/.exec(line)?.[1]
+ );
+ return "Caller lines: " + lines.filter(lineno => lineno != null).join(", ");
+}
+
+add_setup(async () => {
+ // Override default timeout of 3000 ms, to make sure that the test progresses
+ // reasonably quickly. See comment in "function waitForResult" below.
+ // In this whole test, we respond ASAP to omnibox.onInputChanged events, so
+ // it should be safe to choose a relatively low timeout.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.extension.omnibox.timeout", 500]],
+ });
+});
+
+add_task(async function () {
+ // This keyword needs to be unique to prevent history entries from unrelated
+ // tests from appearing in the suggestions list.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: keyword,
+ },
+ },
+
+ background: function () {
+ browser.omnibox.onInputStarted.addListener(() => {
+ browser.test.sendMessage("on-input-started-fired");
+ });
+
+ let synchronous = true;
+ let suggestions = null;
+ let suggestCallback = null;
+
+ browser.omnibox.onInputChanged.addListener((text, suggest) => {
+ if (synchronous && suggestions) {
+ suggest(suggestions);
+ } else {
+ suggestCallback = suggest;
+ }
+ browser.test.sendMessage("on-input-changed-fired", { text });
+ });
+
+ browser.omnibox.onInputCancelled.addListener(() => {
+ browser.test.sendMessage("on-input-cancelled-fired");
+ });
+
+ browser.omnibox.onInputEntered.addListener((text, disposition) => {
+ browser.test.sendMessage("on-input-entered-fired", {
+ text,
+ disposition,
+ });
+ });
+
+ browser.omnibox.onDeleteSuggestion.addListener(text => {
+ browser.test.sendMessage("on-delete-suggestion-fired", { text });
+ });
+
+ browser.test.onMessage.addListener((msg, data) => {
+ switch (msg) {
+ case "set-suggestions":
+ suggestions = data.suggestions;
+ browser.test.sendMessage("suggestions-set");
+ break;
+ case "set-default-suggestion":
+ browser.omnibox.setDefaultSuggestion(data.suggestion);
+ browser.test.sendMessage("default-suggestion-set");
+ break;
+ case "set-synchronous":
+ synchronous = data.synchronous;
+ browser.test.sendMessage("set-synchronous-set");
+ break;
+ case "test-multiple-suggest-calls":
+ suggestions.forEach(suggestion => suggestCallback([suggestion]));
+ browser.test.sendMessage("test-ready");
+ break;
+ case "test-suggestions-after-delay":
+ Promise.resolve().then(() => {
+ suggestCallback(suggestions);
+ browser.test.sendMessage("test-ready");
+ });
+ break;
+ }
+ });
+ },
+ });
+
+ async function expectEvent(event, expected) {
+ info(`Waiting for event: ${event} (${getCallerLines()})`);
+ let actual = await extension.awaitMessage(event);
+ if (!expected) {
+ ok(true, `Expected "${event} to have fired."`);
+ return;
+ }
+ if (expected.text != undefined) {
+ is(
+ actual.text,
+ expected.text,
+ `Expected "${event}" to have fired with text: "${expected.text}".`
+ );
+ }
+ if (expected.disposition) {
+ is(
+ actual.disposition,
+ expected.disposition,
+ `Expected "${event}" to have fired with disposition: "${expected.disposition}".`
+ );
+ }
+ }
+
+ async function waitForResult(index) {
+ info(`waitForResult (${getCallerLines()})`);
+ // When omnibox.onInputChanged is triggered, the "startQuery" method in
+ // UrlbarProviderOmnibox.sys.mjs's startQuery will wait for a fixed amount
+ // of time before releasing the promise, which we observe by the call to
+ // UrlbarTestUtils here.
+ //
+ // To reduce the time that the test takes, we lower this in add_setup, by
+ // overriding the browser.urlbar.extension.omnibox.timeout preference.
+ //
+ // While this is not specific to the "waitForResult" test helper here, the
+ // issue is only observed in waitForResult because it is usually the first
+ // method called after observing "on-input-changed-fired".
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+
+ // Ensure the addition is complete, for proper mouse events on the entries.
+ await new Promise(resolve =>
+ window.requestIdleCallback(resolve, { timeout: 1000 })
+ );
+ return result;
+ }
+
+ async function promiseClickOnItem(index, details) {
+ // The Address Bar panel is animated and updated on a timer, thus it may not
+ // yet be listening to events when we try to click on it. This uses a
+ // polling strategy to repeat the click, if it doesn't go through.
+ let clicked = false;
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ index
+ );
+ element.addEventListener(
+ "mousedown",
+ () => {
+ clicked = true;
+ },
+ { once: true }
+ );
+ while (!clicked) {
+ EventUtils.synthesizeMouseAtCenter(element, details);
+ await new Promise(r => window.requestIdleCallback(r, { timeout: 1000 }));
+ }
+ }
+
+ let inputSessionSerial = 0;
+ async function startInputSession() {
+ gURLBar.focus();
+ gURLBar.value = keyword;
+ EventUtils.sendString(" ");
+ await expectEvent("on-input-started-fired");
+ // Always use a different input at every invokation, so that
+ // waitForResult can distinguish different cases.
+ let char = (inputSessionSerial++ % 10).toString();
+ EventUtils.sendString(char);
+
+ await expectEvent("on-input-changed-fired", { text: char });
+ return char;
+ }
+
+ async function testInputEvents() {
+ gURLBar.focus();
+
+ // Start an input session by typing in .
+ EventUtils.sendString(keyword + " ");
+ await expectEvent("on-input-started-fired");
+
+ // Test canceling the input before any changed events fire.
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await expectEvent("on-input-cancelled-fired");
+
+ EventUtils.sendString(" ");
+ await expectEvent("on-input-started-fired");
+
+ // Test submitting the input before any changed events fire.
+ EventUtils.synthesizeKey("KEY_Enter");
+ await expectEvent("on-input-entered-fired");
+
+ gURLBar.focus();
+
+ // Start an input session by typing in .
+ EventUtils.sendString(keyword + " ");
+ await expectEvent("on-input-started-fired");
+
+ // We should expect input changed events now that the keyword is active.
+ EventUtils.sendString("b");
+ await expectEvent("on-input-changed-fired", { text: "b" });
+
+ EventUtils.sendString("c");
+ await expectEvent("on-input-changed-fired", { text: "bc" });
+
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await expectEvent("on-input-changed-fired", { text: "b" });
+
+ // Even though the input is We should not expect an
+ // input started event to fire since the keyword is active.
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await expectEvent("on-input-changed-fired", { text: "" });
+
+ // Make the keyword inactive by hitting backspace.
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await expectEvent("on-input-cancelled-fired");
+
+ // Activate the keyword by typing a space.
+ // Expect onInputStarted to fire.
+ EventUtils.sendString(" ");
+ await expectEvent("on-input-started-fired");
+
+ // onInputChanged should fire even if a space is entered.
+ EventUtils.sendString(" ");
+ await expectEvent("on-input-changed-fired", { text: " " });
+
+ // The active session should cancel if the input blurs.
+ gURLBar.blur();
+ await expectEvent("on-input-cancelled-fired");
+ }
+
+ async function testSuggestionDeletion() {
+ extension.sendMessage("set-suggestions", {
+ suggestions: [{ content: "a", description: "select a", deletable: true }],
+ });
+ await extension.awaitMessage("suggestions-set");
+
+ gURLBar.focus();
+
+ EventUtils.sendString(keyword);
+ EventUtils.sendString(" select a");
+
+ await expectEvent("on-input-changed-fired");
+
+ // Select the suggestion
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Delete the suggestion
+ await EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+
+ await expectEvent("on-delete-suggestion-fired", { text: "select a" });
+ }
+
+ async function testHeuristicResult(expectedText, setDefaultSuggestion) {
+ if (setDefaultSuggestion) {
+ extension.sendMessage("set-default-suggestion", {
+ suggestion: {
+ description: expectedText,
+ },
+ });
+ await extension.awaitMessage("default-suggestion-set");
+ }
+
+ let text = await startInputSession();
+ let result = await waitForResult(0);
+
+ Assert.equal(
+ result.displayed.title,
+ expectedText,
+ `Expected heuristic result to have title: "${expectedText}".`
+ );
+
+ Assert.equal(
+ result.displayed.action,
+ `${keyword} ${text}`,
+ `Expected heuristic result to have displayurl: "${keyword} ${text}".`
+ );
+
+ let promiseEvent = expectEvent("on-input-entered-fired", {
+ text,
+ disposition: "currentTab",
+ });
+ await promiseClickOnItem(0, {});
+ await promiseEvent;
+ }
+
+ async function testDisposition(
+ suggestionIndex,
+ expectedDisposition,
+ expectedText
+ ) {
+ await startInputSession();
+ await waitForResult(suggestionIndex);
+
+ // Select the suggestion.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: suggestionIndex });
+
+ let promiseEvent = expectEvent("on-input-entered-fired", {
+ text: expectedText,
+ disposition: expectedDisposition,
+ });
+
+ if (expectedDisposition == "currentTab") {
+ await promiseClickOnItem(suggestionIndex, {});
+ } else if (expectedDisposition == "newForegroundTab") {
+ await promiseClickOnItem(suggestionIndex, { accelKey: true });
+ } else if (expectedDisposition == "newBackgroundTab") {
+ await promiseClickOnItem(suggestionIndex, {
+ shiftKey: true,
+ accelKey: true,
+ });
+ }
+ await promiseEvent;
+ }
+
+ async function testSuggestions(info) {
+ extension.sendMessage("set-synchronous", { synchronous: false });
+ await extension.awaitMessage("set-synchronous-set");
+
+ let text = await startInputSession();
+
+ extension.sendMessage(info.test);
+ await extension.awaitMessage("test-ready");
+
+ await waitForResult(info.suggestions.length - 1);
+ // Skip the heuristic result.
+ let index = 1;
+ for (let { content, description } of info.suggestions) {
+ let item = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(
+ item.displayed.title,
+ description,
+ `Expected suggestion to have title: "${description}".`
+ );
+ Assert.equal(
+ item.displayed.action,
+ `${keyword} ${content}`,
+ `Expected suggestion to have displayurl: "${keyword} ${content}".`
+ );
+ index++;
+ }
+
+ let promiseEvent = expectEvent("on-input-entered-fired", {
+ text,
+ disposition: "currentTab",
+ });
+ await promiseClickOnItem(0, {});
+ await promiseEvent;
+ }
+
+ await extension.startup();
+
+ await SimpleTest.promiseFocus(window);
+
+ await testInputEvents();
+
+ await testSuggestionDeletion();
+
+ // Test the heuristic result with default suggestions.
+ await testHeuristicResult(
+ "Generated extension",
+ false /* setDefaultSuggestion */
+ );
+ await testHeuristicResult("hello world", true /* setDefaultSuggestion */);
+ await testHeuristicResult("foo bar", true /* setDefaultSuggestion */);
+
+ let suggestions = [
+ { content: "a", description: "select a" },
+ { content: "b", description: "select b" },
+ { content: "c", description: "select c" },
+ ];
+
+ extension.sendMessage("set-suggestions", { suggestions });
+ await extension.awaitMessage("suggestions-set");
+
+ // Test each suggestion and search disposition.
+ await testDisposition(1, "currentTab", suggestions[0].content);
+ await testDisposition(2, "newForegroundTab", suggestions[1].content);
+ await testDisposition(3, "newBackgroundTab", suggestions[2].content);
+
+ extension.sendMessage("set-suggestions", { suggestions });
+ await extension.awaitMessage("suggestions-set");
+
+ // Test adding suggestions asynchronously.
+ await testSuggestions({
+ test: "test-multiple-suggest-calls",
+ suggestions,
+ });
+ await testSuggestions({
+ test: "test-suggestions-after-delay",
+ suggestions,
+ });
+
+ // When we're the first task to be added, `waitForExplicitFinish()` may not have
+ // been called yet. Let's just do that, otherwise the `monitorConsole` will make
+ // the test fail with a failing assertion.
+ SimpleTest.waitForExplicitFinish();
+ // Start monitoring the console.
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message: new RegExp(
+ `The keyword provided is already registered: "${keyword}"`
+ ),
+ },
+ ]);
+ });
+
+ // Try registering another extension with the same keyword
+ let extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: keyword,
+ },
+ },
+ });
+
+ await extension2.startup();
+
+ // Stop monitoring the console and confirm the correct errors are logged.
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+
+ await extension2.unload();
+ await extension.unload();
+});
+
+add_task(async function test_omnibox_event_page() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@omnibox" } },
+ omnibox: {
+ keyword: keyword,
+ },
+ background: { persistent: false },
+ },
+ background() {
+ browser.omnibox.onInputStarted.addListener(() => {
+ browser.test.sendMessage("onInputStarted");
+ });
+ browser.omnibox.onInputEntered.addListener(() => {});
+ browser.omnibox.onInputChanged.addListener(() => {});
+ browser.omnibox.onInputCancelled.addListener(() => {});
+ browser.omnibox.onDeleteSuggestion.addListener(() => {});
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = [
+ "onInputStarted",
+ "onInputEntered",
+ "onInputChanged",
+ "onInputCancelled",
+ "onDeleteSuggestion",
+ ];
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "omnibox", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "omnibox", event, {
+ primed: true,
+ });
+ }
+
+ // Activate the keyword by typing a space.
+ // Expect onInputStarted to fire.
+ gURLBar.focus();
+ gURLBar.value = keyword;
+ EventUtils.sendString(" ");
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onInputStarted");
+ ok(true, "persistent event woke background");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "omnibox", event, {
+ primed: false,
+ });
+ }
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_openPanel.js b/browser/components/extensions/test/browser/browser_ext_openPanel.js
new file mode 100644
index 0000000000..105cdc834b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_openPanel.js
@@ -0,0 +1,152 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_openPopup_requires_user_interaction() {
+ async function backgroundScript() {
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tabInfo) => {
+ if (changeInfo.status != "complete") {
+ return;
+ }
+ await browser.pageAction.show(tabId);
+
+ await browser.test.assertRejects(
+ browser.pageAction.openPopup(),
+ "pageAction.openPopup may only be called from a user input handler",
+ "The error is informative."
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.open(),
+ "sidebarAction.open may only be called from a user input handler",
+ "The error is informative."
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.close(),
+ "sidebarAction.close may only be called from a user input handler",
+ "The error is informative."
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.toggle(),
+ "sidebarAction.toggle may only be called from a user input handler",
+ "The error is informative."
+ );
+
+ browser.runtime.onMessage.addListener(async msg => {
+ browser.test.assertEq(msg, "from-panel", "correct message received");
+ browser.test.sendMessage("panel-opened");
+ });
+
+ browser.test.sendMessage("ready");
+ });
+ browser.tabs.create({ url: "tab.html" });
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ browser_action: {
+ default_popup: "panel.html",
+ },
+ page_action: {
+ default_popup: "panel.html",
+ },
+ sidebar_action: {
+ default_panel: "panel.html",
+ },
+ },
+ // We don't want the panel open automatically, so need a non-default reason.
+ startupReason: "APP_STARTUP",
+
+ files: {
+ "tab.html": `
+
+
+ openPageAction
+
+
+
+
+
+ `,
+ "panel.html": `
+
+
+
+
+ `,
+ "tab.js": function () {
+ document.getElementById("openPageAction").addEventListener(
+ "click",
+ () => {
+ browser.pageAction.openPopup();
+ },
+ { once: true }
+ );
+ document.getElementById("openSidebarAction").addEventListener(
+ "click",
+ () => {
+ browser.sidebarAction.open();
+ },
+ { once: true }
+ );
+ document.getElementById("closeSidebarAction").addEventListener(
+ "click",
+ () => {
+ browser.sidebarAction.close();
+ },
+ { once: true }
+ );
+ /* eslint-disable mozilla/balanced-listeners */
+ document
+ .getElementById("toggleSidebarAction")
+ .addEventListener("click", () => {
+ browser.sidebarAction.toggle();
+ });
+ /* eslint-enable mozilla/balanced-listeners */
+ },
+ "panel.js": function () {
+ browser.runtime.sendMessage("from-panel");
+ },
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ async function click(id) {
+ let open = extension.awaitMessage("panel-opened");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ id,
+ {},
+ gBrowser.selectedBrowser
+ );
+ return open;
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await click("#openPageAction");
+ closePageAction(extension);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ await click("#openSidebarAction");
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#closeSidebarAction",
+ {},
+ gBrowser.selectedBrowser
+ );
+ await TestUtils.waitForCondition(() => !SidebarUI.isOpen);
+
+ await click("#toggleSidebarAction");
+ await TestUtils.waitForCondition(() => SidebarUI.isOpen);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#toggleSidebarAction",
+ {},
+ gBrowser.selectedBrowser
+ );
+ await TestUtils.waitForCondition(() => !SidebarUI.isOpen);
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_optionsPage_activity.js b/browser/components/extensions/test/browser/browser_ext_optionsPage_activity.js
new file mode 100644
index 0000000000..44ca2509e1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_activity.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_options_activity() {
+ async function backgroundScript() {
+ browser.runtime.openOptionsPage();
+ }
+
+ function optionsScript() {
+ browser.test.sendMessage("options-page:loaded", document.documentURI);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+
+
+
+
+
+
+ Extensions Options
+ options page link
+
+ `,
+ "options.js": optionsScript,
+ },
+ background: backgroundScript,
+ });
+
+ const aboutAddonsTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:addons"
+ );
+
+ await extension.startup();
+
+ await extension.awaitMessage("options-page:loaded");
+
+ const optionsBrowser = getInlineOptionsBrowser(gBrowser.selectedBrowser);
+
+ ok(
+ !optionsBrowser.ownerDocument.hidden,
+ "Parent should be active since it's in the foreground"
+ );
+ ok(
+ optionsBrowser.docShellIsActive,
+ "Should be active since we're in the foreground"
+ );
+
+ let parentVisibilityChange = BrowserTestUtils.waitForEvent(
+ optionsBrowser.ownerDocument,
+ "visibilitychange"
+ );
+ const aboutBlankTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await parentVisibilityChange;
+ ok(
+ !optionsBrowser.docShellIsActive,
+ "Should become inactive since parent was backgrounded"
+ );
+
+ BrowserTestUtils.removeTab(aboutBlankTab);
+ BrowserTestUtils.removeTab(aboutAddonsTab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_optionsPage_browser_style.js b/browser/components/extensions/test/browser/browser_ext_optionsPage_browser_style.js
new file mode 100644
index 0000000000..9fbd4f7fd3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_browser_style.js
@@ -0,0 +1,155 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+async function testOptionsBrowserStyle(optionsUI, assertMessage) {
+ function optionsScript() {
+ browser.test.onMessage.addListener((msgName, optionsUI, assertMessage) => {
+ if (msgName !== "check-style") {
+ browser.test.notifyFail("options-ui-browser_style");
+ }
+
+ let browserStyle =
+ !("browser_style" in optionsUI) || optionsUI.browser_style;
+
+ function verifyButton(buttonElement, expected) {
+ let buttonStyle = window.getComputedStyle(buttonElement);
+ let buttonBackgroundColor = buttonStyle.backgroundColor;
+ if (browserStyle && expected.hasBrowserStyleClass) {
+ browser.test.assertEq(
+ "rgb(9, 150, 248)",
+ buttonBackgroundColor,
+ assertMessage
+ );
+ } else {
+ browser.test.assertTrue(
+ buttonBackgroundColor !== "rgb(9, 150, 248)",
+ assertMessage
+ );
+ }
+ }
+
+ function verifyCheckboxOrRadio(element, expected) {
+ let style = window.getComputedStyle(element);
+ let styledBackground = element.checked
+ ? "rgb(9, 150, 248)"
+ : "rgb(255, 255, 255)";
+ if (browserStyle && expected.hasBrowserStyleClass) {
+ browser.test.assertEq(
+ styledBackground,
+ style.backgroundColor,
+ assertMessage
+ );
+ } else {
+ browser.test.assertTrue(
+ style.backgroundColor != styledBackground,
+ assertMessage
+ );
+ }
+ }
+
+ let normalButton = document.getElementById("normalButton");
+ let browserStyleButton = document.getElementById("browserStyleButton");
+ verifyButton(normalButton, { hasBrowserStyleClass: false });
+ verifyButton(browserStyleButton, { hasBrowserStyleClass: true });
+
+ let normalCheckbox1 = document.getElementById("normalCheckbox1");
+ let normalCheckbox2 = document.getElementById("normalCheckbox2");
+ let browserStyleCheckbox = document.getElementById(
+ "browserStyleCheckbox"
+ );
+ verifyCheckboxOrRadio(normalCheckbox1, { hasBrowserStyleClass: false });
+ verifyCheckboxOrRadio(normalCheckbox2, { hasBrowserStyleClass: false });
+ verifyCheckboxOrRadio(browserStyleCheckbox, {
+ hasBrowserStyleClass: true,
+ });
+
+ let normalRadio1 = document.getElementById("normalRadio1");
+ let normalRadio2 = document.getElementById("normalRadio2");
+ let browserStyleRadio = document.getElementById("browserStyleRadio");
+ verifyCheckboxOrRadio(normalRadio1, { hasBrowserStyleClass: false });
+ verifyCheckboxOrRadio(normalRadio2, { hasBrowserStyleClass: false });
+ verifyCheckboxOrRadio(browserStyleRadio, { hasBrowserStyleClass: true });
+
+ browser.test.notifyPass("options-ui-browser_style");
+ });
+ browser.test.sendMessage("options-ui-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ permissions: ["tabs"],
+ options_ui: optionsUI,
+ },
+ files: {
+ "options.html": `
+
+
+ Default
+ Default
+
+
+ Checkbox
+
+ Checkbox
+
+
+
+ Radio
+
+ Radio
+
+
+
+ `,
+ "options.js": optionsScript,
+ },
+ background() {
+ browser.runtime.openOptionsPage();
+ },
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await extension.startup();
+ await extension.awaitMessage("options-ui-ready");
+
+ extension.sendMessage("check-style", optionsUI, assertMessage);
+ await extension.awaitFinish("options-ui-browser_style");
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+}
+
+add_task(async function test_options_without_setting_browser_style() {
+ await testOptionsBrowserStyle(
+ {
+ page: "options.html",
+ },
+ "Expected correct style when browser_style is excluded"
+ );
+});
+
+add_task(async function test_options_with_browser_style_set_to_true() {
+ await testOptionsBrowserStyle(
+ {
+ page: "options.html",
+ browser_style: true,
+ },
+ "Expected correct style when browser_style is set to `true`"
+ );
+});
+
+add_task(async function test_options_with_browser_style_set_to_false() {
+ await testOptionsBrowserStyle(
+ {
+ page: "options.html",
+ browser_style: false,
+ },
+ "Expected no style when browser_style is set to `false`"
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_optionsPage_links_open_in_tabs.js b/browser/components/extensions/test/browser/browser_ext_optionsPage_links_open_in_tabs.js
new file mode 100644
index 0000000000..147754b344
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_links_open_in_tabs.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_options_links() {
+ async function backgroundScript() {
+ browser.runtime.openOptionsPage();
+ }
+
+ function optionsScript() {
+ browser.test.sendMessage("options-page:loaded", document.documentURI);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+
+
+
+
+
+
+ Extensions Options
+ options page link
+
+ `,
+ "options.js": optionsScript,
+ },
+ background: backgroundScript,
+ });
+
+ const aboutAddonsTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:addons"
+ );
+
+ await extension.startup();
+
+ await extension.awaitMessage("options-page:loaded");
+
+ const optionsBrowser = getInlineOptionsBrowser(gBrowser.selectedBrowser);
+
+ const promiseNewTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://example.com/options-page-link"
+ );
+ await SpecialPowers.spawn(optionsBrowser, [], () =>
+ content.document.querySelector("a").click()
+ );
+ info(
+ "Expect a new tab to be opened when a link is clicked in the options_page embedded inside about:addons"
+ );
+ const newTab = await promiseNewTabOpened;
+ ok(newTab, "Got a new tab created on the expected url");
+ BrowserTestUtils.removeTab(newTab);
+
+ BrowserTestUtils.removeTab(aboutAddonsTab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js b/browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js
new file mode 100644
index 0000000000..809a5605c0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js
@@ -0,0 +1,100 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_tab_options_modals() {
+ function backgroundScript() {
+ browser.runtime.openOptionsPage();
+ }
+
+ function optionsScript() {
+ try {
+ alert("WebExtensions OptionsUI Page Modal");
+
+ browser.test.notifyPass("options-ui-modals");
+ } catch (error) {
+ browser.test.log(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("options-ui-modals");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ permissions: ["tabs"],
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+
+
+
+
+
+ `,
+ "options.js": optionsScript,
+ },
+ background: backgroundScript,
+ });
+
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:addons");
+
+ await extension.startup();
+
+ const onceModalOpened = new Promise(resolve => {
+ const aboutAddonsBrowser = gBrowser.selectedBrowser;
+
+ aboutAddonsBrowser.addEventListener(
+ "DOMWillOpenModalDialog",
+ function onModalDialog(event) {
+ // Wait for the next event tick to make sure the remaining part of the
+ // testcase runs after the dialog gets opened.
+ SimpleTest.executeSoon(resolve);
+ },
+ { once: true, capture: true }
+ );
+ });
+
+ info("Wait the options_ui modal to be opened");
+ await onceModalOpened;
+
+ const optionsBrowser = getInlineOptionsBrowser(gBrowser.selectedBrowser);
+
+ // The stack that contains the tabmodalprompt elements is the parent of
+ // the extensions options_ui browser element.
+ let stack = optionsBrowser.parentNode;
+
+ let dialogs = stack.querySelectorAll("tabmodalprompt");
+ Assert.equal(
+ dialogs.length,
+ 1,
+ "Expect a tab modal opened for the about addons tab"
+ );
+
+ // Verify that the expected stylesheets have been applied on the
+ // tabmodalprompt element (See Bug 1550529).
+ const tabmodalStyle = dialogs[0].ownerGlobal.getComputedStyle(dialogs[0]);
+ is(
+ tabmodalStyle["background-color"],
+ "rgba(26, 26, 26, 0.5)",
+ "Got the expected styles applied to the tabmodalprompt"
+ );
+
+ info("Close the tab modal prompt");
+ dialogs[0].querySelector(".tabmodalprompt-button0").click();
+
+ await extension.awaitFinish("options-ui-modals");
+
+ Assert.equal(
+ stack.querySelectorAll("tabmodalprompt").length,
+ 0,
+ "Expect the tab modal to be closed"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_optionsPage_popups.js b/browser/components/extensions/test/browser/browser_ext_optionsPage_popups.js
new file mode 100644
index 0000000000..168a9c11b5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_popups.js
@@ -0,0 +1,249 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function openContextMenuInOptionsPage(optionsBrowser) {
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+
+ info("Trigger context menu in the extension options page");
+
+ // Instead of BrowserTestUtils.synthesizeMouseAtCenter, we are dispatching a contextmenu
+ // event directly on the target element to prevent intermittent failures on debug builds
+ // (especially linux32-debug), see Bug 1519808 for a rationale.
+ SpecialPowers.spawn(optionsBrowser, [], () => {
+ let el = content.document.querySelector("a");
+ el.dispatchEvent(
+ new content.MouseEvent("contextmenu", {
+ bubbles: true,
+ cancelable: true,
+ view: el.ownerGlobal,
+ })
+ );
+ });
+
+ info("Wait the context menu to be shown");
+ await popupShownPromise;
+
+ return contentAreaContextMenu;
+}
+
+async function contextMenuClosed(contextMenu) {
+ info("Wait context menu popup to be closed");
+ await closeContextMenu(contextMenu);
+ is(contextMenu.state, "closed", "The context menu popup has been closed");
+}
+
+add_task(async function test_tab_options_popups() {
+ async function backgroundScript() {
+ browser.menus.onShown.addListener(info => {
+ browser.test.sendMessage("extension-menus-onShown", info);
+ });
+
+ await browser.menus.create({
+ id: "sidebaronly",
+ title: "sidebaronly",
+ viewTypes: ["sidebar"],
+ });
+ await browser.menus.create({
+ id: "tabonly",
+ title: "tabonly",
+ viewTypes: ["tab"],
+ });
+ await browser.menus.create({ id: "anypage", title: "anypage" });
+
+ browser.runtime.openOptionsPage();
+ }
+
+ function optionsScript() {
+ browser.test.sendMessage("options-page:loaded", document.documentURI);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ permissions: ["tabs", "menus"],
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+
+
+
+
+
+
+ Extensions Options
+ options page link
+
+ `,
+ "options.js": optionsScript,
+ },
+ background: backgroundScript,
+ });
+
+ const aboutAddonsTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:addons"
+ );
+
+ await extension.startup();
+
+ const pageUrl = await extension.awaitMessage("options-page:loaded");
+
+ const optionsBrowser = getInlineOptionsBrowser(gBrowser.selectedBrowser);
+
+ const contentAreaContextMenu = await openContextMenuInOptionsPage(
+ optionsBrowser
+ );
+
+ let contextMenuItemIds = [
+ "context-openlinkintab",
+ "context-openlinkprivate",
+ "context-copylink",
+ ];
+
+ // Test that the "open link in container" menu is available if the containers are enabled
+ // (which is the default on Nightly, but not on Beta).
+ if (Services.prefs.getBoolPref("privacy.userContext.enabled")) {
+ contextMenuItemIds.push("context-openlinkinusercontext-menu");
+ }
+
+ for (const itemID of contextMenuItemIds) {
+ const item = contentAreaContextMenu.querySelector(`#${itemID}`);
+
+ ok(!item.hidden, `${itemID} should not be hidden`);
+ ok(!item.disabled, `${itemID} should not be disabled`);
+ }
+
+ const menuDetails = await extension.awaitMessage("extension-menus-onShown");
+
+ isnot(
+ menuDetails.targetElementId,
+ undefined,
+ "Got a targetElementId in the menu details"
+ );
+ delete menuDetails.targetElementId;
+
+ Assert.deepEqual(
+ menuDetails,
+ {
+ menuIds: ["anypage"],
+ contexts: ["link", "all"],
+ viewType: undefined,
+ frameId: 0,
+ editable: false,
+ linkText: "options page link",
+ linkUrl: "http://mochi.test:8888/",
+ pageUrl,
+ },
+ "Got the expected menu details from menus.onShown"
+ );
+
+ await contextMenuClosed(contentAreaContextMenu);
+
+ BrowserTestUtils.removeTab(aboutAddonsTab);
+
+ await extension.unload();
+});
+
+add_task(async function overrideContext_in_options_page() {
+ function optionsScript() {
+ document.addEventListener(
+ "contextmenu",
+ () => {
+ browser.menus.overrideContext({});
+ browser.test.sendMessage("contextmenu-overridden");
+ },
+ { once: true }
+ );
+ browser.test.sendMessage("options-page:loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ permissions: ["tabs", "menus", "menus.overrideContext"],
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+
+
+
+
+
+
+ Extensions Options
+ options page link
+
+ `,
+ "options.js": optionsScript,
+ },
+ async background() {
+ // Expected to match and be shown.
+ await new Promise(resolve => {
+ browser.menus.create({ id: "bg_1_1", title: "bg_1_1" });
+ browser.menus.create({ id: "bg_1_2", title: "bg_1_2" });
+ // Expected to not match and be hidden.
+ browser.menus.create(
+ {
+ id: "bg_1_3",
+ title: "bg_1_3",
+ targetUrlPatterns: ["*://nomatch/*"],
+ },
+ // menus.create returns a number and gets a callback, the order
+ // is deterministic and so we just need to wait for the last one.
+ resolve
+ );
+ });
+ browser.runtime.openOptionsPage();
+ },
+ });
+
+ const aboutAddonsTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:addons"
+ );
+
+ await extension.startup();
+ await extension.awaitMessage("options-page:loaded");
+
+ const optionsBrowser = getInlineOptionsBrowser(gBrowser.selectedBrowser);
+ const contentAreaContextMenu = await openContextMenuInOptionsPage(
+ optionsBrowser
+ );
+
+ await extension.awaitMessage("contextmenu-overridden");
+
+ const allVisibleMenuItems = Array.from(contentAreaContextMenu.children)
+ .filter(elem => {
+ return !elem.hidden;
+ })
+ .map(elem => elem.id);
+
+ Assert.deepEqual(
+ allVisibleMenuItems,
+ [
+ `${makeWidgetId(extension.id)}-menuitem-_bg_1_1`,
+ `${makeWidgetId(extension.id)}-menuitem-_bg_1_2`,
+ ],
+ "Expected only extension menu items"
+ );
+
+ await contextMenuClosed(contentAreaContextMenu);
+
+ BrowserTestUtils.removeTab(aboutAddonsTab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js b/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
new file mode 100644
index 0000000000..33ddc6db34
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
@@ -0,0 +1,86 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_tab_options_privileges() {
+ function backgroundScript() {
+ browser.runtime.onMessage.addListener(async ({ msgName, tab }) => {
+ if (msgName == "removeTab") {
+ try {
+ const [activeTab] = await browser.tabs.query({ active: true });
+ browser.test.assertEq(
+ tab.id,
+ activeTab.id,
+ "tabs.getCurrent has got the expected tabId"
+ );
+ browser.test.assertEq(
+ tab.windowId,
+ activeTab.windowId,
+ "tabs.getCurrent has got the expected windowId"
+ );
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("options-ui-privileges");
+ } catch (error) {
+ browser.test.log(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("options-ui-privileges");
+ }
+ }
+ });
+ browser.runtime.openOptionsPage();
+ }
+
+ async function optionsScript() {
+ try {
+ let [tab] = await browser.tabs.query({ url: "http://example.com/" });
+ browser.test.assertEq(
+ "http://example.com/",
+ tab.url,
+ "Got the expect tab"
+ );
+
+ tab = await browser.tabs.getCurrent();
+ browser.runtime.sendMessage({ msgName: "removeTab", tab });
+ } catch (error) {
+ browser.test.log(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("options-ui-privileges");
+ }
+ }
+
+ const ID = "options_privileges@tests.mozilla.org";
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["tabs"],
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+
+
+
+
+
+ `,
+ "options.js": optionsScript,
+ },
+ background: backgroundScript,
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ await extension.startup();
+
+ await extension.awaitFinish("options-ui-privileges");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_originControls.js b/browser/components/extensions/test/browser/browser_ext_originControls.js
new file mode 100644
index 0000000000..176eef08bc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_originControls.js
@@ -0,0 +1,867 @@
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { ExtensionPermissions, QuarantinedDomains } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+loadTestSubscript("head_unified_extensions.js");
+
+const RED =
+ "iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIUARQAHY8+4wAAApBJREFUeNrt3cFqAjEUhlEjvv8rXzciiiBGk/He5JxdN2U649dY+KmnEwAAAAAv2uMXEeGOwERntwAEB4IDBAeCAwQHggPBAYIDwQGCA8GB4ADBgeAAwYHgAMGB4EBwgOCgpkuKq2it/r8Li2hbvGKqP6s/PycnHHv9YvSWEgQHCA4EBwgOBAeCAwQHggMEByXM+QRUE6D3suwuPafDn5MTDg50KXnVPSdxa54y/oYDwQGCA8EBggPBAYIDwYHggBE+X5rY3Y3Tey97Nn2eU+rnlGfaZa6Ft5SA4EBwgOBAcCA4QHAgOEBwIDjgZu60y1xrDPtIJxwgOBAcIDgQHAgOEBwIDhAcCA4EBwgOBAcIDgQHCA4EB4IDBAeCAwQHggPBAYIDwQGCA8GB4ADBgeAAwYHgAMGB4GADcz9y2McIgxMOBAeCAwQHggMEB4IDwQGCA8EBggPBATdP6+KIGPRdW7i1LCFi6ALfCQfeUoLgAMGB4ADBgeBAcIDgQHCA4CCdOVvK7quwveQgg7eRTjjwlhIQHAgOBAcIDgQHCA4EB4IDBAfl5dhSdl+17SX3F22rdLlOOBAcCA4QHAgOEBwIDgQHCA4EBwgO0qm5pez6Ce0uSym2jXTCgeAAwYHgQHCA4EBwgOBAcCA4QHBQ3vpbyu47Yns51OLbSCccCA4QHAgOBAcIDgQHCA4EB4ID5jDt+vkObjgFM9dywoHgAMGB4EBwgOBAcIDgQHAgOEBwsA5bysPveMLtpW2kEw4EBwgOBAcIDgQHggMEB4IDBAeCg33ZUqZ/Ql9sL20jnXCA4EBwIDhAcCA4QHAgOBAcIDgQHNOZai3DlhKccCA4QHAgOEBwIDgQHCA4AAAAAGA1VyxaWIohrgXFAAAAAElFTkSuQmCC";
+
+const l10n = new Localization(
+ ["branding/brand.ftl", "browser/extensionsUI.ftl"],
+ true
+);
+
+async function makeExtension({
+ useAddonManager = "temporary",
+ manifest_version = 3,
+ id,
+ permissions,
+ host_permissions,
+ content_scripts,
+ granted,
+ default_area,
+}) {
+ info(
+ `Loading extension ` +
+ JSON.stringify({ id, permissions, host_permissions, granted })
+ );
+
+ let manifest = {
+ manifest_version,
+ browser_specific_settings: { gecko: { id } },
+ permissions,
+ host_permissions,
+ content_scripts,
+ action: {
+ default_popup: "popup.html",
+ default_area: default_area || "navbar",
+ },
+ icons: {
+ 16: "red.png",
+ },
+ };
+ if (manifest_version < 3) {
+ manifest.browser_action = manifest.action;
+ delete manifest.action;
+ }
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest,
+
+ useAddonManager,
+
+ async background() {
+ browser.permissions.onAdded.addListener(({ origins }) => {
+ browser.test.sendMessage("granted", origins.join());
+ });
+ browser.permissions.onRemoved.addListener(({ origins }) => {
+ browser.test.sendMessage("revoked", origins.join());
+ });
+
+ browser.runtime.onInstalled.addListener(async () => {
+ if (browser.menus) {
+ let submenu = browser.menus.create({
+ id: "parent",
+ title: "submenu",
+ contexts: ["action"],
+ });
+ browser.menus.create({
+ id: "child1",
+ title: "child1",
+ parentId: submenu,
+ });
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ id: "child2",
+ title: "child2",
+ parentId: submenu,
+ },
+ resolve
+ );
+ });
+ }
+
+ browser.test.sendMessage("ready");
+ });
+ },
+
+ files: {
+ "red.png": imageBufferFromDataURI(RED),
+ "popup.html": ` Test Popup`,
+ },
+ });
+
+ if (granted) {
+ info("Granting initial permissions.");
+ await ExtensionPermissions.add(id, { permissions: [], origins: granted });
+ }
+
+ await ext.startup();
+ await ext.awaitMessage("ready");
+ return ext;
+}
+
+async function testQuarantinePopup(popup) {
+ let [title, line1, line2] = await l10n.formatMessages([
+ {
+ id: "webext-quarantine-confirmation-title",
+ args: { addonName: "Generated extension" },
+ },
+ "webext-quarantine-confirmation-line-1",
+ "webext-quarantine-confirmation-line-2",
+ ]);
+ let [titleEl, , helpEl] = popup.querySelectorAll("description");
+
+ ok(popup.getAttribute("icon").endsWith("/red.png"), "Correct icon.");
+
+ is(title.value, titleEl.textContent, "Correct title.");
+ is(line1.value + "\n\n" + line2.value, helpEl.textContent, "Correct lines.");
+}
+
+async function testOriginControls(
+ extension,
+ { contextMenuId },
+ {
+ items,
+ selected,
+ click,
+ granted,
+ revoked,
+ attention,
+ quarantined,
+ allowQuarantine,
+ }
+) {
+ info(
+ `Testing ${extension.id} on ${gBrowser.currentURI.spec} with contextMenuId=${contextMenuId}.`
+ );
+
+ let buttonOrWidget;
+ let menu;
+ let nextMenuItemClassName;
+
+ switch (contextMenuId) {
+ case "toolbar-context-menu":
+ let target = `#${CSS.escape(makeWidgetId(extension.id))}-BAP`;
+ buttonOrWidget = document.querySelector(target).parentElement;
+ menu = await openChromeContextMenu(contextMenuId, target);
+ nextMenuItemClassName = "customize-context-manageExtension";
+ break;
+
+ case "unified-extensions-context-menu":
+ await openExtensionsPanel();
+ buttonOrWidget = getUnifiedExtensionsItem(extension.id);
+ menu = await openUnifiedExtensionsContextMenu(extension.id);
+ nextMenuItemClassName = "unified-extensions-context-menu-pin-to-toolbar";
+ break;
+
+ default:
+ throw new Error(`unexpected context menu "${contextMenuId}"`);
+ }
+
+ let doc = menu.ownerDocument;
+ let visibleOriginItems = menu.querySelectorAll(
+ ":is(menuitem, menuseparator):not([hidden])"
+ );
+
+ info("Check expected menu items.");
+ for (let i = 0; i < items.length; i++) {
+ let l10n = doc.l10n.getAttributes(visibleOriginItems[i]);
+ Assert.deepEqual(
+ l10n,
+ items[i],
+ `Visible menu item ${i} has correct l10n attrs.`
+ );
+
+ let checked = visibleOriginItems[i].getAttribute("checked") === "true";
+ is(i === selected, checked, `Expected checked value for item ${i}.`);
+ }
+
+ if (items.length) {
+ is(
+ visibleOriginItems[items.length].nodeName,
+ "menuseparator",
+ "Found separator."
+ );
+ is(
+ visibleOriginItems[items.length + 1].className,
+ nextMenuItemClassName,
+ "All items accounted for."
+ );
+ }
+
+ is(
+ buttonOrWidget.hasAttribute("attention"),
+ !!attention,
+ "Expected attention badge before clicking."
+ );
+
+ Assert.deepEqual(
+ document.l10n.getAttributes(
+ buttonOrWidget.querySelector(".unified-extensions-item-action-button")
+ ),
+ {
+ // eslint-disable-next-line no-nested-ternary
+ id: attention
+ ? quarantined
+ ? "origin-controls-toolbar-button-quarantined"
+ : "origin-controls-toolbar-button-permission-needed"
+ : "origin-controls-toolbar-button",
+ args: {
+ extensionTitle: "Generated extension",
+ },
+ },
+ "Correct l10n message."
+ );
+
+ let itemToClick;
+ if (click) {
+ itemToClick = visibleOriginItems[click];
+ }
+
+ let quarantinePopup;
+ if (itemToClick && quarantined) {
+ quarantinePopup = promisePopupNotificationShown("addon-webext-permissions");
+ }
+
+ // Clicking a menu item of the unified extensions context menu should close
+ // the unified extensions panel automatically.
+ let panelHidden =
+ itemToClick && contextMenuId === "unified-extensions-context-menu"
+ ? BrowserTestUtils.waitForEvent(document, "popuphidden", true)
+ : Promise.resolve();
+
+ await closeChromeContextMenu(contextMenuId, itemToClick);
+ await panelHidden;
+
+ // When there is no menu item to close, we should manually close the unified
+ // extensions panel because simply closing the context menu will not close
+ // it.
+ if (!itemToClick && contextMenuId === "unified-extensions-context-menu") {
+ await closeExtensionsPanel();
+ }
+
+ if (granted) {
+ info("Waiting for the permissions.onAdded event.");
+ let host = await extension.awaitMessage("granted");
+ is(host, granted.join(), "Expected host permission granted.");
+ }
+ if (revoked) {
+ info("Waiting for the permissions.onRemoved event.");
+ let host = await extension.awaitMessage("revoked");
+ is(host, revoked.join(), "Expected host permission revoked.");
+ }
+
+ if (quarantinePopup) {
+ let popup = await quarantinePopup;
+ await testQuarantinePopup(popup);
+
+ if (allowQuarantine) {
+ popup.button.click();
+ } else {
+ popup.secondaryButton.click();
+ }
+ }
+}
+
+// Move the widget to the toolbar or the addons panel (if Unified Extensions
+// is enabled) or the overflow panel otherwise.
+function moveWidget(ext, pinToToolbar = false) {
+ let area = pinToToolbar
+ ? CustomizableUI.AREA_NAVBAR
+ : CustomizableUI.AREA_ADDONS;
+ let widgetId = `${makeWidgetId(ext.id)}-browser-action`;
+ CustomizableUI.addWidgetToArea(widgetId, area);
+}
+
+const originControlsInContextMenu = async options => {
+ // Has no permissions.
+ let ext1 = await makeExtension({ id: "ext1@test" });
+
+ // Has activeTab and (ungranted) example.com permissions.
+ let ext2 = await makeExtension({
+ id: "ext2@test",
+ permissions: ["activeTab"],
+ host_permissions: ["*://example.com/*"],
+ useAddonManager: "permanent",
+ });
+
+ // Has ungranted , and granted example.com.
+ let ext3 = await makeExtension({
+ id: "ext3@test",
+ host_permissions: [""],
+ granted: ["*://example.com/*"],
+ useAddonManager: "permanent",
+ });
+
+ // Has granted .
+ let ext4 = await makeExtension({
+ id: "ext4@test",
+ host_permissions: [""],
+ granted: [""],
+ useAddonManager: "permanent",
+ });
+
+ // MV2 extension with an content script and activeTab.
+ let ext5 = await makeExtension({
+ manifest_version: 2,
+ id: "ext5@test",
+ permissions: ["activeTab"],
+ content_scripts: [
+ {
+ matches: [""],
+ css: [],
+ },
+ ],
+ useAddonManager: "permanent",
+ });
+
+ // Add an extension always visible in the extensions panel.
+ let ext6 = await makeExtension({
+ id: "ext6@test",
+ default_area: "menupanel",
+ });
+
+ let extensions = [ext1, ext2, ext3, ext4, ext5, ext6];
+
+ let unifiedButton;
+ if (options.contextMenuId === "unified-extensions-context-menu") {
+ moveWidget(ext1, false);
+ moveWidget(ext2, false);
+ moveWidget(ext3, false);
+ moveWidget(ext4, false);
+ moveWidget(ext5, false);
+ unifiedButton = document.querySelector("#unified-extensions-button");
+ } else {
+ // TestVerify runs this again in the same Firefox instance, so move the
+ // widgets back to the toolbar for testing outside the unified extensions
+ // panel.
+ moveWidget(ext1, true);
+ moveWidget(ext2, true);
+ moveWidget(ext3, true);
+ moveWidget(ext4, true);
+ moveWidget(ext5, true);
+ }
+
+ const NO_ACCESS = { id: "origin-controls-no-access", args: null };
+ const QUARANTINED = {
+ id: "origin-controls-quarantined-status",
+ args: null,
+ };
+ const ALLOW_QUARANTINED = {
+ id: "origin-controls-quarantined-allow",
+ args: null,
+ };
+ const ACCESS_OPTIONS = { id: "origin-controls-options", args: null };
+ const ALL_SITES = { id: "origin-controls-option-all-domains", args: null };
+ const WHEN_CLICKED = {
+ id: "origin-controls-option-when-clicked",
+ args: null,
+ };
+
+ const UNIFIED_NO_ATTENTION = { id: "unified-extensions-button", args: null };
+ const UNIFIED_ATTENTION = {
+ id: "unified-extensions-button-permissions-needed",
+ args: null,
+ };
+ const UNIFIED_QUARANTINED = {
+ id: "unified-extensions-button-quarantined",
+ args: null,
+ };
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await testOriginControls(ext1, options, { items: [NO_ACCESS] });
+ await testOriginControls(ext2, options, { items: [NO_ACCESS] });
+ await testOriginControls(ext3, options, { items: [NO_ACCESS] });
+ await testOriginControls(ext4, options, { items: [NO_ACCESS] });
+ await testOriginControls(ext5, options, { items: [] });
+
+ if (unifiedButton) {
+ ok(
+ !unifiedButton.hasAttribute("attention"),
+ "No extension will have attention indicator on about:blank."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(unifiedButton),
+ UNIFIED_NO_ATTENTION,
+ "Unified button has no permissions needed tooltip."
+ );
+ }
+ });
+
+ await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
+ const ALWAYS_ON = {
+ id: "origin-controls-option-always-on",
+ args: { domain: "mochi.test" },
+ };
+
+ await testOriginControls(ext1, options, { items: [NO_ACCESS] });
+
+ // Has activeTab.
+ await testOriginControls(ext2, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED],
+ selected: 1,
+ attention: true,
+ });
+
+ // Could access mochi.test when clicked.
+ await testOriginControls(ext3, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
+ selected: 1,
+ attention: true,
+ });
+
+ // Has granted.
+ await testOriginControls(ext4, options, {
+ items: [ACCESS_OPTIONS, ALL_SITES],
+ selected: 1,
+ attention: false,
+ });
+
+ // MV2 extension, has no origin controls, and never flags for attention.
+ await testOriginControls(ext5, options, { items: [], attention: false });
+ if (unifiedButton) {
+ ok(
+ unifiedButton.hasAttribute("attention"),
+ "Both ext2 and ext3 are WHEN_CLICKED for example.com, so show attention indicator."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(unifiedButton),
+ UNIFIED_ATTENTION,
+ "UEB has permissions needed tooltip."
+ );
+ }
+ });
+
+ info("Testing again with mochi.test now quarantined.");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.quarantinedDomains.enabled", true],
+ ["extensions.quarantinedDomains.list", "mochi.test"],
+ ],
+ });
+
+ // Reset quarantined state between test runs.
+ QuarantinedDomains.setUserAllowedAddonIdPref(ext2.id, false);
+ QuarantinedDomains.setUserAllowedAddonIdPref(ext4.id, false);
+ QuarantinedDomains.setUserAllowedAddonIdPref(ext5.id, false);
+
+ await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
+ await testOriginControls(ext1, options, {
+ items: [NO_ACCESS],
+ attention: false,
+ });
+
+ await testOriginControls(ext2, options, {
+ items: [QUARANTINED, ALLOW_QUARANTINED],
+ attention: true,
+ quarantined: true,
+ click: 1,
+ allowQuarantine: false,
+ });
+ // Still quarantined.
+ await testOriginControls(ext2, options, {
+ items: [QUARANTINED, ALLOW_QUARANTINED],
+ attention: true,
+ quarantined: true,
+ click: 1,
+ allowQuarantine: true,
+ });
+ // Not quarantined anymore.
+ await testOriginControls(ext2, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED],
+ selected: 1,
+ attention: true,
+ quarantined: false,
+ });
+
+ await testOriginControls(ext3, options, {
+ items: [QUARANTINED, ALLOW_QUARANTINED],
+ attention: true,
+ quarantined: true,
+ });
+
+ await testOriginControls(ext4, options, {
+ items: [QUARANTINED, ALLOW_QUARANTINED],
+ attention: true,
+ quarantined: true,
+ click: 1,
+ allowQuarantine: true,
+ });
+ await testOriginControls(ext4, options, {
+ items: [ACCESS_OPTIONS, ALL_SITES],
+ selected: 1,
+ attention: false,
+ quarantined: false,
+ });
+
+ // MV2 normally don't have controls, but we always show for quarantined.
+ await testOriginControls(ext5, options, {
+ items: [QUARANTINED, ALLOW_QUARANTINED],
+ attention: true,
+ quarantined: true,
+ click: 1,
+ allowQuarantine: true,
+ });
+ await testOriginControls(ext5, options, {
+ items: [],
+ attention: false,
+ quarantined: false,
+ });
+
+ if (unifiedButton) {
+ ok(unifiedButton.hasAttribute("attention"), "Expected attention UI");
+ Assert.deepEqual(
+ document.l10n.getAttributes(unifiedButton),
+ UNIFIED_QUARANTINED,
+ "Expected attention tooltip text for quarantined domains"
+ );
+ }
+ });
+
+ if (unifiedButton) {
+ extensions.forEach(extension =>
+ moveWidget(extension, /* pinToToolbar */ true)
+ );
+
+ await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
+ ok(unifiedButton.hasAttribute("attention"), "Expected attention UI");
+ Assert.deepEqual(
+ document.l10n.getAttributes(unifiedButton),
+ UNIFIED_QUARANTINED,
+ "Expected attention tooltip text for quarantined domains"
+ );
+
+ await openExtensionsPanel();
+
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+ const supportLink = messages[0].querySelector("a");
+ Assert.equal(
+ supportLink.getAttribute("support-page"),
+ "quarantined-domains",
+ "Expected the correct support page ID"
+ );
+
+ await closeExtensionsPanel();
+ });
+
+ extensions.forEach(extension =>
+ moveWidget(extension, /* pinToToolbar */ false)
+ );
+ }
+
+ await SpecialPowers.popPrefEnv();
+
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ const ALWAYS_ON = {
+ id: "origin-controls-option-always-on",
+ args: { domain: "example.com" },
+ };
+
+ await testOriginControls(ext1, options, { items: [NO_ACCESS] });
+
+ // Click alraedy selected options, expect no permission changes.
+ await testOriginControls(ext2, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
+ selected: 1,
+ click: 1,
+ attention: true,
+ });
+ await testOriginControls(ext3, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
+ selected: 2,
+ click: 2,
+ attention: false,
+ });
+ await testOriginControls(ext4, options, {
+ items: [ACCESS_OPTIONS, ALL_SITES],
+ selected: 1,
+ click: 1,
+ attention: false,
+ });
+
+ await testOriginControls(ext5, options, { items: [], attention: false });
+
+ if (unifiedButton) {
+ ok(
+ unifiedButton.hasAttribute("attention"),
+ "ext2 is WHEN_CLICKED for example.com, show attention indicator."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(unifiedButton),
+ UNIFIED_ATTENTION,
+ "UEB attention for only one extension."
+ );
+ }
+
+ // Click the other option, expect example.com permission granted/revoked.
+ await testOriginControls(ext2, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
+ selected: 1,
+ click: 2,
+ granted: ["*://example.com/*"],
+ attention: true,
+ });
+ if (unifiedButton) {
+ ok(
+ !unifiedButton.hasAttribute("attention"),
+ "Bot ext2 and ext3 are ALWAYS_ON for example.com, so no attention indicator."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(unifiedButton),
+ UNIFIED_NO_ATTENTION,
+ "Unified button has no permissions needed tooltip."
+ );
+ }
+
+ await testOriginControls(ext3, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
+ selected: 2,
+ click: 1,
+ revoked: ["*://example.com/*"],
+ attention: false,
+ });
+ if (unifiedButton) {
+ ok(
+ unifiedButton.hasAttribute("attention"),
+ "ext3 is now WHEN_CLICKED for example.com, show attention indicator."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(unifiedButton),
+ UNIFIED_ATTENTION,
+ "UEB attention for only one extension."
+ );
+ }
+
+ // Other option is now selected.
+ await testOriginControls(ext2, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
+ selected: 2,
+ attention: false,
+ });
+ await testOriginControls(ext3, options, {
+ items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
+ selected: 1,
+ attention: true,
+ });
+
+ if (unifiedButton) {
+ ok(
+ unifiedButton.hasAttribute("attention"),
+ "Still showing the attention indicator."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(unifiedButton),
+ UNIFIED_ATTENTION,
+ "UEB attention for only one extension."
+ );
+ }
+ });
+
+ // Regression test for Bug 1861002.
+ const addonListener = {
+ registered: false,
+ onPropertyChanged(addon, changedProps) {
+ ok(
+ addon,
+ `onPropertyChanged should not be called without an AddonWrapper for changed properties: ${changedProps}`
+ );
+ },
+ };
+ AddonManager.addAddonListener(addonListener);
+ addonListener.registered = true;
+ const unregisterAddonListener = () => {
+ if (!addonListener.registered) {
+ return;
+ }
+ AddonManager.removeAddonListener(addonListener);
+ addonListener.registered = false;
+ };
+ registerCleanupFunction(unregisterAddonListener);
+
+ let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ await Promise.all(extensions.map(e => e.unload()));
+ });
+
+ unregisterAddonListener();
+
+ AddonTestUtils.checkMessages(
+ messages,
+ {
+ forbidden: [
+ {
+ message:
+ /AddonListener threw exception when calling onPropertyChanged/,
+ },
+ ],
+ },
+ "Expect no exception raised from AddonListener onPropertyChanged callbacks"
+ );
+};
+
+add_task(async function originControls_in_browserAction_contextMenu() {
+ await originControlsInContextMenu({ contextMenuId: "toolbar-context-menu" });
+});
+
+add_task(async function originControls_in_unifiedExtensions_contextMenu() {
+ await originControlsInContextMenu({
+ contextMenuId: "unified-extensions-context-menu",
+ });
+});
+
+add_task(async function test_attention_dot_when_pinning_extension() {
+ const extension = await makeExtension({ permissions: ["activeTab"] });
+ await extension.startup();
+
+ const unifiedButton = document.querySelector("#unified-extensions-button");
+ const extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extension.id
+ );
+ const extensionWidget =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(window).node;
+
+ await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
+ // The extensions should be placed in the navbar by default so we do not
+ // expect an attention dot on the Unifed Extensions Button (UEB), only on
+ // the extension (widget) itself.
+ ok(
+ !unifiedButton.hasAttribute("attention"),
+ "expected no attention attribute on the UEB"
+ );
+ ok(
+ extensionWidget.hasAttribute("attention"),
+ "expected attention attribute on the extension widget"
+ );
+
+ // Open the context menu of the extension and unpin the extension.
+ let contextMenu = await openChromeContextMenu(
+ "toolbar-context-menu",
+ `#${CSS.escape(extensionWidgetID)}`
+ );
+ let pinToToolbar = contextMenu.querySelector(
+ ".customize-context-pinToToolbar"
+ );
+ ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item");
+ // Passing the `pinToToolbar` item to `closeChromeContextMenu()` will
+ // activate it before closing the context menu.
+ await closeChromeContextMenu(contextMenu.id, pinToToolbar);
+
+ ok(
+ unifiedButton.hasAttribute("attention"),
+ "expected attention attribute on the UEB"
+ );
+ // We still expect the attention dot on the extension.
+ ok(
+ extensionWidget.hasAttribute("attention"),
+ "expected attention attribute on the extension widget"
+ );
+
+ // Now let's open the unified extensions panel, and pin the same extension
+ // to the toolbar, which should hide the attention dot on the UEB again.
+ await openExtensionsPanel();
+ contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+ pinToToolbar = contextMenu.querySelector(
+ ".unified-extensions-context-menu-pin-to-toolbar"
+ );
+ ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item");
+ const hidden = BrowserTestUtils.waitForEvent(
+ gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ contextMenu.activateItem(pinToToolbar);
+ await hidden;
+
+ ok(
+ !unifiedButton.hasAttribute("attention"),
+ "expected no attention attribute on the UEB"
+ );
+ // We still expect the attention dot on the extension.
+ ok(
+ extensionWidget.hasAttribute("attention"),
+ "expected attention attribute on the extension widget"
+ );
+ });
+
+ await extension.unload();
+});
+
+async function testWithSubmenu(menu, nextItemClassName) {
+ function expectMenuItems() {
+ info("Checking expected menu items.");
+ let [submenu, sep1, ocMessage, sep2, next] = menu.children;
+
+ is(submenu.tagName, "menu", "First item is a submenu.");
+ is(submenu.label, "submenu", "Submenu has the expected label.");
+ is(sep1.tagName, "menuseparator", "Second item is a separator.");
+
+ let l10n = menu.ownerDocument.l10n.getAttributes(ocMessage);
+ is(ocMessage.tagName, "menuitem", "Third is origin controls message.");
+ is(l10n.id, "origin-controls-no-access", "Expected l10n id.");
+
+ is(sep2.tagName, "menuseparator", "Fourth item is a separator.");
+ is(next.className, nextItemClassName, "All items accounted for.");
+ }
+
+ const [submenu] = menu.children;
+ const popup = submenu.querySelector("menupopup");
+
+ // Open and close the submenu repeatedly a few times.
+ for (let i = 0; i < 3; i++) {
+ expectMenuItems();
+
+ const popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ submenu.openMenu(true);
+ await popupShown;
+
+ expectMenuItems();
+
+ const popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ submenu.openMenu(false);
+ await popupHidden;
+ }
+
+ menu.hidePopup();
+}
+
+add_task(async function test_originControls_with_submenus() {
+ let extension = await makeExtension({
+ id: "submenus@test",
+ permissions: ["menus"],
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ info(`Testing with submenus.`);
+ moveWidget(extension, true);
+ let target = `#${CSS.escape(makeWidgetId(extension.id))}-BAP`;
+
+ await testWithSubmenu(
+ await openChromeContextMenu("toolbar-context-menu", target),
+ "customize-context-manageExtension"
+ );
+
+ info(`Testing with submenus inside extensions panel.`);
+ moveWidget(extension, false);
+ await openExtensionsPanel();
+
+ await testWithSubmenu(
+ await openUnifiedExtensionsContextMenu(extension.id),
+ "unified-extensions-context-menu-pin-to-toolbar"
+ );
+
+ await closeExtensionsPanel();
+ });
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_activeTab.js b/browser/components/extensions/test/browser/browser_ext_pageAction_activeTab.js
new file mode 100644
index 0000000000..4ce6b247bf
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_activeTab.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_middle_click_with_activeTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {},
+ permissions: ["activeTab"],
+ },
+
+ async background() {
+ browser.pageAction.onClicked.addListener(async (tab, info) => {
+ browser.test.assertEq(1, info.button, "Expected button value");
+ browser.test.assertEq(
+ "https://example.com/",
+ tab.url,
+ "tab.url has the expected url"
+ );
+ await browser.tabs.insertCSS(tab.id, {
+ code: "body { border: 20px solid red; }",
+ });
+ browser.test.sendMessage("onClick");
+ });
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+ is(
+ ext.tabManager.hasActiveTabPermission(tab),
+ false,
+ "Active tab was not granted permission"
+ );
+
+ await clickPageAction(extension, window, { button: 1 });
+ await extension.awaitMessage("onClick");
+
+ is(
+ ext.tabManager.hasActiveTabPermission(tab),
+ true,
+ "Active tab was granted permission"
+ );
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_middle_click_without_activeTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {},
+ },
+
+ async background() {
+ browser.pageAction.onClicked.addListener(async (tab, info) => {
+ browser.test.assertEq(1, info.button, "Expected button value");
+ browser.test.assertEq(tab.url, undefined, "tab.url is undefined");
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(tab.id, {
+ code: "body { border: 20px solid red; }",
+ }),
+ "Missing host permission for the tab",
+ "expected failure of tabs.insertCSS without permission"
+ );
+ browser.test.sendMessage("onClick");
+ });
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await clickPageAction(extension, window, { button: 1 });
+ await extension.awaitMessage("onClick");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_click_types.js b/browser/components/extensions/test/browser/browser_ext_pageAction_click_types.js
new file mode 100644
index 0000000000..0168ea0ab2
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_click_types.js
@@ -0,0 +1,240 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_setup(async function () {
+ // The page action button is hidden by default.
+ // This tests the use of pageAction when the button is visible.
+ //
+ // TODO(Bug 1704171): this should technically be removed in a follow up
+ // and the tests in this file adapted to keep into account that:
+ // - The pageAction is pinned on the urlbar by default
+ // when shown, and hidden when is not available (same for the
+ // overflow menu when enabled)
+ BrowserPageActions.mainButtonNode.style.visibility = "visible";
+ registerCleanupFunction(() => {
+ BrowserPageActions.mainButtonNode.style.removeProperty("visibility");
+ });
+});
+
+async function test_clickData(testAsNonPersistent = false) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {},
+ background: {
+ persistent: !testAsNonPersistent,
+ scripts: ["background.js"],
+ },
+ },
+
+ files: {
+ "background.js": async function background() {
+ function onClicked(_tab, info) {
+ let button = info.button;
+ let modifiers = info.modifiers;
+ browser.test.sendMessage("onClick", { button, modifiers });
+ }
+
+ browser.pageAction.onClicked.addListener(onClicked);
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("ready");
+ },
+ },
+ });
+
+ const map = {
+ shiftKey: "Shift",
+ altKey: "Alt",
+ metaKey: "Command",
+ ctrlKey: "Ctrl",
+ };
+
+ function assertSingleModifier(info, modifier) {
+ if (modifier === "ctrlKey" && AppConstants.platform === "macosx") {
+ is(
+ info.modifiers.length,
+ 2,
+ `MacCtrl modifier with control click on Mac`
+ );
+ is(
+ info.modifiers[1],
+ "MacCtrl",
+ `MacCtrl modifier with control click on Mac`
+ );
+ } else {
+ is(
+ info.modifiers.length,
+ 1,
+ `No unnecessary modifiers for exactly one key on event`
+ );
+ }
+
+ is(info.modifiers[0], map[modifier], `Correct modifier on click event`);
+ }
+
+ async function testClickPageAction(doClick, doEnterKey) {
+ for (let modifier of Object.keys(map)) {
+ for (let i = 0; i < 2; i++) {
+ let clickEventData = { button: i };
+ clickEventData[modifier] = true;
+ await doClick(extension, window, clickEventData);
+ let info = await extension.awaitMessage("onClick");
+
+ is(info.button, i, `Correct button on click event`);
+ assertSingleModifier(info, modifier);
+ }
+
+ let keypressEventData = {};
+ keypressEventData[modifier] = true;
+ await doEnterKey(extension, keypressEventData);
+ let info = await extension.awaitMessage("onClick");
+
+ is(info.button, 0, `Key command emulates left click`);
+ assertSingleModifier(info, modifier);
+ }
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (testAsNonPersistent) {
+ assertPersistentListeners(extension, "pageAction", "onClicked", {
+ primed: false,
+ });
+ info("Terminating the background event page");
+ await extension.terminateBackground();
+ assertPersistentListeners(extension, "pageAction", "onClicked", {
+ primed: true,
+ });
+ }
+
+ info("Clicking the pageAction");
+ await testClickPageAction(clickPageAction, triggerPageActionWithKeyboard);
+
+ if (testAsNonPersistent) {
+ await extension.awaitMessage("ready");
+ assertPersistentListeners(extension, "pageAction", "onClicked", {
+ primed: false,
+ });
+ }
+
+ await testClickPageAction(
+ clickPageActionInPanel,
+ triggerPageActionWithKeyboardInPanel
+ );
+
+ await extension.unload();
+}
+
+async function test_clickData_reset(testAsNonPersistent = false) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_area: "navbar",
+ },
+ page_action: {},
+ background: {
+ persistent: !testAsNonPersistent,
+ scripts: ["background.js"],
+ },
+ },
+
+ files: {
+ "background.js": async function background() {
+ function onBrowserActionClicked(tab, info) {
+ // openPopup requires user interaction, such as a browser action click.
+ browser.pageAction.openPopup();
+ }
+
+ function onPageActionClicked(tab, info) {
+ browser.test.sendMessage("onClick", info);
+ }
+
+ browser.browserAction.onClicked.addListener(onBrowserActionClicked);
+ browser.pageAction.onClicked.addListener(onPageActionClicked);
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("ready");
+ },
+ },
+ });
+
+ async function clickPageActionWithModifiers() {
+ await clickPageAction(extension, window, { button: 1, shiftKey: true });
+ let info = await extension.awaitMessage("onClick");
+ is(info.button, 1);
+ is(info.modifiers[0], "Shift");
+ }
+
+ function assertInfoReset(info) {
+ is(info.button, 0, `ClickData button reset properly`);
+ is(info.modifiers.length, 0, `ClickData modifiers reset properly`);
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (testAsNonPersistent) {
+ assertPersistentListeners(extension, "pageAction", "onClicked", {
+ primed: false,
+ });
+ info("Terminating the background event page");
+ await extension.terminateBackground();
+ assertPersistentListeners(extension, "pageAction", "onClicked", {
+ primed: true,
+ });
+ }
+
+ info("Clicking the pageAction");
+ await clickPageActionWithModifiers();
+
+ if (testAsNonPersistent) {
+ await extension.awaitMessage("ready");
+ assertPersistentListeners(extension, "pageAction", "onClicked", {
+ primed: false,
+ });
+ }
+
+ await clickBrowserAction(extension);
+ assertInfoReset(await extension.awaitMessage("onClick"));
+
+ await clickPageActionWithModifiers();
+
+ await triggerPageActionWithKeyboard(extension);
+ assertInfoReset(await extension.awaitMessage("onClick"));
+
+ await extension.unload();
+}
+
+add_task(function test_clickData_MV2() {
+ return test_clickData(/* testAsNonPersistent */ false);
+});
+
+add_task(async function test_clickData_MV2_eventPage() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+ await test_clickData(/* testAsNonPersistent */ true);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(function test_clickData_reset_MV2() {
+ return test_clickData_reset(/* testAsNonPersistent */ false);
+});
+
+add_task(async function test_clickData_reset_MV2_eventPage() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+ await test_clickData_reset(/* testAsNonPersistent */ true);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
new file mode 100644
index 0000000000..fde45cf2f5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -0,0 +1,453 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_pageAction.js");
+
+add_task(async function testTabSwitchContext() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ page_action: {
+ default_icon: "default.png",
+ default_popup: "__MSG_popup__",
+ default_title: "Default __MSG_title__ \u263a",
+ },
+
+ default_locale: "en",
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "_locales/en/messages.json": {
+ popup: {
+ message: "default.html",
+ description: "Popup",
+ },
+
+ title: {
+ message: "Title",
+ description: "Title",
+ },
+ },
+
+ "_locales/es_ES/messages.json": {
+ popup: {
+ message: "default.html",
+ description: "Popup",
+ },
+
+ title: {
+ message: "T\u00edtulo",
+ description: "Title",
+ },
+ },
+
+ "default.png": imageBuffer,
+ "1.png": imageBuffer,
+ "2.png": imageBuffer,
+ },
+
+ getTests: function (tabs) {
+ let defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default T\u00edtulo \u263a",
+ },
+ {
+ icon: browser.runtime.getURL("1.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default T\u00edtulo \u263a",
+ },
+ {
+ icon: browser.runtime.getURL("2.png"),
+ popup: browser.runtime.getURL("2.html"),
+ title: "Title 2",
+ },
+ { icon: defaultIcon, popup: "", title: "" },
+ ];
+
+ let promiseTabLoad = details => {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
+ if (tabId == details.id && changed.url == details.url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ };
+
+ return [
+ expect => {
+ browser.test.log("Initial state. No icon visible.");
+ expect(null);
+ },
+ async expect => {
+ browser.test.log(
+ "Show the icon on the first tab, expect default properties."
+ );
+ await browser.pageAction.show(tabs[0]);
+ expect(details[0]);
+ },
+ expect => {
+ browser.test.log(
+ "Change the icon. Expect default properties excluding the icon."
+ );
+ browser.pageAction.setIcon({ tabId: tabs[0], path: "1.png" });
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. No icon visible.");
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?0",
+ });
+ tabs.push(tab.id);
+ expect(null);
+ },
+ async expect => {
+ browser.test.log("Await tab load. No icon visible.");
+ let promise = promiseTabLoad({ id: tabs[1], url: "about:blank?0" });
+ let { url } = await browser.tabs.get(tabs[1]);
+ if (url === "about:blank") {
+ await promise;
+ }
+ expect(null);
+ },
+ async expect => {
+ browser.test.log("Change properties. Expect new properties.");
+ let tabId = tabs[1];
+ await browser.pageAction.show(tabId);
+
+ browser.pageAction.setIcon({ tabId, path: "2.png" });
+ browser.pageAction.setPopup({ tabId, popup: "2.html" });
+ browser.pageAction.setTitle({ tabId, title: "Title 2" });
+
+ expect(details[2]);
+ },
+ async expect => {
+ browser.test.log("Change the hash. Expect same properties.");
+
+ let promise = promiseTabLoad({
+ id: tabs[1],
+ url: "about:blank?0#ref",
+ });
+ browser.tabs.update(tabs[1], { url: "about:blank?0#ref" });
+ await promise;
+
+ expect(details[2]);
+ },
+ expect => {
+ browser.test.log(
+ "Set empty string values. Expect empty strings but default icon."
+ );
+ browser.pageAction.setIcon({ tabId: tabs[1], path: "" });
+ browser.pageAction.setPopup({ tabId: tabs[1], popup: "" });
+ browser.pageAction.setTitle({ tabId: tabs[1], title: "" });
+
+ expect(details[3]);
+ },
+ expect => {
+ browser.test.log("Clear the values. Expect default ones.");
+ browser.pageAction.setIcon({ tabId: tabs[1], path: null });
+ browser.pageAction.setPopup({ tabId: tabs[1], popup: null });
+ browser.pageAction.setTitle({ tabId: tabs[1], title: null });
+
+ expect(details[0]);
+ },
+ async expect => {
+ browser.test.log("Navigate to a new page. Expect icon hidden.");
+
+ // TODO: This listener should not be necessary, but the |tabs.update|
+ // callback currently fires too early in e10s windows.
+ let promise = promiseTabLoad({ id: tabs[1], url: "about:blank?1" });
+
+ browser.tabs.update(tabs[1], { url: "about:blank?1" });
+
+ await promise;
+ expect(null);
+ },
+ async expect => {
+ browser.test.log("Show the icon. Expect default properties again.");
+
+ await browser.pageAction.show(tabs[1]);
+ expect(details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to the first tab. Expect previously set properties."
+ );
+ await browser.tabs.update(tabs[0], { active: true });
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log(
+ "Hide the icon on tab 2. Switch back, expect hidden."
+ );
+ await browser.pageAction.hide(tabs[1]);
+
+ await browser.tabs.update(tabs[1], { active: true });
+ expect(null);
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to tab 1. Expect previous results again."
+ );
+ await browser.tabs.remove(tabs[1]);
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Hide the icon. Expect hidden.");
+
+ await browser.pageAction.hide(tabs[0]);
+ expect(null);
+ },
+ async expect => {
+ browser.test.assertRejects(
+ browser.pageAction.setPopup({
+ tabId: tabs[0],
+ popup: "about:addons",
+ }),
+ /Access denied for URL about:addons/,
+ "unable to set popup to about:addons"
+ );
+
+ expect(null);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testMultipleWindows() {
+ // Disable newtab preloading, so that the tabs.create call below will always
+ // trigger a new load that can be detected by webNavigation.onCompleted.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtab.preload", false]],
+ });
+ await runTests({
+ manifest: {
+ page_action: {
+ default_icon: "default.png",
+ default_popup: "default.html",
+ default_title: "Default Title",
+ },
+ permissions: ["webNavigation"],
+ },
+
+ files: {
+ "default.png": imageBuffer,
+ "tab.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ },
+ {
+ icon: browser.runtime.getURL("tab.png"),
+ popup: browser.runtime.getURL("tab.html"),
+ title: "tab",
+ },
+ ];
+
+ function promiseWebNavigationCompleted(url) {
+ return new Promise(resolve => {
+ // The pageAction visibility state is reset when the location changes.
+ // The webNavigation.onCompleted event is triggered when that happens.
+ browser.webNavigation.onCompleted.addListener(
+ function listener() {
+ browser.webNavigation.onCompleted.removeListener(listener);
+ resolve();
+ },
+ {
+ url: [{ urlEquals: url }],
+ }
+ );
+ });
+ }
+
+ return [
+ async expect => {
+ browser.test.log("Create a new tab, expect hidden pageAction.");
+ let promise = promiseWebNavigationCompleted("about:newtab");
+ let tab = await browser.tabs.create({ active: true });
+ await promise;
+ tabs.push(tab.id);
+ expect(null);
+ },
+ async expect => {
+ browser.test.log("Show the pageAction, expect default values.");
+ await browser.pageAction.show(tabs[1]);
+ expect(details[0]);
+ },
+ async expect => {
+ browser.test.log("Set tab-specific values, expect them.");
+ await browser.pageAction.setIcon({ tabId: tabs[1], path: "tab.png" });
+ await browser.pageAction.setPopup({
+ tabId: tabs[1],
+ popup: "tab.html",
+ });
+ await browser.pageAction.setTitle({ tabId: tabs[1], title: "tab" });
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Open a new window, expect hidden pageAction.");
+ let { id } = await browser.windows.create();
+ windows.push(id);
+ expect(null);
+ },
+ async expect => {
+ browser.test.log(
+ "Move tab from old window to the new one, expect old values."
+ );
+ await browser.tabs.move(tabs[1], { windowId: windows[1], index: -1 });
+ await browser.tabs.update(tabs[1], { active: true });
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Close the initial tab of the new window.");
+ let [{ id }] = await browser.tabs.query({
+ windowId: windows[1],
+ index: 0,
+ });
+ await browser.tabs.remove(id);
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log(
+ "Move the previous tab to a 3rd window, the 2nd one will close."
+ );
+ await browser.windows.create({ tabId: tabs[1] });
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Close the tab, go back to the 1st window.");
+ await browser.tabs.remove(tabs[1]);
+ expect(null);
+ },
+ ];
+ },
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testNavigationClearsData() {
+ let url = "http://example.com/";
+ let default_title = "Default title";
+ let tab_title = "Tab title";
+
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+ let extension,
+ tabs = [];
+ async function addTab(...args) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, ...args);
+ tabs.push(tab);
+ return tab;
+ }
+ async function sendMessage(method, param, expect, msg) {
+ extension.sendMessage({ method, param, expect, msg });
+ await extension.awaitMessage("done");
+ }
+ async function expectTabSpecificData(tab, msg) {
+ let tabId = tabTracker.getId(tab);
+ await sendMessage("isShown", { tabId }, true, msg);
+ await sendMessage("getTitle", { tabId }, tab_title, msg);
+ }
+ async function expectDefaultData(tab, msg) {
+ let tabId = tabTracker.getId(tab);
+ await sendMessage("isShown", { tabId }, false, msg);
+ await sendMessage("getTitle", { tabId }, default_title, msg);
+ }
+ async function setTabSpecificData(tab) {
+ let tabId = tabTracker.getId(tab);
+ await expectDefaultData(
+ tab,
+ "Expect default data before setting tab-specific data."
+ );
+ await sendMessage("show", tabId);
+ await sendMessage("setTitle", { tabId, title: tab_title });
+ await expectTabSpecificData(
+ tab,
+ "Expect tab-specific data after setting it."
+ );
+ }
+
+ info("Load a tab before installing the extension");
+ let tab1 = await addTab(url, true, true);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: { default_title },
+ },
+ background: function () {
+ browser.test.onMessage.addListener(
+ async ({ method, param, expect, msg }) => {
+ let result = await browser.pageAction[method](param);
+ if (expect !== undefined) {
+ browser.test.assertEq(expect, result, msg);
+ }
+ browser.test.sendMessage("done");
+ }
+ );
+ },
+ });
+ await extension.startup();
+
+ info("Set tab-specific data to the existing tab.");
+ await setTabSpecificData(tab1);
+
+ info("Add a hash. Does not cause navigation.");
+ await navigateTab(tab1, url + "#hash");
+ await expectTabSpecificData(
+ tab1,
+ "Adding a hash does not clear tab-specific data"
+ );
+
+ info("Remove the hash. Causes navigation.");
+ await navigateTab(tab1, url);
+ await expectDefaultData(tab1, "Removing hash clears tab-specific data");
+
+ info("Open a new tab, set tab-specific data to it.");
+ let tab2 = await addTab("about:newtab", false, false);
+ await setTabSpecificData(tab2);
+
+ info("Load a page in that tab.");
+ await navigateTab(tab2, url);
+ await expectDefaultData(tab2, "Loading a page clears tab-specific data.");
+
+ info("Set tab-specific data.");
+ await setTabSpecificData(tab2);
+
+ info("Push history state. Does not cause navigation.");
+ await historyPushState(tab2, url + "/path");
+ await expectTabSpecificData(
+ tab2,
+ "history.pushState() does not clear tab-specific data"
+ );
+
+ info("Navigate when the tab is not selected");
+ gBrowser.selectedTab = tab1;
+ await navigateTab(tab2, url);
+ await expectDefaultData(
+ tab2,
+ "Navigating clears tab-specific data, even when not selected."
+ );
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_contextMenu.js b/browser/components/extensions/test/browser/browser_ext_pageAction_contextMenu.js
new file mode 100644
index 0000000000..57ecc889ae
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_contextMenu.js
@@ -0,0 +1,128 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let extData = {
+ manifest: {
+ permissions: ["contextMenus"],
+ page_action: {
+ default_popup: "popup.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "popup.html": `
+
+
+
+
+
+ A Test Popup
+
+
+ `,
+ },
+
+ background: function () {
+ browser.contextMenus.create({
+ id: "clickme-page",
+ title: "Click me!",
+ contexts: ["all"],
+ });
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ const tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("action-shown");
+ });
+ });
+ },
+};
+
+let contextMenuItems = {
+ "context-sep-navigation": "hidden",
+ "context-viewsource": "",
+ "inspect-separator": "hidden",
+ "context-inspect": "hidden",
+ "context-inspect-a11y": "hidden",
+ "context-bookmarkpage": "hidden",
+};
+if (AppConstants.platform == "macosx") {
+ contextMenuItems["context-back"] = "hidden";
+ contextMenuItems["context-forward"] = "hidden";
+ contextMenuItems["context-reload"] = "hidden";
+ contextMenuItems["context-stop"] = "hidden";
+} else {
+ contextMenuItems["context-navigation"] = "hidden";
+}
+
+add_task(async function pageaction_popup_contextmenu() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ await extension.awaitMessage("action-shown");
+
+ await clickPageAction(extension, window);
+
+ let contentAreaContextMenu = await openContextMenuInPopup(extension);
+ let item = contentAreaContextMenu.getElementsByAttribute(
+ "label",
+ "Click me!"
+ );
+ is(item.length, 1, "contextMenu item for page was found");
+ await closeContextMenu(contentAreaContextMenu);
+
+ await extension.unload();
+});
+
+add_task(async function pageaction_popup_contextmenu_hidden_items() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ await extension.awaitMessage("action-shown");
+
+ await clickPageAction(extension, window);
+
+ let contentAreaContextMenu = await openContextMenuInPopup(extension, "#text");
+
+ let item, state;
+ for (const itemID in contextMenuItems) {
+ item = contentAreaContextMenu.querySelector(`#${itemID}`);
+ state = contextMenuItems[itemID];
+
+ if (state !== "") {
+ ok(item[state], `${itemID} is ${state}`);
+
+ if (state !== "hidden") {
+ ok(!item.hidden, `Disabled ${itemID} is not hidden`);
+ }
+ } else {
+ ok(!item.hidden, `${itemID} is not hidden`);
+ ok(!item.disabled, `${itemID} is not disabled`);
+ }
+ }
+
+ await closeContextMenu(contentAreaContextMenu);
+
+ await extension.unload();
+});
+
+add_task(async function pageaction_popup_image_contextmenu() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ await extension.awaitMessage("action-shown");
+
+ await clickPageAction(extension, window);
+
+ let contentAreaContextMenu = await openContextMenuInPopup(
+ extension,
+ "#testimg"
+ );
+
+ let item = contentAreaContextMenu.querySelector("#context-copyimage");
+ ok(!item.hidden);
+ ok(!item.disabled);
+
+ await closeContextMenu(contentAreaContextMenu);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
new file mode 100644
index 0000000000..366827dd1b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -0,0 +1,305 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+PromiseTestUtils.allowMatchingRejectionsGlobally(/packaging errors/);
+
+function assertViewCount(extension, count) {
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+ is(
+ ext.views.size,
+ count,
+ "Should have the expected number of extension views"
+ );
+}
+
+add_task(async function testPageActionPopup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let scriptPage = url =>
+ ` `;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ background: {
+ page: "data/background.html",
+ },
+ page_action: {
+ default_popup: "popup-a.html",
+ },
+ },
+
+ files: {
+ "popup-a.html": scriptPage("popup-a.js"),
+ "popup-a.js": function () {
+ window.onload = () => {
+ let background = window.getComputedStyle(
+ document.body
+ ).backgroundColor;
+ browser.test.assertEq("rgba(0, 0, 0, 0)", background);
+ browser.runtime.sendMessage("from-popup-a");
+ };
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "close-popup") {
+ window.close();
+ }
+ });
+ },
+
+ "data/popup-b.html": scriptPage("popup-b.js"),
+ "data/popup-b.js": function () {
+ browser.runtime.sendMessage("from-popup-b");
+ },
+
+ "data/background.html": scriptPage("background.js"),
+
+ "data/background.js": async function () {
+ let tabId;
+
+ let sendClick;
+ let tests = [
+ () => {
+ sendClick({ expectEvent: false, expectPopup: "a" });
+ },
+ () => {
+ sendClick({ expectEvent: false, expectPopup: "a" });
+ },
+ () => {
+ browser.pageAction.setPopup({ tabId, popup: "popup-b.html" });
+ sendClick({ expectEvent: false, expectPopup: "b" });
+ },
+ () => {
+ sendClick({ expectEvent: false, expectPopup: "b" });
+ },
+ () => {
+ sendClick({
+ expectEvent: true,
+ expectPopup: "b",
+ middleClick: true,
+ });
+ },
+ () => {
+ browser.pageAction.setPopup({ tabId, popup: "" });
+ sendClick({ expectEvent: true, expectPopup: null });
+ },
+ () => {
+ sendClick({ expectEvent: true, expectPopup: null });
+ },
+ () => {
+ browser.pageAction.setPopup({ tabId, popup: "/popup-a.html" });
+ sendClick({
+ expectEvent: false,
+ expectPopup: "a",
+ runNextTest: true,
+ });
+ },
+ () => {
+ browser.test.sendMessage("next-test", { expectClosed: true });
+ },
+ () => {
+ sendClick({
+ expectEvent: false,
+ expectPopup: "a",
+ runNextTest: true,
+ });
+ },
+ () => {
+ browser.test.sendMessage("next-test", { closeOnTabSwitch: true });
+ },
+ ];
+
+ let expect = {};
+ sendClick = ({
+ expectEvent,
+ expectPopup,
+ runNextTest,
+ middleClick,
+ }) => {
+ expect = { event: expectEvent, popup: expectPopup, runNextTest };
+
+ browser.test.sendMessage("send-click", middleClick ? 1 : 0);
+ };
+
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "close-popup") {
+ return;
+ } else if (expect.popup) {
+ browser.test.assertEq(
+ msg,
+ `from-popup-${expect.popup}`,
+ "expected popup opened"
+ );
+ } else {
+ browser.test.fail(`unexpected popup: ${msg}`);
+ }
+
+ expect.popup = null;
+ if (expect.runNextTest) {
+ expect.runNextTest = false;
+ tests.shift()();
+ } else {
+ browser.test.sendMessage("next-test");
+ }
+ });
+
+ browser.pageAction.onClicked.addListener((tab, info) => {
+ if (expect.event) {
+ browser.test.succeed("expected click event received");
+ } else {
+ browser.test.fail("unexpected click event");
+ }
+ expect.event = false;
+
+ if (info.button == 1) {
+ browser.pageAction.openPopup();
+ return;
+ }
+
+ browser.test.sendMessage("next-test");
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "close-popup") {
+ browser.runtime.sendMessage("close-popup");
+ return;
+ }
+
+ if (msg != "next-test") {
+ browser.test.fail("Expecting 'next-test' message");
+ }
+
+ if (expect.event) {
+ browser.test.fail(
+ "Expecting click event before next test but none occurred"
+ );
+ }
+
+ if (expect.popup) {
+ browser.test.fail(
+ "Expecting popup before next test but none were shown"
+ );
+ }
+
+ if (tests.length) {
+ let test = tests.shift();
+ test();
+ } else {
+ browser.test.notifyPass("pageaction-tests-done");
+ }
+ });
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ tabId = tab.id;
+
+ await browser.pageAction.show(tabId);
+ browser.test.sendMessage("next-test");
+ },
+ },
+ });
+
+ extension.onMessage("send-click", button => {
+ clickPageAction(extension, window, { button });
+ });
+
+ let pageActionId, panelId;
+ extension.onMessage("next-test", async function (expecting = {}) {
+ pageActionId = `${makeWidgetId(extension.id)}-page-action`;
+ panelId = `${makeWidgetId(extension.id)}-panel`;
+ let panel = document.getElementById(panelId);
+ if (expecting.expectClosed) {
+ ok(panel, "Expect panel to exist");
+ await promisePopupShown(panel);
+
+ extension.sendMessage("close-popup");
+
+ await promisePopupHidden(panel);
+ ok(true, `Panel is closed`);
+ } else if (expecting.closeOnTabSwitch) {
+ ok(panel, "Expect panel to exist");
+ await promisePopupShown(panel);
+
+ let oldTab = gBrowser.selectedTab;
+ Assert.notEqual(
+ oldTab,
+ gBrowser.tabs[0],
+ "Should have an inactive tab to switch to"
+ );
+
+ let hiddenPromise = promisePopupHidden(panel);
+
+ gBrowser.selectedTab = gBrowser.tabs[0];
+ await hiddenPromise;
+ info("Panel closed");
+
+ gBrowser.selectedTab = oldTab;
+ } else if (panel) {
+ await promisePopupShown(panel);
+ panel.hidePopup();
+ }
+
+ assertViewCount(extension, 1);
+
+ if (panel) {
+ panel = document.getElementById(panelId);
+ is(panel, null, "panel successfully removed from document after hiding");
+ }
+
+ extension.sendMessage("next-test");
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("pageaction-tests-done");
+
+ await extension.unload();
+
+ let node = document.getElementById(pageActionId);
+ is(node, null, "pageAction image removed from document");
+
+ let panel = document.getElementById(panelId);
+ is(panel, null, "pageAction panel removed from document");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testPageActionSecurity() {
+ const URL = "chrome://browser/content/browser.xhtml";
+
+ let apis = ["browser_action", "page_action"];
+
+ for (let api of apis) {
+ info(`TEST ${api} icon url: ${URL}`);
+
+ let messages = [/Access to restricted URI denied/];
+
+ let waitForConsole = new Promise(resolve => {
+ // Not necessary in browser-chrome tests, but monitorConsole gripes
+ // if we don't call it.
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.monitorConsole(resolve, messages);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ [api]: { default_popup: URL },
+ },
+ });
+
+ await Assert.rejects(
+ extension.startup(),
+ /startup failed/,
+ "Manifest rejected"
+ );
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+ }
+});
+
+add_task(forceGC);
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js b/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js
new file mode 100644
index 0000000000..a0b5b8377d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js
@@ -0,0 +1,192 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testPageActionPopupResize() {
+ let browser;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+ background: function () {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ const tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("action-shown");
+ });
+ });
+ },
+
+ files: {
+ "popup.html": `
`,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("action-shown");
+
+ clickPageAction(extension, window);
+
+ browser = await awaitExtensionPanel(extension);
+
+ async function checkSize(height, width) {
+ let dims = await promiseContentDimensions(browser);
+ let { body, root } = dims;
+
+ is(
+ dims.window.innerHeight,
+ height,
+ `Panel window should be ${height}px tall`
+ );
+ is(
+ body.clientHeight,
+ body.scrollHeight,
+ "Panel body should be tall enough to fit its contents"
+ );
+ is(
+ root.clientHeight,
+ root.scrollHeight,
+ "Panel root should be tall enough to fit its contents"
+ );
+
+ if (width) {
+ is(
+ body.clientWidth,
+ body.scrollWidth,
+ "Panel body should be wide enough to fit its contents"
+ );
+
+ // Tolerate if it is 1px too wide, as that may happen with the current
+ // resizing method.
+ Assert.lessOrEqual(
+ Math.abs(dims.window.innerWidth - width),
+ 1,
+ `Panel window should be ${width}px wide`
+ );
+ }
+ }
+
+ function setSize(size) {
+ let elem = content.document.body.firstElementChild;
+ elem.style.height = `${size}px`;
+ elem.style.width = `${size}px`;
+ }
+
+ function setHeight(height) {
+ content.document.body.style.overflow = "hidden";
+ let elem = content.document.body.firstElementChild;
+ elem.style.height = `${height}px`;
+ }
+
+ let sizes = [200, 400, 300];
+
+ for (let size of sizes) {
+ await alterContent(browser, setSize, size);
+ await checkSize(size, size);
+ }
+
+ let dims = await alterContent(browser, setSize, 1400);
+ let { body, root } = dims;
+
+ is(dims.window.innerWidth, 800, "Panel window width");
+ Assert.lessOrEqual(
+ body.clientWidth,
+ 800,
+ `Panel body width ${body.clientWidth} is less than 800`
+ );
+ is(body.scrollWidth, 1400, "Panel body scroll width");
+
+ is(dims.window.innerHeight, 600, "Panel window height");
+ Assert.lessOrEqual(
+ root.clientHeight,
+ 600,
+ `Panel root height (${root.clientHeight}px) is less than 600px`
+ );
+ is(root.scrollHeight, 1400, "Panel root scroll height");
+
+ for (let size of sizes) {
+ await alterContent(browser, setHeight, size);
+ await checkSize(size, null);
+ }
+
+ await extension.unload();
+});
+
+add_task(async function testPageActionPopupReflow() {
+ let browser;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+ background: function () {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ const tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("action-shown");
+ });
+ });
+ },
+
+ files: {
+ "popup.html": `
+
+ The quick mauve fox jumps over the opalescent toad, with its glowing
+ eyes, and its vantablack mouth, and its bottomless chasm where you
+ would hope to find a heart, that looks straight into the deepest
+ pits of hell. The fox shivers, and cowers, and tries to run, but
+ the toad is utterly without pity. It turns, ever so slightly...
+
+ `,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("action-shown");
+
+ clickPageAction(extension, window);
+
+ browser = await awaitExtensionPanel(extension);
+
+ function setSize(size) {
+ content.document.body.style.fontSize = `${size}px`;
+ }
+
+ let dims = await alterContent(browser, setSize, 18);
+
+ is(dims.window.innerWidth, 800, "Panel window should be 800px wide");
+ is(dims.body.clientWidth, 800, "Panel body should be 800px wide");
+ is(
+ dims.body.clientWidth,
+ dims.body.scrollWidth,
+ "Panel body should be wide enough to fit its contents"
+ );
+
+ Assert.greater(
+ dims.window.innerHeight,
+ 36,
+ `Panel window height (${dims.window.innerHeight}px) should be taller than two lines of text.`
+ );
+
+ is(
+ dims.body.clientHeight,
+ dims.body.scrollHeight,
+ "Panel body should be tall enough to fit its contents"
+ );
+ is(
+ dims.root.clientHeight,
+ dims.root.scrollHeight,
+ "Panel root should be tall enough to fit its contents"
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
new file mode 100644
index 0000000000..fd589acdbd
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
@@ -0,0 +1,329 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+PromiseTestUtils.allowMatchingRejectionsGlobally(/packaging errors/);
+
+function getExtension(page_action) {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action,
+ },
+ background: function () {
+ browser.test.onMessage.addListener(
+ async ({ method, param, expect, msg }) => {
+ let result = await browser.pageAction[method](param);
+ if (expect !== undefined) {
+ browser.test.assertEq(expect, result, msg);
+ }
+ browser.test.sendMessage("done");
+ }
+ );
+ },
+ });
+}
+
+async function sendMessage(ext, method, param, expect, msg) {
+ ext.sendMessage({ method, param, expect, msg });
+ await ext.awaitMessage("done");
+}
+
+let tests = [
+ {
+ name: "Test shown for all_urls",
+ page_action: {
+ show_matches: [""],
+ },
+ shown: [true, true, false],
+ },
+ {
+ name: "Test hide_matches overrides all_urls.",
+ page_action: {
+ show_matches: [""],
+ hide_matches: ["*://mochi.test/*"],
+ },
+ shown: [true, false, false],
+ },
+ {
+ name: "Test shown only for show_matches.",
+ page_action: {
+ show_matches: ["*://mochi.test/*"],
+ },
+ shown: [false, true, false],
+ },
+];
+
+// For some reason about:rights and about:about used to behave differently (maybe
+// because only the latter is privileged?) so both should be tested. about:about
+// is used in the test as the base tab.
+let urls = ["http://example.com/", "http://mochi.test:8888/", "about:rights"];
+
+function getId(tab) {
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+ getId = tabTracker.getId.bind(tabTracker); // eslint-disable-line no-func-assign
+ return getId(tab);
+}
+
+async function check(extension, tab, expected, msg) {
+ await promiseAnimationFrame();
+ let widgetId = makeWidgetId(extension.id);
+ let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(widgetId);
+ is(
+ gBrowser.selectedTab,
+ tab,
+ `tab ${tab.linkedBrowser.currentURI.spec} is selected`
+ );
+ let button = document.getElementById(pageActionId);
+ // Sometimes we're hidden, sometimes a parent is hidden via css (e.g. about pages)
+ let hidden =
+ button === null ||
+ button.hidden ||
+ window.getComputedStyle(button).display == "none";
+ is(!hidden, expected, msg + " (computed)");
+ await sendMessage(
+ extension,
+ "isShown",
+ { tabId: getId(tab) },
+ expected,
+ msg + " (isShown)"
+ );
+}
+
+add_task(async function test_pageAction_default_show_tabs() {
+ info(
+ "Check show_matches and hide_matches are respected when opening a new tab or switching to an existing tab."
+ );
+ let switchTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:about",
+ true,
+ true
+ );
+ for (let [i, test] of tests.entries()) {
+ info(`test ${i}: ${test.name}`);
+ let extension = getExtension(test.page_action);
+ await extension.startup();
+ for (let [j, url] of urls.entries()) {
+ let expected = test.shown[j];
+ let msg = `test ${i} url ${j}: page action is ${
+ expected ? "shown" : "hidden"
+ } for ${url}`;
+
+ info("Check new tab.");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ true,
+ true
+ );
+ await check(extension, tab, expected, msg + " (new)");
+
+ info("Check switched tab.");
+ await BrowserTestUtils.switchTab(gBrowser, switchTab);
+ await check(extension, switchTab, false, msg + " (about:about)");
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ await check(extension, tab, expected, msg + " (switched)");
+
+ BrowserTestUtils.removeTab(tab);
+ }
+ await extension.unload();
+ }
+ BrowserTestUtils.removeTab(switchTab);
+});
+
+add_task(async function test_pageAction_default_show_install() {
+ info(
+ "Check show_matches and hide_matches are respected when installing the extension"
+ );
+ for (let [i, test] of tests.entries()) {
+ info(`test ${i}: ${test.name}`);
+ for (let expected of [true, false]) {
+ let j = test.shown.indexOf(expected);
+ if (j === -1) {
+ continue;
+ }
+ let initialTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ urls[j],
+ true,
+ true
+ );
+ let installTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ urls[j],
+ true,
+ true
+ );
+ let extension = getExtension(test.page_action);
+ await extension.startup();
+ let msg = `test ${i} url ${j}: page action is ${
+ expected ? "shown" : "hidden"
+ } for ${urls[j]}`;
+ await check(extension, installTab, expected, msg + " (active)");
+
+ // initialTab has not been activated after installation, so we have not evaluated whether the page
+ // action should be shown in it. Check that pageAction.isShown works anyways.
+ await sendMessage(
+ extension,
+ "isShown",
+ { tabId: getId(initialTab) },
+ expected,
+ msg + " (inactive)"
+ );
+
+ BrowserTestUtils.removeTab(initialTab);
+ BrowserTestUtils.removeTab(installTab);
+ await extension.unload();
+ }
+ }
+});
+
+add_task(async function test_pageAction_history() {
+ info(
+ "Check match patterns are reevaluated when using history.pushState or navigating"
+ );
+ let url1 = "http://example.com/";
+ let url2 = url1 + "path/";
+ let extension = getExtension({
+ show_matches: [url1],
+ hide_matches: [url2],
+ });
+ await extension.startup();
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url1,
+ true,
+ true
+ );
+ await check(extension, tab, true, "page action is shown for " + url1);
+
+ info("Use history.pushState to change the URL without navigating");
+ await historyPushState(tab, url2);
+ await check(extension, tab, false, "page action is hidden for " + url2);
+
+ info("Use hide()");
+ await sendMessage(extension, "hide", getId(tab));
+ await check(extension, tab, false, "page action is still hidden");
+
+ info("Use history.pushState to revert to first url");
+ await historyPushState(tab, url1);
+ await check(
+ extension,
+ tab,
+ false,
+ "hide() has more precedence than pattern matching"
+ );
+
+ info("Select another tab");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url1,
+ true,
+ true
+ );
+
+ info("Perform navigation in the old tab");
+ await navigateTab(tab, url1);
+ await sendMessage(
+ extension,
+ "isShown",
+ { tabId: getId(tab) },
+ true,
+ "Navigating undoes hide(), even when the tab is not selected."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+ await extension.unload();
+});
+
+add_task(async function test_pageAction_all_urls() {
+ info("Check is not allowed in hide_matches");
+ let extension = getExtension({
+ show_matches: ["*://mochi.test/*"],
+ hide_matches: [""],
+ });
+ let rejects = await extension.startup().then(
+ () => false,
+ () => true
+ );
+ is(rejects, true, "startup failed");
+});
+
+add_task(async function test_pageAction_restrictScheme_false() {
+ info(
+ "Check restricted origins are allowed in show_matches for privileged extensions"
+ );
+ let extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: ["mozillaAddons", "tabs"],
+ page_action: {
+ show_matches: ["about:reader*"],
+ hide_matches: ["*://*/*"],
+ },
+ },
+ background: function () {
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
+ if (changeInfo.url && changeInfo.url.startsWith("about:reader")) {
+ browser.test.sendMessage("readerModeEntered");
+ }
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "enterReaderMode") {
+ browser.test.fail(`Received unexpected test message: ${msg}`);
+ return;
+ }
+
+ browser.tabs.toggleReaderMode();
+ });
+ },
+ });
+
+ async function expectPageAction(extension, tab, isShown) {
+ await promiseAnimationFrame();
+ let widgetId = makeWidgetId(extension.id);
+ let pageActionId =
+ BrowserPageActions.urlbarButtonNodeIDForActionID(widgetId);
+ let iconEl = document.getElementById(pageActionId);
+
+ if (isShown) {
+ ok(iconEl && !iconEl.hasAttribute("disabled"), "pageAction is shown");
+ } else {
+ ok(
+ iconEl == null || iconEl.getAttribute("disabled") == "true",
+ "pageAction is hidden"
+ );
+ }
+ }
+
+ const baseUrl = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ );
+ const url = `${baseUrl}/readerModeArticle.html`;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ true,
+ true
+ );
+
+ await extension.startup();
+
+ await expectPageAction(extension, tab, false);
+
+ extension.sendMessage("enterReaderMode");
+ await extension.awaitMessage("readerModeEntered");
+
+ await expectPageAction(extension, tab, true);
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js b/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js
new file mode 100644
index 0000000000..dc323afd94
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js
@@ -0,0 +1,213 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const BASE =
+ "http://example.com/browser/browser/components/extensions/test/browser/";
+
+add_task(async function test_pageAction_basic() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {
+ default_popup: "popup.html",
+ unrecognized_property: "with-a-random-value",
+ },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+
+ `,
+
+ "popup.js": function () {
+ browser.runtime.sendMessage("from-popup");
+ },
+ },
+
+ background: function () {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "from-popup", "correct message received");
+ browser.test.sendMessage("popup");
+ });
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ let tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
+ });
+ },
+ });
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ /Reading manifest: Warning processing page_action.unrecognized_property: An unexpected property was found/,
+ },
+ ]);
+ });
+
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ await extension.awaitMessage("page-action-shown");
+
+ let elem = await getPageActionButton(extension);
+ let parent = window.document.getElementById("page-action-buttons");
+ is(
+ elem && elem.parentNode,
+ parent,
+ `pageAction pinned to urlbar ${elem.parentNode.getAttribute("id")}`
+ );
+
+ clickPageAction(extension);
+
+ await extension.awaitMessage("popup");
+
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
+
+add_task(async function test_pageAction_pinned() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {
+ default_popup: "popup.html",
+ pinned: false,
+ },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+ `,
+ },
+
+ background: function () {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ let tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("page-action-shown");
+
+ // There are plenty of tests for the main action button, we just verify
+ // that we've properly set the pinned value.
+ // This test used to check that the button was not pinned, but that is no
+ // longer supported.
+ // TODO bug 1703537: consider removal of the pinned property.
+ let action = PageActions.actionForID(makeWidgetId(extension.id));
+ ok(action && action.pinnedToUrlbar, "Check pageAction pinning");
+
+ await extension.unload();
+});
+
+add_task(async function test_pageAction_icon_on_subframe_navigation() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {
+ default_popup: "popup.html",
+ },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+ `,
+ },
+
+ background: function () {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ let tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
+ });
+ },
+ });
+
+ await navigateTab(
+ gBrowser.selectedTab,
+ "data:text/html,Top Level Frame "
+ );
+
+ await extension.startup();
+ await extension.awaitMessage("page-action-shown");
+
+ const pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return document.getElementById(pageActionId);
+ }, "pageAction is initially visible");
+
+ info("Create a sub-frame");
+
+ let subframeURL = `${BASE}#subframe-url-1`;
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [subframeURL],
+ async url => {
+ const iframe = this.content.document.createElement("iframe");
+ iframe.setAttribute("id", "test-subframe");
+ iframe.setAttribute("src", url);
+ iframe.setAttribute("style", "height: 200px; width: 200px");
+
+ // Await the initial url to be loaded in the subframe.
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ this.content.document.body.appendChild(iframe);
+ });
+ }
+ );
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return document.getElementById(pageActionId);
+ }, "pageAction should be visible when a subframe is created");
+
+ info("Navigating the sub-frame");
+
+ subframeURL = `${BASE}/file_dummy.html#subframe-url-2`;
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [subframeURL],
+ async url => {
+ const iframe = this.content.document.querySelector(
+ "iframe#test-subframe"
+ );
+
+ // Await the subframe navigation.
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ iframe.setAttribute("src", url);
+ });
+ }
+ );
+
+ info("Subframe location changed");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return document.getElementById(pageActionId);
+ }, "pageAction should be visible after a subframe navigation");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_telemetry.js b/browser/components/extensions/test/browser/browser_ext_pageAction_telemetry.js
new file mode 100644
index 0000000000..fe14a5e6ac
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_telemetry.js
@@ -0,0 +1,228 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const HISTOGRAM = "WEBEXT_PAGEACTION_POPUP_OPEN_MS";
+const HISTOGRAM_KEYED = "WEBEXT_PAGEACTION_POPUP_OPEN_MS_BY_ADDONID";
+
+const EXTENSION_ID1 = "@test-extension1";
+const EXTENSION_ID2 = "@test-extension2";
+
+function snapshotCountsSum(snapshot) {
+ return Object.values(snapshot.values).reduce((a, b) => a + b, 0);
+}
+
+function histogramCountsSum(histogram) {
+ return snapshotCountsSum(histogram.snapshot());
+}
+
+function gleanMetricSamplesCount(gleanMetric) {
+ return snapshotCountsSum(gleanMetric.testGetValue() ?? { values: {} });
+}
+
+add_task(async function testPageActionTelemetry() {
+ let extensionOptions = {
+ manifest: {
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+ background: function () {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ const tabId = tabs[0].id;
+
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("action-shown");
+ });
+ });
+ },
+
+ files: {
+ "popup.html": `
`,
+ },
+ };
+ let extension1 = ExtensionTestUtils.loadExtension({
+ ...extensionOptions,
+ manifest: {
+ ...extensionOptions.manifest,
+ browser_specific_settings: {
+ gecko: { id: EXTENSION_ID1 },
+ },
+ },
+ });
+ let extension2 = ExtensionTestUtils.loadExtension({
+ ...extensionOptions,
+ manifest: {
+ ...extensionOptions.manifest,
+ browser_specific_settings: {
+ gecko: { id: EXTENSION_ID2 },
+ },
+ },
+ });
+
+ let histogram = Services.telemetry.getHistogramById(HISTOGRAM);
+ let histogramKeyed =
+ Services.telemetry.getKeyedHistogramById(HISTOGRAM_KEYED);
+
+ histogram.clear();
+ histogramKeyed.clear();
+ Services.fog.testResetFOG();
+
+ is(
+ histogramCountsSum(histogram),
+ 0,
+ `No data recorded for histogram: ${HISTOGRAM}.`
+ );
+ is(
+ Object.keys(histogramKeyed).length,
+ 0,
+ `No data recorded for histogram: ${HISTOGRAM_KEYED}.`
+ );
+ Assert.deepEqual(
+ Glean.extensionsTiming.pageActionPopupOpen.testGetValue(),
+ undefined,
+ "No data recorded for glean metric extensionsTiming.pageActionPopupOpen"
+ );
+
+ await extension1.startup();
+ await extension1.awaitMessage("action-shown");
+ await extension2.startup();
+ await extension2.awaitMessage("action-shown");
+
+ is(
+ histogramCountsSum(histogram),
+ 0,
+ `No data recorded for histogram after PageAction shown: ${HISTOGRAM}.`
+ );
+ is(
+ Object.keys(histogramKeyed).length,
+ 0,
+ `No data recorded for histogram after PageAction shown: ${HISTOGRAM_KEYED}.`
+ );
+ is(
+ gleanMetricSamplesCount(Glean.extensionsTiming.pageActionPopupOpen),
+ 0,
+ "No data recorded for glean metric extensionsTiming.pageActionPopupOpen"
+ );
+
+ clickPageAction(extension1, window);
+ await awaitExtensionPanel(extension1);
+
+ is(
+ histogramCountsSum(histogram),
+ 1,
+ `Data recorded for first extension for histogram: ${HISTOGRAM}.`
+ );
+ is(
+ gleanMetricSamplesCount(Glean.extensionsTiming.pageActionPopupOpen),
+ 1,
+ `Data recorded for first extension on Glean metric extensionsTiming.pageActionPopupOpen`
+ );
+
+ let keyedSnapshot = histogramKeyed.snapshot();
+ Assert.deepEqual(
+ Object.keys(keyedSnapshot),
+ [EXTENSION_ID1],
+ `Data recorded for first extension histogram: ${HISTOGRAM_KEYED}.`
+ );
+ is(
+ snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]),
+ 1,
+ `Data recorded for first extension for histogram: ${HISTOGRAM_KEYED}.`
+ );
+
+ await closePageAction(extension1, window);
+
+ clickPageAction(extension2, window);
+ await awaitExtensionPanel(extension2);
+
+ is(
+ histogramCountsSum(histogram),
+ 2,
+ `Data recorded for second extension for histogram: ${HISTOGRAM}.`
+ );
+ is(
+ gleanMetricSamplesCount(Glean.extensionsTiming.pageActionPopupOpen),
+ 2,
+ `Data recorded for second extension on Glean metric extensionsTiming.pageActionPopupOpen`
+ );
+
+ keyedSnapshot = histogramKeyed.snapshot();
+ Assert.deepEqual(
+ Object.keys(keyedSnapshot).sort(),
+ [EXTENSION_ID1, EXTENSION_ID2],
+ `Data recorded for second extension histogram: ${HISTOGRAM_KEYED}.`
+ );
+ is(
+ snapshotCountsSum(keyedSnapshot[EXTENSION_ID2]),
+ 1,
+ `Data recorded for second extension for histogram: ${HISTOGRAM_KEYED}.`
+ );
+ is(
+ snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]),
+ 1,
+ `Data recorded for first extension should not change for histogram: ${HISTOGRAM_KEYED}.`
+ );
+
+ await closePageAction(extension2, window);
+
+ clickPageAction(extension2, window);
+ await awaitExtensionPanel(extension2);
+
+ is(
+ histogramCountsSum(histogram),
+ 3,
+ `Data recorded for second opening of popup for histogram: ${HISTOGRAM}.`
+ );
+ is(
+ gleanMetricSamplesCount(Glean.extensionsTiming.pageActionPopupOpen),
+ 3,
+ `Data recorded for second opening popup on Glean metric extensionsTiming.pageActionPopupOpen`
+ );
+
+ keyedSnapshot = histogramKeyed.snapshot();
+ is(
+ snapshotCountsSum(keyedSnapshot[EXTENSION_ID2]),
+ 2,
+ `Data recorded for second opening of popup for histogram: ${HISTOGRAM_KEYED}.`
+ );
+ is(
+ snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]),
+ 1,
+ `Data recorded for first extension should not change for histogram: ${HISTOGRAM_KEYED}.`
+ );
+
+ await closePageAction(extension2, window);
+
+ clickPageAction(extension1, window);
+ await awaitExtensionPanel(extension1);
+
+ is(
+ histogramCountsSum(histogram),
+ 4,
+ `Data recorded for third opening of popup for histogram: ${HISTOGRAM}.`
+ );
+ is(
+ gleanMetricSamplesCount(Glean.extensionsTiming.pageActionPopupOpen),
+ 4,
+ `Data recorded for third opening popup on Glean metric extensionsTiming.pageActionPopupOpen`
+ );
+
+ keyedSnapshot = histogramKeyed.snapshot();
+ is(
+ snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]),
+ 2,
+ `Data recorded for second opening of popup for histogram: ${HISTOGRAM_KEYED}.`
+ );
+ is(
+ snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]),
+ 2,
+ `Data recorded for second extension should not change for histogram: ${HISTOGRAM_KEYED}.`
+ );
+
+ await closePageAction(extension1, window);
+
+ await extension1.unload();
+ await extension2.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_title.js b/browser/components/extensions/test/browser/browser_ext_pageAction_title.js
new file mode 100644
index 0000000000..feeb0a1419
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_title.js
@@ -0,0 +1,275 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_pageAction.js");
+
+add_task(async function testTabSwitchContext() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ page_action: {
+ default_icon: "default.png",
+ default_popup: "__MSG_popup__",
+ default_title: "Default __MSG_title__ \u263a",
+ },
+
+ default_locale: "en",
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "_locales/en/messages.json": {
+ popup: {
+ message: "default.html",
+ description: "Popup",
+ },
+
+ title: {
+ message: "Title",
+ description: "Title",
+ },
+ },
+
+ "_locales/es_ES/messages.json": {
+ popup: {
+ message: "default.html",
+ description: "Popup",
+ },
+
+ title: {
+ message: "T\u00edtulo",
+ description: "Title",
+ },
+ },
+
+ "default.png": imageBuffer,
+ "1.png": imageBuffer,
+ "2.png": imageBuffer,
+ },
+
+ getTests: function (tabs) {
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default T\u00edtulo \u263a",
+ },
+ {
+ icon: browser.runtime.getURL("1.png"),
+ popup: browser.runtime.getURL("default.html"),
+ title: "Default T\u00edtulo \u263a",
+ },
+ {
+ icon: browser.runtime.getURL("2.png"),
+ popup: browser.runtime.getURL("2.html"),
+ title: "Title 2",
+ },
+ {
+ icon: browser.runtime.getURL("2.png"),
+ popup: browser.runtime.getURL("2.html"),
+ title: "",
+ },
+ {
+ icon: browser.runtime.getURL("2.png"),
+ popup: browser.runtime.getURL("2.html"),
+ title: "Default T\u00edtulo \u263a",
+ },
+ ];
+
+ let promiseTabLoad = details => {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
+ if (tabId == details.id && changed.url == details.url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ };
+ return [
+ expect => {
+ browser.test.log("Initial state. No icon visible.");
+ expect(null);
+ },
+ async expect => {
+ browser.test.log(
+ "Show the icon on the first tab, expect default properties."
+ );
+ await browser.pageAction.show(tabs[0]);
+ expect(details[0]);
+ },
+ expect => {
+ browser.test.log(
+ "Change the icon. Expect default properties excluding the icon."
+ );
+ browser.pageAction.setIcon({ tabId: tabs[0], path: "1.png" });
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. No icon visible.");
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?0",
+ });
+ tabs.push(tab.id);
+ expect(null);
+ },
+ async expect => {
+ browser.test.log("Await tab load. No icon visible.");
+ let promise = promiseTabLoad({ id: tabs[1], url: "about:blank?0" });
+ let { url } = await browser.tabs.get(tabs[1]);
+ if (url === "about:blank") {
+ await promise;
+ }
+ expect(null);
+ },
+ async expect => {
+ browser.test.log("Change properties. Expect new properties.");
+ let tabId = tabs[1];
+
+ await browser.pageAction.show(tabId);
+ browser.pageAction.setIcon({ tabId, path: "2.png" });
+ browser.pageAction.setPopup({ tabId, popup: "2.html" });
+ browser.pageAction.setTitle({ tabId, title: "Title 2" });
+
+ expect(details[2]);
+ },
+ async expect => {
+ browser.test.log("Change the hash. Expect same properties.");
+
+ let promise = promiseTabLoad({
+ id: tabs[1],
+ url: "about:blank?0#ref",
+ });
+
+ browser.tabs.update(tabs[1], { url: "about:blank?0#ref" });
+
+ await promise;
+ expect(details[2]);
+ },
+ expect => {
+ browser.test.log("Set empty title. Expect empty title.");
+ browser.pageAction.setTitle({ tabId: tabs[1], title: "" });
+
+ expect(details[3]);
+ },
+ expect => {
+ browser.test.log("Clear the title. Expect default title.");
+ browser.pageAction.setTitle({ tabId: tabs[1], title: null });
+
+ expect(details[4]);
+ },
+ async expect => {
+ browser.test.log("Navigate to a new page. Expect icon hidden.");
+
+ // TODO: This listener should not be necessary, but the |tabs.update|
+ // callback currently fires too early in e10s windows.
+ let promise = promiseTabLoad({ id: tabs[1], url: "about:blank?1" });
+
+ browser.tabs.update(tabs[1], { url: "about:blank?1" });
+
+ await promise;
+ expect(null);
+ },
+ async expect => {
+ browser.test.log("Show the icon. Expect default properties again.");
+ await browser.pageAction.show(tabs[1]);
+ expect(details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to the first tab. Expect previously set properties."
+ );
+ await browser.tabs.update(tabs[0], { active: true });
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log(
+ "Hide the icon on tab 2. Switch back, expect hidden."
+ );
+ await browser.pageAction.hide(tabs[1]);
+ await browser.tabs.update(tabs[1], { active: true });
+ expect(null);
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to tab 1. Expect previous results again."
+ );
+ await browser.tabs.remove(tabs[1]);
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Hide the icon. Expect hidden.");
+ await browser.pageAction.hide(tabs[0]);
+ expect(null);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testDefaultTitle() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ page_action: {
+ default_icon: "icon.png",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "icon.png": imageBuffer,
+ },
+
+ getTests: function (tabs) {
+ let details = [
+ {
+ title: "Foo Extension",
+ popup: "",
+ icon: browser.runtime.getURL("icon.png"),
+ },
+ {
+ title: "Foo Title",
+ popup: "",
+ icon: browser.runtime.getURL("icon.png"),
+ },
+ { title: "", popup: "", icon: browser.runtime.getURL("icon.png") },
+ ];
+
+ return [
+ expect => {
+ browser.test.log("Initial state. No icon visible.");
+ expect(null);
+ },
+ async expect => {
+ browser.test.log(
+ "Show the icon on the first tab, expect extension title as default title."
+ );
+ await browser.pageAction.show(tabs[0]);
+ expect(details[0]);
+ },
+ expect => {
+ browser.test.log("Change the title. Expect new title.");
+ browser.pageAction.setTitle({ tabId: tabs[0], title: "Foo Title" });
+ expect(details[1]);
+ },
+ expect => {
+ browser.test.log("Set empty title. Expect empty title.");
+ browser.pageAction.setTitle({ tabId: tabs[0], title: "" });
+ expect(details[2]);
+ },
+ expect => {
+ browser.test.log("Clear the title. Expect extension title.");
+ browser.pageAction.setTitle({ tabId: tabs[0], title: null });
+ expect(details[0]);
+ },
+ ];
+ },
+ });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_persistent_storage_permission_indication.js b/browser/components/extensions/test/browser/browser_ext_persistent_storage_permission_indication.js
new file mode 100644
index 0000000000..e50a6af135
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_persistent_storage_permission_indication.js
@@ -0,0 +1,131 @@
+/* -- Mode: indent-tabs-mode: nil; js-indent-level: 2 -- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+function openPermissionPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gPermissionPanel._permissionPopup
+ );
+ gPermissionPanel._identityPermissionBox.click();
+ info("Wait permission popup to be shown");
+ return promise;
+}
+
+function closePermissionPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gPermissionPanel._permissionPopup,
+ "popuphidden"
+ );
+ gPermissionPanel._permissionPopup.hidePopup();
+ info("Wait permission popup to be hidden");
+ return promise;
+}
+
+async function testPermissionPopup({ expectPermissionHidden }) {
+ await openPermissionPopup();
+
+ if (expectPermissionHidden) {
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ is(
+ permissionsList.querySelectorAll(
+ ".permission-popup-permission-label-persistent-storage"
+ ).length,
+ 0,
+ "Persistent storage Permission should be hidden"
+ );
+ }
+
+ await closePermissionPopup();
+
+ // We need to test this after the popup has been closed.
+ // The permission icon will be shown as long as the popup is open, event if
+ // no permissions are set.
+ let permissionsGrantedIcon = document.getElementById(
+ "permissions-granted-icon"
+ );
+
+ if (expectPermissionHidden) {
+ ok(
+ BrowserTestUtils.isHidden(permissionsGrantedIcon),
+ "Permission Granted Icon is hidden"
+ );
+ } else {
+ ok(
+ BrowserTestUtils.isVisible(permissionsGrantedIcon),
+ "Permission Granted Icon is visible"
+ );
+ }
+}
+
+add_task(async function testPersistentStoragePermissionHidden() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("url", browser.runtime.getURL("testpage.html"));
+ },
+ manifest: {
+ name: "Test Extension",
+ permissions: ["unlimitedStorage"],
+ },
+ files: {
+ "testpage.html": "Extension Test Page ",
+ },
+ });
+
+ await extension.startup();
+
+ let url = await extension.awaitMessage("url");
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ // Wait the tab to be fully loade, then run the test on the permission prompt.
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await loaded;
+ await testPermissionPopup({ expectPermissionHidden: true });
+ });
+
+ await extension.unload();
+});
+
+add_task(async function testPersistentStoragePermissionVisible() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("url", browser.runtime.getURL("testpage.html"));
+ },
+ manifest: {
+ name: "Test Extension",
+ },
+ files: {
+ "testpage.html": "Extension Test Page ",
+ },
+ });
+
+ await extension.startup();
+
+ let url = await extension.awaitMessage("url");
+
+ let policy = WebExtensionPolicy.getByID(extension.id);
+ let principal = policy.extension.principal;
+ PermissionTestUtils.add(
+ principal,
+ "persistent-storage",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ // Wait the tab to be fully loade, then run the test on the permission prompt.
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await loaded;
+ await testPermissionPopup({ expectPermissionHidden: false });
+ });
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js b/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js
new file mode 100644
index 0000000000..63948ed232
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js
@@ -0,0 +1,113 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testPageActionPopup() {
+ const BASE =
+ "http://example.com/browser/browser/components/extensions/test/browser";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: `${BASE}/file_popup_api_injection_a.html`,
+ default_area: "navbar",
+ },
+ page_action: {
+ default_popup: `${BASE}/file_popup_api_injection_b.html`,
+ },
+ },
+
+ files: {
+ "popup-a.html": `
+ `,
+ "popup-a.js": 'browser.test.sendMessage("from-popup-a");',
+
+ "popup-b.html": `
+ `,
+ "popup-b.js": 'browser.test.sendMessage("from-popup-b");',
+ },
+
+ background: function () {
+ let tabId;
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ tabId = tabs[0].id;
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("ready");
+ });
+ });
+
+ browser.test.onMessage.addListener(() => {
+ browser.browserAction.setPopup({ popup: "/popup-a.html" });
+ browser.pageAction.setPopup({ tabId, popup: "popup-b.html" });
+
+ browser.test.sendMessage("ok");
+ });
+ },
+ });
+
+ let promiseConsoleMessage = pattern =>
+ new Promise(resolve => {
+ Services.console.registerListener(function listener(msg) {
+ if (pattern.test(msg.message)) {
+ resolve(msg.message);
+ Services.console.unregisterListener(listener);
+ }
+ });
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // Check that unprivileged documents don't get the API.
+ // BrowserAction:
+ let awaitMessage = promiseConsoleMessage(
+ /WebExt Privilege Escalation: BrowserAction/
+ );
+ SimpleTest.expectUncaughtException();
+ await clickBrowserAction(extension);
+ await awaitExtensionPanel(extension);
+
+ let message = await awaitMessage;
+ ok(
+ message.includes(
+ "WebExt Privilege Escalation: BrowserAction: typeof(browser) = undefined"
+ ),
+ `No BrowserAction API injection`
+ );
+
+ await closeBrowserAction(extension);
+
+ // PageAction
+ awaitMessage = promiseConsoleMessage(
+ /WebExt Privilege Escalation: PageAction/
+ );
+ SimpleTest.expectUncaughtException();
+ await clickPageAction(extension);
+
+ message = await awaitMessage;
+ ok(
+ message.includes(
+ "WebExt Privilege Escalation: PageAction: typeof(browser) = undefined"
+ ),
+ `No PageAction API injection: ${message}`
+ );
+
+ await closePageAction(extension);
+
+ SimpleTest.expectUncaughtException(false);
+
+ // Check that privileged documents *do* get the API.
+ extension.sendMessage("next");
+ await extension.awaitMessage("ok");
+
+ await clickBrowserAction(extension);
+ await awaitExtensionPanel(extension);
+ await extension.awaitMessage("from-popup-a");
+ await closeBrowserAction(extension);
+
+ await clickPageAction(extension);
+ await extension.awaitMessage("from-popup-b");
+ await closePageAction(extension);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_background.js b/browser/components/extensions/test/browser/browser_ext_popup_background.js
new file mode 100644
index 0000000000..bf0f78b732
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_background.js
@@ -0,0 +1,160 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+async function testPanel(browser, standAlone, background_check) {
+ let panel = getPanelForNode(browser);
+
+ let checkBackground = (background = null) => {
+ if (!standAlone) {
+ return;
+ }
+
+ is(
+ getComputedStyle(panel.panelContent).backgroundColor,
+ background,
+ "Content should have correct background"
+ );
+ };
+
+ function getBackground(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ return content.windowUtils.canvasBackgroundColor;
+ });
+ }
+
+ let setBackground = color => {
+ content.document.body.style.backgroundColor = color;
+ };
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ info("Test that initial background color is applied");
+ let initialBackground = await getBackground(browser);
+ checkBackground(initialBackground);
+ background_check(initialBackground);
+
+ info("Test that dynamically-changed background color is applied");
+ await alterContent(browser, setBackground, "black");
+ checkBackground(await getBackground(browser));
+
+ info("Test that non-opaque background color results in default styling");
+ await alterContent(browser, setBackground, "rgba(1, 2, 3, .9)");
+}
+
+add_task(async function testPopupBackground() {
+ let testCases = [
+ {
+ browser_style: false,
+ popup: `
+
+
+
+ `,
+ background_check: function (bg) {
+ is(bg, "rgb(0, 128, 0)", "Initial background should be green");
+ },
+ },
+ {
+ browser_style: false,
+ popup: `
+
+
+
+ `,
+ background_check: function (bg) {
+ is(bg, "rgb(255, 255, 255)", "Initial background should be white");
+ },
+ },
+ {
+ browser_style: false,
+ popup: `
+
+
+
+
+ `,
+ background_check: function (bg) {
+ is(bg, "rgb(255, 255, 255)", "Initial background should be white");
+ },
+ },
+ {
+ browser_style: false,
+ popup: `
+
+
+
+
+ `,
+ background_check: function (bg) {
+ isnot(
+ bg,
+ "rgb(255, 255, 255)",
+ "Initial background should not be white"
+ );
+ },
+ },
+ ];
+ for (let { browser_style, popup, background_check } of testCases) {
+ info(`Testing browser_style: ${browser_style} popup: ${popup}`);
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ browser.pageAction.show(tabs[0].id);
+ });
+ },
+
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style,
+ },
+
+ page_action: {
+ default_popup: "popup.html",
+ browser_style,
+ },
+ },
+
+ files: {
+ "popup.html": popup,
+ },
+ });
+
+ await extension.startup();
+
+ {
+ info("Test stand-alone browserAction popup");
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser, true, background_check);
+ await closeBrowserAction(extension);
+ }
+
+ {
+ info("Test menu panel browserAction popup");
+
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser, false, background_check);
+ await closeBrowserAction(extension);
+ }
+
+ {
+ info("Test pageAction popup");
+
+ clickPageAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser, true, background_check);
+ await closePageAction(extension);
+ }
+
+ await extension.unload();
+ }
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_corners.js b/browser/components/extensions/test/browser/browser_ext_popup_corners.js
new file mode 100644
index 0000000000..67a53f0e7e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_corners.js
@@ -0,0 +1,165 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testPopupBorderRadius() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ browser.pageAction.show(tabs[0].id);
+ });
+ },
+
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: false,
+ },
+
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ let widget = getBrowserActionWidget(extension);
+ // If the panel doesn't allows embedding in subview then
+ // radius will be 0, otherwise 8. In practice we always
+ // disallow subview.
+ let expectedRadius = widget.disallowSubView ? "8px" : "0px";
+
+ async function testPanel(browser, standAlone = true) {
+ let panel = getPanelForNode(browser);
+ let arrowContent = panel.panelContent;
+
+ let panelStyle = getComputedStyle(arrowContent);
+ is(panelStyle.overflow, "clip", "overflow is clipped");
+
+ let stack = browser.parentNode;
+ let viewNode = stack.parentNode === panel ? browser : stack.parentNode;
+ let viewStyle = getComputedStyle(viewNode);
+
+ let props = [
+ "borderTopLeftRadius",
+ "borderTopRightRadius",
+ "borderBottomRightRadius",
+ "borderBottomLeftRadius",
+ ];
+
+ let bodyStyle = await SpecialPowers.spawn(
+ browser,
+ [props],
+ async function (props) {
+ let bodyStyle = content.getComputedStyle(content.document.body);
+
+ return new Map(props.map(prop => [prop, bodyStyle[prop]]));
+ }
+ );
+
+ for (let prop of props) {
+ if (standAlone) {
+ is(
+ viewStyle[prop],
+ panelStyle[prop],
+ `Panel and view ${prop} should be the same`
+ );
+ is(
+ bodyStyle.get(prop),
+ panelStyle[prop],
+ `Panel and body ${prop} should be the same`
+ );
+ } else {
+ is(viewStyle[prop], expectedRadius, `View node ${prop} should be 0px`);
+ is(
+ bodyStyle.get(prop),
+ expectedRadius,
+ `Body node ${prop} should be 0px`
+ );
+ }
+ }
+ }
+
+ {
+ info("Test stand-alone browserAction popup");
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser);
+ await closeBrowserAction(extension);
+ }
+
+ {
+ info("Test overflowed browserAction popup");
+ const kForceOverflowWidthPx = 450;
+ let overflowPanel = document.getElementById("widget-overflow");
+
+ let originalWindowWidth = window.outerWidth;
+ let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+ ok(
+ !navbar.hasAttribute("overflowing"),
+ "Should start with a non-overflowing toolbar."
+ );
+ window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
+
+ await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing"));
+ ok(
+ navbar.hasAttribute("overflowing"),
+ "Should have an overflowing toolbar."
+ );
+
+ await window.gUnifiedExtensions.togglePanel();
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+
+ is(
+ overflowPanel.state,
+ "closed",
+ "The widget overflow panel should not be open."
+ );
+
+ await testPanel(browser, false);
+ await closeBrowserAction(extension);
+
+ window.resizeTo(originalWindowWidth, window.outerHeight);
+ await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing"));
+ ok(
+ !navbar.hasAttribute("overflowing"),
+ "Should not have an overflowing toolbar."
+ );
+ }
+
+ {
+ info("Test menu panel browserAction popup");
+
+ CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser, false);
+ await closeBrowserAction(extension);
+ }
+
+ {
+ info("Test pageAction popup");
+
+ clickPageAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser);
+ await closePageAction(extension);
+ }
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_focus.js b/browser/components/extensions/test/browser/browser_ext_popup_focus.js
new file mode 100644
index 0000000000..4cf46f2be5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_focus.js
@@ -0,0 +1,88 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const DUMMY_PAGE =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+
+add_task(async function testPageActionFocus() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {
+ default_popup: "popup.html",
+ show_matches: [""],
+ },
+ },
+ files: {
+ "popup.html": `
+
+
+
+ `,
+ "popup.js": function () {
+ window.addEventListener(
+ "focus",
+ event => {
+ browser.test.log("extension popup received focus event");
+ browser.test.assertEq(
+ true,
+ document.hasFocus(),
+ "document should be focused"
+ );
+ browser.test.notifyPass("focused");
+ },
+ { once: true }
+ );
+ browser.test.log(`extension popup loaded`);
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab(DUMMY_PAGE, async () => {
+ await clickPageAction(extension);
+ await extension.awaitFinish("focused");
+ await closePageAction(extension);
+ });
+
+ await extension.unload();
+});
+
+add_task(async function testBrowserActionFocus() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: { default_popup: "popup.html" },
+ },
+ files: {
+ "popup.html": `
+
+
+
+ `,
+ "popup.js": function () {
+ window.addEventListener(
+ "focus",
+ event => {
+ browser.test.log("extension popup received focus event");
+ browser.test.assertEq(
+ true,
+ document.hasFocus(),
+ "document should be focused"
+ );
+ browser.test.notifyPass("focused");
+ },
+ { once: true }
+ );
+ browser.test.log(`extension popup loaded`);
+ },
+ },
+ });
+ await extension.startup();
+
+ await clickBrowserAction(extension);
+ await extension.awaitFinish("focused");
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_links_open_in_tabs.js b/browser/components/extensions/test/browser/browser_ext_popup_links_open_in_tabs.js
new file mode 100644
index 0000000000..1e935e1d0d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_links_open_in_tabs.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_popup_links_open_tabs() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+
+
+
+ Extension Popup
+ popup page link
+
+ `,
+ "popup.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("from-popup", "popup-a");
+ };
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_NAVBAR, 0);
+
+ let promiseActionPopupBrowser = awaitExtensionPanel(extension);
+ clickBrowserAction(extension);
+ await extension.awaitMessage("from-popup");
+ let popupBrowser = await promiseActionPopupBrowser;
+ const promiseNewTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://example.com/popup-page-link"
+ );
+ await SpecialPowers.spawn(popupBrowser, [], () =>
+ content.document.querySelector("a").click()
+ );
+ const newTab = await promiseNewTabOpened;
+ ok(newTab, "Got a new tab created on the expected url");
+ BrowserTestUtils.removeTab(newTab);
+
+ await closeBrowserAction(extension);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_requestPermission.js b/browser/components/extensions/test/browser/browser_ext_popup_requestPermission.js
new file mode 100644
index 0000000000..657d525634
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_requestPermission.js
@@ -0,0 +1,67 @@
+"use strict";
+
+const verifyRequestPermission = async (manifestProps, expectedIcon) => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ optional_permissions: [""],
+ ...manifestProps,
+ },
+
+ files: {
+ "popup.html": ` `,
+ "popup.js": async () => {
+ const success = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({
+ origins: [""],
+ })
+ );
+ });
+ });
+ browser.test.assertTrue(
+ success,
+ "browser.permissions.request promise resolves"
+ );
+ browser.test.sendMessage("done");
+ },
+ },
+ });
+
+ const requestPrompt = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ ok(
+ panel.getAttribute("icon").endsWith(`/${expectedIcon}`),
+ "expected the correct icon on the notification"
+ );
+
+ panel.button.click();
+ });
+ await extension.startup();
+ await clickBrowserAction(extension);
+ await requestPrompt;
+ await extension.awaitMessage("done");
+ await extension.unload();
+};
+
+add_task(async function test_popup_requestPermission_resolve() {
+ await verifyRequestPermission({}, "extensionGeneric.svg");
+});
+
+add_task(async function test_popup_requestPermission_resolve_custom_icon() {
+ let expectedIcon = "icon-32.png";
+
+ await verifyRequestPermission(
+ {
+ icons: {
+ 16: "icon-16.png",
+ 32: expectedIcon,
+ },
+ },
+ expectedIcon
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_select.js b/browser/components/extensions/test/browser/browser_ext_popup_select.js
new file mode 100644
index 0000000000..87bd945a53
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_select.js
@@ -0,0 +1,115 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testPopupSelectPopup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "https://example.com",
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ browser.pageAction.show(tabs[0].id);
+ });
+ },
+
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+
+
+ Foo
+ Bar
+ Baz
+
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ async function testPanel(browser) {
+ const popupPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+
+ // Wait the select element in the popup window to be ready before sending a
+ // mouse event to open the select popup.
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document && content.document.querySelector("#select");
+ });
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter("#select", {}, browser);
+
+ const selectPopup = await popupPromise;
+
+ let elemRect = await SpecialPowers.spawn(browser, [], async function () {
+ let elem = content.document.getElementById("select");
+ let r = elem.getBoundingClientRect();
+
+ return { left: r.left, bottom: r.bottom };
+ });
+
+ let popupRect = selectPopup.getOuterScreenRect();
+ let marginTop = parseFloat(getComputedStyle(selectPopup).marginTop);
+ let marginLeft = parseFloat(getComputedStyle(selectPopup).marginLeft);
+
+ is(
+ Math.floor(browser.screenX + elemRect.left + marginLeft),
+ popupRect.left,
+ "Select popup has the correct x origin"
+ );
+
+ is(
+ Math.floor(browser.screenY + elemRect.bottom + marginTop),
+ popupRect.top,
+ "Select popup has the correct y origin"
+ );
+
+ // Close the select popup before proceeding to the next test.
+ const onPopupHidden = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphidden"
+ );
+ selectPopup.hidePopup();
+ await onPopupHidden;
+ }
+
+ {
+ info("Test browserAction popup");
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser);
+ await closeBrowserAction(extension);
+ }
+
+ {
+ info("Test pageAction popup");
+
+ clickPageAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser);
+ await closePageAction(extension);
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js b/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js
new file mode 100644
index 0000000000..fa2c414047
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js
@@ -0,0 +1,131 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// This test is based on browser_ext_popup_select.js.
+
+const iframeSrc = encodeURIComponent(`
+
+
+
+ Foo
+ Bar
+ Baz
+
+`);
+
+add_task(async function testPopupSelectPopup() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ const browserForPopup = await openBrowserActionPanel(
+ extension,
+ undefined,
+ true
+ );
+
+ const iframe = await SpecialPowers.spawn(browserForPopup, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document && content.document.querySelector("iframe");
+ });
+ const iframeElement = content.document.querySelector("iframe");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return iframeElement.browsingContext;
+ });
+ return iframeElement.browsingContext;
+ });
+
+ const selectRect = await SpecialPowers.spawn(iframe, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document.querySelector("select");
+ });
+ const select = content.document.querySelector("select");
+ const focusPromise = new Promise(resolve => {
+ select.addEventListener("focus", resolve, { once: true });
+ });
+ select.focus();
+ await focusPromise;
+
+ const r = select.getBoundingClientRect();
+
+ return { left: r.left, bottom: r.bottom };
+ });
+
+ const popupPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+
+ BrowserTestUtils.synthesizeMouseAtCenter("select", {}, iframe);
+
+ const selectPopup = await popupPromise;
+
+ let popupRect = selectPopup.getOuterScreenRect();
+ let popupMarginLeft = parseFloat(getComputedStyle(selectPopup).marginLeft);
+ let popupMarginTop = parseFloat(getComputedStyle(selectPopup).marginTop);
+
+ const offsetToSelectedItem =
+ selectPopup.querySelector("menuitem[selected]").getBoundingClientRect()
+ .top - selectPopup.getBoundingClientRect().top;
+ info(
+ `Browser is at ${browserForPopup.screenY}, popup is at ${popupRect.top} with ${offsetToSelectedItem} to the selected item`
+ );
+
+ is(
+ Math.floor(browserForPopup.screenX + selectRect.left),
+ popupRect.left - popupMarginLeft,
+ "Select popup has the correct x origin"
+ );
+
+ // On Mac select popup window appears aligned to the selected option.
+ let expectedY = navigator.platform.includes("Mac")
+ ? Math.floor(browserForPopup.screenY - offsetToSelectedItem)
+ : Math.floor(browserForPopup.screenY + selectRect.bottom);
+ is(
+ expectedY,
+ popupRect.top - popupMarginTop,
+ "Select popup has the correct y origin"
+ );
+
+ const onPopupHidden = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphidden"
+ );
+ selectPopup.hidePopup();
+ await onPopupHidden;
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js b/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js
new file mode 100644
index 0000000000..632b929121
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js
@@ -0,0 +1,135 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function test_popup_sendMessage_reply() {
+ let scriptPage = url =>
+ ` ${url}`;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": async function () {
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg == "popup-ping") {
+ return "popup-pong";
+ }
+ });
+
+ let response = await browser.runtime.sendMessage("background-ping");
+ browser.test.sendMessage("background-ping-response", response);
+ },
+ },
+
+ async background() {
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg == "background-ping") {
+ let response = await browser.runtime.sendMessage("popup-ping");
+
+ browser.test.sendMessage("popup-ping-response", response);
+
+ await new Promise(resolve => {
+ // Wait long enough that we're relatively sure the docShells have
+ // been swapped. Note that this value is fairly arbitrary. The load
+ // event that triggers the swap should happen almost immediately
+ // after the message is sent. The extra quarter of a second gives us
+ // enough leeway that we can expect to respond after the swap in the
+ // vast majority of cases.
+ setTimeout(resolve, 250);
+ });
+
+ return "background-pong";
+ }
+ });
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+
+ await browser.pageAction.show(tab.id);
+
+ browser.test.sendMessage("page-action-ready");
+ },
+ });
+
+ await extension.startup();
+
+ {
+ clickBrowserAction(extension);
+
+ let pong = await extension.awaitMessage("background-ping-response");
+ is(pong, "background-pong", "Got pong");
+
+ pong = await extension.awaitMessage("popup-ping-response");
+ is(pong, "popup-pong", "Got pong");
+
+ await closeBrowserAction(extension);
+ }
+
+ await extension.awaitMessage("page-action-ready");
+
+ {
+ clickPageAction(extension);
+
+ let pong = await extension.awaitMessage("background-ping-response");
+ is(pong, "background-pong", "Got pong");
+
+ pong = await extension.awaitMessage("popup-ping-response");
+ is(pong, "popup-pong", "Got pong");
+
+ await closePageAction(extension);
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_popup_close_then_sendMessage() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ },
+
+ files: {
+ "popup.html": ` ghost`,
+ "popup.js"() {
+ browser.tabs.query({ active: true }).then(() => {
+ // NOTE: the message will be sent _after_ the popup is closed below.
+ browser.runtime.sendMessage("sent-after-closed");
+ });
+ window.close();
+ },
+ },
+
+ async background() {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "sent-after-closed", "Message from popup.");
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ clickBrowserAction(extension);
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
new file mode 100644
index 0000000000..246a83520e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
@@ -0,0 +1,80 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let getExtension = () => {
+ return ExtensionTestUtils.loadExtension({
+ background: async function () {
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("pageAction ready");
+ },
+
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ `,
+ },
+ });
+};
+
+add_task(async function testStandaloneBrowserAction() {
+ info("Test stand-alone browserAction popup");
+
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("pageAction ready");
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ let panel = getPanelForNode(browser);
+
+ await extension.unload();
+
+ is(panel.parentNode, null, "Panel should be removed from the document");
+});
+
+add_task(async function testMenuPanelBrowserAction() {
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("pageAction ready");
+
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ let panel = getPanelForNode(browser);
+
+ await extension.unload();
+
+ is(panel.state, "closed", "Panel should be closed");
+});
+
+add_task(async function testPageAction() {
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("pageAction ready");
+
+ clickPageAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ let panel = getPanelForNode(browser);
+
+ await extension.unload();
+
+ is(panel.parentNode, null, "Panel should be removed from the document");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_crash.js b/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_crash.js
new file mode 100644
index 0000000000..82ece1da3f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_crash.js
@@ -0,0 +1,113 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function connect_from_tab_to_bg_and_crash_tab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ js: ["contentscript.js"],
+ matches: ["http://example.com/?crashme"],
+ },
+ ],
+ },
+
+ background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq("tab_to_bg", port.name, "expected port");
+ browser.test.assertEq(port.sender.frameId, 0, "correct frameId");
+
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ browser.test.sendMessage("bg_runtime_onConnect");
+ });
+ },
+
+ files: {
+ "contentscript.js": function () {
+ let port = browser.runtime.connect({ name: "tab_to_bg" });
+ port.onDisconnect.addListener(() => {
+ browser.test.fail("Unexpected onDisconnect event in content script");
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?crashme"
+ );
+ await extension.awaitMessage("bg_runtime_onConnect");
+ // Force the message manager to disconnect without giving the content a
+ // chance to send an "Extension:Port:Disconnect" message.
+ await BrowserTestUtils.crashFrame(tab.linkedBrowser);
+ await extension.awaitMessage("port_disconnected");
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function connect_from_bg_to_tab_and_crash_tab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ js: ["contentscript.js"],
+ matches: ["http://example.com/?crashme"],
+ },
+ ],
+ },
+
+ background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq("contentscript_ready", msg, "expected message");
+ let port = browser.tabs.connect(sender.tab.id, { name: "bg_to_tab" });
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ });
+ },
+
+ files: {
+ "contentscript.js": function () {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq("bg_to_tab", port.name, "expected port");
+ port.onDisconnect.addListener(() => {
+ browser.test.fail(
+ "Unexpected onDisconnect event in content script"
+ );
+ });
+ browser.test.sendMessage("tab_runtime_onConnect");
+ });
+ browser.runtime.sendMessage("contentscript_ready");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?crashme"
+ );
+ await extension.awaitMessage("tab_runtime_onConnect");
+ // Force the message manager to disconnect without giving the content a
+ // chance to send an "Extension:Port:Disconnect" message.
+ await BrowserTestUtils.crashFrame(tab.linkedBrowser);
+ await extension.awaitMessage("port_disconnected");
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_window_close.js b/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_window_close.js
new file mode 100644
index 0000000000..84bc4a3ff0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_window_close.js
@@ -0,0 +1,39 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Regression test for https://bugzil.la/1392067 .
+add_task(async function connect_from_window_and_close() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq("page_to_bg", port.name, "expected port");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ browser.windows.remove(port.sender.tab.windowId);
+ });
+
+ browser.windows.create({ url: "page.html" });
+ },
+
+ files: {
+ "page.html": ` `,
+ "page.js": function () {
+ let port = browser.runtime.connect({ name: "page_to_bg" });
+ port.onDisconnect.addListener(() => {
+ browser.test.fail("Unexpected onDisconnect event in page");
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("port_disconnected");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_reload_manifest_cache.js b/browser/components/extensions/test/browser/browser_ext_reload_manifest_cache.js
new file mode 100644
index 0000000000..aefa8f42f5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_reload_manifest_cache.js
@@ -0,0 +1,72 @@
+"use strict";
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+add_task(async function test_reload_manifest_startupcache() {
+ const id = "id@tests.mozilla.org";
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ options_ui: {
+ open_in_tab: true,
+ page: "options.html",
+ },
+ optional_permissions: [""],
+ },
+ useAddonManager: "temporary",
+ files: {
+ "options.html": `lol`,
+ },
+ background() {
+ browser.runtime.openOptionsPage();
+ browser.permissions.onAdded.addListener(() => {
+ browser.runtime.openOptionsPage();
+ });
+ },
+ });
+
+ async function waitOptionsTab() {
+ let tab = await BrowserTestUtils.waitForNewTab(gBrowser, url =>
+ url.endsWith("options.html")
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ // Open a non-blank tab to force options to open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+ let optionsTabPromise = waitOptionsTab();
+
+ await ext.startup();
+ await optionsTabPromise;
+
+ let disabledPromise = awaitEvent("shutdown", id);
+ let enabledPromise = awaitEvent("ready", id);
+ optionsTabPromise = waitOptionsTab();
+
+ let addon = await AddonManager.getAddonByID(id);
+ await addon.reload();
+
+ await Promise.all([disabledPromise, enabledPromise, optionsTabPromise]);
+
+ optionsTabPromise = waitOptionsTab();
+ ExtensionPermissions.add(id, {
+ permissions: [],
+ origins: [""],
+ });
+ await optionsTabPromise;
+
+ let policy = WebExtensionPolicy.getByID(id);
+ let optionsUrl = policy.extension.manifest.options_ui.page;
+ ok(optionsUrl.includes(policy.mozExtensionHostname), "Normalized manifest.");
+
+ await BrowserTestUtils.removeTab(tab);
+ await ext.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_request_permissions.js b/browser/components/extensions/test/browser/browser_ext_request_permissions.js
new file mode 100644
index 0000000000..3ba58bccd5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_request_permissions.js
@@ -0,0 +1,121 @@
+"use strict";
+
+// This test case verifies that `permissions.request()` resolves in the
+// expected order.
+add_task(async function test_permissions_prompt() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ optional_permissions: ["history", "bookmarks"],
+ },
+ background: async () => {
+ let hiddenTab = await browser.tabs.create({
+ url: browser.runtime.getURL("hidden.html"),
+ active: false,
+ });
+
+ await browser.tabs.create({
+ url: browser.runtime.getURL("active.html"),
+ active: true,
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "activate-hiddenTab") {
+ await browser.tabs.update(hiddenTab.id, { active: true });
+
+ browser.test.sendMessage("activate-hiddenTab-ok");
+ }
+ });
+ },
+ files: {
+ "active.html": ``,
+ "active.js": async () => {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "request-perms-activeTab") {
+ let granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({ permissions: ["history"] })
+ );
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+
+ browser.test.sendMessage("request-perms-activeTab-ok");
+ }
+ });
+
+ browser.test.sendMessage("activeTab-ready");
+ },
+ "hidden.html": ``,
+ "hidden.js": async () => {
+ let resolved = false;
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "request-perms-hiddenTab") {
+ let granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({ permissions: ["bookmarks"] })
+ );
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+
+ resolved = true;
+
+ browser.test.sendMessage("request-perms-hiddenTab-ok");
+ } else if (msg === "hiddenTab-read-state") {
+ browser.test.sendMessage("hiddenTab-state-value", resolved);
+ }
+ });
+
+ browser.test.sendMessage("hiddenTab-ready");
+ },
+ },
+ });
+ await extension.startup();
+
+ await extension.awaitMessage("activeTab-ready");
+ await extension.awaitMessage("hiddenTab-ready");
+
+ // Call request() on a hidden window.
+ extension.sendMessage("request-perms-hiddenTab");
+
+ let requestPromptForActiveTab = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ // Call request() in the current window.
+ extension.sendMessage("request-perms-activeTab");
+ await requestPromptForActiveTab;
+ await extension.awaitMessage("request-perms-activeTab-ok");
+
+ // Check that initial request() is still pending.
+ extension.sendMessage("hiddenTab-read-state");
+ ok(
+ !(await extension.awaitMessage("hiddenTab-state-value")),
+ "initial request is pending"
+ );
+
+ let requestPromptForHiddenTab = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ extension.sendMessage("activate-hiddenTab");
+ await extension.awaitMessage("activate-hiddenTab-ok");
+ await requestPromptForHiddenTab;
+ await extension.awaitMessage("request-perms-hiddenTab-ok");
+
+ extension.sendMessage("hiddenTab-read-state");
+ ok(
+ await extension.awaitMessage("hiddenTab-state-value"),
+ "initial request is resolved"
+ );
+
+ // The extension tabs are automatically closed upon unload.
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_onPerformanceWarning.js b/browser/components/extensions/test/browser/browser_ext_runtime_onPerformanceWarning.js
new file mode 100644
index 0000000000..b0f62e677a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_onPerformanceWarning.js
@@ -0,0 +1,144 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {
+ Management: {
+ global: { tabTracker },
+ },
+} = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+const {
+ ExtensionUtils: { promiseObserved },
+} = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs");
+
+class TestHangReport {
+ constructor(addonId, scriptBrowser) {
+ this.addonId = addonId;
+ this.scriptBrowser = scriptBrowser;
+ this.QueryInterface = ChromeUtils.generateQI(["nsIHangReport"]);
+ }
+
+ userCanceled() {}
+ terminateScript() {}
+
+ isReportForBrowserOrChildren(frameLoader) {
+ return (
+ !this.scriptBrowser || this.scriptBrowser.frameLoader === frameLoader
+ );
+ }
+}
+
+function dispatchHangReport(extensionId, scriptBrowser) {
+ const hangObserved = promiseObserved("process-hang-report");
+
+ Services.obs.notifyObservers(
+ new TestHangReport(extensionId, scriptBrowser),
+ "process-hang-report"
+ );
+
+ return hangObserved;
+}
+
+function background() {
+ let onPerformanceWarningDetails = null;
+
+ browser.runtime.onPerformanceWarning.addListener(details => {
+ onPerformanceWarningDetails = details;
+ });
+
+ browser.test.onMessage.addListener(message => {
+ if (message === "get-on-performance-warning-details") {
+ browser.test.sendMessage(
+ "on-performance-warning-details",
+ onPerformanceWarningDetails
+ );
+ onPerformanceWarningDetails = null;
+ }
+ });
+}
+
+async function expectOnPerformanceWarningDetails(
+ extension,
+ expectedOnPerformanceWarningDetails
+) {
+ extension.sendMessage("get-on-performance-warning-details");
+
+ let actualOnPerformanceWarningDetails = await extension.awaitMessage(
+ "on-performance-warning-details"
+ );
+ Assert.deepEqual(
+ actualOnPerformanceWarningDetails,
+ expectedOnPerformanceWarningDetails,
+ expectedOnPerformanceWarningDetails
+ ? "runtime.onPerformanceWarning fired with correct details"
+ : "runtime.onPerformanceWarning didn't fire"
+ );
+}
+
+add_task(async function test_should_fire_on_process_hang_report() {
+ const description =
+ "Slow extension content script caused a page hang, user was warned.";
+
+ const extension = ExtensionTestUtils.loadExtension({ background });
+ await extension.startup();
+
+ const notificationPromise = BrowserTestUtils.waitForGlobalNotificationBar(
+ window,
+ "process-hang"
+ );
+
+ const tabs = await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(gBrowser),
+ BrowserTestUtils.openNewForegroundTab(gBrowser),
+ ]);
+
+ // Warning event shouldn't have fired initially.
+ await expectOnPerformanceWarningDetails(extension, null);
+
+ // Hang report fired for the extension and first tab. Warning event with first
+ // tab ID expected.
+ await dispatchHangReport(extension.id, tabs[0].linkedBrowser);
+ await expectOnPerformanceWarningDetails(extension, {
+ category: "content_script",
+ severity: "high",
+ description,
+ tabId: tabTracker.getId(tabs[0]),
+ });
+
+ // Hang report fired for different extension, no warning event expected.
+ await dispatchHangReport("wrong-addon-id", tabs[0].linkedBrowser);
+ await expectOnPerformanceWarningDetails(extension, null);
+
+ // Non-extension hang report fired, no warning event expected.
+ await dispatchHangReport(null, tabs[0].linkedBrowser);
+ await expectOnPerformanceWarningDetails(extension, null);
+
+ // Hang report fired for the extension and second tab. Warning event with
+ // second tab ID expected.
+ await dispatchHangReport(extension.id, tabs[1].linkedBrowser);
+ await expectOnPerformanceWarningDetails(extension, {
+ category: "content_script",
+ severity: "high",
+ description,
+ tabId: tabTracker.getId(tabs[1]),
+ });
+
+ // Hang report fired for the extension with no associated tab. Warning event
+ // with no tab ID expected.
+ await dispatchHangReport(extension.id, null);
+ await expectOnPerformanceWarningDetails(extension, {
+ category: "content_script",
+ severity: "high",
+ description,
+ });
+
+ await Promise.all(tabs.map(BrowserTestUtils.removeTab));
+ await extension.unload();
+
+ // Wait for the process-hang warning bar to be displayed, then ensure it's
+ // cleared to avoid clobbering other tests.
+ const notification = await notificationPromise;
+ Assert.ok(notification.isConnected, "Notification still present");
+ notification.buttonContainer.querySelector("[label='Stop']").click();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
new file mode 100644
index 0000000000..a4b01bc182
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
@@ -0,0 +1,442 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function loadExtension(options) {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: Object.assign(
+ {
+ permissions: ["tabs"],
+ },
+ options.manifest
+ ),
+
+ files: {
+ "options.html": `
+
+
+
+
+
+ `,
+
+ "options.js": function () {
+ window.iAmOption = true;
+ browser.runtime.sendMessage("options.html");
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "ping") {
+ respond("pong");
+ } else if (msg == "connect") {
+ let port = browser.runtime.connect();
+ port.postMessage("ping-from-options-html");
+ port.onMessage.addListener(msg => {
+ if (msg == "ping-from-bg") {
+ browser.test.log("Got outbound options.html pong");
+ browser.test.sendMessage("options-html-outbound-pong");
+ }
+ });
+ }
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.log("Got inbound options.html port");
+
+ port.postMessage("ping-from-options-html");
+ port.onMessage.addListener(msg => {
+ if (msg == "ping-from-bg") {
+ browser.test.log("Got inbound options.html pong");
+ browser.test.sendMessage("options-html-inbound-pong");
+ }
+ });
+ });
+ },
+ },
+
+ background: options.background,
+ });
+
+ await extension.startup();
+
+ return extension;
+}
+
+add_task(async function run_test_inline_options() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = await loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "inline_options@tests.mozilla.org" },
+ },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+
+ background: async function () {
+ let _optionsPromise;
+ let awaitOptions = () => {
+ browser.test.assertFalse(
+ _optionsPromise,
+ "Should not be awaiting options already"
+ );
+
+ return new Promise(resolve => {
+ _optionsPromise = { resolve };
+ });
+ };
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg == "options.html") {
+ if (_optionsPromise) {
+ _optionsPromise.resolve(sender.tab);
+ _optionsPromise = null;
+ } else {
+ browser.test.fail("Saw unexpected options page load");
+ }
+ }
+ });
+
+ try {
+ let [firstTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.log("Open options page. Expect fresh load.");
+
+ let [, optionsTab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+
+ browser.test.assertEq(
+ "about:addons",
+ optionsTab.url,
+ "Tab contains AddonManager"
+ );
+ browser.test.assertTrue(optionsTab.active, "Tab is active");
+ browser.test.assertTrue(
+ optionsTab.id != firstTab.id,
+ "Tab is a new tab"
+ );
+
+ browser.test.assertEq(
+ 0,
+ browser.extension.getViews({ type: "popup" }).length,
+ "viewType is not popup"
+ );
+ browser.test.assertEq(
+ 1,
+ browser.extension.getViews({ type: "tab" }).length,
+ "viewType is tab"
+ );
+ browser.test.assertEq(
+ 1,
+ browser.extension.getViews({ windowId: optionsTab.windowId }).length,
+ "windowId matches"
+ );
+
+ let views = browser.extension.getViews();
+ browser.test.assertEq(
+ 2,
+ views.length,
+ "Expected the options page and the background page"
+ );
+ browser.test.assertTrue(
+ views.includes(window),
+ "One of the views is the background page"
+ );
+ browser.test.assertTrue(
+ views.some(w => w.iAmOption),
+ "One of the views is the options page"
+ );
+
+ browser.test.log("Switch tabs.");
+ await browser.tabs.update(firstTab.id, { active: true });
+
+ browser.test.log(
+ "Open options page again. Expect tab re-selected, no new load."
+ );
+
+ await browser.runtime.openOptionsPage();
+ let [tab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.assertEq(
+ optionsTab.id,
+ tab.id,
+ "Tab is the same as the previous options tab"
+ );
+ browser.test.assertEq(
+ "about:addons",
+ tab.url,
+ "Tab contains AddonManager"
+ );
+
+ browser.test.log("Ping options page.");
+ let pong = await browser.runtime.sendMessage("ping");
+ browser.test.assertEq("pong", pong, "Got pong.");
+
+ let done = new Promise(resolve => {
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "ports-done") {
+ resolve();
+ }
+ });
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.log("Got inbound background port");
+
+ port.postMessage("ping-from-bg");
+ port.onMessage.addListener(msg => {
+ if (msg == "ping-from-options-html") {
+ browser.test.log("Got inbound background pong");
+ browser.test.sendMessage("bg-inbound-pong");
+ }
+ });
+ });
+
+ browser.runtime.sendMessage("connect");
+
+ let port = browser.runtime.connect();
+ port.postMessage("ping-from-bg");
+ port.onMessage.addListener(msg => {
+ if (msg == "ping-from-options-html") {
+ browser.test.log("Got outbound background pong");
+ browser.test.sendMessage("bg-outbound-pong");
+ }
+ });
+
+ await done;
+
+ browser.test.log("Remove options tab.");
+ await browser.tabs.remove(optionsTab.id);
+
+ browser.test.log("Open options page again. Expect fresh load.");
+ [, tab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ browser.test.assertEq(
+ "about:addons",
+ tab.url,
+ "Tab contains AddonManager"
+ );
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != optionsTab.id, "Tab is a new tab");
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("options-ui");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("options-ui");
+ }
+ },
+ });
+
+ await Promise.all([
+ extension.awaitMessage("options-html-inbound-pong"),
+ extension.awaitMessage("options-html-outbound-pong"),
+ extension.awaitMessage("bg-inbound-pong"),
+ extension.awaitMessage("bg-outbound-pong"),
+ ]);
+
+ extension.sendMessage("ports-done");
+
+ await extension.awaitFinish("options-ui");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_tab_options() {
+ info(`Test options opened in a tab`);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = await loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "tab_options@tests.mozilla.org" },
+ },
+ options_ui: {
+ page: "options.html",
+ open_in_tab: true,
+ },
+ },
+
+ background: async function () {
+ let _optionsPromise;
+ let awaitOptions = () => {
+ browser.test.assertFalse(
+ _optionsPromise,
+ "Should not be awaiting options already"
+ );
+
+ return new Promise(resolve => {
+ _optionsPromise = { resolve };
+ });
+ };
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg == "options.html") {
+ if (_optionsPromise) {
+ _optionsPromise.resolve(sender.tab);
+ _optionsPromise = null;
+ } else {
+ browser.test.fail("Saw unexpected options page load");
+ }
+ }
+ });
+
+ let optionsURL = browser.runtime.getURL("options.html");
+
+ try {
+ let [firstTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.log("Open options page. Expect fresh load.");
+ let [, optionsTab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ browser.test.assertEq(
+ optionsURL,
+ optionsTab.url,
+ "Tab contains options.html"
+ );
+ browser.test.assertTrue(optionsTab.active, "Tab is active");
+ browser.test.assertTrue(
+ optionsTab.id != firstTab.id,
+ "Tab is a new tab"
+ );
+
+ browser.test.assertEq(
+ 0,
+ browser.extension.getViews({ type: "popup" }).length,
+ "viewType is not popup"
+ );
+ browser.test.assertEq(
+ 1,
+ browser.extension.getViews({ type: "tab" }).length,
+ "viewType is tab"
+ );
+ browser.test.assertEq(
+ 1,
+ browser.extension.getViews({ windowId: optionsTab.windowId }).length,
+ "windowId matches"
+ );
+
+ let views = browser.extension.getViews();
+ browser.test.assertEq(
+ 2,
+ views.length,
+ "Expected the options page and the background page"
+ );
+ browser.test.assertTrue(
+ views.includes(window),
+ "One of the views is the background page"
+ );
+ browser.test.assertTrue(
+ views.some(w => w.iAmOption),
+ "One of the views is the options page"
+ );
+
+ browser.test.log("Switch tabs.");
+ await browser.tabs.update(firstTab.id, { active: true });
+
+ browser.test.log(
+ "Open options page again. Expect tab re-selected, no new load."
+ );
+
+ await browser.runtime.openOptionsPage();
+ let [tab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.assertEq(
+ optionsTab.id,
+ tab.id,
+ "Tab is the same as the previous options tab"
+ );
+ browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
+
+ // Unfortunately, we can't currently do this, since onMessage doesn't
+ // currently support responses when there are multiple listeners.
+ //
+ // browser.test.log("Ping options page.");
+ // return new Promise(resolve => browser.runtime.sendMessage("ping", resolve));
+
+ browser.test.log("Remove options tab.");
+ await browser.tabs.remove(optionsTab.id);
+
+ browser.test.log("Open options page again. Expect fresh load.");
+ [, tab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != optionsTab.id, "Tab is a new tab");
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("options-ui-tab");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("options-ui-tab");
+ }
+ },
+ });
+
+ await extension.awaitFinish("options-ui-tab");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_options_no_manifest() {
+ info(`Test with no manifest key`);
+
+ let extension = await loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "no_options@tests.mozilla.org" },
+ },
+ },
+
+ async background() {
+ browser.test.log(
+ "Try to open options page when not specified in the manifest."
+ );
+
+ await browser.test.assertRejects(
+ browser.runtime.openOptionsPage(),
+ /No `options_ui` declared/,
+ "Expected error from openOptionsPage()"
+ );
+
+ browser.test.notifyPass("options-no-manifest");
+ },
+ });
+
+ await extension.awaitFinish("options-no-manifest");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js
new file mode 100644
index 0000000000..ac9bbf1ed2
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js
@@ -0,0 +1,122 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function loadExtension(options) {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: Object.assign(
+ {
+ permissions: ["tabs"],
+ },
+ options.manifest
+ ),
+
+ files: {
+ "options.html": `
+
+
+
+
+
+ `,
+
+ "options.js": function () {
+ browser.runtime.sendMessage("options.html");
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "ping") {
+ respond("pong");
+ }
+ });
+ },
+ },
+
+ background: options.background,
+ });
+
+ await extension.startup();
+
+ return extension;
+}
+
+add_task(async function test_inline_options_uninstall() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = await loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "inline_options_uninstall@tests.mozilla.org" },
+ },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+
+ background: async function () {
+ let _optionsPromise;
+ let awaitOptions = () => {
+ browser.test.assertFalse(
+ _optionsPromise,
+ "Should not be awaiting options already"
+ );
+
+ return new Promise(resolve => {
+ _optionsPromise = { resolve };
+ });
+ };
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg == "options.html") {
+ if (_optionsPromise) {
+ _optionsPromise.resolve(sender.tab);
+ _optionsPromise = null;
+ } else {
+ browser.test.fail("Saw unexpected options page load");
+ }
+ }
+ });
+
+ try {
+ let [firstTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.log("Open options page. Expect fresh load.");
+ let [, tab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+
+ browser.test.assertEq(
+ "about:addons",
+ tab.url,
+ "Tab contains AddonManager"
+ );
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != firstTab.id, "Tab is a new tab");
+
+ browser.test.sendMessage("options-ui-open");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ }
+ },
+ });
+
+ await extension.awaitMessage("options-ui-open");
+ await extension.unload();
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:addons",
+ "Add-on manager tab should still be open"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
new file mode 100644
index 0000000000..2530c28a6d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
@@ -0,0 +1,134 @@
+"use strict";
+
+// testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js loads
+// ExtensionTestCommon, and is slated as part of the SimpleTest
+// environment in tools/lint/eslint/eslint-plugin-mozilla/lib/environments/simpletest.js
+// However, nothing but the ExtensionTestUtils global gets put
+// into the scope, and so although eslint thinks this global is
+// available, it really isn't.
+// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
+let { ExtensionTestCommon } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionTestCommon.sys.mjs"
+);
+
+async function makeAndInstallXPI(id, backgroundScript, loadedURL) {
+ let xpi = ExtensionTestCommon.generateXPI({
+ manifest: { browser_specific_settings: { gecko: { id } } },
+ background: backgroundScript,
+ });
+ SimpleTest.registerCleanupFunction(function cleanupXPI() {
+ Services.obs.notifyObservers(xpi, "flush-cache-entry");
+ xpi.remove(false);
+ });
+
+ let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, loadedURL);
+
+ info(`installing ${xpi.path}`);
+ let addon = await AddonManager.installTemporaryAddon(xpi);
+ info("installed");
+
+ // A WebExtension is started asynchronously, we have our test extension
+ // open a new tab to signal that the background script has executed.
+ let loadTab = await loadPromise;
+ BrowserTestUtils.removeTab(loadTab);
+
+ return addon;
+}
+
+add_task(async function test_setuninstallurl_badargs() {
+ async function background() {
+ await browser.test.assertRejects(
+ browser.runtime.setUninstallURL("this is not a url"),
+ /Invalid URL/,
+ "setUninstallURL with an invalid URL should fail"
+ );
+
+ await browser.test.assertRejects(
+ browser.runtime.setUninstallURL("file:///etc/passwd"),
+ /must have the scheme http or https/,
+ "setUninstallURL with an illegal URL should fail"
+ );
+
+ browser.test.notifyPass("setUninstallURL bad params");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ });
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+// Test the documented behavior of setUninstallURL() that passing an
+// empty string is equivalent to not setting an uninstall URL
+// (i.e., no new tab is opened upon uninstall)
+add_task(async function test_setuninstall_empty_url() {
+ async function backgroundScript() {
+ await browser.runtime.setUninstallURL("");
+ browser.tabs.create({ url: "http://example.com/addon_loaded" });
+ }
+
+ let addon = await makeAndInstallXPI(
+ "test_uinstallurl2@tests.mozilla.org",
+ backgroundScript,
+ "http://example.com/addon_loaded"
+ );
+
+ addon.uninstall(true);
+ info("uninstalled");
+
+ // no need to explicitly check for the absence of a new tab,
+ // BrowserTestUtils will eventually complain if one is opened.
+});
+
+// Test the documented behavior of setUninstallURL() that passing an
+// empty string is equivalent to not setting an uninstall URL
+// (i.e., no new tab is opened upon uninstall)
+// here we pass a null value to string and test
+add_task(async function test_setuninstall_null_url() {
+ async function backgroundScript() {
+ await browser.runtime.setUninstallURL(null);
+ browser.tabs.create({ url: "http://example.com/addon_loaded" });
+ }
+
+ let addon = await makeAndInstallXPI(
+ "test_uinstallurl2@tests.mozilla.org",
+ backgroundScript,
+ "http://example.com/addon_loaded"
+ );
+
+ addon.uninstall(true);
+ info("uninstalled");
+
+ // no need to explicitly check for the absence of a new tab,
+ // BrowserTestUtils will eventually complain if one is opened.
+});
+
+add_task(async function test_setuninstallurl() {
+ async function backgroundScript() {
+ await browser.runtime.setUninstallURL(
+ "http://example.com/addon_uninstalled"
+ );
+ browser.tabs.create({ url: "http://example.com/addon_loaded" });
+ }
+
+ let addon = await makeAndInstallXPI(
+ "test_uinstallurl@tests.mozilla.org",
+ backgroundScript,
+ "http://example.com/addon_loaded"
+ );
+
+ // look for a new tab with the uninstall url.
+ let uninstallPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://example.com/addon_uninstalled"
+ );
+
+ addon.uninstall(true);
+ info("uninstalled");
+
+ let uninstalledTab = await uninstallPromise;
+ isnot(uninstalledTab, null, "opened tab with uninstall url");
+ BrowserTestUtils.removeTab(uninstalledTab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_search.js b/browser/components/extensions/test/browser/browser_ext_search.js
new file mode 100644
index 0000000000..c7dab1c9dc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_search.js
@@ -0,0 +1,351 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const SEARCH_TERM = "test";
+const SEARCH_URL = "https://example.org/?q={searchTerms}";
+
+AddonTestUtils.initMochitest(this);
+
+add_task(async function test_search() {
+ async function background(SEARCH_TERM) {
+ browser.test.onMessage.addListener(async (msg, tabIds) => {
+ if (msg !== "removeTabs") {
+ return;
+ }
+
+ await browser.tabs.remove(tabIds);
+ browser.test.sendMessage("onTabsRemoved");
+ });
+
+ function awaitSearchResult() {
+ return new Promise(resolve => {
+ async function listener(tabId, info, changedTab) {
+ if (changedTab.url == "about:blank") {
+ // Ignore events related to the initial tab open.
+ return;
+ }
+
+ if (info.status === "complete") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve({ tabId, url: changedTab.url });
+ }
+ }
+
+ browser.tabs.onUpdated.addListener(listener);
+ });
+ }
+
+ let engines = await browser.search.get();
+ browser.test.sendMessage("engines", engines);
+
+ // Search with no tabId
+ browser.search.search({ query: SEARCH_TERM + "1", engine: "Search Test" });
+ let result = await awaitSearchResult();
+ browser.test.sendMessage("searchLoaded", result);
+
+ // Search with tabId
+ let tab = await browser.tabs.create({});
+ browser.search.search({
+ query: SEARCH_TERM + "2",
+ engine: "Search Test",
+ tabId: tab.id,
+ });
+ result = await awaitSearchResult();
+ browser.test.assertEq(result.tabId, tab.id, "Page loaded in right tab");
+ browser.test.sendMessage("searchLoaded", result);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Search Test",
+ search_url: SEARCH_URL,
+ },
+ },
+ },
+ background: `(${background})("${SEARCH_TERM}")`,
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+ let addonEngines = await extension.awaitMessage("engines");
+ let engines = (await Services.search.getEngines()).filter(
+ engine => !engine.hidden
+ );
+ is(addonEngines.length, engines.length, "Engine lengths are the same.");
+ let defaultEngine = addonEngines.filter(engine => engine.isDefault === true);
+ is(defaultEngine.length, 1, "One default engine");
+ is(
+ defaultEngine[0].name,
+ (await Services.search.getDefault()).name,
+ "Default engine is correct"
+ );
+
+ const result1 = await extension.awaitMessage("searchLoaded");
+ is(
+ result1.url,
+ SEARCH_URL.replace("{searchTerms}", SEARCH_TERM + "1"),
+ "Loaded page matches search"
+ );
+ await TestUtils.waitForCondition(
+ () => !gURLBar.focused,
+ "Wait for unfocusing the urlbar"
+ );
+ info("The urlbar has no focus when searching without tabId");
+
+ const result2 = await extension.awaitMessage("searchLoaded");
+ is(
+ result2.url,
+ SEARCH_URL.replace("{searchTerms}", SEARCH_TERM + "2"),
+ "Loaded page matches search"
+ );
+ await TestUtils.waitForCondition(
+ () => !gURLBar.focused,
+ "Wait for unfocusing the urlbar"
+ );
+ info("The urlbar has no focus when searching with tabId");
+
+ extension.sendMessage("removeTabs", [result1.tabId, result2.tabId]);
+ await extension.awaitMessage("onTabsRemoved");
+
+ await extension.unload();
+});
+
+add_task(async function test_search_default_engine() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search"],
+ },
+ background() {
+ browser.test.onMessage.addListener((msg, tabId) => {
+ browser.test.assertEq(msg, "search");
+ browser.search.search({ query: "searchTermForDefaultEngine", tabId });
+ });
+ browser.test.sendMessage("extension-origin", browser.runtime.getURL("/"));
+ },
+ useAddonManager: "temporary",
+ });
+
+ // Use another extension to intercept and block the search request,
+ // so that there is no outbound network activity that would kill the test.
+ // This method also allows us to verify that:
+ // 1) the search appears as a normal request in the webRequest API.
+ // 2) the request is associated with the triggering extension.
+ let extensionWithObserver = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["webRequest", "webRequestBlocking", "*://*/*"] },
+ async background() {
+ let tab = await browser.tabs.create({ url: "about:blank" });
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ browser.test.log(`Intercepted request ${JSON.stringify(details)}`);
+ browser.tabs.remove(tab.id).then(() => {
+ browser.test.sendMessage("detectedSearch", details);
+ });
+ return { cancel: true };
+ },
+ {
+ tabId: tab.id,
+ types: ["main_frame"],
+ urls: ["*://*/*"],
+ },
+ ["blocking"]
+ );
+ browser.test.sendMessage("ready", tab.id);
+ },
+ });
+ await extension.startup();
+ const EXPECTED_ORIGIN = await extension.awaitMessage("extension-origin");
+
+ await extensionWithObserver.startup();
+ let tabId = await extensionWithObserver.awaitMessage("ready");
+
+ extension.sendMessage("search", tabId);
+ let requestDetails = await extensionWithObserver.awaitMessage(
+ "detectedSearch"
+ );
+ await extension.unload();
+ await extensionWithObserver.unload();
+
+ ok(
+ requestDetails.url.includes("searchTermForDefaultEngine"),
+ `Expected search term in ${requestDetails.url}`
+ );
+ is(
+ requestDetails.originUrl,
+ EXPECTED_ORIGIN,
+ "Search request's should be associated with the originating extension."
+ );
+});
+
+add_task(async function test_search_disposition() {
+ async function background() {
+ let resolvers = {};
+
+ function tabListener(tabId, changeInfo, tab) {
+ if (tab.url == "about:blank") {
+ // Ignore events related to the initial tab open.
+ return;
+ }
+
+ if (changeInfo.status === "complete") {
+ let query = new URL(tab.url).searchParams.get("q");
+ let resolver = resolvers[query];
+ browser.test.assertTrue(resolver, `Found resolver for ${tab.url}`);
+ browser.test.assertTrue(
+ resolver.resolve,
+ `${query} was not resolved yet`
+ );
+ resolver.resolve({
+ tabId,
+ windowId: tab.windowId,
+ });
+ resolver.resolve = null; // resolve can be used only once.
+ }
+ }
+ browser.tabs.onUpdated.addListener(tabListener);
+
+ async function awaitSearchResult(args) {
+ resolvers[args.query] = {};
+ resolvers[args.query].promise = new Promise(
+ _resolve => (resolvers[args.query].resolve = _resolve)
+ );
+ await browser.search.search({ ...args, engine: "Search Test" });
+ let searchResult = await resolvers[args.query].promise;
+ return searchResult;
+ }
+
+ const firstTab = await browser.tabs.create({
+ active: true,
+ url: "about:blank",
+ });
+
+ // Search in new tab (testing default disposition)
+ let result = await awaitSearchResult({
+ query: "DefaultDisposition",
+ });
+ browser.test.assertFalse(
+ result.tabId === firstTab.id,
+ "Query ran in new tab"
+ );
+ browser.test.assertEq(
+ result.windowId,
+ firstTab.windowId,
+ "Query ran in current window"
+ );
+ await browser.tabs.remove(result.tabId); // Cleanup
+
+ // Search in new tab
+ result = await awaitSearchResult({
+ query: "NewTab",
+ disposition: "NEW_TAB",
+ });
+ browser.test.assertFalse(
+ result.tabId === firstTab.id,
+ "Query ran in new tab"
+ );
+ browser.test.assertEq(
+ result.windowId,
+ firstTab.windowId,
+ "Query ran in current window"
+ );
+ await browser.tabs.remove(result.tabId); // Cleanup
+
+ // Search in current tab
+ result = await awaitSearchResult({
+ query: "CurrentTab",
+ disposition: "CURRENT_TAB",
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: firstTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Query ran in current tab in current window"
+ );
+
+ // Search in a specific tab
+ let newTab = await browser.tabs.create({
+ active: false,
+ url: "about:blank",
+ });
+ result = await awaitSearchResult({
+ query: "SpecificTab",
+ tabId: newTab.id,
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: newTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Query ran in specific tab in current window"
+ );
+ await browser.tabs.remove(newTab.id); // Cleanup
+
+ // Search in a new window
+ result = await awaitSearchResult({
+ query: "NewWindow",
+ disposition: "NEW_WINDOW",
+ });
+ browser.test.assertFalse(
+ result.windowId === firstTab.windowId,
+ "Query ran in new window"
+ );
+ await browser.windows.remove(result.windowId); // Cleanup
+ await browser.tabs.remove(firstTab.id); // Cleanup
+
+ // Make sure tabId and disposition can't be used together
+ await browser.test.assertRejects(
+ browser.search.search({
+ query: " ",
+ tabId: 1,
+ disposition: "NEW_WINDOW",
+ }),
+ "Cannot set both 'disposition' and 'tabId'",
+ "Should not be able to set both tabId and disposition"
+ );
+
+ // Make sure we reject if an invalid tabId is used
+ await browser.test.assertRejects(
+ browser.search.search({
+ query: " ",
+ tabId: Number.MAX_SAFE_INTEGER,
+ }),
+ /Invalid tab ID/,
+ "Should not be able to set an invalid tabId"
+ );
+
+ browser.test.notifyPass("disposition");
+ }
+ let searchExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Search Test",
+ search_url: "https://example.org/?q={searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ },
+ background,
+ });
+ await searchExtension.startup();
+ await extension.startup();
+ await extension.awaitFinish("disposition");
+ await extension.unload();
+ await searchExtension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_search_favicon.js b/browser/components/extensions/test/browser/browser_ext_search_favicon.js
new file mode 100644
index 0000000000..4e48dd55fa
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_search_favicon.js
@@ -0,0 +1,184 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+XPCShellContentUtils.initMochitest(this);
+
+// Base64-encoded "Fake icon data".
+const FAKE_ICON_DATA = "RmFrZSBpY29uIGRhdGE=";
+
+// Base64-encoded "HTTP icon data".
+const HTTP_ICON_DATA = "SFRUUCBpY29uIGRhdGE=";
+const HTTP_ICON_URL = "http://example.org/ico.png";
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: ["example.org"],
+});
+server.registerPathHandler("/ico.png", (request, response) => {
+ response.write(atob(HTTP_ICON_DATA));
+});
+
+function promiseEngineIconLoaded(engineName) {
+ return TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (engine, verb) => {
+ engine.QueryInterface(Ci.nsISearchEngine);
+ return (
+ verb == "engine-changed" &&
+ engine.name == engineName &&
+ engine.getIconURL()
+ );
+ }
+ );
+}
+
+add_task(async function test_search_favicon() {
+ let searchExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Engine Only",
+ search_url: "https://example.com/",
+ favicon_url: "someFavicon.png",
+ },
+ },
+ },
+ files: {
+ "someFavicon.png": atob(FAKE_ICON_DATA),
+ },
+ useAddonManager: "temporary",
+ });
+
+ let searchExtWithBadIcon = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Bad Icon",
+ search_url: "https://example.net/",
+ favicon_url: "iDoNotExist.png",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let searchExtWithHttpIcon = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "HTTP Icon",
+ search_url: "https://example.org/",
+ favicon_url: HTTP_ICON_URL,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search"],
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "My Engine",
+ search_url: "https://example.org/",
+ favicon_url: "myFavicon.png",
+ },
+ },
+ },
+ files: {
+ "myFavicon.png": imageBuffer,
+ },
+ useAddonManager: "temporary",
+ async background() {
+ let engines = await browser.search.get();
+ browser.test.sendMessage("engines", {
+ badEngine: engines.find(engine => engine.name === "Bad Icon"),
+ httpEngine: engines.find(engine => engine.name === "HTTP Icon"),
+ myEngine: engines.find(engine => engine.name === "My Engine"),
+ otherEngine: engines.find(engine => engine.name === "Engine Only"),
+ });
+ },
+ });
+
+ await searchExt.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(searchExt);
+
+ await searchExtWithBadIcon.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(searchExtWithBadIcon);
+
+ // TODO bug 1571718: browser.search.get should behave correctly (i.e return
+ // the icon) even if the icon did not finish loading when the API was called.
+ // Currently calling it too early returns undefined, so just wait until the
+ // icon has loaded before calling browser.search.get.
+ let httpIconLoaded = promiseEngineIconLoaded("HTTP Icon");
+ await searchExtWithHttpIcon.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(searchExtWithHttpIcon);
+ await httpIconLoaded;
+
+ await extension.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+ let engines = await extension.awaitMessage("engines");
+
+ // An extension's own icon can surely be accessed by the extension, so its
+ // favIconUrl can be the moz-extension:-URL itself.
+ Assert.deepEqual(
+ engines.myEngine,
+ {
+ name: "My Engine",
+ isDefault: false,
+ alias: undefined,
+ favIconUrl: `moz-extension://${extension.uuid}/myFavicon.png`,
+ },
+ "browser.search.get result for own extension"
+ );
+
+ // favIconUrl of other engines need to be in base64-encoded form.
+ Assert.deepEqual(
+ engines.otherEngine,
+ {
+ name: "Engine Only",
+ isDefault: false,
+ alias: undefined,
+ favIconUrl: `data:image/png;base64,${FAKE_ICON_DATA}`,
+ },
+ "browser.search.get result for other extension"
+ );
+
+ // HTTP URLs should be provided as-is.
+ Assert.deepEqual(
+ engines.httpEngine,
+ {
+ name: "HTTP Icon",
+ isDefault: false,
+ alias: undefined,
+ favIconUrl: `data:image/png;base64,${HTTP_ICON_DATA}`,
+ },
+ "browser.search.get result for extension with HTTP icon URL"
+ );
+
+ // When the favicon does not exists, the favIconUrl must be unset.
+ Assert.deepEqual(
+ engines.badEngine,
+ {
+ name: "Bad Icon",
+ isDefault: false,
+ alias: undefined,
+ favIconUrl: undefined,
+ },
+ "browser.search.get result for other extension with non-existing icon"
+ );
+
+ await extension.unload();
+ await searchExt.unload();
+ await searchExtWithBadIcon.unload();
+ await searchExtWithHttpIcon.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_search_query.js b/browser/components/extensions/test/browser/browser_ext_search_query.js
new file mode 100644
index 0000000000..5258b12605
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_search_query.js
@@ -0,0 +1,174 @@
+"use strict";
+
+add_task(async function test_query() {
+ async function background() {
+ let resolvers = {};
+
+ function tabListener(tabId, changeInfo, tab) {
+ if (tab.url == "about:blank") {
+ // Ignore events related to the initial tab open.
+ return;
+ }
+
+ if (changeInfo.status === "complete") {
+ let query = new URL(tab.url).searchParams.get("q");
+ let resolver = resolvers[query];
+ browser.test.assertTrue(resolver, `Found resolver for ${tab.url}`);
+ browser.test.assertTrue(
+ resolver.resolve,
+ `${query} was not resolved yet`
+ );
+ resolver.resolve({
+ tabId,
+ windowId: tab.windowId,
+ });
+ resolver.resolve = null; // resolve can be used only once.
+ }
+ }
+ browser.tabs.onUpdated.addListener(tabListener);
+
+ async function awaitSearchResult(args) {
+ resolvers[args.text] = {};
+ resolvers[args.text].promise = new Promise(
+ _resolve => (resolvers[args.text].resolve = _resolve)
+ );
+ await browser.search.query(args);
+ let searchResult = await resolvers[args.text].promise;
+ return searchResult;
+ }
+
+ const firstTab = await browser.tabs.create({
+ active: true,
+ url: "about:blank",
+ });
+
+ browser.test.log("Search in current tab (testing default disposition)");
+ let result = await awaitSearchResult({
+ text: "DefaultDisposition",
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: firstTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Defaults to current tab in current window"
+ );
+
+ browser.test.log(
+ "Search in current tab (testing explicit disposition CURRENT_TAB)"
+ );
+ result = await awaitSearchResult({
+ text: "CurrentTab",
+ disposition: "CURRENT_TAB",
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: firstTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Query ran in current tab in current window"
+ );
+
+ browser.test.log("Search in new tab (testing disposition NEW_TAB)");
+ result = await awaitSearchResult({
+ text: "NewTab",
+ disposition: "NEW_TAB",
+ });
+ browser.test.assertFalse(
+ result.tabId === firstTab.id,
+ "Query ran in new tab"
+ );
+ browser.test.assertEq(
+ result.windowId,
+ firstTab.windowId,
+ "Query ran in current window"
+ );
+ await browser.tabs.remove(result.tabId); // Cleanup
+
+ browser.test.log("Search in a specific tab (testing property tabId)");
+ let newTab = await browser.tabs.create({
+ active: false,
+ url: "about:blank",
+ });
+ result = await awaitSearchResult({
+ text: "SpecificTab",
+ tabId: newTab.id,
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: newTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Query ran in specific tab in current window"
+ );
+ await browser.tabs.remove(newTab.id); // Cleanup
+
+ browser.test.log("Search in a new window (testing disposition NEW_WINDOW)");
+ result = await awaitSearchResult({
+ text: "NewWindow",
+ disposition: "NEW_WINDOW",
+ });
+ browser.test.assertFalse(
+ result.windowId === firstTab.windowId,
+ "Query ran in new window"
+ );
+ await browser.windows.remove(result.windowId); // Cleanup
+ await browser.tabs.remove(firstTab.id); // Cleanup
+
+ browser.test.log("Make sure tabId and disposition can't be used together");
+ await browser.test.assertRejects(
+ browser.search.query({
+ text: " ",
+ tabId: 1,
+ disposition: "NEW_WINDOW",
+ }),
+ "Cannot set both 'disposition' and 'tabId'",
+ "Should not be able to set both tabId and disposition"
+ );
+
+ browser.test.log("Make sure we reject if an invalid tabId is used");
+ await browser.test.assertRejects(
+ browser.search.query({
+ text: " ",
+ tabId: Number.MAX_SAFE_INTEGER,
+ }),
+ /Invalid tab ID/,
+ "Should not be able to set an invalid tabId"
+ );
+
+ browser.test.notifyPass("disposition");
+ }
+ const SEARCH_NAME = "Search Test";
+ let searchExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: SEARCH_NAME,
+ search_url: "https://example.org/?q={searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ },
+ background,
+ });
+ // We need to use a fake search engine because
+ // these tests aren't allowed to load actual
+ // webpages, like google.com for example.
+ await searchExtension.startup();
+ await Services.search.setDefault(
+ Services.search.getEngineByName(SEARCH_NAME),
+ Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL
+ );
+ await extension.startup();
+ await extension.awaitFinish("disposition");
+ await extension.unload();
+ await searchExtension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedTab.js b/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedTab.js
new file mode 100644
index 0000000000..c257cbd741
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedTab.js
@@ -0,0 +1,145 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+function getExtension(incognitoOverride) {
+ function background() {
+ browser.test.onMessage.addListener((msg, windowId, sessionId) => {
+ if (msg === "check-sessions") {
+ browser.sessions.getRecentlyClosed().then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ } else if (msg === "forget-tab") {
+ browser.sessions.forgetClosedTab(windowId, sessionId).then(
+ () => {
+ browser.test.sendMessage("forgot-tab");
+ },
+ error => {
+ browser.test.sendMessage("forget-reject", error.message);
+ }
+ );
+ }
+ });
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ incognitoOverride,
+ });
+}
+
+async function openAndCloseTab(window, url) {
+ const tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ const sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+}
+
+add_setup(async function prepare() {
+ // Clean up any session state left by previous tests that might impact this one
+ while (SessionStore.getClosedTabCountForWindow(window) > 0) {
+ SessionStore.forgetClosedTab(window, 0);
+ }
+ await TestUtils.waitForTick();
+});
+
+add_task(async function test_sessions_forget_closed_tab() {
+ let extension = getExtension();
+ await extension.startup();
+
+ let tabUrl = "http://example.com";
+ await openAndCloseTab(window, tabUrl);
+ await openAndCloseTab(window, tabUrl);
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let recentlyClosedLength = recentlyClosed.length;
+ let recentlyClosedTab = recentlyClosed[0].tab;
+
+ // Check that forgetting a tab works properly
+ extension.sendMessage(
+ "forget-tab",
+ recentlyClosedTab.windowId,
+ recentlyClosedTab.sessionId
+ );
+ await extension.awaitMessage("forgot-tab");
+ extension.sendMessage("check-sessions");
+ let remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosedLength - 1,
+ "One tab was forgotten."
+ );
+ is(
+ remainingClosed[0].tab.sessionId,
+ recentlyClosed[1].tab.sessionId,
+ "The correct tab was forgotten."
+ );
+
+ // Check that re-forgetting the same tab fails properly
+ extension.sendMessage(
+ "forget-tab",
+ recentlyClosedTab.windowId,
+ recentlyClosedTab.sessionId
+ );
+ let errormsg = await extension.awaitMessage("forget-reject");
+ is(
+ errormsg,
+ `Could not find closed tab using sessionId ${recentlyClosedTab.sessionId}.`
+ );
+
+ extension.sendMessage("check-sessions");
+ remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosedLength - 1,
+ "No extra tab was forgotten."
+ );
+ is(
+ remainingClosed[0].tab.sessionId,
+ recentlyClosed[1].tab.sessionId,
+ "The correct tab remains."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_forget_closed_tab_private() {
+ let pb_extension = getExtension("spanning");
+ await pb_extension.startup();
+ let extension = getExtension();
+ await extension.startup();
+
+ // Open a private browsing window.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let tabUrl = "http://example.com";
+ await openAndCloseTab(privateWin, tabUrl);
+
+ pb_extension.sendMessage("check-sessions");
+ let recentlyClosed = await pb_extension.awaitMessage("recentlyClosed");
+ let recentlyClosedTab = recentlyClosed[0].tab;
+
+ // Check that forgetting a tab works properly
+ extension.sendMessage(
+ "forget-tab",
+ recentlyClosedTab.windowId,
+ recentlyClosedTab.sessionId
+ );
+ let errormsg = await extension.awaitMessage("forget-reject");
+ ok(/Invalid window ID/.test(errormsg), "could not access window");
+
+ await BrowserTestUtils.closeWindow(privateWin);
+ await extension.unload();
+ await pb_extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedWindow.js b/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedWindow.js
new file mode 100644
index 0000000000..471d2f4440
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedWindow.js
@@ -0,0 +1,121 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function getExtension(incognitoOverride) {
+ function background() {
+ browser.test.onMessage.addListener((msg, sessionId) => {
+ if (msg === "check-sessions") {
+ browser.sessions.getRecentlyClosed().then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ } else if (msg === "forget-window") {
+ browser.sessions.forgetClosedWindow(sessionId).then(
+ () => {
+ browser.test.sendMessage("forgot-window");
+ },
+ error => {
+ browser.test.sendMessage("forget-reject", error.message);
+ }
+ );
+ }
+ });
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ incognitoOverride,
+ });
+}
+
+async function openAndCloseWindow(url = "http://example.com", privateWin) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: privateWin,
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ await BrowserTestUtils.closeWindow(win);
+ await sessionUpdatePromise;
+}
+
+add_task(async function test_sessions_forget_closed_window() {
+ let extension = getExtension();
+ await extension.startup();
+
+ await openAndCloseWindow("about:config");
+ await openAndCloseWindow("about:robots");
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let recentlyClosedWindow = recentlyClosed[0].window;
+
+ // Check that forgetting a window works properly
+ extension.sendMessage("forget-window", recentlyClosedWindow.sessionId);
+ await extension.awaitMessage("forgot-window");
+ extension.sendMessage("check-sessions");
+ let remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosed.length - 1,
+ "One window was forgotten."
+ );
+ is(
+ remainingClosed[0].window.sessionId,
+ recentlyClosed[1].window.sessionId,
+ "The correct window was forgotten."
+ );
+
+ // Check that re-forgetting the same window fails properly
+ extension.sendMessage("forget-window", recentlyClosedWindow.sessionId);
+ let errMsg = await extension.awaitMessage("forget-reject");
+ is(
+ errMsg,
+ `Could not find closed window using sessionId ${recentlyClosedWindow.sessionId}.`
+ );
+
+ extension.sendMessage("check-sessions");
+ remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosed.length - 1,
+ "No extra window was forgotten."
+ );
+ is(
+ remainingClosed[0].window.sessionId,
+ recentlyClosed[1].window.sessionId,
+ "The correct window remains."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_forget_closed_window_private() {
+ let pb_extension = getExtension("spanning");
+ await pb_extension.startup();
+ let extension = getExtension("not_allowed");
+ await extension.startup();
+
+ await openAndCloseWindow("about:config", true);
+ await openAndCloseWindow("about:robots", true);
+
+ pb_extension.sendMessage("check-sessions");
+ let recentlyClosed = await pb_extension.awaitMessage("recentlyClosed");
+ let recentlyClosedWindow = recentlyClosed[0].window;
+
+ extension.sendMessage("forget-window", recentlyClosedWindow.sessionId);
+ await extension.awaitMessage("forgot-window");
+ extension.sendMessage("check-sessions");
+ let remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosed.length - 1,
+ "One window was forgotten."
+ );
+ ok(!recentlyClosedWindow.incognito, "not an incognito window");
+
+ await extension.unload();
+ await pb_extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js
new file mode 100644
index 0000000000..7dd41ecfe9
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js
@@ -0,0 +1,216 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+loadTestSubscript("head_sessions.js");
+
+add_task(async function test_sessions_get_recently_closed() {
+ async function openAndCloseWindow(url = "http://example.com", tabUrls) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ if (tabUrls) {
+ for (let url of tabUrls) {
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ }
+ }
+ await BrowserTestUtils.closeWindow(win);
+ }
+
+ function background() {
+ Promise.all([
+ browser.sessions.getRecentlyClosed(),
+ browser.tabs.query({ active: true, currentWindow: true }),
+ ]).then(([recentlyClosed, tabs]) => {
+ browser.test.sendMessage("initialData", {
+ recentlyClosed,
+ currentWindowId: tabs[0].windowId,
+ });
+ });
+
+ browser.test.onMessage.addListener((msg, filter) => {
+ if (msg == "check-sessions") {
+ browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ // Open and close a window that will be ignored, to prove that we are removing previous entries
+ await openAndCloseWindow();
+
+ await extension.startup();
+
+ let { recentlyClosed, currentWindowId } = await extension.awaitMessage(
+ "initialData"
+ );
+ recordInitialTimestamps(recentlyClosed.map(item => item.lastModified));
+
+ await openAndCloseWindow();
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ checkRecentlyClosed(
+ recentlyClosed.filter(onlyNewItemsFilter),
+ 1,
+ currentWindowId
+ );
+
+ await openAndCloseWindow("about:config", ["about:robots", "about:mozilla"]);
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ // Check for multiple tabs in most recently closed window
+ is(
+ recentlyClosed[0].window.tabs.length,
+ 3,
+ "most recently closed window has the expected number of tabs"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ await openAndCloseWindow();
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let finalResult = recentlyClosed.filter(onlyNewItemsFilter);
+ checkRecentlyClosed(finalResult, 5, currentWindowId);
+
+ isnot(finalResult[0].window, undefined, "first item is a window");
+ is(finalResult[0].tab, undefined, "first item is not a tab");
+ isnot(finalResult[1].tab, undefined, "second item is a tab");
+ is(finalResult[1].window, undefined, "second item is not a window");
+ isnot(finalResult[2].tab, undefined, "third item is a tab");
+ is(finalResult[2].window, undefined, "third item is not a window");
+ isnot(finalResult[3].window, undefined, "fourth item is a window");
+ is(finalResult[3].tab, undefined, "fourth item is not a tab");
+ isnot(finalResult[4].window, undefined, "fifth item is a window");
+ is(finalResult[4].tab, undefined, "fifth item is not a tab");
+
+ // test with filter
+ extension.sendMessage("check-sessions", { maxResults: 2 });
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ checkRecentlyClosed(
+ recentlyClosed.filter(onlyNewItemsFilter),
+ 2,
+ currentWindowId
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_get_recently_closed_navigated() {
+ function background() {
+ browser.sessions
+ .getRecentlyClosed({ maxResults: 1 })
+ .then(recentlyClosed => {
+ let tab = recentlyClosed[0].window.tabs[0];
+ browser.test.assertEq(
+ "http://example.com/",
+ tab.url,
+ "Tab in closed window has the expected url."
+ );
+ browser.test.assertTrue(
+ tab.title.includes("mochitest index"),
+ "Tab in closed window has the expected title."
+ );
+ browser.test.notifyPass("getRecentlyClosed with navigation");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ // Test with a window with navigation history.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ for (let url of ["about:robots", "about:mozilla", "http://example.com/"]) {
+ BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(
+ async function test_sessions_get_recently_closed_empty_history_in_closed_window() {
+ function background() {
+ browser.sessions
+ .getRecentlyClosed({ maxResults: 1 })
+ .then(recentlyClosed => {
+ let win = recentlyClosed[0].window;
+ browser.test.assertEq(
+ 3,
+ win.tabs.length,
+ "The closed window has 3 tabs."
+ );
+ browser.test.assertEq(
+ "about:blank",
+ win.tabs[0].url,
+ "The first tab is about:blank."
+ );
+ browser.test.assertFalse(
+ "url" in win.tabs[1],
+ "The second tab with empty.xpi has no url field due to empty history."
+ );
+ browser.test.assertEq(
+ "http://example.com/",
+ win.tabs[2].url,
+ "The third tab is example.com."
+ );
+ browser.test.notifyPass("getRecentlyClosed with empty history");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ // Test with a window with empty history.
+ let xpi =
+ "http://example.com/browser/browser/components/extensions/test/browser/empty.xpi";
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: newWin.gBrowser,
+ url: xpi,
+ // A tab with broken xpi file doesn't finish loading.
+ waitForLoad: false,
+ });
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: newWin.gBrowser,
+ url: "http://example.com/",
+ });
+ await BrowserTestUtils.closeWindow(newWin);
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
new file mode 100644
index 0000000000..45b1b34be1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
@@ -0,0 +1,93 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+SimpleTest.requestCompleteLog();
+
+loadTestSubscript("head_sessions.js");
+
+async function run_test_extension(incognitoOverride) {
+ function background() {
+ browser.test.onMessage.addListener((msg, filter) => {
+ if (msg == "check-sessions") {
+ browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ incognitoOverride,
+ });
+
+ // Open a private browsing window.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await extension.startup();
+
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+ let privateWinId = windowTracker.getId(privateWin);
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ recordInitialTimestamps(recentlyClosed.map(item => item.lastModified));
+
+ // Open and close two tabs in the private window
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "http://example.com"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "http://example.com"
+ );
+ let sessionPromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionPromise;
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let expectedCount =
+ !incognitoOverride || incognitoOverride == "not_allowed" ? 0 : 2;
+ checkRecentlyClosed(
+ recentlyClosed.filter(onlyNewItemsFilter),
+ expectedCount,
+ privateWinId,
+ true
+ );
+
+ // Close the private window.
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ recentlyClosed.filter(onlyNewItemsFilter).length,
+ 0,
+ "the closed private window info was not found in recently closed data"
+ );
+
+ await extension.unload();
+}
+
+add_task(async function test_sessions_get_recently_closed_default() {
+ await run_test_extension();
+});
+
+add_task(async function test_sessions_get_recently_closed_private_incognito() {
+ await run_test_extension("spanning");
+ await run_test_extension("not_allowed");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
new file mode 100644
index 0000000000..4a513f5131
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
@@ -0,0 +1,292 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function expectedTabInfo(tab, window) {
+ let browser = tab.linkedBrowser;
+ return {
+ url: browser.currentURI.spec,
+ title: browser.contentTitle,
+ favIconUrl: window.gBrowser.getIcon(tab) || undefined,
+ // 'selected' is marked as unsupported in schema, so we've removed it.
+ // For more details, see bug 1337509
+ selected: undefined,
+ };
+}
+
+function checkTabInfo(expected, actual) {
+ for (let prop in expected) {
+ is(
+ actual[prop],
+ expected[prop],
+ `Expected value found for ${prop} of tab object.`
+ );
+ }
+}
+
+add_task(async function test_sessions_get_recently_closed_tabs() {
+ // Below, the test makes assumptions about the last accessed time of tabs that are
+ // not true is we execute fast and reduce the timer precision enough
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.reduceTimerPrecision", false],
+ ["browser.navigation.requireUserInteraction", false],
+ ],
+ });
+
+ async function background() {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "check-sessions") {
+ let recentlyClosed = await browser.sessions.getRecentlyClosed();
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tabBrowser = win.gBrowser.selectedBrowser;
+ for (let url of ["about:robots", "about:mozilla", "about:config"]) {
+ BrowserTestUtils.startLoadingURIString(tabBrowser, url);
+ await BrowserTestUtils.browserLoaded(tabBrowser, false, url);
+ }
+
+ // Ensure that getRecentlyClosed returns correct results after the back
+ // button has been used.
+ let goBackPromise = BrowserTestUtils.waitForLocationChange(
+ win.gBrowser,
+ "about:mozilla"
+ );
+ tabBrowser.goBack();
+ await goBackPromise;
+
+ let expectedTabs = [];
+ let tab = win.gBrowser.selectedTab;
+ // Because there is debounce logic in FaviconLoader.sys.mjs to reduce the
+ // favicon loads, we have to wait some time before checking that icon was
+ // stored properly. If that page doesn't have favicon links, let it timeout.
+ try {
+ await BrowserTestUtils.waitForCondition(
+ () => {
+ return gBrowser.getIcon(tab) != null;
+ },
+ "wait for favicon load to finish",
+ 100,
+ 5
+ );
+ } catch (e) {
+ // This page doesn't have any favicon link, just continue.
+ }
+ expectedTabs.push(expectedTabInfo(tab, win));
+ let lastAccessedTimes = new Map();
+ lastAccessedTimes.set("about:mozilla", tab.lastAccessed);
+
+ for (let url of ["about:robots", "about:buildconfig"]) {
+ tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ try {
+ await BrowserTestUtils.waitForCondition(
+ () => {
+ return gBrowser.getIcon(tab) != null;
+ },
+ "wait for favicon load to finish",
+ 100,
+ 5
+ );
+ } catch (e) {
+ // This page doesn't have any favicon link, just continue.
+ }
+ expectedTabs.push(expectedTabInfo(tab, win));
+ lastAccessedTimes.set(url, tab.lastAccessed);
+ }
+
+ await extension.startup();
+
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ // Test with a closed tab.
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let tabInfo = recentlyClosed[0].tab;
+ let expectedTab = expectedTabs.pop();
+ checkTabInfo(expectedTab, tabInfo);
+ Assert.greater(
+ tabInfo.lastAccessed,
+ lastAccessedTimes.get(tabInfo.url),
+ "lastAccessed has been updated"
+ );
+
+ // Test with a closed window containing tabs.
+ await BrowserTestUtils.closeWindow(win);
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let tabInfos = recentlyClosed[0].window.tabs;
+ is(tabInfos.length, 2, "Expected number of tabs in closed window.");
+ for (let x = 0; x < tabInfos.length; x++) {
+ checkTabInfo(expectedTabs[x], tabInfos[x]);
+ Assert.greater(
+ tabInfos[x].lastAccessed,
+ lastAccessedTimes.get(tabInfos[x].url),
+ "lastAccessed has been updated"
+ );
+ }
+
+ await extension.unload();
+
+ // Test without tabs and host permissions.
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ tabInfos = recentlyClosed[0].window.tabs;
+ is(tabInfos.length, 2, "Expected number of tabs in closed window.");
+ for (let tabInfo of tabInfos) {
+ for (let prop in expectedTabs[0]) {
+ is(
+ undefined,
+ tabInfo[prop],
+ `${prop} of tab object is undefined without tabs permission.`
+ );
+ }
+ }
+
+ await extension.unload();
+
+ // Test with host permission.
+ win = await BrowserTestUtils.openNewBrowserWindow();
+ tabBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(
+ tabBrowser,
+ "http://example.com/testpage"
+ );
+ await BrowserTestUtils.browserLoaded(
+ tabBrowser,
+ false,
+ "http://example.com/testpage"
+ );
+ tab = win.gBrowser.getTabForBrowser(tabBrowser);
+ try {
+ await BrowserTestUtils.waitForCondition(
+ () => {
+ return gBrowser.getIcon(tab) != null;
+ },
+ "wait for favicon load to finish",
+ 100,
+ 5
+ );
+ } catch (e) {
+ // This page doesn't have any favicon link, just continue.
+ }
+ expectedTab = expectedTabInfo(tab, win);
+ await BrowserTestUtils.closeWindow(win);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "http://example.com/*"],
+ },
+ background,
+ });
+ await extension.startup();
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ tabInfo = recentlyClosed[0].window.tabs[0];
+ checkTabInfo(expectedTab, tabInfo);
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_sessions_get_recently_closed_for_loading_non_web_controlled_blank_page() {
+ info("Prepare extension that calls browser.sessions.getRecentlyClosed()");
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background: async () => {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "check-sessions") {
+ let recentlyClosed = await browser.sessions.getRecentlyClosed();
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ }
+ });
+ },
+ });
+
+ info(
+ "Open a page having a link for non web controlled page in _blank target"
+ );
+ const testRoot = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+ let url = `${testRoot}file_has_non_web_controlled_blank_page_link.html`;
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ url
+ );
+
+ info("Open the non web controlled page in _blank target");
+ let onNewTabOpened = new Promise(resolve =>
+ win.gBrowser.addTabsProgressListener({
+ onStateChange(browser, webProgress, request, stateFlags, status) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ win.gBrowser.removeTabsProgressListener(this);
+ resolve(win.gBrowser.getTabForBrowser(browser));
+ }
+ },
+ })
+ );
+ let targetUrl = await SpecialPowers.spawn(
+ win.gBrowser.selectedBrowser,
+ [],
+ () => {
+ const target = content.document.querySelector("a");
+ EventUtils.synthesizeMouseAtCenter(target, {}, content);
+ return target.href;
+ }
+ );
+ let tab = await onNewTabOpened;
+
+ info("Remove tab while loading to get getRecentlyClosed()");
+ await extension.startup();
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ info("Check the result of getRecentlyClosed()");
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ checkTabInfo(
+ {
+ index: 1,
+ url: targetUrl,
+ title: targetUrl,
+ favIconUrl: undefined,
+ selected: undefined,
+ },
+ recentlyClosed[0].tab
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_incognito.js b/browser/components/extensions/test/browser/browser_ext_sessions_incognito.js
new file mode 100644
index 0000000000..aecad9e8ec
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_incognito.js
@@ -0,0 +1,113 @@
+"use strict";
+
+add_task(async function test_sessions_tab_value_private() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(
+ SessionStore.getClosedWindowCount(),
+ 0,
+ "No closed window sessions at start of test"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, pbw) => {
+ if (msg == "value") {
+ await browser.test.assertRejects(
+ browser.sessions.setWindowValue(pbw.windowId, "foo", "bar"),
+ /Invalid window ID/,
+ "should not be able to set incognito window session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.getWindowValue(pbw.windowId, "foo"),
+ /Invalid window ID/,
+ "should not be able to get incognito window session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.removeWindowValue(pbw.windowId, "foo"),
+ /Invalid window ID/,
+ "should not be able to remove incognito window session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.setTabValue(pbw.tabId, "foo", "bar"),
+ /Invalid tab ID/,
+ "should not be able to set incognito tab session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.getTabValue(pbw.tabId, "foo"),
+ /Invalid tab ID/,
+ "should not be able to get incognito tab session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.removeTabValue(pbw.tabId, "foo"),
+ /Invalid tab ID/,
+ "should not be able to remove incognito tab session data"
+ );
+ }
+ if (msg == "restore") {
+ await browser.test.assertRejects(
+ browser.sessions.restore(),
+ /Could not restore object/,
+ "should not be able to restore incognito last window session data"
+ );
+ if (pbw) {
+ await browser.test.assertRejects(
+ browser.sessions.restore(pbw.sessionId),
+ /Could not restore object/,
+ `should not be able to restore incognito session ID ${pbw.sessionId} session data`
+ );
+ }
+ }
+ browser.test.sendMessage("done");
+ });
+ },
+ });
+
+ let winData = await getIncognitoWindow("http://mochi.test:8888/");
+ await extension.startup();
+
+ // Test value set/get APIs on a private window and tab.
+ extension.sendMessage("value", winData.details);
+ await extension.awaitMessage("done");
+
+ // Test restoring a private tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ winData.win.gBrowser,
+ "http://example.com"
+ );
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+ let closedTabData = SessionStore.getClosedTabDataForWindow(winData.win);
+
+ extension.sendMessage("restore", {
+ sessionId: String(closedTabData[0].closedId),
+ });
+ await extension.awaitMessage("done");
+
+ // Test restoring a private window.
+ sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(
+ winData.win.gBrowser.selectedTab
+ );
+ await BrowserTestUtils.closeWindow(winData.win);
+ await sessionUpdatePromise;
+
+ is(
+ SessionStore.getClosedWindowCount(),
+ 0,
+ "The closed window was added to Recently Closed Windows"
+ );
+
+ // If the window gets restored, test will fail with an unclosed window.
+ extension.sendMessage("restore");
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_restore.js b/browser/components/extensions/test/browser/browser_ext_sessions_restore.js
new file mode 100644
index 0000000000..39057519ed
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_restore.js
@@ -0,0 +1,234 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+SimpleTest.requestCompleteLog();
+
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+add_task(async function test_sessions_restore() {
+ function background() {
+ let notificationCount = 0;
+ browser.sessions.onChanged.addListener(() => {
+ notificationCount++;
+ browser.test.sendMessage("notificationCount", notificationCount);
+ });
+ browser.test.onMessage.addListener((msg, data) => {
+ if (msg == "check-sessions") {
+ browser.sessions.getRecentlyClosed().then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ } else if (msg == "restore") {
+ browser.sessions.restore(data).then(sessions => {
+ browser.test.sendMessage("restored", sessions);
+ });
+ } else if (msg == "restore-reject") {
+ browser.sessions.restore("not-a-valid-session-id").then(
+ sessions => {
+ browser.test.fail("restore rejected with an invalid sessionId");
+ },
+ error => {
+ browser.test.assertTrue(
+ error.message.includes(
+ "Invalid sessionId: not-a-valid-session-id."
+ )
+ );
+ browser.test.sendMessage("restore-rejected");
+ }
+ );
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ async function assertNotificationCount(expected) {
+ let notificationCount = await extension.awaitMessage("notificationCount");
+ is(
+ notificationCount,
+ expected,
+ "the expected number of notifications was fired"
+ );
+ }
+
+ await extension.startup();
+
+ const {
+ Management: {
+ global: { windowTracker, tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ function checkLocalTab(tab, expectedUrl) {
+ let realTab = tabTracker.getTab(tab.id);
+ let tabState = JSON.parse(SessionStore.getTabState(realTab));
+ is(
+ tabState.entries[0].url,
+ expectedUrl,
+ "restored tab has the expected url"
+ );
+ }
+
+ await extension.awaitMessage("ready");
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.startLoadingURIString(
+ win.gBrowser.selectedBrowser,
+ "about:config"
+ );
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ for (let url of ["about:robots", "about:mozilla"]) {
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ }
+ await BrowserTestUtils.closeWindow(win);
+ await assertNotificationCount(1);
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+
+ // Check that our expected window is the most recently closed.
+ is(
+ recentlyClosed[0].window.tabs.length,
+ 3,
+ "most recently closed window has the expected number of tabs"
+ );
+
+ // Restore the window.
+ extension.sendMessage("restore");
+ await assertNotificationCount(2);
+ let restored = await extension.awaitMessage("restored");
+
+ is(
+ restored.window.tabs.length,
+ 3,
+ "restore returned a window with the expected number of tabs"
+ );
+ checkLocalTab(restored.window.tabs[0], "about:config");
+ checkLocalTab(restored.window.tabs[1], "about:robots");
+ checkLocalTab(restored.window.tabs[2], "about:mozilla");
+
+ // Close the window again.
+ let window = windowTracker.getWindow(restored.window.id);
+ await BrowserTestUtils.closeWindow(window);
+ await assertNotificationCount(3);
+
+ // Restore the window using the sessionId.
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ extension.sendMessage("restore", recentlyClosed[0].window.sessionId);
+ await assertNotificationCount(4);
+ restored = await extension.awaitMessage("restored");
+
+ is(
+ restored.window.tabs.length,
+ 3,
+ "restore returned a window with the expected number of tabs"
+ );
+ checkLocalTab(restored.window.tabs[0], "about:config");
+ checkLocalTab(restored.window.tabs[1], "about:robots");
+ checkLocalTab(restored.window.tabs[2], "about:mozilla");
+
+ // Close the window again.
+ window = windowTracker.getWindow(restored.window.id);
+ await BrowserTestUtils.closeWindow(window);
+ // notificationCount = yield extension.awaitMessage("notificationCount");
+ await assertNotificationCount(5);
+
+ // Open and close a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+ await assertNotificationCount(6);
+
+ // Restore the most recently closed item.
+ extension.sendMessage("restore");
+ await assertNotificationCount(7);
+ restored = await extension.awaitMessage("restored");
+
+ tab = restored.tab;
+ ok(tab, "restore returned a tab");
+ checkLocalTab(tab, "about:robots");
+
+ // Close the tab again.
+ let realTab = tabTracker.getTab(tab.id);
+ BrowserTestUtils.removeTab(realTab);
+ await assertNotificationCount(8);
+
+ // Restore the tab using the sessionId.
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ extension.sendMessage("restore", recentlyClosed[0].tab.sessionId);
+ await assertNotificationCount(9);
+ restored = await extension.awaitMessage("restored");
+
+ tab = restored.tab;
+ ok(tab, "restore returned a tab");
+ checkLocalTab(tab, "about:robots");
+
+ // Close the tab again.
+ realTab = tabTracker.getTab(tab.id);
+ BrowserTestUtils.removeTab(realTab);
+ await assertNotificationCount(10);
+
+ // Try to restore something with an invalid sessionId.
+ extension.sendMessage("restore-reject");
+ restored = await extension.awaitMessage("restore-rejected");
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_event_page() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@sessions" } },
+ permissions: ["sessions", "tabs"],
+ background: { persistent: false },
+ },
+ background() {
+ browser.sessions.onChanged.addListener(() => {
+ browser.test.sendMessage("changed");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // test events waken background
+ await extension.terminateBackground();
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.startLoadingURIString(
+ win.gBrowser.selectedBrowser,
+ "about:config"
+ );
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ for (let url of ["about:robots", "about:mozilla"]) {
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ }
+ await BrowserTestUtils.closeWindow(win);
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("changed");
+ ok(true, "persistent event woke background");
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js b/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js
new file mode 100644
index 0000000000..679e1fbd6c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js
@@ -0,0 +1,137 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+
+/**
+ This test checks that after closing an extension made tab it restores correctly.
+ The tab is given an expanded triggering principal and we didn't use to serialize
+ these correctly into session history.
+ */
+
+// Check that we can restore a tab modified by an extension.
+add_task(async function test_restoringModifiedTab() {
+ function background() {
+ browser.tabs.create({ url: "http://example.com/" });
+ browser.test.onMessage.addListener((msg, filter) => {
+ if (msg == "change-tab") {
+ browser.tabs.executeScript({ code: 'location.href += "?changedTab";' });
+ }
+ });
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", ""],
+ },
+ browser_action: {
+ default_title: "Navigate current tab via content script",
+ },
+ background,
+ });
+
+ const contentScriptTabURL = "http://example.com/?changedTab";
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({});
+
+ // Open and close a tabs.
+ let tabPromise = BrowserTestUtils.waitForNewTab(
+ win.gBrowser,
+ "http://example.com/",
+ true
+ );
+ await extension.startup();
+ let firstTab = await tabPromise;
+ let locationChange = BrowserTestUtils.waitForLocationChange(
+ win.gBrowser,
+ contentScriptTabURL
+ );
+ extension.sendMessage("change-tab");
+ await locationChange;
+ is(
+ firstTab.linkedBrowser.currentURI.spec,
+ contentScriptTabURL,
+ "Got expected URL"
+ );
+
+ let sessionPromise = BrowserTestUtils.waitForSessionStoreUpdate(firstTab);
+ BrowserTestUtils.removeTab(firstTab);
+ await sessionPromise;
+
+ tabPromise = BrowserTestUtils.waitForNewTab(
+ win.gBrowser,
+ contentScriptTabURL,
+ true
+ );
+ SessionStore.undoCloseTab(win, 0);
+ let restoredTab = await tabPromise;
+ ok(restoredTab, "We returned a tab here");
+ is(
+ restoredTab.linkedBrowser.currentURI.spec,
+ contentScriptTabURL,
+ "Got expected URL"
+ );
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(restoredTab);
+
+ // Close the window.
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_restoringClosedTabWithTooLargeIndex() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, filter) => {
+ if (msg != "restoreTab") {
+ return;
+ }
+ const recentlyClosed = await browser.sessions.getRecentlyClosed({
+ maxResults: 2,
+ });
+ let tabWithTooLargeIndex;
+ for (const info of recentlyClosed) {
+ if (info.tab && info.tab.index > 1) {
+ tabWithTooLargeIndex = info.tab;
+ break;
+ }
+ }
+ const onRestored = tab => {
+ browser.tabs.onCreated.removeListener(onRestored);
+ browser.test.sendMessage("restoredTab", tab);
+ };
+ browser.tabs.onCreated.addListener(onRestored);
+ browser.sessions.restore(tabWithTooLargeIndex.sessionId);
+ });
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "sessions"],
+ },
+ background,
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({});
+ const tabs = await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank?0"),
+ BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank?1"),
+ ]);
+ const promsiedSessionStored = Promise.all([
+ BrowserTestUtils.waitForSessionStoreUpdate(tabs[0]),
+ BrowserTestUtils.waitForSessionStoreUpdate(tabs[1]),
+ ]);
+ // Close the rightmost tab at first
+ BrowserTestUtils.removeTab(tabs[1]);
+ BrowserTestUtils.removeTab(tabs[0]);
+ await promsiedSessionStored;
+
+ await extension.startup();
+ const promisedRestoredTab = extension.awaitMessage("restoredTab");
+ extension.sendMessage("restoreTab");
+ const restoredTab = await promisedRestoredTab;
+ is(restoredTab.index, 1, "Got valid index");
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_restore_private.js b/browser/components/extensions/test/browser/browser_ext_sessions_restore_private.js
new file mode 100644
index 0000000000..2eda77be45
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_restore_private.js
@@ -0,0 +1,236 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+SimpleTest.requestCompleteLog();
+loadTestSubscript("head_sessions.js");
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+async function openAndCloseTab(window, url) {
+ let tab = BrowserTestUtils.addTab(window.gBrowser, url);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, true, url);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+}
+
+async function run_test_extension(incognitoOverride, testData) {
+ const initialURL = gBrowser.selectedBrowser.currentURI.spec;
+ // We'll be closing tabs from a private window and a non-private window and attempting
+ // to call session.restore() at each step. The goal is to compare the actual and
+ // expected outcome when the extension is/isn't configured for incognito window use.
+
+ function background() {
+ browser.test.onMessage.addListener(async (msg, sessionId) => {
+ let result;
+ try {
+ result = await browser.sessions.restore(sessionId);
+ } catch (e) {
+ result = { error: e.message };
+ }
+ browser.test.sendMessage("result", result);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ incognitoOverride,
+ });
+ await extension.startup();
+
+ // Open a private browsing window and with a non-empty tab
+ // (so we dont end up closing the window when the close the other tab)
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ testData.private.initialTabURL
+ );
+
+ // open and close a tab in the non-private window
+ await openAndCloseTab(window, testData.notPrivate.tabToClose);
+
+ let { closedId: nonPrivateClosedTabId, pos: nonPrivateIndex } =
+ SessionStore.getClosedTabDataForWindow(window)[0];
+ if (!testData.notPrivate.expected.error) {
+ testData.notPrivate.expected.index = nonPrivateIndex;
+ }
+
+ // open and close a tab in the private window
+ info(
+ "open & close a tab in the private window with URL: " +
+ testData.private.tabToClose
+ );
+ await openAndCloseTab(privateWin, testData.private.tabToClose);
+ let { pos: privateIndex } =
+ SessionStore.getClosedTabDataForWindow(privateWin)[0];
+ if (!testData.private.expected.error) {
+ testData.private.expected.index = privateIndex;
+ }
+
+ // focus the non-private window to ensure the outcome isn't just a side-effect of the
+ // private window being the top window
+ await SimpleTest.promiseFocus(window);
+
+ // Try to restore the last-closed tab - which was private.
+ // If incognito access is allowed, we should successfully restore it to the private window.
+ // We pass no closedId so it should just try to restore the last closed tab
+ info("Sending 'restore' to attempt restore the closed private tab");
+ extension.sendMessage("restore");
+ let sessionStoreChanged = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ let extResult = await extension.awaitMessage("result");
+ let result = {};
+ if (extResult.tab) {
+ await sessionStoreChanged;
+ // session.restore() was returning "about:blank" as the tab.url,
+ // we'll wait to ensure the correct URL eventually loads in the restored tab
+ await BrowserTestUtils.browserLoaded(
+ privateWin.gBrowser.selectedTab.linkedBrowser,
+ true,
+ testData.private.tabToClose
+ );
+ // only keep the properties we want to compare
+ for (let pname of Object.keys(testData.private.expected)) {
+ result[pname] = extResult.tab[pname];
+ }
+ result.url = privateWin.gBrowser.selectedTab.linkedBrowser.currentURI.spec;
+ } else {
+ // Trim off the sessionId value so we can easily equality-match on the result
+ result.error = extResult.error.replace(/sessionId\s+\d+/, "sessionId");
+ }
+ Assert.deepEqual(
+ result,
+ testData.private.expected,
+ "Restoring the private tab didn't match expected result"
+ );
+
+ await SimpleTest.promiseFocus(privateWin);
+
+ // Try to restore the last-closed tab in the non-private window
+ info("Sending 'restore' to restore the non-private tab");
+ extension.sendMessage("restore", String(nonPrivateClosedTabId));
+ sessionStoreChanged = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ extResult = await extension.awaitMessage("result");
+ result = {};
+
+ if (extResult.tab) {
+ await sessionStoreChanged;
+ await BrowserTestUtils.browserLoaded(
+ window.gBrowser.selectedTab.linkedBrowser,
+ true,
+ testData.notPrivate.tabToClose
+ );
+ // only keep the properties we want to compare
+ for (let pname of Object.keys(testData.notPrivate.expected)) {
+ result[pname] = extResult.tab[pname];
+ }
+ result.url = window.gBrowser.selectedTab.linkedBrowser.currentURI.spec;
+ } else {
+ // Trim off the sessionId value so we can easily equality-match on the result
+ result.error = extResult.error.replace(/sessionId\s+\d+/, "sessionId");
+ }
+ Assert.deepEqual(
+ result,
+ testData.notPrivate.expected,
+ "Restoring the non-private tab didn't match expected result"
+ );
+
+ // Close the private window and cleanup
+ await BrowserTestUtils.closeWindow(privateWin);
+ for (let tab of gBrowser.tabs.filter(
+ tab => !tab.hidden && tab.linkedBrowser.currentURI.spec !== initialURL
+ )) {
+ await BrowserTestUtils.removeTab(tab);
+ }
+ await extension.unload();
+}
+
+const spanningTestData = {
+ private: {
+ initialTabURL: "https://example.com/",
+ tabToClose: "https://example.org/?private",
+ // restore should succeed when incognito is allowed
+ expected: {
+ url: "https://example.org/?private",
+ incognito: true,
+ },
+ },
+ notPrivate: {
+ initialTabURL: "https://example.com/",
+ tabToClose: "https://example.org/?notprivate",
+ expected: {
+ url: "https://example.org/?notprivate",
+ incognito: false,
+ },
+ },
+};
+
+add_task(
+ async function test_sessions_get_recently_closed_private_incognito_spanning() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.closedTabsFromAllWindows", true]],
+ });
+ await run_test_extension("spanning", spanningTestData);
+ SpecialPowers.popPrefEnv();
+ }
+);
+add_task(
+ async function test_sessions_get_recently_closed_private_incognito_spanning_pref_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.closedTabsFromAllWindows", false]],
+ });
+ await run_test_extension("spanning", spanningTestData);
+ SpecialPowers.popPrefEnv();
+ }
+);
+
+const notAllowedTestData = {
+ private: {
+ initialTabURL: "https://example.com/",
+ tabToClose: "https://example.org/?private",
+ // this is expected to fail when incognito is not_allowed
+ expected: {
+ error: "Could not restore object using sessionId.",
+ },
+ },
+ notPrivate: {
+ // we'll open tabs for each URL
+ initialTabURL: "https://example.com/",
+ tabToClose: "https://example.org/?notprivate",
+ expected: {
+ url: "https://example.org/?notprivate",
+ incognito: false,
+ },
+ },
+};
+
+add_task(
+ async function test_sessions_get_recently_closed_private_incognito_not_allowed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.closedTabsFromAllWindows", true]],
+ });
+ await run_test_extension("not_allowed", notAllowedTestData);
+ SpecialPowers.popPrefEnv();
+ }
+);
+
+add_task(
+ async function test_sessions_get_recently_closed_private_incognito_not_allowed_pref_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.closedTabsFromAllWindows", false]],
+ });
+ await run_test_extension("not_allowed", notAllowedTestData);
+ SpecialPowers.popPrefEnv();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_window_tab_value.js b/browser/components/extensions/test/browser/browser_ext_sessions_window_tab_value.js
new file mode 100644
index 0000000000..b21b59fe8c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_window_tab_value.js
@@ -0,0 +1,398 @@
+"use strict";
+
+add_task(async function test_sessions_tab_value() {
+ info("Testing set/get/deleteTabValue.");
+
+ async function background() {
+ let tests = [
+ { key: "tabkey1", value: "Tab Value" },
+ { key: "tabkey2", value: 25 },
+ { key: "tabkey3", value: { val: "Tab Value" } },
+ {
+ key: "tabkey4",
+ value: function () {
+ return null;
+ },
+ },
+ ];
+
+ async function test(params) {
+ let { key, value } = params;
+ let tabs = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ let currentTabId = tabs[0].id;
+
+ browser.sessions.setTabValue(currentTabId, key, value);
+
+ let testValue1 = await browser.sessions.getTabValue(currentTabId, key);
+ let valueType = typeof value;
+
+ browser.test.log(
+ `Test that setting, getting and deleting tab value behaves properly when value is type "${valueType}"`
+ );
+
+ if (valueType == "string") {
+ browser.test.assertEq(
+ value,
+ testValue1,
+ `Value for key '${key}' should be '${value}'.`
+ );
+ browser.test.assertEq(
+ "string",
+ typeof testValue1,
+ "typeof value should be '${valueType}'."
+ );
+ } else if (valueType == "number") {
+ browser.test.assertEq(
+ value,
+ testValue1,
+ `Value for key '${key}' should be '${value}'.`
+ );
+ browser.test.assertEq(
+ "number",
+ typeof testValue1,
+ "typeof value should be '${valueType}'."
+ );
+ } else if (valueType == "object") {
+ let innerVal1 = value.val;
+ let innerVal2 = testValue1.val;
+ browser.test.assertEq(
+ innerVal1,
+ innerVal2,
+ `Value for key '${key}' should be '${innerVal1}'.`
+ );
+ } else if (valueType == "function") {
+ browser.test.assertEq(
+ null,
+ testValue1,
+ `Value for key '${key}' is non-JSON-able and should be 'null'.`
+ );
+ }
+
+ // Remove the tab key/value.
+ browser.sessions.removeTabValue(currentTabId, key);
+
+ // This should now return undefined.
+ testValue1 = await browser.sessions.getTabValue(currentTabId, key);
+ browser.test.assertEq(
+ undefined,
+ testValue1,
+ `Key has been deleted and value for key "${key}" should be 'undefined'.`
+ );
+ }
+
+ for (let params of tests) {
+ await test(params);
+ }
+
+ // Attempt to remove a non-existent key, should not throw error.
+ let tabs = await browser.tabs.query({ currentWindow: true, active: true });
+ await browser.sessions.removeTabValue(tabs[0].id, "non-existent-key");
+ browser.test.succeed(
+ "Attempting to remove a non-existent key should not fail."
+ );
+
+ browser.test.sendMessage("testComplete");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("testComplete");
+ ok(true, "Testing completed for set/get/deleteTabValue.");
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_tab_value_persistence() {
+ info("Testing for persistence of set tab values.");
+
+ async function background() {
+ let key = "tabkey1";
+ let value1 = "Tab Value 1a";
+ let value2 = "Tab Value 1b";
+
+ browser.test.log(
+ "Test that two different tabs hold different values for a given key."
+ );
+
+ await browser.tabs.create({ url: "http://example.com" });
+
+ // Wait until the newly created tab has completed loading or it will still have
+ // about:blank url when it gets removed and will not appear in the removed tabs history.
+ browser.webNavigation.onCompleted.addListener(
+ async function newTabListener(details) {
+ browser.webNavigation.onCompleted.removeListener(newTabListener);
+
+ let tabs = await browser.tabs.query({ currentWindow: true });
+
+ let tabId_1 = tabs[0].id;
+ let tabId_2 = tabs[1].id;
+
+ browser.sessions.setTabValue(tabId_1, key, value1);
+ browser.sessions.setTabValue(tabId_2, key, value2);
+
+ let testValue1 = await browser.sessions.getTabValue(tabId_1, key);
+ let testValue2 = await browser.sessions.getTabValue(tabId_2, key);
+
+ browser.test.assertEq(
+ value1,
+ testValue1,
+ `Value for key '${key}' should be '${value1}'.`
+ );
+ browser.test.assertEq(
+ value2,
+ testValue2,
+ `Value for key '${key}' should be '${value2}'.`
+ );
+
+ browser.test.log(
+ "Test that value is copied to duplicated tab for a given key."
+ );
+
+ let duptab = await browser.tabs.duplicate(tabId_2);
+ let tabId_3 = duptab.id;
+
+ let testValue3 = await browser.sessions.getTabValue(tabId_3, key);
+
+ browser.test.assertEq(
+ value2,
+ testValue3,
+ `Value for key '${key}' should be '${value2}'.`
+ );
+
+ browser.test.log(
+ "Test that restored tab still holds the value for a given key."
+ );
+
+ await browser.tabs.remove([tabId_3]);
+
+ let sessions = await browser.sessions.getRecentlyClosed({
+ maxResults: 1,
+ });
+
+ let sessionData = await browser.sessions.restore(
+ sessions[0].tab.sessionId
+ );
+ let restoredId = sessionData.tab.id;
+
+ let testValue = await browser.sessions.getTabValue(restoredId, key);
+
+ browser.test.assertEq(
+ value2,
+ testValue,
+ `Value for key '${key}' should be '${value2}'.`
+ );
+
+ await browser.tabs.remove(tabId_2);
+ await browser.tabs.remove(restoredId);
+
+ browser.test.sendMessage("testComplete");
+ },
+ { url: [{ hostContains: "example.com" }] }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions", "tabs", "webNavigation"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("testComplete");
+ ok(true, "Testing completed for persistance of set tab values.");
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_window_value() {
+ info("Testing set/get/deleteWindowValue.");
+
+ async function background() {
+ let tests = [
+ { key: "winkey1", value: "Window Value" },
+ { key: "winkey2", value: 25 },
+ { key: "winkey3", value: { val: "Window Value" } },
+ {
+ key: "winkey4",
+ value: function () {
+ return null;
+ },
+ },
+ ];
+
+ async function test(params) {
+ let { key, value } = params;
+ let win = await browser.windows.getCurrent();
+ let currentWinId = win.id;
+
+ browser.sessions.setWindowValue(currentWinId, key, value);
+
+ let testValue1 = await browser.sessions.getWindowValue(currentWinId, key);
+ let valueType = typeof value;
+
+ browser.test.log(
+ `Test that setting, getting and deleting window value behaves properly when value is type "${valueType}"`
+ );
+
+ if (valueType == "string") {
+ browser.test.assertEq(
+ value,
+ testValue1,
+ `Value for key '${key}' should be '${value}'.`
+ );
+ browser.test.assertEq(
+ "string",
+ typeof testValue1,
+ "typeof value should be '${valueType}'."
+ );
+ } else if (valueType == "number") {
+ browser.test.assertEq(
+ value,
+ testValue1,
+ `Value for key '${key}' should be '${value}'.`
+ );
+ browser.test.assertEq(
+ "number",
+ typeof testValue1,
+ "typeof value should be '${valueType}'."
+ );
+ } else if (valueType == "object") {
+ let innerVal1 = value.val;
+ let innerVal2 = testValue1.val;
+ browser.test.assertEq(
+ innerVal1,
+ innerVal2,
+ `Value for key '${key}' should be '${innerVal1}'.`
+ );
+ } else if (valueType == "function") {
+ browser.test.assertEq(
+ null,
+ testValue1,
+ `Value for key '${key}' is non-JSON-able and should be 'null'.`
+ );
+ }
+
+ // Remove the window key/value.
+ browser.sessions.removeWindowValue(currentWinId, key);
+
+ // This should return undefined as the key no longer exists.
+ testValue1 = await browser.sessions.getWindowValue(currentWinId, key);
+ browser.test.assertEq(
+ undefined,
+ testValue1,
+ `Key has been deleted and value for key '${key}' should be 'undefined'.`
+ );
+ }
+
+ for (let params of tests) {
+ await test(params);
+ }
+
+ // Attempt to remove a non-existent key, should not throw error.
+ let win = await browser.windows.getCurrent();
+ await browser.sessions.removeWindowValue(win.id, "non-existent-key");
+ browser.test.succeed(
+ "Attempting to remove a non-existent key should not fail."
+ );
+
+ browser.test.sendMessage("testComplete");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("testComplete");
+ ok(true, "Testing completed for set/get/deleteWindowValue.");
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_window_value_persistence() {
+ info(
+ "Testing that different values for the same key in different windows are persisted properly."
+ );
+
+ async function background() {
+ let key = "winkey1";
+ let value1 = "Window Value 1a";
+ let value2 = "Window Value 1b";
+
+ let window1 = await browser.windows.getCurrent();
+ let window2 = await browser.windows.create({});
+
+ let window1Id = window1.id;
+ let window2Id = window2.id;
+
+ browser.sessions.setWindowValue(window1Id, key, value1);
+ browser.sessions.setWindowValue(window2Id, key, value2);
+
+ let testValue1 = await browser.sessions.getWindowValue(window1Id, key);
+ let testValue2 = await browser.sessions.getWindowValue(window2Id, key);
+
+ browser.test.assertEq(
+ value1,
+ testValue1,
+ `Value for key '${key}' should be '${value1}'.`
+ );
+ browser.test.assertEq(
+ value2,
+ testValue2,
+ `Value for key '${key}' should be '${value2}'.`
+ );
+
+ await browser.windows.remove(window2Id);
+ browser.test.sendMessage("testComplete");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("testComplete");
+ ok(true, "Testing completed for persistance of set window values.");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js b/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js
new file mode 100644
index 0000000000..74eaa6e634
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js
@@ -0,0 +1,881 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+const EXTENSION1_ID = "extension1@mozilla.com";
+const EXTENSION2_ID = "extension2@mozilla.com";
+const DEFAULT_SEARCH_STORE_TYPE = "default_search";
+const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";
+
+AddonTestUtils.initMochitest(this);
+SearchTestUtils.init(this);
+
+const DEFAULT_ENGINE = {
+ id: "basic",
+ name: "basic",
+ loadPath: "[addon]basic@search.mozilla.org",
+ submissionUrl:
+ "https://mochi.test:8888/browser/browser/components/search/test/browser/?search=&foo=1",
+};
+const ALTERNATE_ENGINE = {
+ id: "simple",
+ name: "Simple Engine",
+ loadPath: "[addon]simple@search.mozilla.org",
+ submissionUrl: "https://example.com/?sourceId=Mozilla-search&search=",
+};
+const ALTERNATE2_ENGINE = {
+ id: "simple",
+ name: "another",
+ loadPath: "",
+ submissionUrl: "",
+};
+
+async function restoreDefaultEngine() {
+ let engine = Services.search.getEngineByName(DEFAULT_ENGINE.name);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+}
+
+function clearTelemetry() {
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+}
+
+async function checkTelemetry(source, prevEngine, newEngine) {
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "change_default",
+ value: source,
+ extra: {
+ prev_id: prevEngine.id,
+ new_id: newEngine.id,
+ new_name: newEngine.name,
+ new_load_path: newEngine.loadPath,
+ // Telemetry has a limit of 80 characters.
+ new_sub_url: newEngine.submissionUrl.slice(0, 80),
+ },
+ },
+ ],
+ { category: "search", method: "engine" }
+ );
+
+ let snapshot = await Glean.searchEngineDefault.changed.testGetValue();
+ delete snapshot[0].timestamp;
+ Assert.deepEqual(
+ snapshot[0],
+ {
+ category: "search.engine.default",
+ name: "changed",
+ extra: {
+ change_source: source,
+ previous_engine_id: prevEngine.id,
+ new_engine_id: newEngine.id,
+ new_display_name: newEngine.name,
+ new_load_path: newEngine.loadPath,
+ new_submission_url: newEngine.submissionUrl,
+ },
+ },
+ "Should have received the correct event details"
+ );
+}
+
+add_setup(async function () {
+ let searchExtensions = getChromeDir(getResolvedURI(gTestPath));
+ searchExtensions.append("search-engines");
+
+ await SearchTestUtils.useMochitestEngines(searchExtensions);
+
+ SearchTestUtils.useMockIdleService();
+ let response = await fetch(`resource://search-extensions/engines.json`);
+ let json = await response.json();
+ await SearchTestUtils.updateRemoteSettingsConfig(json.data);
+
+ registerCleanupFunction(async () => {
+ let settingsWritten = SearchTestUtils.promiseSearchNotification(
+ "write-settings-to-disk-complete"
+ );
+ await SearchTestUtils.updateRemoteSettingsConfig();
+ await settingsWritten;
+ });
+});
+
+/* This tests setting a default engine. */
+add_task(async function test_extension_setting_default_engine() {
+ clearTelemetry();
+
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await checkTelemetry("addon-install", DEFAULT_ENGINE, ALTERNATE_ENGINE);
+
+ clearTelemetry();
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+
+ await checkTelemetry("addon-uninstall", ALTERNATE_ENGINE, DEFAULT_ENGINE);
+});
+
+/* This tests what happens when the engine you're setting it to is hidden. */
+add_task(async function test_extension_setting_default_engine_hidden() {
+ let engine = Services.search.getEngineByName(ALTERNATE_ENGINE.name);
+ engine.hidden = true;
+
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ "Default engine should have remained as the default"
+ );
+ is(
+ ExtensionSettingsStore.getSetting("default_search", "defaultSearch"),
+ null,
+ "The extension should not have been recorded as having set the default search"
+ );
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+ engine.hidden = false;
+});
+
+// Test the popup displayed when trying to add a non-built-in default
+// search engine.
+add_task(async function test_extension_setting_default_engine_external() {
+ const NAME = "Example Engine";
+
+ // Load an extension that tries to set the default engine,
+ // and wait for the ensuing prompt.
+ async function startExtension(win = window) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ icons: {
+ 48: "icon.png",
+ 96: "icon@2x.png",
+ },
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: NAME,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ files: {
+ "icon.png": "",
+ "icon@2x.png": "",
+ },
+ useAddonManager: "temporary",
+ });
+
+ let [panel] = await Promise.all([
+ promisePopupNotificationShown("addon-webext-defaultsearch", win),
+ extension.startup(),
+ ]);
+
+ isnot(
+ panel,
+ null,
+ "Doorhanger was displayed for non-built-in default engine"
+ );
+
+ return { panel, extension };
+ }
+
+ // First time around, don't accept the default engine.
+ let { panel, extension } = await startExtension();
+ ok(
+ panel.getAttribute("icon").endsWith("/icon.png"),
+ "expected custom icon set on the notification"
+ );
+
+ panel.secondaryButton.click();
+
+ await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ "Default engine was not changed after rejecting prompt"
+ );
+
+ await extension.unload();
+
+ clearTelemetry();
+
+ // Do it again, this time accept the prompt.
+ ({ panel, extension } = await startExtension());
+
+ panel.button.click();
+ await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");
+
+ is(
+ (await Services.search.getDefault()).name,
+ NAME,
+ "Default engine was changed after accepting prompt"
+ );
+
+ await checkTelemetry("addon-install", DEFAULT_ENGINE, {
+ id: "other-Example Engine",
+ name: "Example Engine",
+ loadPath: "[addon]extension1@mozilla.com",
+ submissionUrl: "https://example.com/?q=",
+ });
+ clearTelemetry();
+
+ // Do this twice to make sure we're definitely handling disable/enable
+ // correctly. Disabling and enabling the addon here like this also
+ // replicates the behavior when an addon is added then removed in the
+ // blocklist.
+ let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
+ let addon = await AddonManager.getAddonByID(EXTENSION1_ID);
+ await addon.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name} after disabling`
+ );
+
+ await checkTelemetry(
+ "addon-uninstall",
+ {
+ id: "other-Example Engine",
+ name: "Example Engine",
+ loadPath: "[addon]extension1@mozilla.com",
+ submissionUrl: "https://example.com/?q=",
+ },
+ DEFAULT_ENGINE
+ );
+ clearTelemetry();
+
+ let opened = promisePopupNotificationShown(
+ "addon-webext-defaultsearch",
+ window
+ );
+ await addon.enable();
+ panel = await opened;
+ panel.button.click();
+ await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");
+
+ is(
+ (await Services.search.getDefault()).name,
+ NAME,
+ `Default engine is ${NAME} after enabling`
+ );
+
+ await checkTelemetry("addon-install", DEFAULT_ENGINE, {
+ id: "other-Example Engine",
+ name: "Example Engine",
+ loadPath: "[addon]extension1@mozilla.com",
+ submissionUrl: "https://example.com/?q=",
+ });
+
+ await extension.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ "Default engine is reverted after uninstalling extension."
+ );
+
+ // One more time, this time close the window where the prompt
+ // appears instead of explicitly accepting or denying it.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank");
+
+ ({ extension } = await startExtension(win));
+
+ await BrowserTestUtils.closeWindow(win);
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ "Default engine is unchanged when prompt is dismissed"
+ );
+
+ await extension.unload();
+});
+
+/* This tests that uninstalling add-ons maintains the proper
+ * search default. */
+add_task(async function test_extension_setting_multiple_default_engine() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ await ext2.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+});
+
+/* This tests that uninstalling add-ons in reverse order maintains the proper
+ * search default. */
+add_task(
+ async function test_extension_setting_multiple_default_engine_reversed() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ await ext2.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+ }
+);
+
+/* This tests that when the user changes the search engine and the add-on
+ * is unistalled, search stays with the user's choice. */
+add_task(async function test_user_changing_default_engine() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ let engine = Services.search.getEngineByName(ALTERNATE2_ENGINE.name);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ // This simulates the preferences UI when the setting is changed.
+ ExtensionSettingsStore.select(
+ ExtensionSettingsStore.SETTING_USER_SET,
+ DEFAULT_SEARCH_STORE_TYPE,
+ DEFAULT_SEARCH_SETTING_NAME
+ );
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ restoreDefaultEngine();
+});
+
+/* This tests that when the user changes the search engine while it is
+ * disabled, user choice is maintained when the add-on is reenabled. */
+add_task(async function test_user_change_with_disabling() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ let engine = Services.search.getEngineByName(ALTERNATE2_ENGINE.name);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ // This simulates the preferences UI when the setting is changed.
+ ExtensionSettingsStore.select(
+ ExtensionSettingsStore.SETTING_USER_SET,
+ DEFAULT_SEARCH_STORE_TYPE,
+ DEFAULT_SEARCH_SETTING_NAME
+ );
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
+ let addon = await AddonManager.getAddonByID(EXTENSION1_ID);
+ await addon.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let processedPromise = awaitEvent("searchEngineProcessed", EXTENSION1_ID);
+ await addon.enable();
+ await processedPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ await ext1.unload();
+ await restoreDefaultEngine();
+});
+
+/* This tests that when two add-ons are installed that change default
+ * search and the first one is disabled, before the second one is installed,
+ * when the first one is reenabled, the second add-on keeps the search. */
+add_task(async function test_two_addons_with_first_disabled_before_second() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION2_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
+ let addon1 = await AddonManager.getAddonByID(EXTENSION1_ID);
+ await addon1.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let enabledPromise = awaitEvent("ready", EXTENSION1_ID);
+ await addon1.enable();
+ await enabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ await ext2.unload();
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+});
+
+/* This tests that when two add-ons are installed that change default
+ * search and the first one is disabled, the second one maintains
+ * the search. */
+add_task(async function test_two_addons_with_first_disabled() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION2_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
+ let addon1 = await AddonManager.getAddonByID(EXTENSION1_ID);
+ await addon1.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let enabledPromise = awaitEvent("ready", EXTENSION1_ID);
+ await addon1.enable();
+ await enabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ await ext2.unload();
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+});
+
+/* This tests that when two add-ons are installed that change default
+ * search and the second one is disabled, the first one properly
+ * gets the search. */
+add_task(async function test_two_addons_with_second_disabled() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION2_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let disabledPromise = awaitEvent("shutdown", EXTENSION2_ID);
+ let addon2 = await AddonManager.getAddonByID(EXTENSION2_ID);
+ await addon2.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ let defaultPromise = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ // No prompt, because this is switching to an app-provided engine.
+ await addon2.enable();
+ await defaultPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ await ext2.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
new file mode 100644
index 0000000000..afc1e4f9b9
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
@@ -0,0 +1,268 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+let extData = {
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+
+ background: function () {
+ browser.test.onMessage.addListener(async ({ msg, data }) => {
+ if (msg === "set-panel") {
+ await browser.sidebarAction.setPanel({ panel: null });
+ browser.test.assertEq(
+ await browser.sidebarAction.getPanel({}),
+ browser.runtime.getURL("sidebar.html"),
+ "Global panel can be reverted to the default."
+ );
+ } else if (msg === "isOpen") {
+ let { arg = {}, result } = data;
+ let isOpen = await browser.sidebarAction.isOpen(arg);
+ browser.test.assertEq(result, isOpen, "expected value from isOpen");
+ }
+ browser.test.sendMessage("done");
+ });
+ },
+};
+
+function getExtData(manifestUpdates = {}) {
+ return {
+ ...extData,
+ manifest: {
+ ...extData.manifest,
+ ...manifestUpdates,
+ },
+ };
+}
+
+async function sendMessage(ext, msg, data = undefined) {
+ ext.sendMessage({ msg, data });
+ await ext.awaitMessage("done");
+}
+
+add_task(async function sidebar_initial_install() {
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ "sidebar box is not visible"
+ );
+ let extension = ExtensionTestUtils.loadExtension(getExtData());
+ await extension.startup();
+ await extension.awaitMessage("sidebar");
+
+ // Test sidebar is opened on install
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+
+ await extension.unload();
+ // Test that the sidebar was closed on unload.
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ "sidebar box is not visible"
+ );
+});
+
+add_task(async function sidebar__install_closed() {
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ "sidebar box is not visible"
+ );
+ let tempExtData = getExtData();
+ tempExtData.manifest.sidebar_action.open_at_install = false;
+ let extension = ExtensionTestUtils.loadExtension(tempExtData);
+ await extension.startup();
+
+ // Test sidebar is closed on install
+ ok(document.getElementById("sidebar-box").hidden, "sidebar box is hidden");
+
+ await extension.unload();
+ // This is the default value
+ tempExtData.manifest.sidebar_action.open_at_install = true;
+});
+
+add_task(async function sidebar_two_sidebar_addons() {
+ let extension2 = ExtensionTestUtils.loadExtension(getExtData());
+ await extension2.startup();
+ // Test sidebar is opened on install
+ await extension2.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+
+ // Test second sidebar install opens new sidebar
+ let extension3 = ExtensionTestUtils.loadExtension(getExtData());
+ await extension3.startup();
+ // Test sidebar is opened on install
+ await extension3.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+ await extension3.unload();
+
+ // We just close the sidebar on uninstall of the current sidebar.
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ "sidebar box is not visible"
+ );
+
+ await extension2.unload();
+});
+
+add_task(async function sidebar_empty_panel() {
+ let extension = ExtensionTestUtils.loadExtension(getExtData());
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+ ok(
+ !document.getElementById("sidebar-box").hidden,
+ "sidebar box is visible in first window"
+ );
+ await sendMessage(extension, "set-panel");
+ await extension.unload();
+});
+
+add_task(async function sidebar_isOpen() {
+ info("Load extension1");
+ let extension1 = ExtensionTestUtils.loadExtension(getExtData());
+ await extension1.startup();
+
+ info("Test extension1's sidebar is opened on install");
+ await extension1.awaitMessage("sidebar");
+ await sendMessage(extension1, "isOpen", { result: true });
+ let sidebar1ID = SidebarUI.currentID;
+
+ info("Load extension2");
+ let extension2 = ExtensionTestUtils.loadExtension(getExtData());
+ await extension2.startup();
+
+ info("Test extension2's sidebar is opened on install");
+ await extension2.awaitMessage("sidebar");
+ await sendMessage(extension1, "isOpen", { result: false });
+ await sendMessage(extension2, "isOpen", { result: true });
+
+ info("Switch back to extension1's sidebar");
+ SidebarUI.show(sidebar1ID);
+ await extension1.awaitMessage("sidebar");
+ await sendMessage(extension1, "isOpen", { result: true });
+ await sendMessage(extension2, "isOpen", { result: false });
+
+ info("Test passing a windowId parameter");
+ let windowId = window.docShell.outerWindowID;
+ let WINDOW_ID_CURRENT = -2;
+ await sendMessage(extension1, "isOpen", { arg: { windowId }, result: true });
+ await sendMessage(extension2, "isOpen", { arg: { windowId }, result: false });
+ await sendMessage(extension1, "isOpen", {
+ arg: { windowId: WINDOW_ID_CURRENT },
+ result: true,
+ });
+ await sendMessage(extension2, "isOpen", {
+ arg: { windowId: WINDOW_ID_CURRENT },
+ result: false,
+ });
+
+ info("Open a new window");
+ open("", "", "noopener");
+ let newWin = Services.wm.getMostRecentWindow("navigator:browser");
+
+ info("The new window has no sidebar");
+ await sendMessage(extension1, "isOpen", { result: false });
+ await sendMessage(extension2, "isOpen", { result: false });
+
+ info("But the original window still does");
+ await sendMessage(extension1, "isOpen", { arg: { windowId }, result: true });
+ await sendMessage(extension2, "isOpen", { arg: { windowId }, result: false });
+
+ info("Close the new window");
+ newWin.close();
+
+ info("Close the sidebar in the original window");
+ SidebarUI.hide();
+ await sendMessage(extension1, "isOpen", { result: false });
+ await sendMessage(extension2, "isOpen", { result: false });
+
+ await extension1.unload();
+ await extension2.unload();
+});
+
+add_task(async function testShortcuts() {
+ function verifyShortcut(id, commandKey) {
+ // We're just testing the command key since the modifiers have different
+ // icons on different platforms.
+ let menuitem = document.getElementById(
+ `sidebarswitcher_menu_${makeWidgetId(id)}-sidebar-action`
+ );
+ ok(menuitem.hasAttribute("key"), "The menu item has a key specified");
+ let key = document.getElementById(menuitem.getAttribute("key"));
+ ok(key, "The key attribute finds the related key element");
+ ok(
+ menuitem.getAttribute("acceltext").endsWith(commandKey),
+ "The shortcut has the right key"
+ );
+ }
+
+ let extension1 = ExtensionTestUtils.loadExtension(
+ getExtData({
+ commands: {
+ _execute_sidebar_action: {
+ suggested_key: {
+ default: "Ctrl+Shift+I",
+ },
+ },
+ },
+ })
+ );
+ let extension2 = ExtensionTestUtils.loadExtension(
+ getExtData({
+ commands: {
+ _execute_sidebar_action: {
+ suggested_key: {
+ default: "Ctrl+Shift+E",
+ },
+ },
+ },
+ })
+ );
+
+ await extension1.startup();
+ await extension1.awaitMessage("sidebar");
+
+ // Open and close the switcher panel to trigger shortcut content rendering.
+ let switcherPanelShown = promisePopupShown(SidebarUI._switcherPanel);
+ SidebarUI.showSwitcherPanel();
+ await switcherPanelShown;
+ let switcherPanelHidden = promisePopupHidden(SidebarUI._switcherPanel);
+ SidebarUI.hideSwitcherPanel();
+ await switcherPanelHidden;
+
+ // Test that the key is set for the extension after the shortcuts are rendered.
+ verifyShortcut(extension1.id, "I");
+
+ await extension2.startup();
+ await extension2.awaitMessage("sidebar");
+
+ // Once the switcher panel has been opened new shortcuts should be added
+ // automatically.
+ verifyShortcut(extension2.id, "E");
+
+ await extension1.unload();
+ await extension2.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_browser_style.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_browser_style.js
new file mode 100644
index 0000000000..866d7a3b3d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_browser_style.js
@@ -0,0 +1,90 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testSidebarBrowserStyle(sidebarAction, assertMessage) {
+ function sidebarScript() {
+ browser.test.onMessage.addListener((msgName, info, assertMessage) => {
+ if (msgName !== "check-style") {
+ browser.test.notifyFail("sidebar-browser-style");
+ }
+
+ let style = window.getComputedStyle(document.getElementById("button"));
+ let buttonBackgroundColor = style.backgroundColor;
+ let browserStyleBackgroundColor = "rgb(9, 150, 248)";
+ if (!("browser_style" in info) || info.browser_style) {
+ browser.test.assertEq(
+ browserStyleBackgroundColor,
+ buttonBackgroundColor,
+ assertMessage
+ );
+ } else {
+ browser.test.assertTrue(
+ browserStyleBackgroundColor !== buttonBackgroundColor,
+ assertMessage
+ );
+ }
+
+ browser.test.notifyPass("sidebar-browser-style");
+ });
+ browser.test.sendMessage("sidebar-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: sidebarAction,
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "panel.html": `
+
+
+ Default
+
+ `,
+ "panel.js": sidebarScript,
+ },
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await extension.startup();
+ await extension.awaitMessage("sidebar-ready");
+
+ extension.sendMessage("check-style", sidebarAction, assertMessage);
+ await extension.awaitFinish("sidebar-browser-style");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_sidebar_without_setting_browser_style() {
+ await testSidebarBrowserStyle(
+ {
+ default_panel: "panel.html",
+ },
+ "Expected correct style when browser_style is excluded"
+ );
+});
+
+add_task(async function test_sidebar_with_browser_style_set_to_true() {
+ await testSidebarBrowserStyle(
+ {
+ default_panel: "panel.html",
+ browser_style: true,
+ },
+ "Expected correct style when browser_style is set to `true`"
+ );
+});
+
+add_task(async function test_sidebar_with_browser_style_set_to_false() {
+ await testSidebarBrowserStyle(
+ {
+ default_panel: "panel.html",
+ browser_style: false,
+ },
+ "Expected no style when browser_style is set to `false`"
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js
new file mode 100644
index 0000000000..621d2d1180
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js
@@ -0,0 +1,74 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_sidebar_click_isAppTab_behavior() {
+ function sidebarScript() {
+ browser.tabs.onUpdated.addListener(function onUpdated(
+ tabId,
+ changeInfo,
+ tab
+ ) {
+ if (
+ changeInfo.status == "complete" &&
+ tab.url == "http://mochi.test:8888/"
+ ) {
+ browser.tabs.remove(tab.id);
+ browser.test.notifyPass("sidebar-click");
+ }
+ });
+ window.addEventListener(
+ "load",
+ () => {
+ browser.test.sendMessage("sidebar-ready");
+ },
+ { once: true }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: {
+ default_panel: "panel.html",
+ browser_style: false,
+ },
+ permissions: ["tabs"],
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "panel.html": `
+
+
+
+
+
+
+ Bugzilla
+ `,
+ "panel.js": sidebarScript,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("sidebar-ready");
+
+ // This test fails if docShell.isAppTab has not been set to true.
+ let content = SidebarUI.browser.contentWindow;
+
+ // Wait for the layout to be flushed, otherwise this test may
+ // fail intermittently if synthesizeMouseAtCenter is being called
+ // while the sidebar is still opening and the browser window layout
+ // being recomputed.
+ await content.promiseDocumentFlushed(() => {});
+
+ info("Clicking link in extension sidebar");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testlink",
+ {},
+ content.gBrowser.selectedBrowser
+ );
+ await extension.awaitFinish("sidebar-click");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
new file mode 100644
index 0000000000..7057037a5e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -0,0 +1,683 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function runTests(options) {
+ async function background(getTests) {
+ async function checkDetails(expecting, details) {
+ let title = await browser.sidebarAction.getTitle(details);
+ browser.test.assertEq(
+ expecting.title,
+ title,
+ "expected value from getTitle in " + JSON.stringify(details)
+ );
+
+ let panel = await browser.sidebarAction.getPanel(details);
+ browser.test.assertEq(
+ expecting.panel,
+ panel,
+ "expected value from getPanel in " + JSON.stringify(details)
+ );
+ }
+
+ let tabs = [];
+ let windows = [];
+ let tests = getTests(tabs, windows);
+
+ {
+ let tabId = 0xdeadbeef;
+ let calls = [
+ () => browser.sidebarAction.setTitle({ tabId, title: "foo" }),
+ () => browser.sidebarAction.setIcon({ tabId, path: "foo.png" }),
+ () => browser.sidebarAction.setPanel({ tabId, panel: "foo.html" }),
+ ];
+
+ for (let call of calls) {
+ await browser.test.assertRejects(
+ new Promise(resolve => resolve(call())),
+ RegExp(`Invalid tab ID: ${tabId}`),
+ "Expected invalid tab ID error"
+ );
+ }
+ }
+
+ // Runs the next test in the `tests` array, checks the results,
+ // and passes control back to the outer test scope.
+ function nextTest() {
+ let test = tests.shift();
+
+ test(async (expectTab, expectWindow, expectGlobal, expectDefault) => {
+ expectGlobal = { ...expectDefault, ...expectGlobal };
+ expectWindow = { ...expectGlobal, ...expectWindow };
+ expectTab = { ...expectWindow, ...expectTab };
+
+ // Check that the API returns the expected values, and then
+ // run the next test.
+ let [{ windowId, id: tabId }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await checkDetails(expectTab, { tabId });
+ await checkDetails(expectWindow, { windowId });
+ await checkDetails(expectGlobal, {});
+
+ // Check that the actual icon has the expected values, then
+ // run the next test.
+ browser.test.sendMessage("nextTest", expectTab, windowId, tests.length);
+ });
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg != "runNextTest") {
+ browser.test.fail("Expecting 'runNextTest' message");
+ }
+
+ nextTest();
+ });
+
+ let [{ id, windowId }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ tabs.push(id);
+ windows.push(windowId);
+
+ browser.test.sendMessage("background-page-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: options.manifest,
+ useAddonManager: "temporary",
+
+ files: options.files || {},
+
+ background: `(${background})(${options.getTests})`,
+ });
+
+ let sidebarActionId;
+ function checkDetails(details, windowId) {
+ let { document } = Services.wm.getOuterWindowWithId(windowId);
+ if (!sidebarActionId) {
+ sidebarActionId = `${makeWidgetId(extension.id)}-sidebar-action`;
+ }
+
+ let menuId = `menubar_menu_${sidebarActionId}`;
+ let menu = document.getElementById(menuId);
+ ok(menu, "menu exists");
+
+ let title = details.title || options.manifest.name;
+
+ is(getListStyleImage(menu), details.icon, "icon URL is correct");
+ is(menu.getAttribute("label"), title, "image label is correct");
+ }
+
+ let awaitFinish = new Promise(resolve => {
+ extension.onMessage("nextTest", (expecting, windowId, testsRemaining) => {
+ checkDetails(expecting, windowId);
+
+ if (testsRemaining) {
+ extension.sendMessage("runNextTest");
+ } else {
+ resolve();
+ }
+ });
+ });
+
+ // Wait for initial sidebar load.
+ SidebarUI.browser.addEventListener(
+ "load",
+ async () => {
+ // Wait for the background page listeners to be ready and
+ // then start the tests.
+ await extension.awaitMessage("background-page-ready");
+ extension.sendMessage("runNextTest");
+ },
+ { capture: true, once: true }
+ );
+
+ await extension.startup();
+
+ await awaitFinish;
+ await extension.unload();
+}
+
+let sidebar = `
+
+
+
+
+ A Test Sidebar
+
+`;
+
+add_task(async function testTabSwitchContext() {
+ await runTests({
+ manifest: {
+ sidebar_action: {
+ default_icon: "default.png",
+ default_panel: "__MSG_panel__",
+ default_title: "Default __MSG_title__",
+ },
+
+ default_locale: "en",
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "default.html": sidebar,
+ "global.html": sidebar,
+ "2.html": sidebar,
+
+ "_locales/en/messages.json": {
+ panel: {
+ message: "default.html",
+ description: "Panel",
+ },
+
+ title: {
+ message: "Title",
+ description: "Title",
+ },
+ },
+
+ "default.png": imageBuffer,
+ "global.png": imageBuffer,
+ "1.png": imageBuffer,
+ "2.png": imageBuffer,
+ },
+
+ getTests: function (tabs) {
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ panel: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ },
+ { icon: browser.runtime.getURL("1.png") },
+ {
+ icon: browser.runtime.getURL("2.png"),
+ panel: browser.runtime.getURL("2.html"),
+ title: "Title 2",
+ },
+ {
+ icon: browser.runtime.getURL("global.png"),
+ panel: browser.runtime.getURL("global.html"),
+ title: "Global Title",
+ },
+ {
+ icon: browser.runtime.getURL("1.png"),
+ panel: browser.runtime.getURL("2.html"),
+ },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Change the icon in the current tab. Expect default properties excluding the icon."
+ );
+ await browser.sidebarAction.setIcon({
+ tabId: tabs[0],
+ path: "1.png",
+ });
+
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. Expect default properties.");
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?0",
+ });
+ tabs.push(tab.id);
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change properties. Expect new properties.");
+ let tabId = tabs[1];
+ await Promise.all([
+ browser.sidebarAction.setIcon({ tabId, path: "2.png" }),
+ browser.sidebarAction.setPanel({ tabId, panel: "2.html" }),
+ browser.sidebarAction.setTitle({ tabId, title: "Title 2" }),
+ ]);
+ expect(details[2], null, null, details[0]);
+ },
+ expect => {
+ browser.test.log("Navigate to a new page. Expect no changes.");
+
+ // TODO: This listener should not be necessary, but the |tabs.update|
+ // callback currently fires too early in e10s windows.
+ browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
+ if (tabId == tabs[1] && changed.url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ expect(details[2], null, null, details[0]);
+ }
+ });
+
+ browser.tabs.update(tabs[1], { url: "about:blank?1" });
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to the first tab. Expect previously set properties."
+ );
+ await browser.tabs.update(tabs[0], { active: true });
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Change global values, expect those changes reflected."
+ );
+ await Promise.all([
+ browser.sidebarAction.setIcon({ path: "global.png" }),
+ browser.sidebarAction.setPanel({ panel: "global.html" }),
+ browser.sidebarAction.setTitle({ title: "Global Title" }),
+ ]);
+
+ expect(details[1], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to tab 2. Expect former tab values, and new global values from previous step."
+ );
+ await browser.tabs.update(tabs[1], { active: true });
+
+ expect(details[2], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Delete tab, switch back to tab 1. Expect previous results again."
+ );
+ await browser.tabs.remove(tabs[1]);
+ expect(details[1], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. Expect new global properties.");
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?2",
+ });
+ tabs.push(tab.id);
+ expect(null, null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Delete tab.");
+ await browser.tabs.remove(tabs[2]);
+ expect(details[1], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Change tab panel.");
+ let tabId = tabs[0];
+ await browser.sidebarAction.setPanel({ tabId, panel: "2.html" });
+ expect(details[4], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Revert tab panel.");
+ let tabId = tabs[0];
+ await browser.sidebarAction.setPanel({ tabId, panel: null });
+ expect(details[1], null, details[3], details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testDefaultTitle() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ sidebar_action: {
+ default_icon: "icon.png",
+ default_panel: "sidebar.html",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "sidebar.html": sidebar,
+ "icon.png": imageBuffer,
+ },
+
+ getTests: function (tabs) {
+ let details = [
+ {
+ title: "Foo Extension",
+ panel: browser.runtime.getURL("sidebar.html"),
+ icon: browser.runtime.getURL("icon.png"),
+ },
+ { title: "Foo Title" },
+ { title: "Bar Title" },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state. Expect default extension title.");
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change the tab title. Expect new title.");
+ browser.sidebarAction.setTitle({
+ tabId: tabs[0],
+ title: "Foo Title",
+ });
+
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change the global title. Expect same properties.");
+ browser.sidebarAction.setTitle({ title: "Bar Title" });
+
+ expect(details[1], null, details[2], details[0]);
+ },
+ async expect => {
+ browser.test.log("Clear the tab title. Expect new global title.");
+ browser.sidebarAction.setTitle({ tabId: tabs[0], title: null });
+
+ expect(null, null, details[2], details[0]);
+ },
+ async expect => {
+ browser.test.log("Clear the global title. Expect default title.");
+ browser.sidebarAction.setTitle({ title: null });
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.assertRejects(
+ browser.sidebarAction.setPanel({ panel: "about:addons" }),
+ /Access denied for URL about:addons/,
+ "unable to set panel to about:addons"
+ );
+
+ expect(null, null, null, details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testPropertyRemoval() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ sidebar_action: {
+ default_icon: "default.png",
+ default_panel: "default.html",
+ default_title: "Default Title",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "default.html": sidebar,
+ "global.html": sidebar,
+ "global2.html": sidebar,
+ "window.html": sidebar,
+ "tab.html": sidebar,
+ "default.png": imageBuffer,
+ "global.png": imageBuffer,
+ "global2.png": imageBuffer,
+ "window.png": imageBuffer,
+ "tab.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ panel: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ },
+ {
+ icon: browser.runtime.getURL("global.png"),
+ panel: browser.runtime.getURL("global.html"),
+ title: "global",
+ },
+ {
+ icon: browser.runtime.getURL("window.png"),
+ panel: browser.runtime.getURL("window.html"),
+ title: "window",
+ },
+ {
+ icon: browser.runtime.getURL("tab.png"),
+ panel: browser.runtime.getURL("tab.html"),
+ title: "tab",
+ },
+ { icon: defaultIcon, title: "" },
+ {
+ icon: browser.runtime.getURL("global2.png"),
+ panel: browser.runtime.getURL("global2.html"),
+ title: "global2",
+ },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set global values, expect the new values.");
+ browser.sidebarAction.setIcon({ path: "global.png" });
+ browser.sidebarAction.setPanel({ panel: "global.html" });
+ browser.sidebarAction.setTitle({ title: "global" });
+ expect(null, null, details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[0];
+ browser.sidebarAction.setIcon({ windowId, path: "window.png" });
+ browser.sidebarAction.setPanel({ windowId, panel: "window.html" });
+ browser.sidebarAction.setTitle({ windowId, title: "window" });
+ expect(null, details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set tab values, expect the new values.");
+ let tabId = tabs[0];
+ browser.sidebarAction.setIcon({ tabId, path: "tab.png" });
+ browser.sidebarAction.setPanel({ tabId, panel: "tab.html" });
+ browser.sidebarAction.setTitle({ tabId, title: "tab" });
+ expect(details[3], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set empty tab values.");
+ let tabId = tabs[0];
+ browser.sidebarAction.setIcon({ tabId, path: "" });
+ browser.sidebarAction.setPanel({ tabId, panel: "" });
+ browser.sidebarAction.setTitle({ tabId, title: "" });
+ expect(details[4], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove tab values, expect window values.");
+ let tabId = tabs[0];
+ browser.sidebarAction.setIcon({ tabId, path: null });
+ browser.sidebarAction.setPanel({ tabId, panel: null });
+ browser.sidebarAction.setTitle({ tabId, title: null });
+ expect(null, details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove window values, expect global values.");
+ let windowId = windows[0];
+ browser.sidebarAction.setIcon({ windowId, path: null });
+ browser.sidebarAction.setPanel({ windowId, panel: null });
+ browser.sidebarAction.setTitle({ windowId, title: null });
+ expect(null, null, details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Change global values, expect the new values.");
+ browser.sidebarAction.setIcon({ path: "global2.png" });
+ browser.sidebarAction.setPanel({ panel: "global2.html" });
+ browser.sidebarAction.setTitle({ title: "global2" });
+ expect(null, null, details[5], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove global values, expect defaults.");
+ browser.sidebarAction.setIcon({ path: null });
+ browser.sidebarAction.setPanel({ panel: null });
+ browser.sidebarAction.setTitle({ title: null });
+ expect(null, null, null, details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testMultipleWindows() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ sidebar_action: {
+ default_icon: "default.png",
+ default_panel: "default.html",
+ default_title: "Default Title",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "default.html": sidebar,
+ "window1.html": sidebar,
+ "window2.html": sidebar,
+ "default.png": imageBuffer,
+ "window1.png": imageBuffer,
+ "window2.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ panel: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ },
+ {
+ icon: browser.runtime.getURL("window1.png"),
+ panel: browser.runtime.getURL("window1.html"),
+ title: "window1",
+ },
+ {
+ icon: browser.runtime.getURL("window2.png"),
+ panel: browser.runtime.getURL("window2.html"),
+ title: "window2",
+ },
+ { title: "tab" },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[0];
+ browser.sidebarAction.setIcon({ windowId, path: "window1.png" });
+ browser.sidebarAction.setPanel({ windowId, panel: "window1.html" });
+ browser.sidebarAction.setTitle({ windowId, title: "window1" });
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab, expect window values.");
+ let tab = await browser.tabs.create({ active: true });
+ tabs.push(tab.id);
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a tab title, expect it.");
+ await browser.sidebarAction.setTitle({
+ tabId: tabs[1],
+ title: "tab",
+ });
+ expect(details[3], details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Open a new window, expect default values.");
+ let { id } = await browser.windows.create();
+ windows.push(id);
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[1];
+ browser.sidebarAction.setIcon({ windowId, path: "window2.png" });
+ browser.sidebarAction.setPanel({ windowId, panel: "window2.html" });
+ browser.sidebarAction.setTitle({ windowId, title: "window2" });
+ expect(null, details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Move tab from old window to the new one. Tab-specific data" +
+ " is preserved but inheritance is from the new window"
+ );
+ await browser.tabs.move(tabs[1], { windowId: windows[1], index: -1 });
+ await browser.tabs.update(tabs[1], { active: true });
+ expect(details[3], details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Close the initial tab of the new window.");
+ let [{ id }] = await browser.tabs.query({
+ windowId: windows[1],
+ index: 0,
+ });
+ await browser.tabs.remove(id);
+ expect(details[3], details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Move the previous tab to a 3rd window, the 2nd one will close."
+ );
+ await browser.windows.create({ tabId: tabs[1] });
+ expect(details[3], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Close the tab, go back to the 1st window.");
+ await browser.tabs.remove(tabs[1]);
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Assert failures for bad parameters. Expect no change"
+ );
+
+ let calls = {
+ setIcon: { path: "default.png" },
+ setPanel: { panel: "default.html" },
+ setTitle: { title: "Default Title" },
+ getPanel: {},
+ getTitle: {},
+ };
+ for (let [method, arg] of Object.entries(calls)) {
+ browser.test.assertThrows(
+ () => browser.sidebarAction[method]({ ...arg, windowId: -3 }),
+ /-3 is too small \(must be at least -2\)/,
+ method + " with invalid windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction[method]({
+ ...arg,
+ tabId: tabs[0],
+ windowId: windows[0],
+ }),
+ /Only one of tabId and windowId can be specified/,
+ method + " with both tabId and windowId"
+ );
+ }
+
+ expect(null, details[1], null, details[0]);
+ },
+ ];
+ },
+ });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js
new file mode 100644
index 0000000000..3317e6b7e0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js
@@ -0,0 +1,133 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let extData = {
+ manifest: {
+ permissions: ["contextMenus"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+
+ background: function () {
+ browser.contextMenus.create({
+ id: "clickme-page",
+ title: "Click me!",
+ contexts: ["all"],
+ onclick(info, tab) {
+ browser.test.sendMessage("menu-click", tab);
+ },
+ });
+ },
+};
+
+let contextMenuItems = {
+ "context-sep-navigation": "hidden",
+ "context-viewsource": "",
+ "inspect-separator": "hidden",
+ "context-inspect": "hidden",
+ "context-inspect-a11y": "hidden",
+ "context-bookmarkpage": "hidden",
+};
+if (AppConstants.platform == "macosx") {
+ contextMenuItems["context-back"] = "hidden";
+ contextMenuItems["context-forward"] = "hidden";
+ contextMenuItems["context-reload"] = "hidden";
+ contextMenuItems["context-stop"] = "hidden";
+} else {
+ contextMenuItems["context-navigation"] = "hidden";
+}
+
+add_task(async function sidebar_contextmenu() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+
+ let contentAreaContextMenu = await openContextMenuInSidebar();
+ let item = contentAreaContextMenu.getElementsByAttribute(
+ "label",
+ "Click me!"
+ );
+ is(item.length, 1, "contextMenu item for page was found");
+
+ item[0].click();
+ await closeContextMenu(contentAreaContextMenu);
+ let tab = await extension.awaitMessage("menu-click");
+ is(
+ tab,
+ null,
+ "tab argument is optional, and missing in clicks from sidebars"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function sidebar_contextmenu_hidden_items() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+
+ let contentAreaContextMenu = await openContextMenuInSidebar("#text");
+
+ let item, state;
+ for (const itemID in contextMenuItems) {
+ item = contentAreaContextMenu.querySelector(`#${itemID}`);
+ state = contextMenuItems[itemID];
+
+ if (state !== "") {
+ ok(item[state], `${itemID} is ${state}`);
+
+ if (state !== "hidden") {
+ ok(!item.hidden, `Disabled ${itemID} is not hidden`);
+ }
+ } else {
+ ok(!item.hidden, `${itemID} is not hidden`);
+ ok(!item.disabled, `${itemID} is not disabled`);
+ }
+ }
+
+ await closeContextMenu(contentAreaContextMenu);
+
+ await extension.unload();
+});
+
+add_task(async function sidebar_image_contextmenu() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+
+ let contentAreaContextMenu = await openContextMenuInSidebar("#testimg");
+
+ let item = contentAreaContextMenu.querySelector("#context-copyimage");
+ ok(!item.hidden);
+ ok(!item.disabled);
+
+ await closeContextMenu(contentAreaContextMenu);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js
new file mode 100644
index 0000000000..d50d96b822
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js
@@ -0,0 +1,72 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+add_task(async function sidebar_httpAuthPrompt() {
+ let data = {
+ manifest: {
+ permissions: ["https://example.com/*"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+ "sidebar.js": function () {
+ fetch(
+ "https://example.com/browser/browser/components/extensions/test/browser/authenticate.sjs?user=user&pass=pass",
+ { credentials: "include" }
+ ).then(response => {
+ browser.test.sendMessage("fetchResult", response.ok);
+ });
+ },
+ },
+ };
+
+ // Wait for the http auth prompt and close it with accept button.
+ let promptPromise = PromptTestUtils.handleNextPrompt(
+ SidebarUI.browser.contentWindow,
+ {
+ modalType: Services.prompt.MODAL_TYPE_WINDOW,
+ promptType: "promptUserAndPass",
+ },
+ { buttonNumClick: 0, loginInput: "user", passwordInput: "pass" }
+ );
+
+ let extension = ExtensionTestUtils.loadExtension(data);
+ await extension.startup();
+ let fetchResultPromise = extension.awaitMessage("fetchResult");
+
+ await promptPromise;
+ ok(true, "Extension fetch should trigger auth prompt.");
+
+ let responseOk = await fetchResultPromise;
+ ok(responseOk, "Login should succeed.");
+
+ await extension.unload();
+
+ // Cleanup
+ await new Promise(resolve =>
+ Services.clearData.deleteData(
+ Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
+ resolve
+ )
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js
new file mode 100644
index 0000000000..221447cf2e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js
@@ -0,0 +1,139 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_sidebarAction_not_allowed() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(async pbw => {
+ await browser.test.assertRejects(
+ browser.sidebarAction.setTitle({
+ windowId: pbw.windowId,
+ title: "test",
+ }),
+ /Invalid window ID/,
+ "should not be able to set title with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.setTitle({
+ tabId: pbw.tabId,
+ title: "test",
+ }),
+ /Invalid tab ID/,
+ "should not be able to set title"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.getTitle({
+ windowId: pbw.windowId,
+ }),
+ /Invalid window ID/,
+ "should not be able to get title with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.getTitle({
+ tabId: pbw.tabId,
+ }),
+ /Invalid tab ID/,
+ "should not be able to get title with tabId"
+ );
+
+ await browser.test.assertRejects(
+ browser.sidebarAction.setIcon({
+ windowId: pbw.windowId,
+ path: "test",
+ }),
+ /Invalid window ID/,
+ "should not be able to set icon with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.setIcon({
+ tabId: pbw.tabId,
+ path: "test",
+ }),
+ /Invalid tab ID/,
+ "should not be able to set icon with tabId"
+ );
+
+ await browser.test.assertRejects(
+ browser.sidebarAction.setPanel({
+ windowId: pbw.windowId,
+ panel: "test",
+ }),
+ /Invalid window ID/,
+ "should not be able to set panel with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.setPanel({
+ tabId: pbw.tabId,
+ panel: "test",
+ }),
+ /Invalid tab ID/,
+ "should not be able to set panel with tabId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.getPanel({
+ windowId: pbw.windowId,
+ }),
+ /Invalid window ID/,
+ "should not be able to get panel with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.getPanel({
+ tabId: pbw.tabId,
+ }),
+ /Invalid tab ID/,
+ "should not be able to get panel with tabId"
+ );
+
+ await browser.test.assertRejects(
+ browser.sidebarAction.isOpen({
+ windowId: pbw.windowId,
+ }),
+ /Invalid window ID/,
+ "should not be able to determine openness with windowId"
+ );
+
+ browser.test.notifyPass("pass");
+ });
+ },
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+ });
+
+ await extension.startup();
+ let sidebarID = `${makeWidgetId(extension.id)}-sidebar-action`;
+ ok(SidebarUI.sidebars.has(sidebarID), "sidebar exists in non-private window");
+
+ let winData = await getIncognitoWindow();
+
+ let hasSidebar = winData.win.SidebarUI.sidebars.has(sidebarID);
+ ok(!hasSidebar, "sidebar does not exist in private window");
+ // Test API access to private window data.
+ extension.sendMessage(winData.details);
+ await extension.awaitFinish("pass");
+
+ await BrowserTestUtils.closeWindow(winData.win);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js
new file mode 100644
index 0000000000..55c83ee0b1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js
@@ -0,0 +1,76 @@
+"use strict";
+
+function background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq(port.name, "ernie", "port name correct");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "The port is implicitly closed without errors when the other context unloads"
+ );
+ port.disconnect();
+ browser.test.sendMessage("disconnected");
+ });
+ browser.test.sendMessage("connected");
+ });
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.runtime.connect({ name: "ernie" });
+ };
+ },
+ },
+};
+
+add_task(async function test_sidebar_disconnect() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ let connected = extension.awaitMessage("connected");
+ await extension.startup();
+ await connected;
+
+ // Bug 1445080 fixes currentURI, test to avoid future breakage.
+ let currentURI = window.SidebarUI.browser.contentDocument.getElementById(
+ "webext-panels-browser"
+ ).currentURI;
+ is(currentURI.scheme, "moz-extension", "currentURI is set correctly");
+
+ // switching sidebar to another extension
+ let extension2 = ExtensionTestUtils.loadExtension(extensionData);
+ let switched = Promise.all([
+ extension.awaitMessage("disconnected"),
+ extension2.awaitMessage("connected"),
+ ]);
+ await extension2.startup();
+ await switched;
+
+ // switching sidebar to built-in sidebar
+ let disconnected = extension2.awaitMessage("disconnected");
+ window.SidebarUI.show("viewBookmarksSidebar");
+ await disconnected;
+
+ await extension.unload();
+ await extension2.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js
new file mode 100644
index 0000000000..7af75cdc19
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js
@@ -0,0 +1,48 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function sidebar_tab_query_bug_1340739() {
+ let data = {
+ manifest: {
+ permissions: ["tabs"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+ "sidebar.js": function () {
+ Promise.all([
+ browser.tabs.query({}).then(tabs => {
+ browser.test.assertEq(
+ 1,
+ tabs.length,
+ "got tab without currentWindow"
+ );
+ }),
+ browser.tabs.query({ currentWindow: true }).then(tabs => {
+ browser.test.assertEq(1, tabs.length, "got tab with currentWindow");
+ }),
+ ]).then(() => {
+ browser.test.sendMessage("sidebar");
+ });
+ },
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(data);
+ await extension.startup();
+ await extension.awaitMessage("sidebar");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js
new file mode 100644
index 0000000000..58f2b07797
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js
@@ -0,0 +1,69 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let extData = {
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+};
+
+add_task(async function sidebar_windows() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+ ok(
+ !document.getElementById("sidebar-box").hidden,
+ "sidebar box is visible in first window"
+ );
+ // Check that the menuitem has our image styling.
+ let elements = document.getElementsByClassName("webextension-menuitem");
+ // ui is in flux, at time of writing we potentially have 3 menuitems, later
+ // it may be two or one, just make sure one is there.
+ ok(!!elements.length, "have a menuitem");
+ let style = elements[0].getAttribute("style");
+ ok(style.includes("webextension-menuitem-image"), "this menu has style");
+
+ let secondSidebar = extension.awaitMessage("sidebar");
+
+ // SidebarUI relies on window.opener being set, which is normal behavior when
+ // using menu or key commands to open a new browser window.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await secondSidebar;
+ ok(
+ !win.document.getElementById("sidebar-box").hidden,
+ "sidebar box is visible in second window"
+ );
+ // Check that the menuitem has our image styling.
+ elements = win.document.getElementsByClassName("webextension-menuitem");
+ ok(!!elements.length, "have a menuitem");
+ style = elements[0].getAttribute("style");
+ ok(style.includes("webextension-menuitem-image"), "this menu has style");
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebar_requestPermission.js b/browser/components/extensions/test/browser/browser_ext_sidebar_requestPermission.js
new file mode 100644
index 0000000000..393efcf99e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebar_requestPermission.js
@@ -0,0 +1,43 @@
+"use strict";
+
+add_task(async function test_sidebar_requestPermission_resolve() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: {
+ default_panel: "panel.html",
+ browser_style: false,
+ },
+ optional_permissions: ["tabs"],
+ },
+ useAddonManager: "temporary",
+ files: {
+ "panel.html": ` `,
+ "panel.js": async () => {
+ const success = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({
+ permissions: ["tabs"],
+ })
+ );
+ });
+ });
+ browser.test.assertTrue(
+ success,
+ "browser.permissions.request promise resolves"
+ );
+ browser.test.sendMessage("done");
+ },
+ },
+ });
+
+ const requestPrompt = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+ await extension.startup();
+ await requestPrompt;
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_simple.js b/browser/components/extensions/test/browser/browser_ext_simple.js
new file mode 100644
index 0000000000..4d9d7c73fa
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_simple.js
@@ -0,0 +1,60 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_simple() {
+ let extensionData = {
+ manifest: {
+ name: "Simple extension test",
+ version: "1.0",
+ manifest_version: 2,
+ description: "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ info("load complete");
+ await extension.startup();
+ info("startup complete");
+ await extension.unload();
+ info("extension unloaded successfully");
+});
+
+add_task(async function test_background() {
+ function backgroundScript() {
+ browser.test.log("running background script");
+
+ browser.test.onMessage.addListener((x, y) => {
+ browser.test.assertEq(x, 10, "x is 10");
+ browser.test.assertEq(y, 20, "y is 20");
+
+ browser.test.notifyPass("background test passed");
+ });
+
+ browser.test.sendMessage("running", 1);
+ }
+
+ let extensionData = {
+ background: "(" + backgroundScript.toString() + ")()",
+ manifest: {
+ name: "Simple extension test",
+ version: "1.0",
+ manifest_version: 2,
+ description: "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ info("load complete");
+ let [, x] = await Promise.all([
+ extension.startup(),
+ extension.awaitMessage("running"),
+ ]);
+ is(x, 1, "got correct value from extension");
+ info("startup complete");
+ extension.sendMessage(10, 20);
+ await extension.awaitFinish();
+ info("test complete");
+ await extension.unload();
+ info("extension unloaded successfully");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_slow_script.js b/browser/components/extensions/test/browser/browser_ext_slow_script.js
new file mode 100644
index 0000000000..bd9369a904
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_slow_script.js
@@ -0,0 +1,72 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const DEFAULT_PROCESS_COUNT = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref("dom.ipc.processCount");
+
+add_task(async function test_slow_content_script() {
+ // Make sure we get a new process for our tab, or our reportProcessHangs
+ // preferences value won't apply to it.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["dom.ipc.keepProcessesAlive.web", 0],
+ ],
+ });
+ await SpecialPowers.popPrefEnv();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT * 2],
+ ["dom.ipc.processPrelaunch.enabled", false],
+ ["dom.ipc.reportProcessHangs", true],
+ ["dom.max_script_run_time.require_critical_input", false],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ name: "Slow Script Extension",
+
+ content_scripts: [
+ {
+ matches: ["http://example.com/"],
+ js: ["content.js"],
+ },
+ ],
+ },
+
+ files: {
+ "content.js": function () {
+ while (true) {
+ // Busy wait.
+ }
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let alert = BrowserTestUtils.waitForGlobalNotificationBar(
+ window,
+ "process-hang"
+ );
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
+ let notification = await alert;
+ let text = notification.messageText.textContent;
+
+ ok(text.includes("\u201cSlow Script Extension\u201d"), "Label is correct");
+
+ let stopButton = notification.buttonContainer.querySelector("[label='Stop']");
+ stopButton.click();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js b/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js
new file mode 100644
index 0000000000..622916edda
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js
@@ -0,0 +1,100 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ let messages_received = [];
+
+ let tabId;
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertTrue(!!port, "tab to background port received");
+ browser.test.assertEq(
+ "tab-connection-name",
+ port.name,
+ "port name should be defined and equal to connectInfo.name"
+ );
+ browser.test.assertTrue(
+ !!port.sender.tab,
+ "port.sender.tab should be defined"
+ );
+ browser.test.assertEq(
+ tabId,
+ port.sender.tab.id,
+ "port.sender.tab.id should be equal to the expected tabId"
+ );
+
+ port.onMessage.addListener(msg => {
+ messages_received.push(msg);
+
+ if (messages_received.length == 1) {
+ browser.test.assertEq(
+ "tab to background port message",
+ msg,
+ "'tab to background' port message received"
+ );
+ port.postMessage("background to tab port message");
+ }
+
+ if (messages_received.length == 2) {
+ browser.test.assertTrue(
+ !!msg.tabReceived,
+ "'background to tab' reply port message received"
+ );
+ browser.test.assertEq(
+ "background to tab port message",
+ msg.tabReceived,
+ "reply port content contains the message received"
+ );
+
+ browser.test.notifyPass("tabRuntimeConnect.pass");
+ }
+ });
+ });
+
+ browser.tabs.create({ url: "tab.html" }, tab => {
+ tabId = tab.id;
+ });
+ },
+
+ files: {
+ "tab.js": function () {
+ let port = browser.runtime.connect({ name: "tab-connection-name" });
+ port.postMessage("tab to background port message");
+ port.onMessage.addListener(msg => {
+ port.postMessage({ tabReceived: msg });
+ });
+ },
+ "tab.html": `
+
+
+
+ test tab extension page
+
+
+
+
+ test tab extension page
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabRuntimeConnect.pass");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_attention.js b/browser/components/extensions/test/browser/browser_ext_tabs_attention.js
new file mode 100644
index 0000000000..0f267460f3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_attention.js
@@ -0,0 +1,64 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function tabsAttention() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?2",
+ true
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?1",
+ true
+ );
+ gBrowser.selectedTab = tab2;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "http://example.com/*"],
+ },
+
+ background: async function () {
+ function onActive(tabId, changeInfo, tab) {
+ browser.test.assertFalse(
+ changeInfo.attention,
+ "changeInfo.attention should be false"
+ );
+ browser.test.assertFalse(
+ tab.attention,
+ "tab.attention should be false"
+ );
+ browser.test.assertTrue(tab.active, "tab.active should be true");
+ browser.test.notifyPass("tabsAttention");
+ }
+
+ function onUpdated(tabId, changeInfo, tab) {
+ browser.test.assertTrue(
+ changeInfo.attention,
+ "changeInfo.attention should be true"
+ );
+ browser.test.assertTrue(tab.attention, "tab.attention should be true");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ browser.tabs.onUpdated.addListener(onActive);
+ browser.tabs.update(tabId, { active: true });
+ }
+
+ browser.tabs.onUpdated.addListener(onUpdated, {
+ properties: ["attention"],
+ });
+ const tabs = await browser.tabs.query({ index: 1 });
+ browser.tabs.executeScript(tabs[0].id, {
+ code: `alert("tab attention")`,
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabsAttention");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_audio.js b/browser/components/extensions/test/browser/browser_ext_tabs_audio.js
new file mode 100644
index 0000000000..978c3697c8
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_audio.js
@@ -0,0 +1,261 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank?1"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank?2"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ async function background() {
+ function promiseUpdated(tabId, attr) {
+ return new Promise(resolve => {
+ let onUpdated = (tabId_, changeInfo, tab) => {
+ if (tabId == tabId_ && attr in changeInfo) {
+ browser.tabs.onUpdated.removeListener(onUpdated);
+
+ resolve({ changeInfo, tab });
+ }
+ };
+ browser.tabs.onUpdated.addListener(onUpdated);
+ });
+ }
+
+ let deferred = {};
+ browser.test.onMessage.addListener((message, tabId, result) => {
+ if (message == "change-tab-done" && deferred[tabId]) {
+ deferred[tabId].resolve(result);
+ }
+ });
+
+ function changeTab(tabId, attr, on) {
+ return new Promise((resolve, reject) => {
+ deferred[tabId] = { resolve, reject };
+ browser.test.sendMessage("change-tab", tabId, attr, on);
+ });
+ }
+
+ try {
+ let tabs = await browser.tabs.query({ lastFocusedWindow: true });
+ browser.test.assertEq(tabs.length, 3, "We have three tabs");
+
+ for (let tab of tabs) {
+ // Note: We want to check that these are actual boolean values, not
+ // just that they evaluate as false.
+ browser.test.assertEq(false, tab.mutedInfo.muted, "Tab is not muted");
+ browser.test.assertEq(
+ undefined,
+ tab.mutedInfo.reason,
+ "Tab has no muted info reason"
+ );
+ browser.test.assertEq(false, tab.audible, "Tab is not audible");
+ }
+
+ let windowId = tabs[0].windowId;
+ let tabIds = [tabs[1].id, tabs[2].id];
+
+ browser.test.log(
+ "Test initial queries for muted and audible return no tabs"
+ );
+ let silent = await browser.tabs.query({ windowId, audible: false });
+ let audible = await browser.tabs.query({ windowId, audible: true });
+ let muted = await browser.tabs.query({ windowId, muted: true });
+ let nonMuted = await browser.tabs.query({ windowId, muted: false });
+
+ browser.test.assertEq(3, silent.length, "Three silent tabs");
+ browser.test.assertEq(0, audible.length, "No audible tabs");
+
+ browser.test.assertEq(0, muted.length, "No muted tabs");
+ browser.test.assertEq(3, nonMuted.length, "Three non-muted tabs");
+
+ browser.test.log(
+ "Toggle muted and audible externally on one tab each, and check results"
+ );
+ [muted, audible] = await Promise.all([
+ promiseUpdated(tabIds[0], "mutedInfo"),
+ promiseUpdated(tabIds[1], "audible"),
+ changeTab(tabIds[0], "muted", true),
+ changeTab(tabIds[1], "audible", true),
+ ]);
+
+ for (let obj of [muted.changeInfo, muted.tab]) {
+ browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted");
+ browser.test.assertEq(
+ "user",
+ obj.mutedInfo.reason,
+ "Tab was muted by the user"
+ );
+ }
+
+ browser.test.assertEq(
+ true,
+ audible.changeInfo.audible,
+ "Tab audible state changed"
+ );
+ browser.test.assertEq(true, audible.tab.audible, "Tab is audible");
+
+ browser.test.log(
+ "Re-check queries. Expect one audible and one muted tab"
+ );
+ silent = await browser.tabs.query({ windowId, audible: false });
+ audible = await browser.tabs.query({ windowId, audible: true });
+ muted = await browser.tabs.query({ windowId, muted: true });
+ nonMuted = await browser.tabs.query({ windowId, muted: false });
+
+ browser.test.assertEq(2, silent.length, "Two silent tabs");
+ browser.test.assertEq(1, audible.length, "One audible tab");
+
+ browser.test.assertEq(1, muted.length, "One muted tab");
+ browser.test.assertEq(2, nonMuted.length, "Two non-muted tabs");
+
+ browser.test.assertEq(true, muted[0].mutedInfo.muted, "Tab is muted");
+ browser.test.assertEq(
+ "user",
+ muted[0].mutedInfo.reason,
+ "Tab was muted by the user"
+ );
+
+ browser.test.assertEq(true, audible[0].audible, "Tab is audible");
+
+ browser.test.log(
+ "Toggle muted internally on two tabs, and check results"
+ );
+ [nonMuted, muted] = await Promise.all([
+ promiseUpdated(tabIds[0], "mutedInfo"),
+ promiseUpdated(tabIds[1], "mutedInfo"),
+ browser.tabs.update(tabIds[0], { muted: false }),
+ browser.tabs.update(tabIds[1], { muted: true }),
+ ]);
+
+ for (let obj of [nonMuted.changeInfo, nonMuted.tab]) {
+ browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted");
+ }
+ for (let obj of [muted.changeInfo, muted.tab]) {
+ browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted");
+ }
+
+ for (let obj of [
+ nonMuted.changeInfo,
+ nonMuted.tab,
+ muted.changeInfo,
+ muted.tab,
+ ]) {
+ browser.test.assertEq(
+ "extension",
+ obj.mutedInfo.reason,
+ "Mute state changed by extension"
+ );
+
+ browser.test.assertEq(
+ browser.runtime.id,
+ obj.mutedInfo.extensionId,
+ "Mute state changed by extension"
+ );
+ }
+
+ browser.test.log("Test that mutedInfo is preserved by sessionstore");
+ let tab = await changeTab(tabIds[1], "duplicate").then(browser.tabs.get);
+
+ browser.test.assertEq(true, tab.mutedInfo.muted, "Tab is muted");
+
+ browser.test.assertEq(
+ "extension",
+ tab.mutedInfo.reason,
+ "Mute state changed by extension"
+ );
+
+ browser.test.assertEq(
+ browser.runtime.id,
+ tab.mutedInfo.extensionId,
+ "Mute state changed by extension"
+ );
+
+ browser.test.log("Unmute externally, and check results");
+ [nonMuted] = await Promise.all([
+ promiseUpdated(tabIds[1], "mutedInfo"),
+ changeTab(tabIds[1], "muted", false),
+ browser.tabs.remove(tab.id),
+ ]);
+
+ for (let obj of [nonMuted.changeInfo, nonMuted.tab]) {
+ browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted");
+ browser.test.assertEq(
+ "user",
+ obj.mutedInfo.reason,
+ "Mute state changed by user"
+ );
+ }
+
+ browser.test.notifyPass("tab-audio");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tab-audio");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ extension.onMessage("change-tab", (tabId, attr, on) => {
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let tab = tabTracker.getTab(tabId);
+
+ if (attr == "muted") {
+ // Ideally we'd simulate a click on the tab audio icon for this, but the
+ // handler relies on CSS :hover states, which are complicated and fragile
+ // to simulate.
+ if (tab.muted != on) {
+ tab.toggleMuteAudio();
+ }
+ } else if (attr == "audible") {
+ let browser = tab.linkedBrowser;
+ if (on) {
+ browser.audioPlaybackStarted();
+ } else {
+ browser.audioPlaybackStopped();
+ }
+ } else if (attr == "duplicate") {
+ // This is a bit of a hack. It won't be necessary once we have
+ // `tabs.duplicate`.
+ let newTab = gBrowser.duplicateTab(tab);
+ BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(
+ () => {
+ extension.sendMessage(
+ "change-tab-done",
+ tabId,
+ tabTracker.getId(newTab)
+ );
+ }
+ );
+ return;
+ }
+
+ extension.sendMessage("change-tab-done", tabId);
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("tab-audio");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_autoDiscardable.js b/browser/components/extensions/test/browser/browser_ext_tabs_autoDiscardable.js
new file mode 100644
index 0000000000..74043e2a3a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_autoDiscardable.js
@@ -0,0 +1,177 @@
+"use strict";
+
+add_task(async function test_autoDiscardable() {
+ let files = {
+ "schema.json": JSON.stringify([
+ {
+ namespace: "experiments",
+ functions: [
+ {
+ name: "unload",
+ type: "function",
+ async: true,
+ description:
+ "Unload the least recently used tab using Firefox's built-in tab unloader mechanism",
+ parameters: [],
+ },
+ ],
+ },
+ ]),
+ "parent.js": () => {
+ const { TabUnloader } = ChromeUtils.importESModule(
+ "resource:///modules/TabUnloader.sys.mjs"
+ );
+ const { ExtensionError } = ExtensionUtils;
+ this.experiments = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ experiments: {
+ async unload() {
+ try {
+ await TabUnloader.unloadLeastRecentlyUsedTab(null);
+ } catch (error) {
+ // We need to do this, otherwise failures won't bubble up to the test properly.
+ throw ExtensionError(error);
+ }
+ },
+ },
+ };
+ }
+ };
+ },
+ };
+
+ async function background() {
+ let firstTab = await browser.tabs.create({
+ active: false,
+ url: "https://example.org/",
+ });
+
+ // Make sure setting and getting works properly
+ browser.test.assertTrue(
+ firstTab.autoDiscardable,
+ "autoDiscardable should always be true by default"
+ );
+ let result = await browser.tabs.update(firstTab.id, {
+ autoDiscardable: false,
+ });
+ browser.test.assertFalse(
+ result.autoDiscardable,
+ "autoDiscardable should be false after setting it as such"
+ );
+ result = await browser.tabs.update(firstTab.id, {
+ autoDiscardable: true,
+ });
+ browser.test.assertTrue(
+ result.autoDiscardable,
+ "autoDiscardable should be true after setting it as such"
+ );
+ result = await browser.tabs.update(firstTab.id, {
+ autoDiscardable: false,
+ });
+ browser.test.assertFalse(
+ result.autoDiscardable,
+ "autoDiscardable should be false after setting it as such"
+ );
+
+ // Make sure the tab can't be unloaded when autoDiscardable is false
+ await browser.experiments.unload();
+ result = await browser.tabs.get(firstTab.id);
+ browser.test.assertFalse(
+ result.discarded,
+ "Tab should not unload when autoDiscardable is false"
+ );
+
+ // Make sure the tab CAN be unloaded when autoDiscardable is true
+ await browser.tabs.update(firstTab.id, {
+ autoDiscardable: true,
+ });
+ await browser.experiments.unload();
+ result = await browser.tabs.get(firstTab.id);
+ browser.test.assertTrue(
+ result.discarded,
+ "Tab should unload when autoDiscardable is true"
+ );
+
+ // Make sure filtering for discardable tabs works properly
+ result = await browser.tabs.query({ autoDiscardable: true });
+ browser.test.assertEq(
+ 2,
+ result.length,
+ "tabs.query should return 2 when autoDiscardable is true "
+ );
+ await browser.tabs.update(firstTab.id, {
+ autoDiscardable: false,
+ });
+ result = await browser.tabs.query({ autoDiscardable: true });
+ browser.test.assertEq(
+ 1,
+ result.length,
+ "tabs.query should return 1 when autoDiscardable is false"
+ );
+
+ let onUpdatedPromise = {};
+ onUpdatedPromise.promise = new Promise(
+ resolve => (onUpdatedPromise.resolve = resolve)
+ );
+
+ // Make sure onUpdated works
+ async function testOnUpdatedEvent(autoDiscardable) {
+ browser.test.log(`Testing autoDiscardable = ${autoDiscardable}`);
+ let onUpdated;
+ let promise = new Promise(resolve => {
+ onUpdated = (tabId, changeInfo, tabInfo) => {
+ browser.test.assertEq(
+ firstTab.id,
+ tabId,
+ "The updated tab's ID should match the correct tab"
+ );
+ browser.test.assertDeepEq(
+ { autoDiscardable },
+ changeInfo,
+ "The updated tab's changeInfo should be correct"
+ );
+ browser.test.assertEq(
+ tabInfo.autoDiscardable,
+ autoDiscardable,
+ "The updated tab's tabInfo should be correct"
+ );
+ resolve();
+ };
+ });
+ browser.tabs.onUpdated.addListener(onUpdated, {
+ properties: ["autoDiscardable"],
+ });
+ await browser.tabs.update(firstTab.id, { autoDiscardable });
+ await promise;
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ }
+
+ await testOnUpdatedEvent(true);
+ await testOnUpdatedEvent(false);
+
+ await browser.tabs.remove(firstTab.id); // Cleanup
+ browser.test.notifyPass("autoDiscardable");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: ["tabs"],
+ experiment_apis: {
+ experiments: {
+ schema: "schema.json",
+ parent: {
+ scopes: ["addon_parent"],
+ script: "parent.js",
+ paths: [["experiments"]],
+ },
+ },
+ },
+ },
+ background,
+ files,
+ });
+ await extension.startup();
+ await extension.awaitFinish("autoDiscardable");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_containerIsolation.js b/browser/components/extensions/test/browser/browser_ext_tabs_containerIsolation.js
new file mode 100644
index 0000000000..1960366bb5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_containerIsolation.js
@@ -0,0 +1,360 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+XPCShellContentUtils.initMochitest(this);
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: ["www.example.com"],
+});
+server.registerPathHandler("/", (request, response) => {
+ response.setHeader("Content-Type", "text/html; charset=UTF-8", false);
+ response.write(`
+
+
+
+
+
+ This is example.com page content
+
+
+ `);
+});
+
+add_task(async function containerIsolation_restricted() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.userContextIsolation.enabled", true],
+ ["privacy.userContext.enabled", true],
+ ],
+ });
+
+ let helperExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+
+ async background() {
+ browser.test.onMessage.addListener(async message => {
+ let tab;
+ if (message?.subject !== "createTab") {
+ browser.test.fail(
+ `Unexpected test message received: ${JSON.stringify(message)}`
+ );
+ return;
+ }
+
+ tab = await browser.tabs.create({
+ url: message.data.url,
+ cookieStoreId: message.data.cookieStoreId,
+ });
+ browser.test.sendMessage("tabCreated", tab.id);
+ browser.test.assertEq(
+ message.data.cookieStoreId,
+ tab.cookieStoreId,
+ "Created tab is associated with the expected cookieStoreId"
+ );
+ });
+ },
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies", "", "tabHide"],
+ },
+ async background() {
+ const [restrictedTab, unrestrictedTab] = await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => resolve(message));
+ });
+
+ // Check that print preview method fails
+ await browser.test.assertRejects(
+ browser.tabs.printPreview(),
+ /Cannot access activeTab/,
+ "should refuse to print a preview of the tab for the container which doesn't have permission"
+ );
+
+ // Check that save As PDF method fails
+ await browser.test.assertRejects(
+ browser.tabs.saveAsPDF({}),
+ /Cannot access activeTab/,
+ "should refuse to save as PDF of the tab for the container which doesn't have permission"
+ );
+
+ // Check that create method fails
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-container-1" }),
+ /Cannot access firefox-container-1/,
+ "should refuse to create container tab for the container which doesn't have permission"
+ );
+
+ // Check that detect language method fails
+ await browser.test.assertRejects(
+ browser.tabs.detectLanguage(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to detect language of a tab for the container which doesn't have permission"
+ );
+
+ // Check that move tabs method fails
+ await browser.test.assertRejects(
+ browser.tabs.move(restrictedTab, { index: 1 }),
+ /Invalid tab ID/,
+ "should refuse to move tab for the container which doesn't have permission"
+ );
+
+ // Check that duplicate method fails.
+ await browser.test.assertRejects(
+ browser.tabs.duplicate(restrictedTab),
+ /Invalid tab ID:/,
+ "should refuse to duplicate tab for the container which doesn't have permission"
+ );
+
+ // Check that captureTab method fails.
+ await browser.test.assertRejects(
+ browser.tabs.captureTab(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to capture the tab for the container which doesn't have permission"
+ );
+
+ // Check that discard method fails.
+ await browser.test.assertRejects(
+ browser.tabs.discard([restrictedTab]),
+ /Invalid tab ID/,
+ "should refuse to discard the tab for the container which doesn't have permission "
+ );
+
+ // Check that get method fails.
+ await browser.test.assertRejects(
+ browser.tabs.get(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to get the tab for the container which doesn't have permissiond"
+ );
+
+ // Check that highlight method fails.
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ populate: true, tabs: [restrictedTab] }),
+ `No tab at index: ${restrictedTab}`,
+ "should refuse to highlight the tab for the container which doesn't have permission"
+ );
+
+ // Test for moveInSuccession method of tab
+
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([restrictedTab]),
+ /Invalid tab ID/,
+ "should refuse to moveInSuccession for the container which doesn't have permission"
+ );
+
+ // Check that executeScript method fails.
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(restrictedTab, { frameId: 0 }),
+ /Invalid tab ID/,
+ "should refuse to execute a script of the tab for the container which doesn't have permission"
+ );
+
+ // Check that getZoom method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.getZoom(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to zoom the tab for the container which doesn't have permission"
+ );
+
+ // Check that getZoomSetting method fails.
+ await browser.test.assertRejects(
+ browser.tabs.getZoomSettings(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse the setting of zoom of the tab for the container which doesn't have permission"
+ );
+
+ //Test for hide method of tab
+ await browser.test.assertRejects(
+ browser.tabs.hide(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to hide a tab for the container which doesn't have permission"
+ );
+
+ // Check that insertCSS method fails.
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(restrictedTab, { frameId: 0 }),
+ /Invalid tab ID/,
+ "should refuse to insert a stylesheet to the tab for the container which doesn't have permission"
+ );
+
+ // Check that removeCSS method fails.
+ await browser.test.assertRejects(
+ browser.tabs.removeCSS(restrictedTab, { frameId: 0 }),
+ /Invalid tab ID/,
+ "should refuse to remove a stylesheet to the tab for the container which doesn't have permission"
+ );
+
+ //Test for show method of tab
+ await browser.test.assertRejects(
+ browser.tabs.show([restrictedTab]),
+ /Invalid tab ID/,
+ "should refuse to show the tab for the container which doesn't have permission"
+ );
+
+ // Check that toggleReaderMode method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.toggleReaderMode(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to toggle reader mode in the tab for the container which doesn't have permission"
+ );
+
+ // Check that setZoom method fails.
+ await browser.test.assertRejects(
+ browser.tabs.setZoom(restrictedTab, 0),
+ /Invalid tab ID/,
+ "should refuse to set zoom of the tab for the container which doesn't have permission"
+ );
+
+ // Check that setZoomSettings method fails.
+ await browser.test.assertRejects(
+ browser.tabs.setZoomSettings(restrictedTab, { defaultZoomFactor: 1 }),
+ /Invalid tab ID/,
+ "should refuse to set zoom setting of the tab for the container which doesn't have permission"
+ );
+
+ // Check that goBack method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.goBack(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to go back to the tab for the container which doesn't have permission"
+ );
+
+ // Check that goForward method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.goForward(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to go forward to the tab for the container which doesn't have permission"
+ );
+
+ // Check that update method fails.
+ await browser.test.assertRejects(
+ browser.tabs.update(restrictedTab, { highlighted: true }),
+ /Invalid tab ID/,
+ "should refuse to update the tab for the container which doesn't have permission"
+ );
+
+ // Check that reload method fails.
+ await browser.test.assertRejects(
+ browser.tabs.reload(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to reload tab for the container which doesn't have permission"
+ );
+
+ //Test for warmup method of tab
+ await browser.test.assertRejects(
+ browser.tabs.warmup(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to warmup a tab for the container which doesn't have permission"
+ );
+
+ let gettab = await browser.tabs.get(unrestrictedTab);
+ browser.test.assertEq(
+ gettab.cookieStoreId,
+ "firefox-container-2",
+ "get tab should open"
+ );
+
+ let lang = await browser.tabs.detectLanguage(unrestrictedTab);
+ await browser.test.assertEq(
+ "en",
+ lang,
+ "English document should be detected"
+ );
+
+ let duptab = await browser.tabs.duplicate(unrestrictedTab);
+
+ browser.test.assertEq(
+ duptab.cookieStoreId,
+ "firefox-container-2",
+ "duplicated tab should open"
+ );
+ await browser.tabs.remove(duptab.id);
+
+ let moved = await browser.tabs.move(unrestrictedTab, {
+ index: 0,
+ });
+ browser.test.assertEq(moved.length, 1, "move() returned no moved tab");
+
+ //Test for query method of tab
+ let tabs = await browser.tabs.query({
+ cookieStoreId: "firefox-container-1",
+ });
+ await browser.test.assertEq(
+ 0,
+ tabs.length,
+ "should not return restricted container's tab"
+ );
+
+ tabs = await browser.tabs.query({});
+ await browser.test.assertEq(
+ tabs
+ .map(tab => tab.cookieStoreId)
+ .sort()
+ .join(","),
+ "firefox-container-2,firefox-default",
+ "should return two tabs - firefox-default and firefox-container-2"
+ );
+
+ // Check that remove method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.remove([restrictedTab]),
+ /Invalid tab ID/,
+ "should refuse to remove tab for the container which doesn't have permission"
+ );
+
+ let removedPromise = new Promise(resolve => {
+ browser.tabs.onRemoved.addListener(tabId => {
+ browser.test.assertEq(unrestrictedTab, tabId, "expected remove tab");
+ resolve();
+ });
+ });
+ await browser.tabs.remove(unrestrictedTab);
+ await removedPromise;
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await helperExtension.startup();
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/",
+ cookieStoreId: "firefox-container-2",
+ },
+ });
+ const unrestrictedTab = await helperExtension.awaitMessage("tabCreated");
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/",
+ cookieStoreId: "firefox-container-1",
+ },
+ });
+ const restrictedTab = await helperExtension.awaitMessage("tabCreated");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.userContextIsolation.defaults.restricted", "[1]"]],
+ });
+
+ await extension.startup();
+ extension.sendMessage([restrictedTab, unrestrictedTab]);
+
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await helperExtension.unload();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
new file mode 100644
index 0000000000..27ea5d92bf
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
@@ -0,0 +1,328 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_setup(async function () {
+ // make sure userContext is enabled.
+ return SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+});
+
+add_task(async function () {
+ info("Start testing tabs.create with cookieStoreId");
+
+ let testCases = [
+ // No private window
+ {
+ privateTab: false,
+ cookieStoreId: null,
+ success: true,
+ expectedCookieStoreId: "firefox-default",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-default",
+ success: true,
+ expectedCookieStoreId: "firefox-default",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-container-1",
+ success: true,
+ expectedCookieStoreId: "firefox-container-1",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-container-2",
+ success: true,
+ expectedCookieStoreId: "firefox-container-2",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-container-42",
+ failure: "exist",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-private",
+ failure: "defaultToPrivate",
+ },
+ { privateTab: false, cookieStoreId: "wow", failure: "illegal" },
+
+ // Private window
+ {
+ privateTab: true,
+ cookieStoreId: null,
+ success: true,
+ expectedCookieStoreId: "firefox-private",
+ },
+ {
+ privateTab: true,
+ cookieStoreId: "firefox-private",
+ success: true,
+ expectedCookieStoreId: "firefox-private",
+ },
+ {
+ privateTab: true,
+ cookieStoreId: "firefox-default",
+ failure: "privateToDefault",
+ },
+ {
+ privateTab: true,
+ cookieStoreId: "firefox-container-1",
+ failure: "privateToDefault",
+ },
+ { privateTab: true, cookieStoreId: "wow", failure: "illegal" },
+ ];
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+
+ background: function () {
+ function testTab(data, tab) {
+ browser.test.assertTrue(data.success, "we want a success");
+ browser.test.assertTrue(!!tab, "we have a tab");
+ browser.test.assertEq(
+ data.expectedCookieStoreId,
+ tab.cookieStoreId,
+ "tab should have the correct cookieStoreId"
+ );
+ }
+
+ async function runTest(data) {
+ try {
+ // Tab Creation
+ let tab;
+ try {
+ tab = await browser.tabs.create({
+ windowId: data.privateTab
+ ? this.privateWindowId
+ : this.defaultWindowId,
+ cookieStoreId: data.cookieStoreId,
+ });
+
+ browser.test.assertTrue(!data.failure, "we want a success");
+ } catch (error) {
+ browser.test.assertTrue(!!data.failure, "we want a failure");
+
+ if (data.failure == "illegal") {
+ browser.test.assertEq(
+ `Illegal cookieStoreId: ${data.cookieStoreId}`,
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "defaultToPrivate") {
+ browser.test.assertEq(
+ "Illegal to set private cookieStoreId in a non-private window",
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "privateToDefault") {
+ browser.test.assertEq(
+ "Illegal to set non-private cookieStoreId in a private window",
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "exist") {
+ browser.test.assertEq(
+ `No cookie store exists with ID ${data.cookieStoreId}`,
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else {
+ browser.test.fail("The test is broken");
+ }
+
+ browser.test.sendMessage("test-done");
+ return;
+ }
+
+ // Tests for tab creation
+ testTab(data, tab);
+
+ {
+ // Tests for tab querying
+ let [tab] = await browser.tabs.query({
+ windowId: data.privateTab
+ ? this.privateWindowId
+ : this.defaultWindowId,
+ cookieStoreId: data.cookieStoreId,
+ });
+
+ browser.test.assertTrue(tab != undefined, "Tab found!");
+ testTab(data, tab);
+ }
+
+ let stores = await browser.cookies.getAllCookieStores();
+
+ let store = stores.find(store => store.id === tab.cookieStoreId);
+ browser.test.assertTrue(!!store, "We have a store for this tab.");
+ browser.test.assertTrue(
+ store.tabIds.includes(tab.id),
+ "tabIds includes this tab."
+ );
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.sendMessage("test-done");
+ } catch (e) {
+ browser.test.fail("An exception has been thrown");
+ }
+ }
+
+ async function initialize() {
+ let win = await browser.windows.create({ incognito: true });
+ this.privateWindowId = win.id;
+
+ win = await browser.windows.create({ incognito: false });
+ this.defaultWindowId = win.id;
+
+ browser.test.sendMessage("ready");
+ }
+
+ async function shutdown() {
+ await browser.windows.remove(this.privateWindowId);
+ await browser.windows.remove(this.defaultWindowId);
+ browser.test.sendMessage("gone");
+ }
+
+ // Waiting for messages
+ browser.test.onMessage.addListener((msg, data) => {
+ if (msg == "be-ready") {
+ initialize();
+ } else if (msg == "test") {
+ runTest(data);
+ } else {
+ browser.test.assertTrue("finish", msg, "Shutting down");
+ shutdown();
+ }
+ });
+ },
+ });
+
+ await extension.startup();
+
+ info("Tests must be ready...");
+ extension.sendMessage("be-ready");
+ await extension.awaitMessage("ready");
+ info("Tests are ready to run!");
+
+ for (let test of testCases) {
+ info(`test tab.create with cookieStoreId: "${test.cookieStoreId}"`);
+ extension.sendMessage("test", test);
+ await extension.awaitMessage("test-done");
+ }
+
+ info("Waiting for shutting down...");
+ extension.sendMessage("finish");
+ await extension.awaitMessage("gone");
+
+ await extension.unload();
+});
+
+add_task(async function userContext_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are currently disabled/,
+ "should refuse to open container tab when contextual identities are disabled"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function tabs_query_cookiestoreid_nocookiepermission() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let tab = await browser.tabs.create({});
+ browser.test.assertEq(
+ "firefox-default",
+ tab.cookieStoreId,
+ "Expecting cookieStoreId for new tab"
+ );
+ let query = await browser.tabs.query({
+ index: tab.index,
+ cookieStoreId: tab.cookieStoreId,
+ });
+ browser.test.assertEq(
+ "firefox-default",
+ query[0].cookieStoreId,
+ "Expecting cookieStoreId for new tab through browser.tabs.query"
+ );
+ await browser.tabs.remove(tab.id);
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function tabs_query_multiple_cookiestoreId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies"],
+ },
+
+ async background() {
+ let tab1 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-1",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab1.cookieStoreId}`);
+
+ let tab2 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-2",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab2.cookieStoreId}`);
+
+ let tab3 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-3",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab3.cookieStoreId}`);
+
+ let tabs = await browser.tabs.query({
+ cookieStoreId: ["firefox-container-1", "firefox-container-2"],
+ });
+
+ browser.test.assertEq(
+ 2,
+ tabs.length,
+ "Expecting tabs for firefox-container-1 and firefox-container-2"
+ );
+
+ browser.test.assertEq(
+ "firefox-container-1",
+ tabs[0].cookieStoreId,
+ "Expecting tab for firefox-container-1 cookieStoreId"
+ );
+
+ browser.test.assertEq(
+ "firefox-container-2",
+ tabs[1].cookieStoreId,
+ "Expecting tab forfirefox-container-2 cookieStoreId"
+ );
+
+ await browser.tabs.remove([tab1.id, tab2.id, tab3.id]);
+ browser.test.sendMessage("test-done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("test-done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId_private.js b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId_private.js
new file mode 100644
index 0000000000..556aa78288
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId_private.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function perma_private_browsing_mode() {
+ // make sure userContext is enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ Assert.equal(
+ Services.prefs.getBoolPref("browser.privatebrowsing.autostart"),
+ true,
+ "Permanent private browsing is enabled"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ let win = await browser.windows.create({});
+ browser.test.assertTrue(
+ win.incognito,
+ "New window should be private when perma-PBM is enabled."
+ );
+ await browser.test.assertRejects(
+ browser.tabs.create({
+ cookieStoreId: "firefox-container-1",
+ windowId: win.id,
+ }),
+ /Illegal to set non-private cookieStoreId in a private window/,
+ "should refuse to open container tab in private browsing window"
+ );
+ await browser.windows.remove(win.id);
+
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_create.js b/browser/components/extensions/test/browser/browser_ext_tabs_create.js
new file mode 100644
index 0000000000..a3b6e78331
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create.js
@@ -0,0 +1,299 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_create_options() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+ gBrowser.selectedTab = tab;
+
+ // TODO: Multiple windows.
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Using pre-loaded new tab pages interferes with onUpdated events.
+ // It probably shouldn't.
+ ["browser.newtab.preload", false],
+ // Some test cases below load http and check the behavior of https-first.
+ ["dom.security.https_first", true],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ background: { page: "bg/background.html" },
+ },
+
+ files: {
+ "bg/blank.html": ` `,
+
+ "bg/background.html": `
+
+
+ `,
+
+ "bg/background.js": function () {
+ let activeTab;
+ let activeWindow;
+
+ function runTests() {
+ const DEFAULTS = {
+ index: 2,
+ windowId: activeWindow,
+ active: true,
+ pinned: false,
+ url: "about:newtab",
+ // 'selected' is marked as unsupported in schema, so we've removed it.
+ // For more details, see bug 1337509
+ selected: undefined,
+ mutedInfo: {
+ muted: false,
+ extensionId: undefined,
+ reason: undefined,
+ },
+ };
+
+ let tests = [
+ {
+ create: { url: "https://example.com/" },
+ result: { url: "https://example.com/" },
+ },
+ {
+ create: { url: "view-source:https://example.com/" },
+ result: { url: "view-source:https://example.com/" },
+ },
+ {
+ create: { url: "blank.html" },
+ result: { url: browser.runtime.getURL("bg/blank.html") },
+ },
+ {
+ create: { url: "https://example.com/", openInReaderMode: true },
+ result: {
+ url: `about:reader?url=${encodeURIComponent(
+ "https://example.com/"
+ )}`,
+ },
+ },
+ {
+ create: {},
+ result: { url: "about:newtab" },
+ },
+ {
+ create: { active: false },
+ result: { active: false },
+ },
+ {
+ create: { active: true },
+ result: { active: true },
+ },
+ {
+ create: { pinned: true },
+ result: { pinned: true, index: 0 },
+ },
+ {
+ create: { pinned: true, active: true },
+ result: { pinned: true, active: true, index: 0 },
+ },
+ {
+ create: { pinned: true, active: false },
+ result: { pinned: true, active: false, index: 0 },
+ },
+ {
+ create: { index: 1 },
+ result: { index: 1 },
+ },
+ {
+ create: { index: 1, active: false },
+ result: { index: 1, active: false },
+ },
+ {
+ create: { windowId: activeWindow },
+ result: { windowId: activeWindow },
+ },
+ {
+ create: { index: 9999 },
+ result: { index: 2 },
+ },
+ {
+ // https-first redirects http to https.
+ create: { url: "http://example.com/" },
+ result: { url: "https://example.com/" },
+ },
+ {
+ // https-first redirects http to https.
+ create: { url: "view-source:http://example.com/" },
+ result: { url: "view-source:https://example.com/" },
+ },
+ {
+ // Despite https-first, the about:reader URL does not change.
+ create: { url: "http://example.com/", openInReaderMode: true },
+ result: {
+ url: `about:reader?url=${encodeURIComponent(
+ "http://example.com/"
+ )}`,
+ },
+ },
+ {
+ create: { muted: true },
+ result: {
+ mutedInfo: {
+ muted: true,
+ extensionId: browser.runtime.id,
+ reason: "extension",
+ },
+ },
+ },
+ {
+ create: { muted: false },
+ result: {
+ mutedInfo: {
+ muted: false,
+ extensionId: undefined,
+ reason: undefined,
+ },
+ },
+ },
+ ];
+
+ async function nextTest() {
+ if (!tests.length) {
+ browser.test.notifyPass("tabs.create");
+ return;
+ }
+
+ let test = tests.shift();
+ let expected = Object.assign({}, DEFAULTS, test.result);
+
+ browser.test.log(
+ `Testing tabs.create(${JSON.stringify(
+ test.create
+ )}), expecting ${JSON.stringify(test.result)}`
+ );
+
+ let updatedPromise = new Promise(resolve => {
+ let onUpdated = (changedTabId, changed) => {
+ if (changed.url) {
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ resolve({ tabId: changedTabId, url: changed.url });
+ }
+ };
+ browser.tabs.onUpdated.addListener(onUpdated);
+ });
+
+ let createdPromise = new Promise(resolve => {
+ let onCreated = tab => {
+ browser.test.assertTrue(
+ "id" in tab,
+ `Expected tabs.onCreated callback to receive tab object`
+ );
+ resolve();
+ };
+ browser.tabs.onCreated.addListener(onCreated);
+ });
+
+ let [tab] = await Promise.all([
+ browser.tabs.create(test.create),
+ createdPromise,
+ ]);
+ let tabId = tab.id;
+
+ for (let key of Object.keys(expected)) {
+ if (key === "url") {
+ // FIXME: This doesn't get updated until later in the load cycle.
+ continue;
+ }
+
+ if (key === "mutedInfo") {
+ for (let key of Object.keys(expected.mutedInfo)) {
+ browser.test.assertEq(
+ expected.mutedInfo[key],
+ tab.mutedInfo[key],
+ `Expected value for tab.mutedInfo.${key}`
+ );
+ }
+ } else {
+ browser.test.assertEq(
+ expected[key],
+ tab[key],
+ `Expected value for tab.${key}`
+ );
+ }
+ }
+
+ let updated = await updatedPromise;
+ browser.test.assertEq(
+ tabId,
+ updated.tabId,
+ `Expected value for tab.id`
+ );
+ browser.test.assertEq(
+ expected.url,
+ updated.url,
+ `Expected value for tab.url`
+ );
+
+ await browser.tabs.remove(tabId);
+ await browser.tabs.update(activeTab, { active: true });
+
+ nextTest();
+ }
+
+ nextTest();
+ }
+
+ browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+ activeTab = tabs[0].id;
+ activeWindow = tabs[0].windowId;
+
+ runTests();
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.create");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_create_with_popup() {
+ const extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let normalWin = await browser.windows.create();
+ let lastFocusedNormalWin = await browser.windows.getLastFocused({});
+ browser.test.assertEq(
+ lastFocusedNormalWin.id,
+ normalWin.id,
+ "The normal window is the last focused window."
+ );
+ let popupWin = await browser.windows.create({ type: "popup" });
+ let lastFocusedPopupWin = await browser.windows.getLastFocused({});
+ browser.test.assertEq(
+ lastFocusedPopupWin.id,
+ popupWin.id,
+ "The popup window is the last focused window."
+ );
+ let newtab = await browser.tabs.create({});
+ browser.test.assertEq(
+ normalWin.id,
+ newtab.windowId,
+ "New tab was created in last focused normal window."
+ );
+ await Promise.all([
+ browser.windows.remove(normalWin.id),
+ browser.windows.remove(popupWin.id),
+ ]);
+ browser.test.sendMessage("complete");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("complete");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js b/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js
new file mode 100644
index 0000000000..55bb33f26e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js
@@ -0,0 +1,79 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const FILE_URL = Services.io.newFileURI(
+ new FileUtils.File(getTestFilePath("file_dummy.html"))
+).spec;
+
+async function testTabsCreateInvalidURL(tabsCreateURL) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.test.sendMessage("ready");
+ browser.test.onMessage.addListener((msg, tabsCreateURL) => {
+ browser.tabs.create({ url: tabsCreateURL }, tab => {
+ browser.test.assertEq(
+ undefined,
+ tab,
+ "on error tab should be undefined"
+ );
+ browser.test.assertTrue(
+ /Illegal URL/.test(browser.runtime.lastError.message),
+ "runtime.lastError should report the expected error message"
+ );
+
+ // Remove the opened tab is any.
+ if (tab) {
+ browser.tabs.remove(tab.id);
+ }
+ browser.test.sendMessage("done");
+ });
+ });
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("ready");
+
+ info(`test tab.create on invalid URL "${tabsCreateURL}"`);
+
+ extension.sendMessage("start", tabsCreateURL);
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+}
+
+add_task(async function () {
+ info("Start testing tabs.create on invalid URLs");
+
+ let dataURLPage = `data:text/html,
+
+
+
+
+
+
+ data url page
+
+ `;
+
+ let testCases = [
+ { tabsCreateURL: "about:addons" },
+ {
+ tabsCreateURL: "javascript:console.log('tabs.update execute javascript')",
+ },
+ { tabsCreateURL: dataURLPage },
+ { tabsCreateURL: FILE_URL },
+ ];
+
+ for (let { tabsCreateURL } of testCases) {
+ await testTabsCreateInvalidURL(tabsCreateURL);
+ }
+
+ info("done");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_create_url.js b/browser/components/extensions/test/browser/browser_ext_tabs_create_url.js
new file mode 100644
index 0000000000..91cafa6e7e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create_url.js
@@ -0,0 +1,230 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function runWithDisabledPrivateBrowsing(callback) {
+ const { EnterprisePolicyTesting, PoliciesPrefTracker } =
+ ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+ );
+
+ PoliciesPrefTracker.start();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: { DisablePrivateBrowsing: true },
+ });
+
+ try {
+ await callback();
+ } finally {
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ EnterprisePolicyTesting.resetRunOnceState();
+ PoliciesPrefTracker.stop();
+ }
+}
+
+add_task(async function test_urlbar_focus() {
+ // Disable preloaded new tab because the urlbar is automatically focused when
+ // a preloaded new tab is opened, while this test is supposed to test that the
+ // implementation of tabs.create automatically focuses the urlbar of new tabs.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtab.preload", false]],
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.tabs.onUpdated.addListener(function onUpdated(_, info, tab) {
+ if (info.status === "complete" && tab.url !== "about:blank") {
+ browser.test.sendMessage("complete");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ }
+ });
+ browser.test.onMessage.addListener(async (cmd, ...args) => {
+ const result = await browser.tabs[cmd](...args);
+ browser.test.sendMessage("result", result);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ // Test content is focused after opening a regular url
+ extension.sendMessage("create", { url: "https://example.com" });
+ const [tab1] = await Promise.all([
+ extension.awaitMessage("result"),
+ extension.awaitMessage("complete"),
+ ]);
+
+ is(
+ document.activeElement.tagName,
+ "browser",
+ "Content focused after opening a web page"
+ );
+
+ extension.sendMessage("remove", tab1.id);
+ await extension.awaitMessage("result");
+
+ // Test urlbar is focused after opening an empty tab
+ extension.sendMessage("create", {});
+ const tab2 = await extension.awaitMessage("result");
+
+ const active = document.activeElement;
+ info(
+ `Active element: ${active.tagName}, id: ${active.id}, class: ${active.className}`
+ );
+
+ const parent = active.parentNode;
+ info(
+ `Parent element: ${parent.tagName}, id: ${parent.id}, class: ${parent.className}`
+ );
+
+ info(`After opening an empty tab, gURLBar.focused: ${gURLBar.focused}`);
+
+ is(active.tagName, "html:input", "Input element focused");
+ is(active.id, "urlbar-input", "Urlbar focused");
+
+ extension.sendMessage("remove", tab2.id);
+ await extension.awaitMessage("result");
+
+ await extension.unload();
+});
+
+add_task(async function default_url() {
+ const extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ function promiseNonBlankTab() {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId,
+ changeInfo,
+ tab
+ ) {
+ if (changeInfo.status === "complete" && tab.url !== "about:blank") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve(tab);
+ }
+ });
+ });
+ }
+
+ browser.test.onMessage.addListener(
+ async (msg, { incognito, expectedNewWindowUrl, expectedNewTabUrl }) => {
+ browser.test.assertEq(
+ "start",
+ msg,
+ `Start test, incognito=${incognito}`
+ );
+
+ let tabPromise = promiseNonBlankTab();
+ let win;
+ try {
+ win = await browser.windows.create({ incognito });
+ browser.test.assertEq(
+ 1,
+ win.tabs.length,
+ "Expected one tab in the new window."
+ );
+ } catch (e) {
+ browser.test.assertEq(
+ expectedNewWindowUrl,
+ e.message,
+ "Expected error"
+ );
+ browser.test.sendMessage("done");
+ return;
+ }
+ let tab = await tabPromise;
+ browser.test.assertEq(
+ expectedNewWindowUrl,
+ tab.url,
+ "Expected default URL of new window"
+ );
+
+ tabPromise = promiseNonBlankTab();
+ await browser.tabs.create({ windowId: win.id });
+ tab = await tabPromise;
+ browser.test.assertEq(
+ expectedNewTabUrl,
+ tab.url,
+ "Expected default URL of new tab"
+ );
+
+ await browser.windows.remove(win.id);
+ browser.test.sendMessage("done");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+
+ extension.sendMessage("start", {
+ incognito: false,
+ expectedNewWindowUrl: "about:home",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ extension.sendMessage("start", {
+ incognito: true,
+ expectedNewWindowUrl: "about:privatebrowsing",
+ expectedNewTabUrl: "about:privatebrowsing",
+ });
+ await extension.awaitMessage("done");
+
+ info("Testing with multiple homepages.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.startup.homepage", "about:robots|about:blank|about:home"]],
+ });
+ extension.sendMessage("start", {
+ incognito: false,
+ expectedNewWindowUrl: "about:robots",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ extension.sendMessage("start", {
+ incognito: true,
+ expectedNewWindowUrl: "about:privatebrowsing",
+ expectedNewTabUrl: "about:privatebrowsing",
+ });
+ await extension.awaitMessage("done");
+ await SpecialPowers.popPrefEnv();
+
+ info("Testing with perma-private browsing mode.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+ extension.sendMessage("start", {
+ incognito: false,
+ expectedNewWindowUrl: "about:home",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ extension.sendMessage("start", {
+ incognito: true,
+ expectedNewWindowUrl: "about:home",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ await SpecialPowers.popPrefEnv();
+
+ info("Testing with disabled private browsing mode.");
+ await runWithDisabledPrivateBrowsing(async () => {
+ extension.sendMessage("start", {
+ incognito: false,
+ expectedNewWindowUrl: "about:home",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ extension.sendMessage("start", {
+ incognito: true,
+ expectedNewWindowUrl:
+ "`incognito` cannot be used if incognito mode is disabled",
+ });
+ await extension.awaitMessage("done");
+ });
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_discard.js b/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
new file mode 100644
index 0000000000..074a0f4ce1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
@@ -0,0 +1,98 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global gBrowser */
+"use strict";
+
+add_task(async function test_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({ currentWindow: true });
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ async function finishTest() {
+ try {
+ await browser.tabs.discard(tabs[0].id);
+ await browser.tabs.discard(tabs[2].id);
+ browser.test.succeed(
+ "attempting to discard an already discarded tab or the active tab should not throw error"
+ );
+ } catch (e) {
+ browser.test.fail(
+ "attempting to discard an already discarded tab or the active tab should not throw error"
+ );
+ }
+ let discardedTab = await browser.tabs.get(tabs[2].id);
+ browser.test.assertEq(
+ false,
+ discardedTab.discarded,
+ "attempting to discard the active tab should not have succeeded"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.discard(999999999),
+ /Invalid tab ID/,
+ "attempt to discard invalid tabId should throw"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.discard([999999999, tabs[1].id]),
+ /Invalid tab ID/,
+ "attempt to discard a valid and invalid tabId should throw"
+ );
+ discardedTab = await browser.tabs.get(tabs[1].id);
+ browser.test.assertEq(
+ false,
+ discardedTab.discarded,
+ "tab is still not discarded"
+ );
+
+ browser.test.notifyPass("test-finished");
+ }
+
+ browser.tabs.onUpdated.addListener(async function (tabId, updatedInfo) {
+ if ("discarded" in updatedInfo) {
+ browser.test.assertEq(
+ tabId,
+ tabs[0].id,
+ "discarding tab triggered onUpdated"
+ );
+ let discardedTab = await browser.tabs.get(tabs[0].id);
+ browser.test.assertEq(
+ true,
+ discardedTab.discarded,
+ "discarded tab discard property"
+ );
+
+ await finishTest();
+ }
+ });
+
+ browser.tabs.discard(tabs[0].id);
+ },
+ });
+
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.browsers[0],
+ "http://example.com"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.browsers[0]);
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+
+ await extension.startup();
+
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_discard_reversed.js b/browser/components/extensions/test/browser/browser_ext_tabs_discard_reversed.js
new file mode 100644
index 0000000000..5fad30a6fb
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discard_reversed.js
@@ -0,0 +1,129 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function tabs_discarded_load_and_discard() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+ async background() {
+ browser.test.sendMessage("ready");
+ const SHIP = await new Promise(resolve =>
+ browser.test.onMessage.addListener((msg, data) => {
+ resolve(data);
+ })
+ );
+
+ const PAGE_URL_BEFORE = "http://example.com/initiallyDiscarded";
+ const PAGE_URL =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+ // Tabs without titles default to URLs without scheme, according to the
+ // logic of tabbrowser.js's setTabTitle/_setTabLabel.
+ // TODO bug 1695512: discarded tabs should also follow this logic instead
+ // of using the unmodified original URL.
+ const PAGE_TITLE_BEFORE = PAGE_URL_BEFORE;
+ const PAGE_TITLE_INITIAL = PAGE_URL.replace("http://", "");
+ const PAGE_TITLE = "Dummy test page";
+
+ function assertDeepEqual(expected, actual, message) {
+ browser.test.assertDeepEq(expected, actual, message);
+ }
+
+ let tab = await browser.tabs.create({
+ url: PAGE_URL_BEFORE,
+ discarded: true,
+ });
+ const TAB_ID = tab.id;
+ browser.test.assertTrue(tab.discarded, "Tab initially discarded");
+ browser.test.assertEq(PAGE_URL_BEFORE, tab.url, "Initial URL");
+ browser.test.assertEq(PAGE_TITLE_BEFORE, tab.title, "Initial title");
+
+ const observedChanges = {
+ discarded: [],
+ title: [],
+ url: [],
+ };
+ function tabsOnUpdatedAfterLoad(tabId, changeInfo, tab) {
+ browser.test.assertEq(TAB_ID, tabId, "tabId for tabs.onUpdated");
+ for (let [prop, value] of Object.entries(changeInfo)) {
+ observedChanges[prop].push(value);
+ }
+ }
+ browser.tabs.onUpdated.addListener(tabsOnUpdatedAfterLoad, {
+ properties: ["discarded", "url", "title"],
+ });
+
+ // Load new URL to transition from discarded:true to discarded:false.
+ await new Promise(resolve => {
+ browser.webNavigation.onCompleted.addListener(details => {
+ browser.test.assertEq(TAB_ID, details.tabId, "onCompleted for tab");
+ browser.test.assertEq(PAGE_URL, details.url, "URL ater load");
+ resolve();
+ });
+ browser.tabs.update(TAB_ID, { url: PAGE_URL });
+ });
+ assertDeepEqual(
+ [false],
+ observedChanges.discarded,
+ "changes to tab.discarded after update"
+ );
+ // TODO bug 1669356: the tabs.onUpdated events should only see the
+ // requested URL and its title. However, the current implementation
+ // reports several events (including url/title "changes") as part of
+ // "restoring" the lazy browser prior to loading the requested URL.
+
+ let expectedUrlChanges = [PAGE_URL_BEFORE, PAGE_URL];
+ if (SHIP && observedChanges.url.length === 1) {
+ // Except when SHIP is enabled, which turns this into a race,
+ // so sometimes only the final URL is seen (see bug 1696815#c22).
+ expectedUrlChanges = [PAGE_URL];
+ }
+
+ assertDeepEqual(
+ expectedUrlChanges,
+ observedChanges.url,
+ "changes to tab.url after update"
+ );
+ assertDeepEqual(
+ [PAGE_TITLE_INITIAL, PAGE_TITLE],
+ observedChanges.title,
+ "changes to tab.title after update"
+ );
+
+ tab = await browser.tabs.get(TAB_ID);
+ browser.test.assertFalse(tab.discarded, "tab.discarded after load");
+ browser.test.assertEq(PAGE_URL, tab.url, "tab.url after load");
+ browser.test.assertEq(PAGE_TITLE, tab.title, "tab.title after load");
+
+ // Reset counters.
+ observedChanges.discarded.length = 0;
+ observedChanges.title.length = 0;
+ observedChanges.url.length = 0;
+
+ // Transition from discarded:false to discarded:true
+ await browser.tabs.discard(TAB_ID);
+ assertDeepEqual(
+ [true],
+ observedChanges.discarded,
+ "changes to tab.discarded after discard"
+ );
+ assertDeepEqual([], observedChanges.url, "tab.url not changed");
+ assertDeepEqual([], observedChanges.title, "tab.title not changed");
+
+ tab = await browser.tabs.get(TAB_ID);
+ browser.test.assertTrue(tab.discarded, "tab.discarded after discard");
+ browser.test.assertEq(PAGE_URL, tab.url, "tab.url after discard");
+ browser.test.assertEq(PAGE_TITLE, tab.title, "tab.title after discard");
+
+ await browser.tabs.remove(TAB_ID);
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ extension.sendMessage("SHIP", Services.appinfo.sessionHistoryInParent);
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js b/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
new file mode 100644
index 0000000000..48c57b5a05
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
@@ -0,0 +1,386 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global gBrowser SessionStore */
+"use strict";
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+let lazyTabState = {
+ entries: [
+ {
+ url: "http://example.com/",
+ triggeringPrincipal_base64,
+ title: "Example Domain",
+ },
+ ],
+};
+
+add_task(async function test_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+
+ background() {
+ browser.webNavigation.onCompleted.addListener(
+ async details => {
+ browser.test.log(`webNav onCompleted received for ${details.tabId}`);
+ let updatedTab = await browser.tabs.get(details.tabId);
+ browser.test.assertEq(
+ false,
+ updatedTab.discarded,
+ "lazy to non-lazy update discard property"
+ );
+ browser.test.notifyPass("test-finished");
+ },
+ { url: [{ hostContains: "example.com" }] }
+ );
+
+ browser.tabs.onCreated.addListener(function (tab) {
+ browser.test.assertEq(
+ true,
+ tab.discarded,
+ "non-lazy tab onCreated discard property"
+ );
+ browser.tabs.update(tab.id, { active: true });
+ });
+ },
+ });
+
+ await extension.startup();
+
+ let testTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ createLazyBrowser: true,
+ });
+ SessionStore.setTabState(testTab, lazyTabState);
+
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(testTab);
+});
+
+// Regression test for Bug 1819794.
+add_task(async function test_create_discarded_with_cookieStoreId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextualIdentities", "cookies"],
+ },
+ async background() {
+ const [{ cookieStoreId }] = await browser.contextualIdentities.query({});
+ browser.test.assertEq(
+ "firefox-container-1",
+ cookieStoreId,
+ "Got expected cookieStoreId"
+ );
+ await browser.tabs.create({
+ url: `http://example.com/#${cookieStoreId}`,
+ cookieStoreId,
+ discarded: true,
+ });
+ await browser.tabs.create({
+ url: `http://example.com/#no-container`,
+ discarded: true,
+ });
+ },
+ // Needed by ExtensionSettingsStore (as a side-effect of contextualIdentities permission).
+ useAddonManager: "temporary",
+ });
+
+ const tabContainerPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "TabOpen",
+ false,
+ evt => {
+ return evt.target.getAttribute("usercontextid", "1");
+ }
+ ).then(evt => evt.target);
+ const tabDefaultPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "TabOpen",
+ false,
+ evt => {
+ return !evt.target.hasAttribute("usercontextid");
+ }
+ ).then(evt => evt.target);
+
+ await extension.startup();
+
+ const tabContainer = await tabContainerPromise;
+ ok(
+ tabContainer.hasAttribute("pending"),
+ "new container tab should be discarded"
+ );
+ const tabContainerState = SessionStore.getTabState(tabContainer);
+ is(
+ JSON.parse(tabContainerState).userContextId,
+ 1,
+ `Expect a userContextId associated to the new discarded container tab: ${tabContainerState}`
+ );
+
+ const tabDefault = await tabDefaultPromise;
+ ok(
+ tabDefault.hasAttribute("pending"),
+ "new non-container tab should be discarded"
+ );
+ const tabDefaultState = SessionStore.getTabState(tabDefault);
+ is(
+ JSON.parse(tabDefaultState).userContextId,
+ 0,
+ `Expect userContextId 0 associated to the new discarded non-container tab: ${tabDefaultState}`
+ );
+
+ BrowserTestUtils.removeTab(tabContainer);
+ BrowserTestUtils.removeTab(tabDefault);
+ await extension.unload();
+});
+
+// If discard is called immediately after creating a new tab, the new tab may not have loaded,
+// and the sessionstore for that tab is not ready for discarding. The result was a corrupted
+// sessionstore for the tab, which when the tab was activated, resulted in a tab with partial
+// state.
+add_task(async function test_create_then_discard() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+
+ background: async function () {
+ let createdTab;
+
+ browser.tabs.onUpdated.addListener((tabId, updatedInfo) => {
+ if (!updatedInfo.discarded) {
+ return;
+ }
+
+ browser.webNavigation.onCompleted.addListener(
+ async details => {
+ browser.test.assertEq(
+ createdTab.id,
+ details.tabId,
+ "created tab navigation is completed"
+ );
+ let activeTab = await browser.tabs.get(details.tabId);
+ browser.test.assertEq(
+ "http://example.com/",
+ details.url,
+ "created tab url is correct"
+ );
+ browser.test.assertEq(
+ "http://example.com/",
+ activeTab.url,
+ "created tab url is correct"
+ );
+ browser.tabs.remove(details.tabId);
+ browser.test.notifyPass("test-finished");
+ },
+ { url: [{ hostContains: "example.com" }] }
+ );
+
+ browser.tabs.update(tabId, { active: true });
+ });
+
+ createdTab = await browser.tabs.create({
+ url: "http://example.com/",
+ active: false,
+ });
+ browser.tabs.discard(createdTab.id);
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
+
+add_task(async function test_create_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+
+ background() {
+ let tabOpts = {
+ url: "http://example.com/",
+ active: false,
+ discarded: true,
+ title: "discarded tab",
+ };
+
+ browser.webNavigation.onCompleted.addListener(
+ async details => {
+ let activeTab = await browser.tabs.get(details.tabId);
+ browser.test.assertEq(
+ tabOpts.url,
+ activeTab.url,
+ "restored tab url matches active tab url"
+ );
+ browser.test.assertEq(
+ "mochitest index /",
+ activeTab.title,
+ "restored tab title is correct"
+ );
+ browser.tabs.remove(details.tabId);
+ browser.test.notifyPass("test-finished");
+ },
+ { url: [{ hostContains: "example.com" }] }
+ );
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.assertEq(
+ tabOpts.active,
+ tab.active,
+ "lazy tab is not active"
+ );
+ browser.test.assertEq(
+ tabOpts.discarded,
+ tab.discarded,
+ "lazy tab is discarded"
+ );
+ browser.test.assertEq(tabOpts.url, tab.url, "lazy tab url is correct");
+ browser.test.assertEq(
+ tabOpts.title,
+ tab.title,
+ "lazy tab title is correct"
+ );
+ browser.tabs.update(tab.id, { active: true });
+ });
+
+ browser.tabs.create(tabOpts);
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
+
+add_task(async function test_discarded_private_tab_restored() {
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+
+ background() {
+ let isDiscarding = false;
+ browser.tabs.onUpdated.addListener(
+ async function listener(tabId, changeInfo, tab) {
+ const { active, discarded, incognito } = tab;
+ if (!incognito || active || discarded || isDiscarding) {
+ return;
+ }
+ // Remove the onUpdated listener to prevent intermittent failure
+ // to be hit if the listener gets called again for unrelated
+ // tabs.onUpdated events that may get fired after the test case got
+ // the tab-discarded test message that was expecting.
+ isDiscarding = true;
+ browser.tabs.onUpdated.removeListener(listener);
+ browser.test.log(
+ `Test extension discarding ${tabId}: ${JSON.stringify(changeInfo)}`
+ );
+ await browser.tabs.discard(tabId);
+ browser.test.sendMessage("tab-discarded");
+ },
+ { properties: ["status"] }
+ );
+ },
+ });
+
+ // Open a private browsing window.
+ const privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await extension.startup();
+
+ const newTab = await BrowserTestUtils.addTab(
+ privateWin.gBrowser,
+ "https://example.com/"
+ );
+ await extension.awaitMessage("tab-discarded");
+ is(newTab.getAttribute("pending"), "true", "private tab should be discarded");
+
+ const promiseTabLoaded = BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+
+ info("Switching to the discarded background tab");
+ await BrowserTestUtils.switchTab(privateWin.gBrowser, newTab);
+
+ info("Wait for the restored tab to complete loading");
+ await promiseTabLoaded;
+ is(
+ newTab.hasAttribute("pending"),
+ false,
+ "discarded private tab should have been restored"
+ );
+
+ is(
+ newTab.linkedBrowser.currentURI.spec,
+ "https://example.com/",
+ "Got the expected url on the restored tab"
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function test_update_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", ""],
+ },
+
+ background() {
+ browser.test.onMessage.addListener(async msg => {
+ let [tab] = await browser.tabs.query({ url: "http://example.com/" });
+ if (msg == "update") {
+ await browser.tabs.update(tab.id, { url: "https://example.com/" });
+ } else {
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ }
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let lazyTab = BrowserTestUtils.addTab(gBrowser, "http://example.com/", {
+ createLazyBrowser: true,
+ lazyTabTitle: "Example Domain",
+ });
+
+ let tabBrowserInsertedPromise = BrowserTestUtils.waitForEvent(
+ lazyTab,
+ "TabBrowserInserted"
+ );
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ /Lazy browser prematurely inserted via 'loadURI' property access:/,
+ forbid: true,
+ },
+ ]);
+ });
+
+ extension.sendMessage("update");
+ await tabBrowserInsertedPromise;
+
+ await BrowserTestUtils.waitForBrowserStateChange(
+ lazyTab.linkedBrowser,
+ "https://example.com/",
+ stateFlags => {
+ return (
+ stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
+ stateFlags & Ci.nsIWebProgressListener.STATE_STOP
+ );
+ }
+ );
+
+ await TestUtils.waitForTick();
+ BrowserTestUtils.removeTab(lazyTab);
+
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js b/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
new file mode 100644
index 0000000000..50c56ea796
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
@@ -0,0 +1,316 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testDuplicateTab() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let [source] = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+
+ let tab = await browser.tabs.duplicate(source.id);
+
+ browser.test.assertEq(
+ "http://example.net/",
+ tab.url,
+ "duplicated tab should have the same URL as the source tab"
+ );
+ browser.test.assertEq(
+ source.index + 1,
+ tab.index,
+ "duplicated tab should open next to the source tab"
+ );
+ browser.test.assertTrue(
+ tab.active,
+ "duplicated tab should be active by default"
+ );
+
+ await browser.tabs.remove([source.id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate");
+ await extension.unload();
+});
+
+add_task(async function testDuplicateTabLazily() {
+ async function background() {
+ let tabLoadComplete = new Promise(resolve => {
+ browser.test.onMessage.addListener((message, tabId, result) => {
+ if (message == "duplicate-tab-done") {
+ resolve(tabId);
+ }
+ });
+ });
+
+ function awaitLoad(tabId) {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId_,
+ changed,
+ tab
+ ) {
+ if (tabId == tabId_ && changed.status == "complete") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ }
+
+ try {
+ let url =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+ let tab = await browser.tabs.create({ url });
+ let startTabId = tab.id;
+
+ await awaitLoad(startTabId);
+ browser.test.sendMessage("duplicate-tab", startTabId);
+
+ let unloadedTabId = await tabLoadComplete;
+ let loadedtab = await browser.tabs.get(startTabId);
+ browser.test.assertEq(
+ "Dummy test page",
+ loadedtab.title,
+ "Title should be returned for loaded pages"
+ );
+ browser.test.assertEq(
+ "complete",
+ loadedtab.status,
+ "Tab status should be complete for loaded pages"
+ );
+
+ let unloadedtab = await browser.tabs.get(unloadedTabId);
+ browser.test.assertEq(
+ "Dummy test page",
+ unloadedtab.title,
+ "Title should be returned after page has been unloaded"
+ );
+
+ await browser.tabs.remove([tab.id, unloadedTabId]);
+ browser.test.notifyPass("tabs.hasCorrectTabTitle");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tabs.hasCorrectTabTitle");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ extension.onMessage("duplicate-tab", tabId => {
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let tab = tabTracker.getTab(tabId);
+ // This is a bit of a hack to load a tab in the background.
+ let newTab = gBrowser.duplicateTab(tab, true, { skipLoad: true });
+
+ BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(
+ () => {
+ extension.sendMessage("duplicate-tab-done", tabTracker.getId(newTab));
+ }
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.hasCorrectTabTitle");
+ await extension.unload();
+});
+
+add_task(async function testDuplicatePinnedTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ gBrowser.pinTab(tab);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let [source] = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+ let tab = await browser.tabs.duplicate(source.id);
+
+ browser.test.assertEq(
+ source.index + 1,
+ tab.index,
+ "duplicated tab should open next to the source tab"
+ );
+ browser.test.assertFalse(
+ tab.pinned,
+ "duplicated tab should not be pinned by default, even if source tab is"
+ );
+
+ await browser.tabs.remove([source.id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.pinned");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.pinned");
+ await extension.unload();
+});
+
+add_task(async function testDuplicateTabInBackground() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+ let tab = await browser.tabs.duplicate(tabs[0].id, { active: false });
+ // Should not be the active tab
+ browser.test.assertFalse(tab.active);
+
+ await browser.tabs.remove([tabs[0].id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.background");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.background");
+ await extension.unload();
+});
+
+add_task(async function testDuplicateTabAtIndex() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+ let tab = await browser.tabs.duplicate(tabs[0].id, { index: 0 });
+ browser.test.assertEq(0, tab.index);
+
+ await browser.tabs.remove([tabs[0].id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.index");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.index");
+ await extension.unload();
+});
+
+add_task(async function testDuplicatePinnedTabAtIncorrectIndex() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ gBrowser.pinTab(tab);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+ let tab = await browser.tabs.duplicate(tabs[0].id, { index: 0 });
+ browser.test.assertEq(1, tab.index);
+ browser.test.assertFalse(
+ tab.pinned,
+ "Duplicated tab should not be pinned"
+ );
+
+ await browser.tabs.remove([tabs[0].id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.incorrect_index");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.incorrect_index");
+ await extension.unload();
+});
+
+add_task(async function testDuplicateResolvePromiseRightAway() {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_slowed_document.sjs"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ // The host permission matches the above URL. No :8888 due to bug 1468162.
+ permissions: ["tabs", "http://mochi.test/"],
+ },
+
+ background: async function () {
+ let [source] = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+
+ let resolvedRightAway = true;
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo, tab) => {
+ resolvedRightAway = false;
+ },
+ { urls: [source.url] }
+ );
+
+ let tab = await browser.tabs.duplicate(source.id);
+ // if the promise is resolved before any onUpdated event has been fired,
+ // then the promise has been resolved before waiting for the tab to load
+ browser.test.assertTrue(
+ resolvedRightAway,
+ "tabs.duplicate() should resolve as soon as possible"
+ );
+
+ // Regression test for bug 1559216
+ let code = "document.URL";
+ let [result] = await browser.tabs.executeScript(tab.id, { code });
+ browser.test.assertEq(
+ source.url,
+ result,
+ "APIs such as tabs.executeScript should be queued until tabs.duplicate has restored the tab"
+ );
+
+ await browser.tabs.remove([source.id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.resolvePromiseRightAway");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.resolvePromiseRightAway");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_events.js b/browser/components/extensions/test/browser/browser_ext_tabs_events.js
new file mode 100644
index 0000000000..fe9317b4a6
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_events.js
@@ -0,0 +1,794 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// A single monitor for the tests. If it receives any
+// incognito data in event listeners it will fail.
+let monitor;
+add_task(async function startup() {
+ monitor = await startIncognitoMonitorExtension();
+});
+registerCleanupFunction(async function finish() {
+ await monitor.unload();
+});
+
+// Test tab events from private windows, the monitor above will fail
+// if it receives any.
+add_task(async function test_tab_events_incognito_monitored() {
+ async function background() {
+ let incognito = true;
+ let events = [];
+ let eventPromise;
+ let checkEvents = () => {
+ if (eventPromise && events.length >= eventPromise.names.length) {
+ eventPromise.resolve();
+ }
+ };
+
+ browser.tabs.onCreated.addListener(tab => {
+ events.push({ type: "onCreated", tab });
+ checkEvents();
+ });
+
+ browser.tabs.onAttached.addListener((tabId, info) => {
+ events.push(Object.assign({ type: "onAttached", tabId }, info));
+ checkEvents();
+ });
+
+ browser.tabs.onDetached.addListener((tabId, info) => {
+ events.push(Object.assign({ type: "onDetached", tabId }, info));
+ checkEvents();
+ });
+
+ browser.tabs.onRemoved.addListener((tabId, info) => {
+ events.push(Object.assign({ type: "onRemoved", tabId }, info));
+ checkEvents();
+ });
+
+ browser.tabs.onMoved.addListener((tabId, info) => {
+ events.push(Object.assign({ type: "onMoved", tabId }, info));
+ checkEvents();
+ });
+
+ async function expectEvents(names) {
+ browser.test.log(`Expecting events: ${names.join(", ")}`);
+
+ await new Promise(resolve => {
+ eventPromise = { names, resolve };
+ checkEvents();
+ });
+
+ browser.test.assertEq(
+ names.length,
+ events.length,
+ "Got expected number of events"
+ );
+ for (let [i, name] of names.entries()) {
+ browser.test.assertEq(
+ name,
+ i in events && events[i].type,
+ `Got expected ${name} event`
+ );
+ }
+ return events.splice(0);
+ }
+
+ try {
+ let firstWindow = await browser.windows.create({
+ url: "about:blank",
+ incognito,
+ });
+ let otherWindow = await browser.windows.create({
+ url: "about:blank",
+ incognito,
+ });
+
+ let windowId = firstWindow.id;
+ let otherWindowId = otherWindow.id;
+
+ // Wait for a tab in each window
+ await expectEvents(["onCreated", "onCreated"]);
+ let initialTab = (
+ await browser.tabs.query({
+ active: true,
+ windowId: otherWindowId,
+ })
+ )[0];
+
+ browser.test.log("Create tab in window 1");
+ let tab = await browser.tabs.create({
+ windowId,
+ index: 0,
+ url: "about:blank",
+ });
+ let oldIndex = tab.index;
+ browser.test.assertEq(0, oldIndex, "Tab has the expected index");
+ browser.test.assertEq(tab.incognito, incognito, "Tab is incognito");
+
+ let [created] = await expectEvents(["onCreated"]);
+ browser.test.assertEq(tab.id, created.tab.id, "Got expected tab ID");
+ browser.test.assertEq(
+ oldIndex,
+ created.tab.index,
+ "Got expected tab index"
+ );
+
+ browser.test.log("Move tab to window 2");
+ await browser.tabs.move([tab.id], { windowId: otherWindowId, index: 0 });
+
+ let [detached, attached] = await expectEvents([
+ "onDetached",
+ "onAttached",
+ ]);
+ browser.test.assertEq(
+ tab.id,
+ detached.tabId,
+ "Expected onDetached tab ID"
+ );
+ browser.test.assertEq(
+ oldIndex,
+ detached.oldPosition,
+ "Expected old index"
+ );
+ browser.test.assertEq(
+ windowId,
+ detached.oldWindowId,
+ "Expected old window ID"
+ );
+
+ browser.test.assertEq(
+ tab.id,
+ attached.tabId,
+ "Expected onAttached tab ID"
+ );
+ browser.test.assertEq(0, attached.newPosition, "Expected new index");
+ browser.test.assertEq(
+ otherWindowId,
+ attached.newWindowId,
+ "Expected new window ID"
+ );
+
+ browser.test.log("Move tab within the same window");
+ let [moved] = await browser.tabs.move([tab.id], { index: 1 });
+ browser.test.assertEq(1, moved.index, "Expected new index");
+
+ [moved] = await expectEvents(["onMoved"]);
+ browser.test.assertEq(tab.id, moved.tabId, "Expected tab ID");
+ browser.test.assertEq(0, moved.fromIndex, "Expected old index");
+ browser.test.assertEq(1, moved.toIndex, "Expected new index");
+ browser.test.assertEq(
+ otherWindowId,
+ moved.windowId,
+ "Expected window ID"
+ );
+
+ browser.test.log("Remove tab");
+ await browser.tabs.remove(tab.id);
+ let [removed] = await expectEvents(["onRemoved"]);
+
+ browser.test.assertEq(
+ tab.id,
+ removed.tabId,
+ "Expected removed tab ID for tabs.remove"
+ );
+ browser.test.assertEq(
+ otherWindowId,
+ removed.windowId,
+ "Expected removed tab window ID"
+ );
+ // Note: We want to test for the actual boolean value false here.
+ browser.test.assertEq(
+ false,
+ removed.isWindowClosing,
+ "Expected isWindowClosing value"
+ );
+
+ browser.test.log("Close second window");
+ await browser.windows.remove(otherWindowId);
+ [removed] = await expectEvents(["onRemoved"]);
+ browser.test.assertEq(
+ initialTab.id,
+ removed.tabId,
+ "Expected removed tab ID for windows.remove"
+ );
+ browser.test.assertEq(
+ otherWindowId,
+ removed.windowId,
+ "Expected removed tab window ID"
+ );
+ browser.test.assertEq(
+ true,
+ removed.isWindowClosing,
+ "Expected isWindowClosing value"
+ );
+
+ browser.test.log("Create additional tab in window 1");
+ tab = await browser.tabs.create({ windowId, url: "about:blank" });
+ await expectEvents(["onCreated"]);
+ browser.test.assertEq(tab.incognito, incognito, "Tab is incognito");
+
+ browser.test.log("Create a new window, adopting the new tab");
+ // We have to explicitly wait for the event here, since its timing is
+ // not predictable.
+ let promiseAttached = new Promise(resolve => {
+ browser.tabs.onAttached.addListener(function listener(tabId) {
+ browser.tabs.onAttached.removeListener(listener);
+ resolve();
+ });
+ });
+
+ let [window] = await Promise.all([
+ browser.windows.create({ tabId: tab.id, incognito }),
+ promiseAttached,
+ ]);
+
+ [detached, attached] = await expectEvents(["onDetached", "onAttached"]);
+
+ browser.test.assertEq(
+ tab.id,
+ detached.tabId,
+ "Expected onDetached tab ID"
+ );
+ browser.test.assertEq(
+ 1,
+ detached.oldPosition,
+ "Expected onDetached old index"
+ );
+ browser.test.assertEq(
+ windowId,
+ detached.oldWindowId,
+ "Expected onDetached old window ID"
+ );
+
+ browser.test.assertEq(
+ tab.id,
+ attached.tabId,
+ "Expected onAttached tab ID"
+ );
+ browser.test.assertEq(
+ 0,
+ attached.newPosition,
+ "Expected onAttached new index"
+ );
+ browser.test.assertEq(
+ window.id,
+ attached.newWindowId,
+ "Expected onAttached new window id"
+ );
+
+ browser.test.log(
+ "Close the new window by moving the tab into former window"
+ );
+ await browser.tabs.move(tab.id, { index: 1, windowId });
+ [detached, attached] = await expectEvents(["onDetached", "onAttached"]);
+
+ browser.test.assertEq(
+ tab.id,
+ detached.tabId,
+ "Expected onDetached tab ID"
+ );
+ browser.test.assertEq(
+ 0,
+ detached.oldPosition,
+ "Expected onDetached old index"
+ );
+ browser.test.assertEq(
+ window.id,
+ detached.oldWindowId,
+ "Expected onDetached old window ID"
+ );
+
+ browser.test.assertEq(
+ tab.id,
+ attached.tabId,
+ "Expected onAttached tab ID"
+ );
+ browser.test.assertEq(
+ 1,
+ attached.newPosition,
+ "Expected onAttached new index"
+ );
+ browser.test.assertEq(
+ windowId,
+ attached.newWindowId,
+ "Expected onAttached new window id"
+ );
+ browser.test.assertEq(tab.incognito, incognito, "Tab is incognito");
+
+ browser.test.log("Remove the tab");
+ await browser.tabs.remove(tab.id);
+ browser.windows.remove(windowId);
+
+ browser.test.notifyPass("tabs-events");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tabs-events");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background,
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs-events");
+ await extension.unload();
+});
+
+add_task(async function testTabEventsSize() {
+ function background() {
+ function sendSizeMessages(tab, type) {
+ browser.test.sendMessage(`${type}-dims`, {
+ width: tab.width,
+ height: tab.height,
+ });
+ }
+
+ browser.tabs.onCreated.addListener(tab => {
+ sendSizeMessages(tab, "on-created");
+ });
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.status == "complete") {
+ sendSizeMessages(tab, "on-updated");
+ }
+ });
+
+ browser.test.onMessage.addListener(async (msg, arg) => {
+ if (msg === "create-tab") {
+ let tab = await browser.tabs.create({ url: "https://example.com/" });
+ sendSizeMessages(tab, "create");
+ browser.test.sendMessage("created-tab-id", tab.id);
+ } else if (msg === "update-tab") {
+ let tab = await browser.tabs.update(arg, {
+ url: "https://example.org/",
+ });
+ sendSizeMessages(tab, "update");
+ } else if (msg === "remove-tab") {
+ browser.tabs.remove(arg);
+ browser.test.sendMessage("tab-removed");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background,
+ });
+
+ const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
+ registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref(RESOLUTION_PREF);
+ });
+
+ function checkDimensions(dims, type) {
+ is(
+ dims.width,
+ gBrowser.selectedBrowser.clientWidth,
+ `tab from ${type} reports expected width`
+ );
+ is(
+ dims.height,
+ gBrowser.selectedBrowser.clientHeight,
+ `tab from ${type} reports expected height`
+ );
+ }
+
+ await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ for (let resolution of [2, 1]) {
+ SpecialPowers.setCharPref(RESOLUTION_PREF, String(resolution));
+ is(
+ window.devicePixelRatio,
+ resolution,
+ "window has the required resolution"
+ );
+
+ extension.sendMessage("create-tab");
+ let tabId = await extension.awaitMessage("created-tab-id");
+
+ checkDimensions(await extension.awaitMessage("create-dims"), "create");
+ checkDimensions(
+ await extension.awaitMessage("on-created-dims"),
+ "onCreated"
+ );
+ checkDimensions(
+ await extension.awaitMessage("on-updated-dims"),
+ "onUpdated"
+ );
+
+ extension.sendMessage("update-tab", tabId);
+
+ checkDimensions(await extension.awaitMessage("update-dims"), "update");
+ checkDimensions(
+ await extension.awaitMessage("on-updated-dims"),
+ "onUpdated"
+ );
+
+ extension.sendMessage("remove-tab", tabId);
+ await extension.awaitMessage("tab-removed");
+ }
+
+ await extension.unload();
+ SpecialPowers.clearUserPref(RESOLUTION_PREF);
+}).skip(); // Bug 1614075 perma-fail comparing devicePixelRatio
+
+add_task(async function testTabRemovalEvent() {
+ async function background() {
+ let events = [];
+
+ function awaitLoad(tabId) {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId_,
+ changed,
+ tab
+ ) {
+ if (tabId == tabId_ && changed.status == "complete") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ }
+
+ chrome.tabs.onRemoved.addListener((tabId, info) => {
+ browser.test.assertEq(
+ 0,
+ events.length,
+ "No events recorded before onRemoved."
+ );
+ events.push("onRemoved");
+ browser.test.log(
+ "Make sure the removed tab is not available in the tabs.query callback."
+ );
+ chrome.tabs.query({}, tabs => {
+ for (let tab of tabs) {
+ browser.test.assertTrue(
+ tab.id != tabId,
+ "Tab query should not include removed tabId"
+ );
+ }
+ });
+ });
+
+ try {
+ let url =
+ "https://example.com/browser/browser/components/extensions/test/browser/context.html";
+ let tab = await browser.tabs.create({ url: url });
+ await awaitLoad(tab.id);
+
+ chrome.tabs.onActivated.addListener(info => {
+ browser.test.assertEq(
+ 1,
+ events.length,
+ "One event recorded before onActivated."
+ );
+ events.push("onActivated");
+ browser.test.assertEq(
+ "onRemoved",
+ events[0],
+ "onRemoved fired before onActivated."
+ );
+ browser.test.notifyPass("tabs-events");
+ });
+
+ await browser.tabs.remove(tab.id);
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tabs-events");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs-events");
+ await extension.unload();
+});
+
+add_task(async function testTabCreateRelated() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.opentabfor.middleclick", true],
+ ["browser.tabs.insertRelatedAfterCurrent", true],
+ ],
+ });
+
+ async function background() {
+ let created;
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.log(`tabs.onCreated, index=${tab.index}`);
+ browser.test.assertEq(1, tab.index, "expecting tab index of 1");
+ created = tab.id;
+ });
+ browser.tabs.onMoved.addListener((id, info) => {
+ browser.test.log(
+ `tabs.onMoved, from ${info.fromIndex} to ${info.toIndex}`
+ );
+ browser.test.fail("tabMoved was received");
+ });
+ browser.tabs.onRemoved.addListener((tabId, info) => {
+ browser.test.assertEq(created, tabId, "removed id same as created");
+ browser.test.sendMessage("tabRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ // Create a *opener* tab page which has a link to "example.com".
+ let pageURL =
+ "https://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+ let openerTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ pageURL
+ );
+ gBrowser.moveTabTo(openerTab, 0);
+
+ await extension.startup();
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://example.com/#linkclick",
+ true
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#link_to_example_com",
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ let openTab = await newTabPromise;
+ is(
+ openTab.linkedBrowser.currentURI.spec,
+ "https://example.com/#linkclick",
+ "Middle click should open site to correct url."
+ );
+ BrowserTestUtils.removeTab(openTab);
+
+ await extension.awaitMessage("tabRemoved");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(openerTab);
+});
+
+add_task(async function testLastTabRemoval() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.closeWindowWithLastTab", false]],
+ });
+
+ async function background() {
+ let windowId;
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.assertEq(
+ windowId,
+ tab.windowId,
+ "expecting onCreated after onRemoved on the same window"
+ );
+ browser.test.sendMessage("tabCreated", `${tab.width}x${tab.height}`);
+ });
+ browser.tabs.onRemoved.addListener((tabId, info) => {
+ windowId = info.windowId;
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background,
+ });
+
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await extension.startup();
+
+ const oldBrowser = newWin.gBrowser.selectedBrowser;
+ const expectedDims = `${oldBrowser.clientWidth}x${oldBrowser.clientHeight}`;
+ BrowserTestUtils.removeTab(newWin.gBrowser.selectedTab);
+
+ const actualDims = await extension.awaitMessage("tabCreated");
+ is(
+ actualDims,
+ expectedDims,
+ "created tab reports a size same to the removed last tab"
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(newWin);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testTabActivationEvent() {
+ async function background() {
+ function makeExpectable() {
+ let expectation = null,
+ resolver = null;
+ const expectable = param => {
+ if (expectation === null) {
+ browser.test.fail("unexpected call to expectable");
+ } else {
+ try {
+ resolver(expectation(param));
+ } catch (e) {
+ resolver(Promise.reject(e));
+ } finally {
+ expectation = null;
+ }
+ }
+ };
+ expectable.expect = e => {
+ expectation = e;
+ return new Promise(r => {
+ resolver = r;
+ });
+ };
+ return expectable;
+ }
+ try {
+ const listener = makeExpectable();
+ browser.tabs.onActivated.addListener(listener);
+
+ const [
+ ,
+ {
+ tabs: [tab1],
+ },
+ ] = await Promise.all([
+ listener.expect(info => {
+ browser.test.assertEq(
+ undefined,
+ info.previousTabId,
+ "previousTabId should not be defined when window is first opened"
+ );
+ }),
+ browser.windows.create({ url: "about:blank" }),
+ ]);
+ const [, tab2] = await Promise.all([
+ listener.expect(info => {
+ browser.test.assertEq(
+ tab1.id,
+ info.previousTabId,
+ "Got expected previousTabId"
+ );
+ }),
+ browser.tabs.create({ url: "about:blank" }),
+ ]);
+
+ await Promise.all([
+ listener.expect(info => {
+ browser.test.assertEq(tab1.id, info.tabId, "Got expected tabId");
+ browser.test.assertEq(
+ tab2.id,
+ info.previousTabId,
+ "Got expected previousTabId"
+ );
+ }),
+ browser.tabs.update(tab1.id, { active: true }),
+ ]);
+
+ await Promise.all([
+ listener.expect(info => {
+ browser.test.assertEq(tab2.id, info.tabId, "Got expected tabId");
+ browser.test.assertEq(
+ undefined,
+ info.previousTabId,
+ "previousTabId should not be defined when previous tab was closed"
+ );
+ }),
+ browser.tabs.remove(tab1.id),
+ ]);
+
+ await browser.tabs.remove(tab2.id);
+
+ browser.test.notifyPass("tabs-events");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tabs-events");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs-events");
+ await extension.unload();
+});
+
+add_task(async function test_tabs_event_page() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@tabs" } },
+ permissions: ["tabs"],
+ background: { persistent: false },
+ },
+ background() {
+ const EVENTS = [
+ "onActivated",
+ "onAttached",
+ "onDetached",
+ "onRemoved",
+ "onMoved",
+ "onHighlighted",
+ "onUpdated",
+ ];
+ browser.tabs.onCreated.addListener(() => {
+ browser.test.sendMessage("onCreated");
+ });
+ for (let event of EVENTS) {
+ browser.tabs[event].addListener(() => {});
+ }
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = [
+ "onActivated",
+ "onAttached",
+ "onCreated",
+ "onDetached",
+ "onRemoved",
+ "onMoved",
+ "onHighlighted",
+ "onUpdated",
+ ];
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "tabs", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "tabs", event, {
+ primed: true,
+ });
+ }
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onCreated");
+ ok(true, "persistent event woke background");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "tabs", event, {
+ primed: false,
+ });
+ }
+ await BrowserTestUtils.closeWindow(win);
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_events_order.js b/browser/components/extensions/test/browser/browser_ext_tabs_events_order.js
new file mode 100644
index 0000000000..8e86d72c90
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_events_order.js
@@ -0,0 +1,208 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function testTabEvents() {
+ async function background() {
+ /** The list of active tab ID's */
+ let tabIds = [];
+
+ /**
+ * Stores the events that fire for each tab.
+ *
+ * events {
+ * tabId1: [event1, event2, ...],
+ * tabId2: [event1, event2, ...],
+ * }
+ */
+ let events = {};
+
+ browser.tabs.onActivated.addListener(info => {
+ if (info.tabId in events) {
+ events[info.tabId].push("onActivated");
+ } else {
+ events[info.tabId] = ["onActivated"];
+ }
+ });
+
+ browser.tabs.onCreated.addListener(info => {
+ if (info.id in events) {
+ events[info.id].push("onCreated");
+ } else {
+ events[info.id] = ["onCreated"];
+ }
+ });
+
+ browser.tabs.onHighlighted.addListener(info => {
+ if (info.tabIds[0] in events) {
+ events[info.tabIds[0]].push("onHighlighted");
+ } else {
+ events[info.tabIds[0]] = ["onHighlighted"];
+ }
+ });
+
+ /**
+ * Asserts that the expected events are fired for the tab with id = tabId.
+ * The events associated to the specified tab are removed after this check is made.
+ *
+ * @param {number} tabId
+ * @param {Array} expectedEvents
+ */
+ async function expectEvents(tabId, expectedEvents) {
+ browser.test.log(`Expecting events: ${expectedEvents.join(", ")}`);
+
+ // Wait up to 5000 ms for the expected number of events.
+ for (
+ let i = 0;
+ i < 50 &&
+ (!events[tabId] || events[tabId].length < expectedEvents.length);
+ i++
+ ) {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ browser.test.assertEq(
+ expectedEvents.length,
+ events[tabId].length,
+ `Got expected number of events for ${tabId}`
+ );
+
+ for (let name of expectedEvents) {
+ browser.test.assertTrue(
+ events[tabId].includes(name),
+ `Got expected ${name} event`
+ );
+ }
+
+ if (expectedEvents.includes("onCreated")) {
+ browser.test.assertEq(
+ events[tabId].indexOf("onCreated"),
+ 0,
+ "onCreated happened first"
+ );
+ }
+
+ delete events[tabId];
+ }
+
+ /**
+ * Opens a new tab and asserts that the correct events are fired.
+ *
+ * @param {number} windowId
+ */
+ async function openTab(windowId) {
+ browser.test.assertEq(
+ 0,
+ Object.keys(events).length,
+ "No events remaining before testing openTab."
+ );
+
+ let tab = await browser.tabs.create({ windowId });
+
+ tabIds.push(tab.id);
+ browser.test.log(`Opened tab ${tab.id}`);
+
+ await expectEvents(tab.id, ["onCreated", "onActivated", "onHighlighted"]);
+ }
+
+ /**
+ * Opens a new window and asserts that the correct events are fired.
+ *
+ * @param {Array} urls A list of urls for which to open tabs in the new window.
+ */
+ async function openWindow(urls) {
+ browser.test.assertEq(
+ 0,
+ Object.keys(events).length,
+ "No events remaining before testing openWindow."
+ );
+
+ let window = await browser.windows.create({ url: urls });
+ browser.test.log(`Opened new window ${window.id}`);
+
+ for (let [i] of urls.entries()) {
+ let tab = window.tabs[i];
+ tabIds.push(tab.id);
+
+ let expectedEvents = ["onCreated", "onActivated", "onHighlighted"];
+ if (i !== 0) {
+ expectedEvents.splice(1);
+ }
+ await expectEvents(window.tabs[i].id, expectedEvents);
+ }
+ }
+
+ /**
+ * Highlights an existing tab and asserts that the correct events are fired.
+ *
+ * @param {number} tabId
+ */
+ async function highlightTab(tabId) {
+ browser.test.assertEq(
+ 0,
+ Object.keys(events).length,
+ "No events remaining before testing highlightTab."
+ );
+
+ browser.test.log(`Highlighting tab ${tabId}`);
+ let tab = await browser.tabs.update(tabId, { active: true });
+
+ browser.test.assertEq(tab.id, tabId, `Tab ${tab.id} highlighted`);
+
+ await expectEvents(tab.id, ["onActivated", "onHighlighted"]);
+ }
+
+ /**
+ * The main entry point to the tests.
+ */
+ let tabs = await browser.tabs.query({ active: true, currentWindow: true });
+
+ let activeWindow = tabs[0].windowId;
+ await Promise.all([
+ openTab(activeWindow),
+ openTab(activeWindow),
+ openTab(activeWindow),
+ ]);
+
+ await Promise.all([
+ highlightTab(tabIds[0]),
+ highlightTab(tabIds[1]),
+ highlightTab(tabIds[2]),
+ ]);
+
+ // If we open these windows in parallel, there is a risk
+ // that a window will be occluded by the next one before
+ // it can finish first paint, which will prevent
+ // the firing of browser-delayed-startup-finished
+ await openWindow(["http://example.com"]);
+ await openWindow(["http://example.com", "http://example.org"]);
+ await openWindow([
+ "http://example.com",
+ "http://example.org",
+ "http://example.net",
+ ]);
+
+ browser.test.assertEq(
+ 0,
+ Object.keys(events).length,
+ "No events remaining after tests."
+ );
+
+ await Promise.all(tabIds.map(id => browser.tabs.remove(id)));
+
+ browser.test.notifyPass("tabs.highlight");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.highlight");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
new file mode 100644
index 0000000000..c02aef3da9
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
@@ -0,0 +1,453 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScript() {
+ let { MessageChannel } = ChromeUtils.importESModule(
+ "resource://testing-common/MessageChannel.sys.mjs"
+ );
+
+ function countMM(messageManagerMap) {
+ let count = 0;
+ // List of permanent message managers in the main process. We should not
+ // count them in the test because MessageChannel unsubscribes when the
+ // message manager closes, which never happens to these, of course.
+ let globalMMs = [Services.mm, Services.ppmm, Services.ppmm.getChildAt(0)];
+ for (let mm of messageManagerMap.keys()) {
+ if (!globalMMs.includes(mm)) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ let messageManagersSize = countMM(MessageChannel.messageManagers);
+
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const URL = BASE + "file_iframe_document.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL, true);
+
+ async function background() {
+ try {
+ // This promise is meant to be resolved when browser.tabs.executeScript({file: "script.js"})
+ // is called and the content script does message back, registering the runtime.onMessage
+ // listener here is meant to prevent intermittent failures due to a race on executing the
+ // array of promises passed to the `await Promise.all(...)` below.
+ const promiseRuntimeOnMessage = new Promise(resolve => {
+ browser.runtime.onMessage.addListener(message => {
+ browser.test.assertEq(
+ "script ran",
+ message,
+ "Expected runtime message"
+ );
+ resolve();
+ });
+ });
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ let frames = await browser.webNavigation.getAllFrames({ tabId: tab.id });
+ browser.test.assertEq(3, frames.length, "Expect exactly three frames");
+ browser.test.assertEq(0, frames[0].frameId, "Main frame has frameId:0");
+ browser.test.assertTrue(frames[1].frameId > 0, "Subframe has a valid id");
+
+ browser.test.log(
+ `FRAMES: ${frames[1].frameId} ${JSON.stringify(frames)}\n`
+ );
+ await Promise.all([
+ browser.tabs
+ .executeScript({
+ code: "42",
+ })
+ .then(result => {
+ browser.test.assertEq(
+ 1,
+ result.length,
+ "Expected one callback result"
+ );
+ browser.test.assertEq(42, result[0], "Expected callback result");
+ }),
+
+ browser.tabs
+ .executeScript({
+ file: "script.js",
+ code: "42",
+ })
+ .then(
+ result => {
+ browser.test.fail(
+ "Expected not to be able to execute a script with both file and code"
+ );
+ },
+ error => {
+ browser.test.assertTrue(
+ /a 'code' or a 'file' property, but not both/.test(
+ error.message
+ ),
+ "Got expected error"
+ );
+ }
+ ),
+
+ browser.tabs
+ .executeScript({
+ file: "script.js",
+ })
+ .then(result => {
+ browser.test.assertEq(
+ 1,
+ result.length,
+ "Expected one callback result"
+ );
+ browser.test.assertEq(
+ undefined,
+ result[0],
+ "Expected callback result"
+ );
+ }),
+
+ browser.tabs
+ .executeScript({
+ file: "script2.js",
+ })
+ .then(result => {
+ browser.test.assertEq(
+ 1,
+ result.length,
+ "Expected one callback result"
+ );
+ browser.test.assertEq(27, result[0], "Expected callback result");
+ }),
+
+ browser.tabs
+ .executeScript({
+ code: "location.href;",
+ allFrames: true,
+ })
+ .then(result => {
+ browser.test.assertTrue(
+ Array.isArray(result),
+ "Result is an array"
+ );
+
+ browser.test.assertEq(
+ 2,
+ result.length,
+ "Result has correct length"
+ );
+
+ browser.test.assertTrue(
+ /\/file_iframe_document\.html$/.test(result[0]),
+ "First result is correct"
+ );
+ browser.test.assertEq(
+ "http://mochi.test:8888/",
+ result[1],
+ "Second result is correct"
+ );
+ }),
+
+ browser.tabs
+ .executeScript({
+ code: "location.href;",
+ allFrames: true,
+ matchAboutBlank: true,
+ })
+ .then(result => {
+ browser.test.assertTrue(
+ Array.isArray(result),
+ "Result is an array"
+ );
+
+ browser.test.assertEq(
+ 3,
+ result.length,
+ "Result has correct length"
+ );
+
+ browser.test.assertTrue(
+ /\/file_iframe_document\.html$/.test(result[0]),
+ "First result is correct"
+ );
+ browser.test.assertEq(
+ "http://mochi.test:8888/",
+ result[1],
+ "Second result is correct"
+ );
+ browser.test.assertEq(
+ "about:blank",
+ result[2],
+ "Thirds result is correct"
+ );
+ }),
+
+ browser.tabs
+ .executeScript({
+ code: "location.href;",
+ runAt: "document_end",
+ })
+ .then(result => {
+ browser.test.assertEq(1, result.length, "Expected callback result");
+ browser.test.assertEq(
+ "string",
+ typeof result[0],
+ "Result is a string"
+ );
+
+ browser.test.assertTrue(
+ /\/file_iframe_document\.html$/.test(result[0]),
+ "Result is correct"
+ );
+ }),
+
+ browser.tabs
+ .executeScript({
+ code: "window",
+ })
+ .then(
+ result => {
+ browser.test.fail(
+ "Expected error when returning non-structured-clonable object"
+ );
+ },
+ error => {
+ browser.test.assertEq(
+ "",
+ error.fileName,
+ "Got expected fileName"
+ );
+ browser.test.assertEq(
+ "Script '' result is non-structured-clonable data",
+ error.message,
+ "Got expected error"
+ );
+ }
+ ),
+
+ browser.tabs
+ .executeScript({
+ code: "Promise.resolve(window)",
+ })
+ .then(
+ result => {
+ browser.test.fail(
+ "Expected error when returning non-structured-clonable object"
+ );
+ },
+ error => {
+ browser.test.assertEq(
+ "",
+ error.fileName,
+ "Got expected fileName"
+ );
+ browser.test.assertEq(
+ "Script '' result is non-structured-clonable data",
+ error.message,
+ "Got expected error"
+ );
+ }
+ ),
+
+ browser.tabs
+ .executeScript({
+ file: "script3.js",
+ })
+ .then(
+ result => {
+ browser.test.fail(
+ "Expected error when returning non-structured-clonable object"
+ );
+ },
+ error => {
+ const expected =
+ /Script '.*script3.js' result is non-structured-clonable data/;
+ browser.test.assertTrue(
+ expected.test(error.message),
+ "Got expected error"
+ );
+ browser.test.assertTrue(
+ error.fileName.endsWith("script3.js"),
+ "Got expected fileName"
+ );
+ }
+ ),
+
+ browser.tabs
+ .executeScript({
+ frameId: Number.MAX_SAFE_INTEGER,
+ code: "42",
+ })
+ .then(
+ result => {
+ browser.test.fail(
+ "Expected error when specifying invalid frame ID"
+ );
+ },
+ error => {
+ browser.test.assertEq(
+ `Invalid frame IDs: [${Number.MAX_SAFE_INTEGER}].`,
+ error.message,
+ "Got expected error"
+ );
+ }
+ ),
+
+ browser.tabs
+ .create({ url: "http://example.net/", active: false })
+ .then(async tab => {
+ await browser.tabs
+ .executeScript(tab.id, {
+ code: "42",
+ })
+ .then(
+ result => {
+ browser.test.fail(
+ "Expected error when trying to execute on invalid domain"
+ );
+ },
+ error => {
+ browser.test.assertEq(
+ "Missing host permission for the tab",
+ error.message,
+ "Got expected error"
+ );
+ }
+ );
+
+ await browser.tabs.remove(tab.id);
+ }),
+
+ browser.tabs
+ .executeScript({
+ code: "Promise.resolve(42)",
+ })
+ .then(result => {
+ browser.test.assertEq(
+ 42,
+ result[0],
+ "Got expected promise resolution value as result"
+ );
+ }),
+
+ browser.tabs
+ .executeScript({
+ code: "location.href;",
+ runAt: "document_end",
+ allFrames: true,
+ })
+ .then(result => {
+ browser.test.assertTrue(
+ Array.isArray(result),
+ "Result is an array"
+ );
+
+ browser.test.assertEq(
+ 2,
+ result.length,
+ "Result has correct length"
+ );
+
+ browser.test.assertTrue(
+ /\/file_iframe_document\.html$/.test(result[0]),
+ "First result is correct"
+ );
+ browser.test.assertEq(
+ "http://mochi.test:8888/",
+ result[1],
+ "Second result is correct"
+ );
+ }),
+
+ browser.tabs
+ .executeScript({
+ code: "location.href;",
+ frameId: frames[0].frameId,
+ })
+ .then(result => {
+ browser.test.assertEq(1, result.length, "Expected one result");
+ browser.test.assertTrue(
+ /\/file_iframe_document\.html$/.test(result[0]),
+ `Result for main frame (frameId:0) is correct: ${result[0]}`
+ );
+ }),
+
+ browser.tabs
+ .executeScript({
+ code: "location.href;",
+ frameId: frames[1].frameId,
+ })
+ .then(result => {
+ browser.test.assertEq(1, result.length, "Expected one result");
+ browser.test.assertEq(
+ "http://mochi.test:8888/",
+ result[0],
+ "Result for frameId[1] is correct"
+ );
+ }),
+
+ browser.tabs.create({ url: "http://example.com/" }).then(async tab => {
+ let result = await browser.tabs.executeScript(tab.id, {
+ code: "location.href",
+ });
+
+ browser.test.assertEq(
+ "http://example.com/",
+ result[0],
+ "Script executed correctly in new tab"
+ );
+
+ await browser.tabs.remove(tab.id);
+ }),
+
+ promiseRuntimeOnMessage,
+ ]);
+
+ browser.test.notifyPass("executeScript");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "http://mochi.test/",
+ "http://example.com/",
+ "webNavigation",
+ ],
+ },
+
+ background,
+
+ files: {
+ "script.js": function () {
+ browser.runtime.sendMessage("script ran");
+ },
+
+ "script2.js": "27",
+
+ "script3.js": "window",
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("executeScript");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Make sure that we're not holding on to references to closed message
+ // managers.
+ is(
+ countMM(MessageChannel.messageManagers),
+ messageManagersSize,
+ "Message manager count"
+ );
+ is(MessageChannel.pendingResponses.size, 0, "Pending response count");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_about_blank.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_about_blank.js
new file mode 100644
index 0000000000..34af25ff9b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_about_blank.js
@@ -0,0 +1,33 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScript_at_about_blank() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ host_permissions: ["*://*/*"], // allows script in top-level about:blank.
+ },
+ async background() {
+ try {
+ const tab = await browser.tabs.create({ url: "about:blank" });
+ const result = await browser.tabs.executeScript(tab.id, {
+ code: "location.href",
+ matchAboutBlank: true,
+ });
+ browser.test.assertEq(
+ "about:blank",
+ result[0],
+ "Script executed correctly in new tab"
+ );
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("executeScript");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript");
+ }
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("executeScript");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
new file mode 100644
index 0000000000..6b460243b0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
@@ -0,0 +1,361 @@
+"use strict";
+
+async function testHasNoPermission(params) {
+ let contentSetup = params.contentSetup || (() => Promise.resolve());
+
+ async function background(contentSetup) {
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.assertEq(msg, "execute-script");
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript({
+ file: "script.js",
+ }),
+ /Missing host permission for the tab/
+ );
+
+ browser.test.notifyPass("executeScript");
+ });
+
+ await contentSetup();
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: params.manifest,
+
+ background: `(${background})(${contentSetup})`,
+
+ files: {
+ "script.js": function () {
+ browser.runtime.sendMessage("first script ran");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (params.setup) {
+ await params.setup(extension);
+ }
+
+ extension.sendMessage("execute-script");
+
+ await extension.awaitFinish("executeScript");
+ await extension.unload();
+}
+
+add_task(async function testBadPermissions() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ info("Test no special permissions");
+ await testHasNoPermission({
+ manifest: { permissions: [] },
+ });
+
+ info("Test tabs permissions");
+ await testHasNoPermission({
+ manifest: { permissions: ["tabs"] },
+ });
+
+ info("Test no special permissions, commands, key press");
+ await testHasNoPermission({
+ manifest: {
+ permissions: [],
+ commands: {
+ "test-tabs-executeScript": {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: function () {
+ browser.commands.onCommand.addListener(function (command) {
+ if (command == "test-tabs-executeScript") {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ }
+ });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test no special permissions, _execute_browser_action command");
+ await testHasNoPermission({
+ manifest: {
+ permissions: [],
+ browser_action: {},
+ commands: {
+ _execute_browser_action: {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: function () {
+ browser.browserAction.onClicked.addListener(() => {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test no special permissions, _execute_page_action command");
+ await testHasNoPermission({
+ manifest: {
+ permissions: [],
+ page_action: {},
+ commands: {
+ _execute_page_action: {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: async function () {
+ browser.pageAction.onClicked.addListener(() => {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ });
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test active tab, commands, no key press");
+ await testHasNoPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ commands: {
+ "test-tabs-executeScript": {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ });
+
+ info("Test active tab, browser action, no click");
+ await testHasNoPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ });
+
+ info("Test active tab, page action, no click");
+ await testHasNoPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ page_action: {},
+ },
+ contentSetup: async function () {
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testMatchDataURI() {
+ // allow top level data: URI navigations, otherwise
+ // window.location.href = data: would be blocked
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", false]],
+ });
+
+ const target = ExtensionTestUtils.loadExtension({
+ files: {
+ "page.html": `
+
+
+
+ `,
+ "page.js": function () {
+ browser.test.onMessage.addListener((msg, url) => {
+ if (msg !== "navigate") {
+ return;
+ }
+ window.location.href = url;
+ });
+ },
+ },
+ background() {
+ browser.tabs.create({
+ active: true,
+ url: browser.runtime.getURL("page.html"),
+ });
+ },
+ });
+
+ const scripts = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["", "webNavigation"],
+ },
+ background() {
+ browser.webNavigation.onCompleted.addListener(({ url, frameId }) => {
+ browser.test.log(`Document loading complete: ${url}`);
+ if (frameId === 0) {
+ browser.test.sendMessage("tab-ready", url);
+ }
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "execute") {
+ return;
+ }
+ await browser.test.assertRejects(
+ browser.tabs.executeScript({
+ code: "location.href;",
+ allFrames: true,
+ }),
+ /Missing host permission/,
+ "Should not execute in `data:` frame"
+ );
+
+ browser.test.sendMessage("done");
+ });
+ },
+ });
+
+ await scripts.startup();
+ await target.startup();
+
+ // Test extension page with a data: iframe.
+ const page = await scripts.awaitMessage("tab-ready");
+ ok(page.endsWith("page.html"), "Extension page loaded into a tab");
+
+ scripts.sendMessage("execute");
+ await scripts.awaitMessage("done");
+
+ // Test extension tab navigated to a data: URI.
+ const data = "data:text/html;charset=utf-8,also-inherits";
+ target.sendMessage("navigate", data);
+
+ const url = await scripts.awaitMessage("tab-ready");
+ is(url, data, "Extension tab navigated to a data: URI");
+
+ scripts.sendMessage("execute");
+ await scripts.awaitMessage("done");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await scripts.unload();
+ await target.unload();
+});
+
+add_task(async function testBadURL() {
+ async function background() {
+ let promises = [
+ new Promise(resolve => {
+ browser.tabs.executeScript(
+ {
+ file: "http://example.com/script.js",
+ },
+ result => {
+ browser.test.assertEq(undefined, result, "Result value");
+
+ browser.test.assertTrue(
+ browser.runtime.lastError instanceof Error,
+ "runtime.lastError is Error"
+ );
+
+ browser.test.assertTrue(
+ browser.runtime.lastError instanceof Error,
+ "runtime.lastError is Error"
+ );
+
+ browser.test.assertEq(
+ "Files to be injected must be within the extension",
+ browser.runtime.lastError && browser.runtime.lastError.message,
+ "runtime.lastError value"
+ );
+
+ browser.test.assertEq(
+ "Files to be injected must be within the extension",
+ browser.runtime.lastError && browser.runtime.lastError.message,
+ "runtime.lastError value"
+ );
+
+ resolve();
+ }
+ );
+ }),
+
+ browser.tabs
+ .executeScript({
+ file: "http://example.com/script.js",
+ })
+ .catch(error => {
+ browser.test.assertTrue(error instanceof Error, "Error is Error");
+
+ browser.test.assertEq(
+ null,
+ browser.runtime.lastError,
+ "runtime.lastError value"
+ );
+
+ browser.test.assertEq(
+ null,
+ browser.runtime.lastError,
+ "runtime.lastError value"
+ );
+
+ browser.test.assertEq(
+ "Files to be injected must be within the extension",
+ error && error.message,
+ "error value"
+ );
+ }),
+ ];
+
+ await Promise.all(promises);
+
+ browser.test.notifyPass("executeScript-lastError");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [""],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("executeScript-lastError");
+
+ await extension.unload();
+});
+
+// TODO bug 1435100: Test that |executeScript| fails if the tab has navigated
+// to a new page, and no longer matches our expected state. This involves
+// intentionally trying to trigger a race condition.
+
+add_task(forceGC);
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_file.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_file.js
new file mode 100644
index 0000000000..a8ba389602
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_file.js
@@ -0,0 +1,93 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const FILE_URL = Services.io.newFileURI(
+ new FileUtils.File(getTestFilePath("file_dummy.html"))
+).spec;
+
+add_task(async function testExecuteScript_at_file_url() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "file:///*"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async () => {
+ try {
+ const [tab] = await browser.tabs.query({ url: "file://*/*/*dummy*" });
+ const result = await browser.tabs.executeScript(tab.id, {
+ code: "location.protocol",
+ });
+ browser.test.assertEq(
+ "file:",
+ result[0],
+ "Script executed correctly in new tab"
+ );
+ browser.test.notifyPass("executeScript");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript");
+ }
+ });
+ },
+ });
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE_URL);
+
+ extension.sendMessage();
+ await extension.awaitFinish("executeScript");
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
+
+add_task(async function testExecuteScript_at_file_url_with_activeTab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ background() {
+ browser.browserAction.onClicked.addListener(async tab => {
+ try {
+ const result = await browser.tabs.executeScript(tab.id, {
+ code: "location.protocol",
+ });
+ browser.test.assertEq(
+ "file:",
+ result[0],
+ "Script executed correctly in active tab"
+ );
+ browser.test.notifyPass("executeScript");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript");
+ }
+ });
+
+ browser.test.onMessage.addListener(async () => {
+ await browser.test.assertRejects(
+ browser.tabs.executeScript({ code: "location.protocol" }),
+ /Missing host permission for the tab/,
+ "activeTab not active yet, executeScript should be rejected"
+ );
+ browser.test.sendMessage("next-step");
+ });
+ },
+ });
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE_URL);
+
+ extension.sendMessage();
+ await extension.awaitMessage("next-step");
+
+ await clickBrowserAction(extension);
+ await extension.awaitFinish("executeScript");
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
new file mode 100644
index 0000000000..e9d008bf92
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
@@ -0,0 +1,190 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+async function testHasPermission(params) {
+ let contentSetup = params.contentSetup || (() => Promise.resolve());
+
+ async function background(contentSetup) {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq(msg, "script ran", "script ran");
+ browser.test.notifyPass("executeScript");
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "execute-script");
+
+ browser.tabs.executeScript({
+ file: "script.js",
+ });
+ });
+
+ await contentSetup();
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: params.manifest,
+
+ background: `(${background})(${contentSetup})`,
+
+ files: {
+ "panel.html": `
+
+
+
+
+ `,
+ "script.js": function () {
+ browser.runtime.sendMessage("script ran");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (params.setup) {
+ await params.setup(extension);
+ }
+
+ extension.sendMessage("execute-script");
+
+ await extension.awaitFinish("executeScript");
+
+ if (params.tearDown) {
+ await params.tearDown(extension);
+ }
+
+ await extension.unload();
+}
+
+add_task(async function testGoodPermissions() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ info("Test activeTab permission with a command key press");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ commands: {
+ "test-tabs-executeScript": {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: function () {
+ browser.commands.onCommand.addListener(function (command) {
+ if (command == "test-tabs-executeScript") {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ }
+ });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test activeTab permission with _execute_browser_action command");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ commands: {
+ _execute_browser_action: {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: function () {
+ browser.browserAction.onClicked.addListener(() => {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test activeTab permission with _execute_page_action command");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ page_action: {},
+ commands: {
+ _execute_page_action: {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: async function () {
+ browser.pageAction.onClicked.addListener(() => {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ });
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test activeTab permission with a context menu click");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab", "contextMenus"],
+ },
+ contentSetup: function () {
+ browser.contextMenus.create({ title: "activeTab", contexts: ["all"] });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "a[href]",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await awaitPopupShown;
+
+ let item = contextMenu.querySelector("[label=activeTab]");
+
+ contextMenu.activateItem(item);
+
+ await awaitPopupHidden;
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(forceGC);
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_multiple.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_multiple.js
new file mode 100644
index 0000000000..4e9cc907da
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_multiple.js
@@ -0,0 +1,61 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScript() {
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const URL = BASE + "file_dummy.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL, true);
+
+ async function background() {
+ try {
+ await browser.tabs.executeScript({ code: "this.foo = 'bar'" });
+ await browser.tabs.executeScript({ file: "script.js" });
+
+ let [result1] = await browser.tabs.executeScript({
+ code: "[this.foo, this.bar]",
+ });
+ let [result2] = await browser.tabs.executeScript({ file: "script2.js" });
+
+ browser.test.assertEq(
+ "bar,baz",
+ String(result1),
+ "executeScript({code}) result"
+ );
+ browser.test.assertEq(
+ "bar,baz",
+ String(result2),
+ "executeScript({file}) result"
+ );
+
+ browser.test.notifyPass("executeScript-multiple");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript-multiple");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+
+ background,
+
+ files: {
+ "script.js": function () {
+ this.bar = "baz";
+ },
+
+ "script2.js": "[this.foo, this.bar]",
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("executeScript-multiple");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
new file mode 100644
index 0000000000..e8e1f1255f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
@@ -0,0 +1,80 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScriptAtOnUpdated() {
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const URL = BASE + "file_iframe_document.html";
+ // This is a regression test for bug 1325830.
+ // The bug (executeScript not completing any more) occurred when executeScript
+ // was called early at the onUpdated event, unless the tabs.create method is
+ // called. So this test does not use tabs.create to open new tabs.
+ // Note that if this test is run together with other tests that do call
+ // tabs.create, then this test case does not properly test the conditions of
+ // the regression any more. To verify that the regression has been resolved,
+ // this test must be run in isolation.
+
+ function background() {
+ // Using variables to prevent listeners from running more than once, instead
+ // of removing the listener. This is to minimize any IPC, since the bug that
+ // is being tested is sensitive to timing.
+ let ignore = false;
+ let url;
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (ignore) {
+ return;
+ }
+ if (url && changeInfo.status === "loading" && tab.url === url) {
+ ignore = true;
+ browser.tabs
+ .executeScript(tabId, {
+ code: "document.URL",
+ })
+ .then(
+ results => {
+ browser.test.assertEq(
+ url,
+ results[0],
+ "Content script should run"
+ );
+ browser.test.notifyPass("executeScript-at-onUpdated");
+ },
+ error => {
+ browser.test.fail(`Unexpected error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("executeScript-at-onUpdated");
+ }
+ );
+ // (running this log call after executeScript to minimize IPC between
+ // onUpdated and executeScript.)
+ browser.test.log(`Found expected navigation to ${url}`);
+ } else {
+ // The bug occurs when executeScript is called before a tab is
+ // initialized.
+ browser.tabs.executeScript(tabId, { code: "" });
+ }
+ });
+ browser.test.onMessage.addListener(testUrl => {
+ url = testUrl;
+ browser.test.sendMessage("open-test-tab");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/", "tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ extension.sendMessage(URL);
+ await extension.awaitMessage("open-test-tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL, true);
+
+ await extension.awaitFinish("executeScript-at-onUpdated");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js
new file mode 100644
index 0000000000..bab0182a3f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js
@@ -0,0 +1,134 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/**
+ * These tests ensure that the runAt argument to tabs.executeScript delays
+ * script execution until the document has reached the correct state.
+ *
+ * Since tests of this nature are especially race-prone, it relies on a
+ * server-JS script to delay the completion of our test page's load cycle long
+ * enough for us to attempt to load our scripts in the earlies phase we support.
+ *
+ * And since we can't actually rely on that timing, it retries any attempts that
+ * fail to load as early as expected, but don't load at any illegal time.
+ */
+
+add_task(async function testExecuteScript() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ true
+ );
+
+ async function background() {
+ let tab;
+
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const URL = BASE + "file_slowed_document.sjs";
+
+ const MAX_TRIES = 10;
+
+ let onUpdatedPromise = (tabId, url, status) => {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(_, changed, tab) {
+ if (tabId == tab.id && changed.status == status && tab.url == url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ };
+
+ try {
+ [tab] = await browser.tabs.query({ active: true, currentWindow: true });
+
+ let success = false;
+ for (let tries = 0; !success && tries < MAX_TRIES; tries++) {
+ let url = `${URL}?with-iframe&r=${Math.random()}`;
+
+ let loadingPromise = onUpdatedPromise(tab.id, url, "loading");
+ let completePromise = onUpdatedPromise(tab.id, url, "complete");
+
+ // TODO: Test allFrames and frameId.
+
+ await browser.tabs.update({ url });
+ await loadingPromise;
+
+ let states = await Promise.all(
+ [
+ // Send the executeScript requests in the reverse order that we expect
+ // them to execute in, to avoid them passing only because of timing
+ // races.
+ browser.tabs.executeScript({
+ code: "document.readyState",
+ // Testing default `runAt`.
+ }),
+ browser.tabs.executeScript({
+ code: "document.readyState",
+ runAt: "document_idle",
+ }),
+ browser.tabs.executeScript({
+ code: "document.readyState",
+ runAt: "document_end",
+ }),
+ browser.tabs.executeScript({
+ code: "document.readyState",
+ runAt: "document_start",
+ }),
+ ].reverse()
+ );
+
+ browser.test.log(`Got states: ${states}`);
+
+ // Make sure that none of our scripts executed earlier than expected,
+ // regardless of retries.
+ browser.test.assertTrue(
+ states[1] == "interactive" || states[1] == "complete",
+ `document_end state is valid: ${states[1]}`
+ );
+ browser.test.assertTrue(
+ states[2] == "interactive" || states[2] == "complete",
+ `document_idle state is valid: ${states[2]}`
+ );
+
+ // If we have the earliest valid states for each script, we're done.
+ // Otherwise, try again.
+ success =
+ states[0] == "loading" &&
+ states[1] == "interactive" &&
+ states[2] == "interactive" &&
+ states[3] == "interactive";
+
+ await completePromise;
+ }
+
+ browser.test.assertTrue(
+ success,
+ "Got the earliest expected states at least once"
+ );
+
+ browser.test.notifyPass("executeScript-runAt");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript-runAt");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/", "tabs"],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("executeScript-runAt");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js b/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js
new file mode 100644
index 0000000000..9304d3a5b4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js
@@ -0,0 +1,86 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ browser_action: { default_popup: "popup.html" },
+ },
+
+ files: {
+ "tab.js": function () {
+ let url = document.location.href;
+
+ browser.tabs.getCurrent(currentTab => {
+ browser.test.assertEq(
+ currentTab.url,
+ url,
+ "getCurrent in non-active background tab"
+ );
+
+ // Activate the tab.
+ browser.tabs.onActivated.addListener(function listener({ tabId }) {
+ if (tabId == currentTab.id) {
+ browser.tabs.onActivated.removeListener(listener);
+
+ browser.tabs.getCurrent(currentTab => {
+ browser.test.assertEq(
+ currentTab.id,
+ tabId,
+ "in active background tab"
+ );
+ browser.test.assertEq(
+ currentTab.url,
+ url,
+ "getCurrent in non-active background tab"
+ );
+
+ browser.test.sendMessage("tab-finished");
+ });
+ }
+ });
+ browser.tabs.update(currentTab.id, { active: true });
+ });
+ },
+
+ "popup.js": function () {
+ browser.tabs.getCurrent(tab => {
+ browser.test.assertEq(tab, undefined, "getCurrent in popup script");
+ browser.test.sendMessage("popup-finished");
+ });
+ },
+
+ "tab.html": ` `,
+ "popup.html": ` `,
+ },
+
+ background: function () {
+ browser.tabs.getCurrent(tab => {
+ browser.test.assertEq(
+ tab,
+ undefined,
+ "getCurrent in background script"
+ );
+ browser.test.sendMessage("background-finished");
+ });
+
+ browser.tabs.create({ url: "tab.html", active: false });
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("background-finished");
+ await extension.awaitMessage("tab-finished");
+
+ clickBrowserAction(extension);
+ await awaitExtensionPanel(extension);
+ await extension.awaitMessage("popup-finished");
+ await closeBrowserAction(extension);
+
+ // The extension tab is automatically closed when the extension unloads.
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_goBack_goForward.js b/browser/components/extensions/test/browser/browser_ext_tabs_goBack_goForward.js
new file mode 100644
index 0000000000..2ab960699d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_goBack_goForward.js
@@ -0,0 +1,113 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_tabs_goBack_goForward() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "tab1.html": `
+
+ tab1
+ `,
+ "tab2.html": `
+
+ tab2
+ `,
+ },
+
+ async background() {
+ let tabUpdatedCount = 0;
+ let tab = {};
+
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tabInfo) => {
+ if (changeInfo.status !== "complete" || tabId !== tab.id) {
+ return;
+ }
+
+ tabUpdatedCount++;
+ switch (tabUpdatedCount) {
+ case 1:
+ browser.test.assertEq(
+ "tab1",
+ tabInfo.title,
+ "tab1 is found as expected"
+ );
+ browser.tabs.update(tabId, { url: "tab2.html" });
+ break;
+
+ case 2:
+ browser.test.assertEq(
+ "tab2",
+ tabInfo.title,
+ "tab2 is found as expected"
+ );
+ browser.tabs.update(tabId, { url: "tab1.html" });
+ break;
+
+ case 3:
+ browser.test.assertEq(
+ "tab1",
+ tabInfo.title,
+ "tab1 is found as expected"
+ );
+ browser.tabs.goBack();
+ break;
+
+ case 4:
+ browser.test.assertEq(
+ "tab2",
+ tabInfo.title,
+ "tab2 is found after navigating backward with empty parameter"
+ );
+ browser.tabs.goBack(tabId);
+ break;
+
+ case 5:
+ browser.test.assertEq(
+ "tab1",
+ tabInfo.title,
+ "tab1 is found after navigating backward with tabId as parameter"
+ );
+ browser.tabs.goForward();
+ break;
+
+ case 6:
+ browser.test.assertEq(
+ "tab2",
+ tabInfo.title,
+ "tab2 is found after navigating forward with empty parameter"
+ );
+ browser.tabs.goForward(tabId);
+ break;
+
+ case 7:
+ browser.test.assertEq(
+ "tab1",
+ tabInfo.title,
+ "tab1 is found after navigating forward with tabId as parameter"
+ );
+ await browser.tabs.remove(tabId);
+ browser.test.notifyPass("tabs.goBack.goForward");
+ break;
+
+ default:
+ break;
+ }
+ });
+
+ tab = await browser.tabs.create({ url: "tab1.html", active: true });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.goBack.goForward");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_hide.js b/browser/components/extensions/test/browser/browser_ext_tabs_hide.js
new file mode 100644
index 0000000000..89c50db692
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_hide.js
@@ -0,0 +1,375 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionControlledPopup:
+ "resource:///modules/ExtensionControlledPopup.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+async function doorhangerTest(testFn) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "tabHide"],
+ icons: {
+ 48: "addon-icon.png",
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ let tabs = await browser.tabs.query(data);
+ await browser.tabs[msg](tabs.map(t => t.id));
+ browser.test.sendMessage("done");
+ });
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ // Open some tabs so we can hide them.
+ let firstTab = gBrowser.selectedTab;
+ let tabs = [
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?one",
+ true,
+ true
+ ),
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?two",
+ true,
+ true
+ ),
+ ];
+ gBrowser.selectedTab = firstTab;
+
+ await testFn(extension);
+
+ BrowserTestUtils.removeTab(tabs[0]);
+ BrowserTestUtils.removeTab(tabs[1]);
+
+ await extension.unload();
+}
+
+add_task(function test_doorhanger_keep() {
+ return doorhangerTest(async function (extension) {
+ is(gBrowser.visibleTabs.length, 3, "There are 3 visible tabs");
+
+ // Hide the first tab, expect the doorhanger.
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(document);
+ let popupShown = promisePopupShown(panel);
+ extension.sendMessage("hide", { url: "*://*/?one" });
+ await extension.awaitMessage("done");
+ await popupShown;
+
+ is(gBrowser.visibleTabs.length, 2, "There are 2 visible tabs now");
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "alltabs-button",
+ "The doorhanger is anchored to the all tabs button"
+ );
+
+ // Click the Keep Tabs Hidden button.
+ let popupnotification = document.getElementById(
+ "extension-tab-hide-notification"
+ );
+ let popupHidden = promisePopupHidden(panel);
+ popupnotification.button.click();
+ await popupHidden;
+
+ // Hide another tab and ensure the popup didn't open.
+ extension.sendMessage("hide", { url: "*://*/?two" });
+ await extension.awaitMessage("done");
+ is(panel.state, "closed", "The popup is still closed");
+ is(gBrowser.visibleTabs.length, 1, "There's one visible tab now");
+
+ extension.sendMessage("show", {});
+ await extension.awaitMessage("done");
+ });
+});
+
+add_task(function test_doorhanger_disable() {
+ return doorhangerTest(async function (extension) {
+ is(gBrowser.visibleTabs.length, 3, "There are 3 visible tabs");
+
+ // Hide the first tab, expect the doorhanger.
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(document);
+ let popupShown = promisePopupShown(panel);
+ extension.sendMessage("hide", { url: "*://*/?one" });
+ await extension.awaitMessage("done");
+ await popupShown;
+
+ is(gBrowser.visibleTabs.length, 2, "There are 2 visible tabs now");
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "alltabs-button",
+ "The doorhanger is anchored to the all tabs button"
+ );
+
+ // verify the contents of the description.
+ let popupnotification = document.getElementById(
+ "extension-tab-hide-notification"
+ );
+ let description = popupnotification.querySelector(
+ "#extension-tab-hide-notification-description"
+ );
+ let addon = await AddonManager.getAddonByID(extension.id);
+ ok(
+ description.textContent.includes(addon.name),
+ "The extension name is in the description"
+ );
+ let images = Array.from(description.querySelectorAll("image"));
+ is(images.length, 2, "There are two images");
+ ok(
+ images.some(img => img.src.includes("addon-icon.png")),
+ "There's an icon for the extension"
+ );
+ ok(
+ images.some(img =>
+ getComputedStyle(img).backgroundImage.includes("arrow-down.svg")
+ ),
+ "There's an icon for the all tabs menu"
+ );
+
+ // Click the Disable Extension button.
+ let popupHidden = promisePopupHidden(panel);
+ popupnotification.secondaryButton.click();
+ await popupHidden;
+ await new Promise(executeSoon);
+
+ is(gBrowser.visibleTabs.length, 3, "There are 3 visible tabs again");
+ is(addon.userDisabled, true, "The extension is now disabled");
+ });
+});
+
+add_task(async function test_tabs_showhide() {
+ async function background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ switch (msg) {
+ case "hideall": {
+ let tabs = await browser.tabs.query({ hidden: false });
+ browser.test.assertEq(tabs.length, 5, "got 5 tabs");
+ let ids = tabs.map(tab => tab.id);
+ browser.test.log(`working with ids ${JSON.stringify(ids)}`);
+
+ let hidden = await browser.tabs.hide(ids);
+ browser.test.assertEq(hidden.length, 3, "hid 3 tabs");
+ tabs = await browser.tabs.query({ hidden: true });
+ ids = tabs.map(tab => tab.id);
+ browser.test.assertEq(
+ JSON.stringify(hidden.sort()),
+ JSON.stringify(ids.sort()),
+ "hidden tabIds match"
+ );
+
+ browser.test.sendMessage("hidden", { hidden });
+ break;
+ }
+ case "showall": {
+ let tabs = await browser.tabs.query({ hidden: true });
+ for (let tab of tabs) {
+ browser.test.assertTrue(tab.hidden, "tab is hidden");
+ }
+ let ids = tabs.map(tab => tab.id);
+ browser.tabs.show(ids);
+ browser.test.sendMessage("shown");
+ break;
+ }
+ }
+ });
+ }
+
+ let extdata = {
+ manifest: { permissions: ["tabs", "tabHide"] },
+ background,
+ useAddonManager: "temporary", // So the doorhanger can find the addon.
+ };
+ let extension = ExtensionTestUtils.loadExtension(extdata);
+ await extension.startup();
+
+ let sessData = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ {
+ entries: [
+ { url: "https://example.com/", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "https://mochi.test:8888/", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ {
+ entries: [
+ { url: "http://test1.example.com/", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ // Set up a test session with 2 windows and 5 tabs.
+ let oldState = SessionStore.getBrowserState();
+ let restored = TestUtils.topicObserved("sessionstore-browser-state-restored");
+ SessionStore.setBrowserState(JSON.stringify(sessData));
+ await restored;
+
+ if (!Services.prefs.getBoolPref("browser.tabs.tabmanager.enabled")) {
+ for (let win of BrowserWindowIterator()) {
+ let allTabsButton = win.document.getElementById("alltabs-button");
+ is(
+ getComputedStyle(allTabsButton).display,
+ "none",
+ "The all tabs button is hidden"
+ );
+ }
+ }
+
+ // Attempt to hide all the tabs, however the active tab in each window cannot
+ // be hidden, so the result will be 3 hidden tabs.
+ extension.sendMessage("hideall");
+ await extension.awaitMessage("hidden");
+
+ // We have 2 windows in this session. Otherwin is the non-current window.
+ // In each window, the first tab will be the selected tab and should not be
+ // hidden. The rest of the tabs should be hidden at this point. Hidden
+ // status was already validated inside the extension, this double checks
+ // from chrome code.
+ let otherwin;
+ for (let win of BrowserWindowIterator()) {
+ if (win != window) {
+ otherwin = win;
+ }
+ let tabs = Array.from(win.gBrowser.tabs);
+ ok(!tabs[0].hidden, "first tab not hidden");
+ for (let i = 1; i < tabs.length; i++) {
+ ok(tabs[i].hidden, "tab hidden value is correct");
+ let id = SessionStore.getCustomTabValue(tabs[i], "hiddenBy");
+ is(id, extension.id, "tab hiddenBy value is correct");
+ await TabStateFlusher.flush(tabs[i].linkedBrowser);
+ }
+
+ let allTabsButton = win.document.getElementById("alltabs-button");
+ isnot(
+ getComputedStyle(allTabsButton).display,
+ "none",
+ "The all tabs button is visible"
+ );
+ }
+
+ // Close the other window then restore it to test that the tabs are
+ // restored with proper hidden state, and the correct extension id.
+ await BrowserTestUtils.closeWindow(otherwin);
+
+ otherwin = SessionStore.undoCloseWindow(0);
+ await BrowserTestUtils.waitForEvent(otherwin, "load");
+ let tabs = Array.from(otherwin.gBrowser.tabs);
+ ok(!tabs[0].hidden, "first tab not hidden");
+ for (let i = 1; i < tabs.length; i++) {
+ ok(tabs[i].hidden, "tab hidden value is correct");
+ let id = SessionStore.getCustomTabValue(tabs[i], "hiddenBy");
+ is(id, extension.id, "tab hiddenBy value is correct");
+ }
+
+ // Test closing the last visible tab, the next tab which is hidden should become
+ // the selectedTab and will be visible.
+ ok(!otherwin.gBrowser.selectedTab.hidden, "selected tab is not hidden");
+ BrowserTestUtils.removeTab(otherwin.gBrowser.selectedTab);
+ ok(!otherwin.gBrowser.selectedTab.hidden, "tab was unhidden");
+
+ // Showall will unhide any remaining hidden tabs.
+ extension.sendMessage("showall");
+ await extension.awaitMessage("shown");
+
+ // Check from chrome code that all tabs are visible again.
+ for (let win of BrowserWindowIterator()) {
+ let tabs = Array.from(win.gBrowser.tabs);
+ for (let i = 0; i < tabs.length; i++) {
+ ok(!tabs[i].hidden, "tab hidden value is correct");
+ }
+ }
+
+ // Close second window.
+ await BrowserTestUtils.closeWindow(otherwin);
+
+ await extension.unload();
+
+ // Restore pre-test state.
+ restored = TestUtils.topicObserved("sessionstore-browser-state-restored");
+ SessionStore.setBrowserState(oldState);
+ await restored;
+});
+
+// Test our shutdown handling. Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown. We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_shutdown() {
+ let tabs = [
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/",
+ true,
+ true
+ ),
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true,
+ true
+ ),
+ ];
+
+ async function background() {
+ let tabs = await browser.tabs.query({ url: "http://example.com/" });
+ let testTab = tabs[0];
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if ("hidden" in changeInfo) {
+ browser.test.assertEq(tabId, testTab.id, "correct tab was hidden");
+ browser.test.assertTrue(changeInfo.hidden, "tab is hidden");
+ browser.test.assertEq(tab.url, testTab.url, "tab has correct URL");
+ browser.test.sendMessage("changeInfo");
+ }
+ });
+
+ let hidden = await browser.tabs.hide(testTab.id);
+ browser.test.assertEq(hidden[0], testTab.id, "tab was hidden");
+ tabs = await browser.tabs.query({ hidden: true });
+ browser.test.assertEq(tabs[0].id, testTab.id, "tab was hidden");
+ browser.test.sendMessage("ready");
+ }
+
+ let extdata = {
+ manifest: { permissions: ["tabs", "tabHide"] },
+ useAddonManager: "temporary", // For testing onShutdown.
+ background,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extdata);
+ await extension.startup();
+
+ // test onUpdated
+ await Promise.all([
+ extension.awaitMessage("ready"),
+ extension.awaitMessage("changeInfo"),
+ ]);
+ Assert.ok(tabs[0].hidden, "Tab is hidden by extension");
+
+ await extension.unload();
+
+ Assert.ok(!tabs[0].hidden, "Tab is not hidden after unloading extension");
+ BrowserTestUtils.removeTab(tabs[0]);
+ BrowserTestUtils.removeTab(tabs[1]);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_hide_update.js b/browser/components/extensions/test/browser/browser_ext_tabs_hide_update.js
new file mode 100644
index 0000000000..7fbf185704
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_hide_update.js
@@ -0,0 +1,146 @@
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const ID = "@test-tabs-addon";
+
+async function updateExtension(ID, options) {
+ let xpi = AddonTestUtils.createTempWebExtensionFile(options);
+ await Promise.all([
+ AddonTestUtils.promiseWebExtensionStartup(ID),
+ AddonManager.installTemporaryAddon(xpi),
+ ]);
+}
+
+async function disableExtension(ID) {
+ let disabledPromise = awaitEvent("shutdown", ID);
+ let addon = await AddonManager.getAddonByID(ID);
+ await addon.disable();
+ await disabledPromise;
+}
+
+function getExtension() {
+ async function background() {
+ let tabs = await browser.tabs.query({ url: "http://example.com/" });
+ let testTab = tabs[0];
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if ("hidden" in changeInfo) {
+ browser.test.assertEq(tabId, testTab.id, "correct tab was hidden");
+ browser.test.assertTrue(changeInfo.hidden, "tab is hidden");
+ browser.test.sendMessage("changeInfo");
+ }
+ });
+
+ let hidden = await browser.tabs.hide(testTab.id);
+ browser.test.assertEq(hidden[0], testTab.id, "tabs.hide hide the tab");
+ tabs = await browser.tabs.query({ hidden: true });
+ browser.test.assertEq(
+ tabs[0].id,
+ testTab.id,
+ "tabs.query result was hidden"
+ );
+ browser.test.sendMessage("ready");
+ }
+
+ let extdata = {
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: ID,
+ },
+ },
+ permissions: ["tabs", "tabHide"],
+ },
+ background,
+ useAddonManager: "temporary",
+ };
+ return ExtensionTestUtils.loadExtension(extdata);
+}
+
+// Test our update handling. Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown. We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_update() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+
+ const extension = getExtension();
+ await extension.startup();
+
+ // test onUpdated
+ await Promise.all([
+ extension.awaitMessage("ready"),
+ extension.awaitMessage("changeInfo"),
+ ]);
+ Assert.ok(tab.hidden, "Tab is hidden by extension");
+
+ // Test that update doesn't hide tabs when tabHide permission is present.
+ let extdata = {
+ manifest: {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: ID,
+ },
+ },
+ permissions: ["tabs", "tabHide"],
+ },
+ };
+ await updateExtension(ID, extdata);
+ Assert.ok(tab.hidden, "Tab is hidden hidden after update");
+
+ // Test that update does hide tabs when tabHide permission is removed.
+ extdata.manifest = {
+ version: "3.0",
+ browser_specific_settings: {
+ gecko: {
+ id: ID,
+ },
+ },
+ permissions: ["tabs"],
+ };
+ await updateExtension(ID, extdata);
+ Assert.ok(!tab.hidden, "Tab is not hidden hidden after update");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test our update handling. Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown. We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_disable() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+
+ const extension = getExtension();
+ await extension.startup();
+
+ // test onUpdated
+ await Promise.all([
+ extension.awaitMessage("ready"),
+ extension.awaitMessage("changeInfo"),
+ ]);
+ Assert.ok(tab.hidden, "Tab is hidden by extension");
+
+ // Test that disable does hide tabs.
+ await disableExtension(ID);
+ Assert.ok(!tab.hidden, "Tab is not hidden hidden after disable");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js b/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
new file mode 100644
index 0000000000..d622b79e7a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
@@ -0,0 +1,118 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global gBrowser */
+"use strict";
+
+add_task(async function test_highlighted() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ async function testHighlighted(activeIndex, highlightedIndices) {
+ let tabs = await browser.tabs.query({ currentWindow: true });
+ for (let { index, active, highlighted } of tabs) {
+ browser.test.assertEq(
+ index == activeIndex,
+ active,
+ "Check Tab.active: " + index
+ );
+ let expected =
+ highlightedIndices.includes(index) || index == activeIndex;
+ browser.test.assertEq(
+ expected,
+ highlighted,
+ "Check Tab.highlighted: " + index
+ );
+ }
+ let highlightedTabs = await browser.tabs.query({
+ currentWindow: true,
+ highlighted: true,
+ });
+ browser.test.assertEq(
+ highlightedIndices
+ .concat(activeIndex)
+ .sort((a, b) => a - b)
+ .join(),
+ highlightedTabs.map(tab => tab.index).join(),
+ "Check tabs.query with highlighted:true provides the expected tabs"
+ );
+ }
+
+ browser.test.log(
+ "Check that last tab is active, and no other is highlighted"
+ );
+ await testHighlighted(2, []);
+
+ browser.test.log("Highlight first and second tabs");
+ await browser.tabs.highlight({ tabs: [0, 1] });
+ await testHighlighted(0, [1]);
+
+ browser.test.log("Highlight second and first tabs");
+ await browser.tabs.highlight({ tabs: [1, 0] });
+ await testHighlighted(1, [0]);
+
+ browser.test.log("Test that highlight fails for invalid data");
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ tabs: [] }),
+ /No highlighted tab/,
+ "Attempt to highlight no tab should throw"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ windowId: 999999999, tabs: 0 }),
+ /Invalid window ID: 999999999/,
+ "Attempt to highlight tabs in invalid window should throw"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ tabs: 999999999 }),
+ /No tab at index: 999999999/,
+ "Attempt to highlight invalid tab index should throw"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ tabs: [2, 999999999] }),
+ /No tab at index: 999999999/,
+ "Attempt to highlight invalid tab index should throw"
+ );
+
+ browser.test.log(
+ "Highlighted tabs shouldn't be affected by failures above"
+ );
+ await testHighlighted(1, [0]);
+
+ browser.test.log("Highlight last tab");
+ let window = await browser.tabs.highlight({ tabs: 2 });
+ await testHighlighted(2, []);
+
+ browser.test.assertEq(
+ 3,
+ window.tabs.length,
+ "Returned window should be populated"
+ );
+
+ window = await browser.tabs.highlight({ tabs: 2, populate: false });
+ browser.test.assertFalse(
+ "tabs" in window,
+ "Returned window shouldn't be populated"
+ );
+
+ browser.test.notifyPass("test-finished");
+ },
+ });
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_incognito_not_allowed.js b/browser/components/extensions/test/browser/browser_ext_tabs_incognito_not_allowed.js
new file mode 100644
index 0000000000..e998f64afc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_incognito_not_allowed.js
@@ -0,0 +1,155 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScriptIncognitoNotAllowed() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_iframe_document.html";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ // captureTab requires all_urls permission
+ permissions: ["", "tabs", "tabHide"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async pbw => {
+ // expect one tab from the non-pb window
+ let tabs = await browser.tabs.query({ windowId: pbw.windowId });
+ browser.test.assertEq(
+ 0,
+ tabs.length,
+ "unable to query tabs in private window"
+ );
+ tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(
+ 1,
+ tabs.length,
+ "unable to query active tab in private window"
+ );
+ browser.test.assertTrue(
+ tabs[0].windowId != pbw.windowId,
+ "unable to query active tab in private window"
+ );
+
+ // apis that take a tabId
+ let tabIdAPIs = [
+ "captureTab",
+ "detectLanguage",
+ "duplicate",
+ "get",
+ "hide",
+ "reload",
+ "getZoomSettings",
+ "getZoom",
+ "toggleReaderMode",
+ ];
+ for (let name of tabIdAPIs) {
+ await browser.test.assertRejects(
+ browser.tabs[name](pbw.tabId),
+ /Invalid tab ID/,
+ `should not be able to ${name}`
+ );
+ }
+ await browser.test.assertRejects(
+ browser.tabs.captureVisibleTab(pbw.windowId),
+ /Invalid window ID/,
+ "should not be able to duplicate"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.create({
+ windowId: pbw.windowId,
+ url: "http://mochi.test/",
+ }),
+ /Invalid window ID/,
+ "unable to create tab in private window"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(pbw.tabId, { code: "document.URL" }),
+ /Invalid tab ID/,
+ "should not be able to executeScript"
+ );
+ let currentTab = await browser.tabs.getCurrent();
+ browser.test.assertTrue(
+ !currentTab,
+ "unable to get current tab in private window"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ windowId: pbw.windowId, tabs: [pbw.tabId] }),
+ /Invalid window ID/,
+ "should not be able to highlight"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(pbw.tabId, {
+ code: "* { background: rgb(42, 42, 42) }",
+ }),
+ /Invalid tab ID/,
+ "should not be able to insertCSS"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.move(pbw.tabId, {
+ index: 0,
+ windowId: tabs[0].windowId,
+ }),
+ /Invalid tab ID/,
+ "unable to move tab to private window"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.move(tabs[0].id, { index: 0, windowId: pbw.windowId }),
+ /Invalid window ID/,
+ "unable to move tab to private window"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.printPreview(),
+ /Cannot access activeTab/,
+ "unable to printpreview tab"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.removeCSS(pbw.tabId, {}),
+ /Invalid tab ID/,
+ "unable to remove tab css"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.sendMessage(pbw.tabId, "test"),
+ /Could not establish connection/,
+ "unable to sendmessage"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.setZoomSettings(pbw.tabId, {}),
+ /Invalid tab ID/,
+ "should not be able to set zoom settings"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.setZoom(pbw.tabId, 3),
+ /Invalid tab ID/,
+ "should not be able to set zoom"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.update(pbw.tabId, {}),
+ /Invalid tab ID/,
+ "should not be able to update tab"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([pbw.tabId], tabs[0].id),
+ /Invalid tab ID/,
+ "should not be able to moveInSuccession"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([tabs[0].id], pbw.tabId),
+ /Invalid tab ID/,
+ "should not be able to moveInSuccession"
+ );
+
+ browser.test.notifyPass("pass");
+ });
+ },
+ });
+
+ let winData = await getIncognitoWindow(url);
+ await extension.startup();
+
+ extension.sendMessage(winData.details);
+
+ await extension.awaitFinish("pass");
+ await BrowserTestUtils.closeWindow(winData.win);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js b/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js
new file mode 100644
index 0000000000..1a4bbd0c74
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js
@@ -0,0 +1,312 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+add_task(async function testExecuteScript() {
+ let { MessageChannel } = ChromeUtils.importESModule(
+ "resource://testing-common/MessageChannel.sys.mjs"
+ );
+
+ // When the first extension is started, ProxyMessenger.init adds MessageChannel
+ // listeners for Services.mm and Services.ppmm, and they are never unsubscribed.
+ // We have to exclude them after the extension has been unloaded to get an accurate
+ // test.
+ function getMessageManagersSize(messageManagers) {
+ return Array.from(messageManagers).filter(([mm]) => {
+ return ![Services.mm, Services.ppmm].includes(mm);
+ }).length;
+ }
+
+ let messageManagersSize = getMessageManagersSize(
+ MessageChannel.messageManagers
+ );
+ let responseManagersSize = MessageChannel.responseManagers.size;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ async function background() {
+ let tasks = [
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ file: "file2.css",
+ });
+ },
+ },
+ {
+ background: "rgb(42, 42, 42)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ });
+ },
+ },
+ {
+ background: "rgb(43, 43, 43)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs
+ .insertCSS({
+ code: "* { background: rgb(100, 100, 100) !important }",
+ cssOrigin: "author",
+ })
+ .then(r =>
+ browser.tabs.insertCSS({
+ code: "* { background: rgb(43, 43, 43) !important }",
+ cssOrigin: "author",
+ })
+ );
+ },
+ },
+ {
+ background: "rgb(100, 100, 100)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ // User has higher importance
+ return browser.tabs
+ .insertCSS({
+ code: "* { background: rgb(100, 100, 100) !important }",
+ cssOrigin: "user",
+ })
+ .then(r =>
+ browser.tabs.insertCSS({
+ code: "* { background: rgb(44, 44, 44) !important }",
+ cssOrigin: "author",
+ })
+ );
+ },
+ },
+ ];
+
+ function checkCSS() {
+ let computedStyle = window.getComputedStyle(document.body);
+ return [computedStyle.backgroundColor, computedStyle.color];
+ }
+
+ try {
+ for (let { promise, background, foreground } of tasks) {
+ let result = await promise();
+
+ browser.test.assertEq(undefined, result, "Expected callback result");
+
+ [result] = await browser.tabs.executeScript({
+ code: `(${checkCSS})()`,
+ });
+
+ browser.test.assertEq(
+ background,
+ result[0],
+ "Expected background color"
+ );
+ browser.test.assertEq(
+ foreground,
+ result[1],
+ "Expected foreground color"
+ );
+ }
+
+ browser.test.notifyPass("insertCSS");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("insertCSS");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+
+ background,
+
+ files: {
+ "file2.css": "* { color: rgb(0, 113, 4) }",
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("insertCSS");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Make sure that we're not holding on to references to closed message
+ // managers.
+ is(
+ getMessageManagersSize(MessageChannel.messageManagers),
+ messageManagersSize,
+ "Message manager count"
+ );
+ is(
+ MessageChannel.responseManagers.size,
+ responseManagersSize,
+ "Response manager count"
+ );
+ is(MessageChannel.pendingResponses.size, 0, "Pending response count");
+});
+
+add_task(async function testInsertCSS_cleanup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ async function background() {
+ await browser.tabs.insertCSS({ code: "* { background: rgb(42, 42, 42) }" });
+ await browser.tabs.insertCSS({ file: "customize_fg_color.css" });
+
+ browser.test.notifyPass("insertCSS");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+ background,
+ files: {
+ "customize_fg_color.css": `* { color: rgb(255, 0, 0) }`,
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("insertCSS");
+
+ const getTabContentComputedStyle = async () => {
+ let computedStyle = content.getComputedStyle(content.document.body);
+ return [computedStyle.backgroundColor, computedStyle.color];
+ };
+
+ const appliedStyles = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ getTabContentComputedStyle
+ );
+
+ is(
+ appliedStyles[0],
+ "rgb(42, 42, 42)",
+ "The injected CSS code has been applied as expected"
+ );
+ is(
+ appliedStyles[1],
+ "rgb(255, 0, 0)",
+ "The injected CSS file has been applied as expected"
+ );
+
+ await extension.unload();
+
+ const unloadedStyles = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ getTabContentComputedStyle
+ );
+
+ is(
+ unloadedStyles[0],
+ "rgba(0, 0, 0, 0)",
+ "The injected CSS code has been removed as expected"
+ );
+ is(
+ unloadedStyles[1],
+ "rgb(0, 0, 0)",
+ "The injected CSS file has been removed as expected"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Verify that no removeSheet/removeSheetUsingURIString errors are logged while
+// cleaning up css injected using a manifest content script or tabs.insertCSS.
+add_task(async function test_csscode_cleanup_on_closed_windows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://example.com/*"],
+ content_scripts: [
+ {
+ matches: ["http://example.com/*"],
+ css: ["content.css"],
+ run_at: "document_start",
+ },
+ ],
+ },
+
+ files: {
+ "content.css": "body { min-width: 15px; }",
+ },
+
+ async background() {
+ browser.runtime.onConnect.addListener(port => {
+ port.onDisconnect.addListener(() => {
+ browser.test.sendMessage("port-disconnected");
+ });
+ browser.test.sendMessage("port-connected");
+ });
+
+ await browser.tabs.create({
+ url: "http://example.com/",
+ active: true,
+ });
+
+ await browser.tabs.insertCSS({
+ code: "body { max-width: 50px; }",
+ });
+
+ // Create a port, as a way to detect when the content script has been
+ // destroyed and any removeSheet error already collected (if it has been
+ // raised during the content scripts cleanup).
+ await browser.tabs.executeScript({
+ code: `(${function () {
+ const { maxWidth, minWidth } = window.getComputedStyle(document.body);
+ browser.test.sendMessage("body-styles", { maxWidth, minWidth });
+ browser.runtime.connect();
+ }})();`,
+ });
+ },
+ });
+
+ await extension.startup();
+
+ let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ info("Waiting for content scripts to be injected");
+
+ const { maxWidth, minWidth } = await extension.awaitMessage("body-styles");
+ is(maxWidth, "50px", "tabs.insertCSS applied");
+ is(minWidth, "15px", "manifest.content_scripts CSS applied");
+
+ await extension.awaitMessage("port-connected");
+ const tab = gBrowser.selectedTab;
+
+ info("Close tab and wait for content script port to be disconnected");
+ BrowserTestUtils.removeTab(tab);
+ await extension.awaitMessage("port-disconnected");
+ });
+
+ // Look for nsIDOMWindowUtils.removeSheet and
+ // nsIDOMWindowUtils.removeSheetUsingURIString errors.
+ AddonTestUtils.checkMessages(
+ messages,
+ {
+ forbidden: [{ errorMessage: /nsIDOMWindowUtils.removeSheet/ }],
+ },
+ "Expect no remoteSheet errors"
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js b/browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js
new file mode 100644
index 0000000000..c4738d7f2e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js
@@ -0,0 +1,52 @@
+"use strict";
+
+add_task(async function testLastAccessed() {
+ let past = Date.now();
+
+ for (let url of ["https://example.com/?1", "https://example.com/?2"]) {
+ let tab = BrowserTestUtils.addTab(gBrowser, url, { skipAnimation: true });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ browser.test.onMessage.addListener(async function (msg, past) {
+ let [tab1] = await browser.tabs.query({
+ url: "https://example.com/?1",
+ });
+ let [tab2] = await browser.tabs.query({
+ url: "https://example.com/?2",
+ });
+
+ browser.test.assertTrue(tab1 && tab2, "Expected tabs were found");
+
+ let now = Date.now();
+
+ browser.test.assertTrue(
+ past <= tab1.lastAccessed,
+ "lastAccessed of tab 1 is later than the test start time."
+ );
+ browser.test.assertTrue(
+ tab1.lastAccessed < tab2.lastAccessed,
+ "lastAccessed of tab 2 is later than lastAccessed of tab 1."
+ );
+ browser.test.assertTrue(
+ tab2.lastAccessed <= now,
+ "lastAccessed of tab 2 is earlier than now."
+ );
+
+ await browser.tabs.remove([tab1.id, tab2.id]);
+
+ browser.test.notifyPass("tabs.lastAccessed");
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.sendMessage("past", past);
+ await extension.awaitFinish("tabs.lastAccessed");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_lazy.js b/browser/components/extensions/test/browser/browser_ext_tabs_lazy.js
new file mode 100644
index 0000000000..68205089d5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_lazy.js
@@ -0,0 +1,49 @@
+"use strict";
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+const SESSION = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ {
+ entries: [
+ { url: "https://example.com/", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+add_task(async function () {
+ SessionStore.setBrowserState(JSON.stringify(SESSION));
+ await promiseWindowRestored(window);
+ const tab = gBrowser.tabs[1];
+
+ is(tab.getAttribute("pending"), "true", "The tab is pending restore");
+ is(tab.linkedBrowser.isConnected, false, "The tab is lazy");
+
+ async function background() {
+ const [tab] = await browser.tabs.query({ url: "https://example.com/" });
+ browser.test.assertRejects(
+ browser.tabs.sendMessage(tab.id, "void"),
+ /Could not establish connection. Receiving end does not exist/,
+ "No recievers in a tab pending restore."
+ );
+ browser.test.notifyPass("lazy");
+ }
+
+ const manifest = { permissions: ["tabs"] };
+ const extension = ExtensionTestUtils.loadExtension({ manifest, background });
+
+ await extension.startup();
+ await extension.awaitFinish("lazy");
+ await extension.unload();
+
+ is(tab.getAttribute("pending"), "true", "The tab is still pending restore");
+ is(tab.linkedBrowser.isConnected, false, "The tab is still lazy");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_array.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_array.js
new file mode 100644
index 0000000000..72b4adbc31
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_array.js
@@ -0,0 +1,95 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function moveMultiple() {
+ let tabs = [];
+ for (let k of [1, 2, 3, 4]) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ `https://example.com/?${k}`
+ );
+ tabs.push(tab);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["tabs"] },
+ background: async function () {
+ function num(url) {
+ return parseInt(url.slice(-1), 10);
+ }
+
+ async function check(expected) {
+ let tabs = await browser.tabs.query({ url: "https://example.com/*" });
+ let endings = tabs.map(tab => num(tab.url));
+ browser.test.assertTrue(
+ expected.every((v, i) => v === endings[i]),
+ `Tab order should be ${expected}, got ${endings}.`
+ );
+ }
+
+ async function reset() {
+ let tabs = await browser.tabs.query({ url: "https://example.com/*" });
+ await browser.tabs.move(
+ tabs.sort((a, b) => num(a.url) - num(b.url)).map(tab => tab.id),
+ { index: 0 }
+ );
+ }
+
+ async function move(moveIndexes, moveTo) {
+ let tabs = await browser.tabs.query({ url: "https://example.com/*" });
+ await browser.tabs.move(
+ moveIndexes.map(e => tabs[e - 1].id),
+ {
+ index: moveTo,
+ }
+ );
+ }
+
+ let tests = [
+ { move: [2], index: 0, result: [2, 1, 3, 4] },
+ { move: [2], index: -1, result: [1, 3, 4, 2] },
+ // Start -> After first tab -> After second tab
+ { move: [4, 3], index: 0, result: [4, 3, 1, 2] },
+ // [1, 2, 3, 4] -> [1, 4, 2, 3] -> [1, 4, 3, 2]
+ { move: [4, 3], index: 1, result: [1, 4, 3, 2] },
+ // [1, 2, 3, 4] -> [2, 3, 1, 4] -> [3, 1, 2, 4]
+ { move: [1, 2], index: 2, result: [3, 1, 2, 4] },
+ // [1, 2, 3, 4] -> [1, 2, 4, 3] -> [2, 4, 1, 3]
+ { move: [4, 1], index: 2, result: [2, 4, 1, 3] },
+ // [1, 2, 3, 4] -> [2, 3, 1, 4] -> [2, 3, 1, 4]
+ { move: [1, 4], index: 2, result: [2, 3, 1, 4] },
+ ];
+
+ for (let test of tests) {
+ await reset();
+ await move(test.move, test.index);
+ await check(test.result);
+ }
+
+ let firstId = (
+ await browser.tabs.query({
+ url: "https://example.com/*",
+ })
+ )[0].id;
+ // Assuming that tab.id of 12345 does not exist.
+ await browser.test.assertRejects(
+ browser.tabs.move([firstId, 12345], { index: -1 }),
+ /Invalid tab/,
+ "Should receive invalid tab error"
+ );
+ // The first argument got moved, the second on failed.
+ await check([2, 3, 1, 4]);
+
+ browser.test.notifyPass("tabs.move");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move");
+ await extension.unload();
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_array_multiple_windows.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_array_multiple_windows.js
new file mode 100644
index 0000000000..484197cbc5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_array_multiple_windows.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function moveMultipleWindows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["tabs"] },
+ background: async function () {
+ let numToId = new Map();
+ let idToNum = new Map();
+ let windowToInitialTabs = new Map();
+
+ async function createWindow(nums) {
+ let window = await browser.windows.create({
+ url: nums.map(k => `https://example.com/?${k}`),
+ });
+ let tabIds = window.tabs.map(tab => tab.id);
+ windowToInitialTabs.set(window.id, tabIds);
+ for (let i = 0; i < nums.length; ++i) {
+ numToId.set(nums[i], tabIds[i]);
+ idToNum.set(tabIds[i], nums[i]);
+ }
+ return window.id;
+ }
+
+ let win1 = await createWindow([0, 1, 2, 3, 4]);
+ let win2 = await createWindow([5, 6, 7, 8, 9]);
+
+ async function getNums(windowId) {
+ let tabs = await browser.tabs.query({ windowId });
+ return tabs.map(tab => idToNum.get(tab.id));
+ }
+
+ async function check(msg, expected) {
+ let nums1 = getNums(win1);
+ let nums2 = getNums(win2);
+ browser.test.assertEq(
+ JSON.stringify(expected),
+ JSON.stringify({ win1: await nums1, win2: await nums2 }),
+ `Check ${msg}`
+ );
+ }
+
+ async function reset() {
+ for (let [windowId, tabIds] of windowToInitialTabs) {
+ await browser.tabs.move(tabIds, { index: 0, windowId });
+ }
+ }
+
+ async function move(nums, params) {
+ await browser.tabs.move(
+ nums.map(k => numToId.get(k)),
+ params
+ );
+ }
+
+ let tests = [
+ {
+ move: [1, 6],
+ params: { index: 0 },
+ result: { win1: [1, 0, 2, 3, 4], win2: [6, 5, 7, 8, 9] },
+ },
+ {
+ move: [6, 1],
+ params: { index: 0 },
+ result: { win1: [1, 0, 2, 3, 4], win2: [6, 5, 7, 8, 9] },
+ },
+ {
+ move: [1, 6],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0, 2, 3, 4], win2: [1, 6, 5, 7, 8, 9] },
+ },
+ {
+ move: [6, 1],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0, 2, 3, 4], win2: [6, 1, 5, 7, 8, 9] },
+ },
+ {
+ move: [1, 6],
+ params: { index: -1 },
+ result: { win1: [0, 2, 3, 4, 1], win2: [5, 7, 8, 9, 6] },
+ },
+ {
+ move: [6, 1],
+ params: { index: -1 },
+ result: { win1: [0, 2, 3, 4, 1], win2: [5, 7, 8, 9, 6] },
+ },
+ {
+ move: [1, 6],
+ params: { index: -1, windowId: win2 },
+ result: { win1: [0, 2, 3, 4], win2: [5, 7, 8, 9, 1, 6] },
+ },
+ {
+ move: [6, 1],
+ params: { index: -1, windowId: win2 },
+ result: { win1: [0, 2, 3, 4], win2: [5, 7, 8, 9, 6, 1] },
+ },
+ {
+ move: [2, 1, 7, 6],
+ params: { index: 3 },
+ result: { win1: [0, 3, 2, 1, 4], win2: [5, 8, 7, 6, 9] },
+ },
+ {
+ move: [1, 2, 3, 4],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0], win2: [1, 2, 3, 4, 5, 6, 7, 8, 9] },
+ },
+ {
+ move: [0, 1, 2, 3],
+ params: { index: 5, windowId: win2 },
+ result: { win1: [4], win2: [5, 6, 7, 8, 9, 0, 1, 2, 3] },
+ },
+ {
+ move: [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0], win2: [1, 2, 3, 4, 5, 6, 7, 8, 9] },
+ },
+ {
+ move: [5, 6, 7, 8, 9, 0, 1, 2, 3],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [4], win2: [5, 6, 7, 8, 9, 0, 1, 2, 3] },
+ },
+ {
+ move: [5, 1, 6, 2, 7, 3, 8, 4, 9],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0], win2: [5, 1, 6, 2, 7, 3, 8, 4, 9] },
+ },
+ {
+ move: [5, 1, 6, 2, 7, 3, 8, 4, 9],
+ params: { index: 1, windowId: win2 },
+ result: { win1: [0], win2: [5, 1, 6, 2, 7, 3, 8, 4, 9] },
+ },
+ {
+ move: [5, 1, 6, 2, 7, 3, 8, 4, 9],
+ params: { index: 999, windowId: win2 },
+ result: { win1: [0], win2: [5, 1, 6, 2, 7, 3, 8, 4, 9] },
+ },
+ ];
+
+ const initial = { win1: [0, 1, 2, 3, 4], win2: [5, 6, 7, 8, 9] };
+ await check("initial", initial);
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test));
+ await move(test.move, test.params);
+ await check("move", test.result);
+ await reset();
+ await check("reset", initial);
+ }
+
+ await browser.windows.remove(win1);
+ await browser.windows.remove(win2);
+ browser.test.notifyPass("tabs.move");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_discarded.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_discarded.js
new file mode 100644
index 0000000000..ebee4fbc90
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_discarded.js
@@ -0,0 +1,94 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function move_discarded_to_window() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["tabs"] },
+ background: async function () {
+ // Create a discarded tab
+ let url = "https://example.com/";
+ let tab = await browser.tabs.create({ url, discarded: true });
+ browser.test.assertEq(true, tab.discarded, "Tab should be discarded");
+ browser.test.assertEq(url, tab.url, "Tab URL should be correct");
+
+ // Create a new window
+ let { id: windowId } = await browser.windows.create();
+
+ // Move the tab into that window
+ [tab] = await browser.tabs.move(tab.id, { windowId, index: -1 });
+ browser.test.assertTrue(tab.discarded, "Tab should still be discarded");
+ browser.test.assertEq(url, tab.url, "Tab URL should still be correct");
+
+ await browser.windows.remove(windowId);
+ browser.test.notifyPass("tabs.move");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move");
+ await extension.unload();
+});
+
+add_task(async function move_hidden_discarded_to_window() {
+ let extensionWithoutTabsPermission = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["https://example.com/"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.hidden) {
+ browser.test.assertEq(
+ tab.url,
+ "https://example.com/?hideme",
+ "tab.url is correctly observed without tabs permission"
+ );
+ browser.test.sendMessage("onUpdated_checked");
+ }
+ });
+ // Listener with "urls" filter, regression test for
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1695346
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo, tab) => {
+ browser.test.assertTrue(changeInfo.hidden, "tab was hidden");
+ browser.test.sendMessage("onUpdated_urls_filter");
+ },
+ {
+ properties: ["hidden"],
+ urls: ["https://example.com/?hideme"],
+ }
+ );
+ },
+ });
+ await extensionWithoutTabsPermission.startup();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["tabs", "tabHide"] },
+ // ExtensionControlledPopup's populateDescription method requires an addon:
+ useAddonManager: "temporary",
+ async background() {
+ let url = "https://example.com/?hideme";
+ let tab = await browser.tabs.create({ url, discarded: true });
+ await browser.tabs.hide(tab.id);
+
+ let { id: windowId } = await browser.windows.create();
+
+ // Move the tab into that window
+ [tab] = await browser.tabs.move(tab.id, { windowId, index: -1 });
+ browser.test.assertTrue(tab.discarded, "Tab should still be discarded");
+ browser.test.assertTrue(tab.hidden, "Tab should still be hidden");
+ browser.test.assertEq(url, tab.url, "Tab URL should still be correct");
+
+ await browser.windows.remove(windowId);
+ browser.test.notifyPass("move_hidden_discarded_to_window");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("move_hidden_discarded_to_window");
+ await extension.unload();
+
+ await extensionWithoutTabsPermission.awaitMessage("onUpdated_checked");
+ await extensionWithoutTabsPermission.awaitMessage("onUpdated_urls_filter");
+ await extensionWithoutTabsPermission.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js
new file mode 100644
index 0000000000..56d82b3d3a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js
@@ -0,0 +1,178 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ incognitoOverride: "spanning",
+ async background() {
+ const URL = "https://example.com/";
+ let mainWindow = await browser.windows.getCurrent();
+ let newWindow = await browser.windows.create({
+ url: [URL, URL],
+ });
+ let privateWindow = await browser.windows.create({
+ incognito: true,
+ url: [URL, URL],
+ });
+
+ browser.tabs.onUpdated.addListener(() => {
+ // Bug 1398272: Adding onUpdated listener broke tab IDs across windows.
+ });
+
+ let tab = newWindow.tabs[0].id;
+ let privateTab = privateWindow.tabs[0].id;
+
+ // Assuming that this windowId does not exist.
+ await browser.test.assertRejects(
+ browser.tabs.move(tab, { windowId: 123144576, index: 0 }),
+ /Invalid window/,
+ "Should receive invalid window error"
+ );
+
+ // Test that a tab cannot be moved to a private window.
+ let moved = await browser.tabs.move(tab, {
+ windowId: privateWindow.id,
+ index: 0,
+ });
+ browser.test.assertEq(
+ moved.length,
+ 0,
+ "tab was not moved to private window"
+ );
+ // Test that a private tab cannot be moved to a non-private window.
+ moved = await browser.tabs.move(privateTab, {
+ windowId: newWindow.id,
+ index: 0,
+ });
+ browser.test.assertEq(
+ moved.length,
+ 0,
+ "tab was not moved from private window"
+ );
+
+ // Verify tabs did not move between windows via another query.
+ let windows = await browser.windows.getAll({ populate: true });
+ let newWin2 = windows.find(w => w.id === newWindow.id);
+ browser.test.assertTrue(newWin2, "Found window");
+ browser.test.assertEq(
+ newWin2.tabs.length,
+ 2,
+ "Window still has two tabs"
+ );
+ for (let origTab of newWindow.tabs) {
+ browser.test.assertTrue(
+ newWin2.tabs.find(t => t.id === origTab.id),
+ `Window still has tab ${origTab.id}`
+ );
+ }
+
+ let privateWin2 = windows.find(w => w.id === privateWindow.id);
+ browser.test.assertTrue(privateWin2 !== null, "Found private window");
+ browser.test.assertEq(
+ privateWin2.incognito,
+ true,
+ "Private window is still private"
+ );
+ browser.test.assertEq(
+ privateWin2.tabs.length,
+ 2,
+ "Private window still has two tabs"
+ );
+ for (let origTab of privateWindow.tabs) {
+ browser.test.assertTrue(
+ privateWin2.tabs.find(t => t.id === origTab.id),
+ `Private window still has tab ${origTab.id}`
+ );
+ }
+
+ // Move a tab from one non-private window to another
+ await browser.tabs.move(tab, { windowId: mainWindow.id, index: 0 });
+
+ mainWindow = await browser.windows.get(mainWindow.id, { populate: true });
+ browser.test.assertTrue(
+ mainWindow.tabs.find(t => t.id === tab),
+ "Moved tab is in main window"
+ );
+
+ newWindow = await browser.windows.get(newWindow.id, { populate: true });
+ browser.test.assertEq(
+ newWindow.tabs.length,
+ 1,
+ "New window has 1 tab left"
+ );
+ browser.test.assertTrue(
+ newWindow.tabs[0].id != tab,
+ "Moved tab is no longer in original window"
+ );
+
+ await browser.windows.remove(newWindow.id);
+ await browser.windows.remove(privateWindow.id);
+ await browser.tabs.remove(tab);
+
+ browser.test.notifyPass("tabs.move.window");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move.window");
+ await extension.unload();
+});
+
+add_task(async function test_currentWindowAfterTabMoved() {
+ const files = {
+ "current.html": " ",
+ "current.js": function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "current") {
+ browser.windows.getCurrent(win => {
+ browser.test.sendMessage("id", win.id);
+ });
+ }
+ });
+ browser.test.sendMessage("ready");
+ },
+ };
+
+ async function background() {
+ let tabId;
+
+ const url = browser.runtime.getURL("current.html");
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "move") {
+ await browser.windows.create({ tabId });
+ browser.test.sendMessage("moved");
+ } else if (msg === "close") {
+ await browser.tabs.remove(tabId);
+ browser.test.sendMessage("done");
+ }
+ });
+
+ let tab = await browser.tabs.create({ url });
+ tabId = tab.id;
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({ files, background });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ extension.sendMessage("current");
+ const first = await extension.awaitMessage("id");
+
+ extension.sendMessage("move");
+ await extension.awaitMessage("moved");
+
+ extension.sendMessage("current");
+ const second = await extension.awaitMessage("id");
+
+ isnot(first, second, "current window id is different after moving the tab");
+
+ extension.sendMessage("close");
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js
new file mode 100644
index 0000000000..559cd7dd46
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js
@@ -0,0 +1,64 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ const URL = "https://example.com/";
+ let mainWin = await browser.windows.getCurrent();
+ let tab1 = await browser.tabs.create({ url: URL });
+ let tab2 = await browser.tabs.create({ url: URL });
+
+ let newWin = await browser.windows.create({ url: [URL, URL] });
+ browser.test.assertEq(newWin.tabs.length, 2, "New window has 2 tabs");
+ let [tab3, tab4] = newWin.tabs;
+
+ // move tabs in both windows to index 0 in a single call
+ await browser.tabs.move([tab2.id, tab4.id], { index: 0 });
+
+ tab1 = await browser.tabs.get(tab1.id);
+ browser.test.assertEq(
+ tab1.windowId,
+ mainWin.id,
+ "tab 1 is still in main window"
+ );
+
+ tab2 = await browser.tabs.get(tab2.id);
+ browser.test.assertEq(
+ tab2.windowId,
+ mainWin.id,
+ "tab 2 is still in main window"
+ );
+ browser.test.assertEq(tab2.index, 0, "tab 2 moved to index 0");
+
+ tab3 = await browser.tabs.get(tab3.id);
+ browser.test.assertEq(
+ tab3.windowId,
+ newWin.id,
+ "tab 3 is still in new window"
+ );
+
+ tab4 = await browser.tabs.get(tab4.id);
+ browser.test.assertEq(
+ tab4.windowId,
+ newWin.id,
+ "tab 4 is still in new window"
+ );
+ browser.test.assertEq(tab4.index, 0, "tab 4 moved to index 0");
+
+ await browser.tabs.remove([tab1.id, tab2.id]);
+ await browser.windows.remove(newWin.id);
+
+ browser.test.notifyPass("tabs.move.multiple");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move.multiple");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js
new file mode 100644
index 0000000000..3898540fa0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ const URL = "https://example.com/";
+
+ let mainWin = await browser.windows.getCurrent();
+ let tab = await browser.tabs.create({ url: URL });
+
+ let newWin = await browser.windows.create({ url: URL });
+ let tab2 = newWin.tabs[0];
+ await browser.tabs.update(tab2.id, { pinned: true });
+
+ // Try to move a tab before the pinned tab. The move should be ignored.
+ let moved = await browser.tabs.move(tab.id, {
+ windowId: newWin.id,
+ index: 0,
+ });
+ browser.test.assertEq(moved.length, 0, "move() returned no moved tab");
+
+ tab = await browser.tabs.get(tab.id);
+ browser.test.assertEq(
+ tab.windowId,
+ mainWin.id,
+ "Tab stayed in its original window"
+ );
+
+ await browser.tabs.remove(tab.id);
+ await browser.windows.remove(newWin.id);
+ browser.test.notifyPass("tabs.move.pin");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move.pin");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js b/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js
new file mode 100644
index 0000000000..19146fbe42
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js
@@ -0,0 +1,96 @@
+"use strict";
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+const NEWTAB_PRIVATE_ALLOWED = "browser.newtab.privateAllowed";
+const NEWTAB_EXTENSION_CONTROLLED = "browser.newtab.extensionControlled";
+const NEWTAB_URI = "webext-newtab-1.html";
+
+function promisePrefChange(pref) {
+ return new Promise((resolve, reject) => {
+ Services.prefs.addObserver(pref, function observer() {
+ Services.prefs.removeObserver(pref, observer);
+ resolve(arguments);
+ });
+ });
+}
+
+function verifyPrefSettings(controlled, allowed) {
+ is(
+ Services.prefs.getBoolPref(NEWTAB_EXTENSION_CONTROLLED, false),
+ controlled,
+ "newtab extension controlled"
+ );
+ is(
+ Services.prefs.getBoolPref(NEWTAB_PRIVATE_ALLOWED, false),
+ allowed,
+ "newtab private permission after permission change"
+ );
+
+ if (controlled) {
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI),
+ "Newtab url is overridden by the extension."
+ );
+ }
+ if (controlled && allowed) {
+ ok(
+ BROWSER_NEW_TAB_URL.endsWith(NEWTAB_URI),
+ "active newtab url is overridden by the extension."
+ );
+ } else {
+ let expectednewTab = controlled ? "about:privatebrowsing" : "about:newtab";
+ is(BROWSER_NEW_TAB_URL, expectednewTab, "active newtab url is default.");
+ }
+}
+
+async function promiseUpdatePrivatePermission(allowed, extension) {
+ info(`update private allowed permission`);
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+ await Promise.all([
+ promisePrefChange(NEWTAB_PRIVATE_ALLOWED),
+ ExtensionPermissions[allowed ? "add" : "remove"](
+ extension.id,
+ { permissions: ["internal:privateBrowsingAllowed"], origins: [] },
+ ext
+ ),
+ ]);
+
+ verifyPrefSettings(true, allowed);
+}
+
+add_task(async function test_new_tab_private() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "@private-newtab",
+ },
+ },
+ chrome_url_overrides: {
+ newtab: NEWTAB_URI,
+ },
+ },
+ files: {
+ NEWTAB_URI: `
+
+
+
+
+
+
+
+ `,
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ verifyPrefSettings(true, false);
+
+ await promiseUpdatePrivatePermission(true, extension);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js b/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js
new file mode 100644
index 0000000000..b48047abde
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js
@@ -0,0 +1,35 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function test_onCreated_active() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ browser.tabs.onCreated.addListener(tab => {
+ browser.tabs.remove(tab.id);
+ browser.test.sendMessage("onCreated", tab);
+ });
+ browser.tabs.onUpdated.addListener((tabId, changes, tab) => {
+ browser.test.assertEq(
+ '["status"]',
+ JSON.stringify(Object.keys(changes)),
+ "Should get no update other than 'status' during tab creation."
+ );
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ BrowserOpenTab();
+
+ let tab = await extension.awaitMessage("onCreated");
+ is(true, tab.active, "Tab should be active");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js b/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js
new file mode 100644
index 0000000000..a614dc6144
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js
@@ -0,0 +1,130 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function test_onHighlighted() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ async function expectHighlighted(fn, action) {
+ let resolve;
+ let promise = new Promise(r => {
+ resolve = r;
+ });
+ let expected;
+ let events = [];
+ let listener = highlightInfo => {
+ events.push(highlightInfo);
+ if (expected && expected.length >= events.length) {
+ resolve();
+ }
+ };
+ browser.tabs.onHighlighted.addListener(listener);
+ expected = (await fn()) || [];
+ if (events.length < expected.length) {
+ await promise;
+ }
+ let unexpected = events.splice(expected.length);
+ browser.test.assertEq(
+ JSON.stringify(expected),
+ JSON.stringify(events),
+ `Should get ${expected.length} expected onHighlighted events when ${action}`
+ );
+ if (unexpected.length) {
+ browser.test.fail(
+ `${unexpected.length} unexpected onHighlighted events when ${action}: ` +
+ JSON.stringify(unexpected)
+ );
+ }
+ browser.tabs.onHighlighted.removeListener(listener);
+ }
+
+ let [{ id, windowId }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ let windows = [windowId];
+ let tabs = [id];
+
+ await expectHighlighted(async () => {
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?1",
+ });
+ tabs.push(tab.id);
+ return [{ tabIds: [tabs[1]], windowId: windows[0] }];
+ }, "creating a new active tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.update(tabs[0], { active: true });
+ return [{ tabIds: [tabs[0]], windowId: windows[0] }];
+ }, "selecting former tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [0, 1] });
+ return [{ tabIds: [tabs[0], tabs[1]], windowId: windows[0] }];
+ }, "highlighting both tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [1, 0] });
+ return [{ tabIds: [tabs[0], tabs[1]], windowId: windows[0] }];
+ }, "highlighting same tabs but changing selected one");
+
+ await expectHighlighted(async () => {
+ let tab = await browser.tabs.create({
+ active: false,
+ url: "about:blank?2",
+ });
+ tabs.push(tab.id);
+ }, "create a new inactive tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [2, 0, 1] });
+ return [{ tabIds: [tabs[0], tabs[1], tabs[2]], windowId: windows[0] }];
+ }, "highlighting all tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.move(tabs[1], { index: 0 });
+ }, "reordering tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [0] });
+ return [{ tabIds: [tabs[1]], windowId: windows[0] }];
+ }, "highlighting moved tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [0] });
+ }, "highlighting again");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [2, 1, 0] });
+ return [{ tabIds: [tabs[1], tabs[0], tabs[2]], windowId: windows[0] }];
+ }, "highlighting all tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [2, 0, 1] });
+ }, "highlighting same tabs with different order");
+
+ await expectHighlighted(async () => {
+ let window = await browser.windows.create({ tabId: tabs[2] });
+ windows.push(window.id);
+ // Bug 1481185: on Chrome it's [tabs[1], tabs[0]] instead of [tabs[0]]
+ return [
+ { tabIds: [tabs[0]], windowId: windows[0] },
+ { tabIds: [tabs[2]], windowId: windows[1] },
+ ];
+ }, "moving selected tab into a new window");
+
+ await browser.tabs.remove(tabs.slice(1));
+ browser.test.notifyPass("test-finished");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js
new file mode 100644
index 0000000000..a59fa21f8a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js
@@ -0,0 +1,339 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(async function () {
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await focusWindow(win1);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/*/context_tabs_onUpdated_page.html"],
+ js: ["content-script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+
+ background: function () {
+ let pageURL =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html";
+
+ let expectedSequence = [
+ { status: "loading" },
+ { status: "loading", url: pageURL },
+ { status: "complete" },
+ ];
+ let collectedSequence = [];
+
+ browser.tabs.onUpdated.addListener(function (tabId, updatedInfo) {
+ // onUpdated also fires with updatedInfo.faviconUrl, so explicitly
+ // check for updatedInfo.status before recording the event.
+ if ("status" in updatedInfo) {
+ collectedSequence.push(updatedInfo);
+ }
+ });
+
+ browser.runtime.onMessage.addListener(function () {
+ if (collectedSequence.length !== expectedSequence.length) {
+ browser.test.assertEq(
+ JSON.stringify(expectedSequence),
+ JSON.stringify(collectedSequence),
+ "got unexpected number of updateInfo data"
+ );
+ } else {
+ for (let i = 0; i < expectedSequence.length; i++) {
+ browser.test.assertEq(
+ expectedSequence[i].status,
+ collectedSequence[i].status,
+ "check updatedInfo status"
+ );
+ if (expectedSequence[i].url || collectedSequence[i].url) {
+ browser.test.assertEq(
+ expectedSequence[i].url,
+ collectedSequence[i].url,
+ "check updatedInfo url"
+ );
+ }
+ }
+ }
+
+ browser.test.notifyPass("tabs.onUpdated");
+ });
+
+ browser.tabs.create({ url: pageURL });
+ },
+ files: {
+ "content-script.js": `
+ window.addEventListener("message", function(evt) {
+ if (evt.data == "frame-updated") {
+ browser.runtime.sendMessage("load-completed");
+ }
+ }, true);
+ `,
+ },
+ });
+
+ await Promise.all([
+ extension.startup(),
+ extension.awaitFinish("tabs.onUpdated"),
+ ]);
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(win1);
+});
+
+async function do_test_update(background, withPermissions = true) {
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await focusWindow(win1);
+
+ let manifest = {};
+ if (withPermissions) {
+ manifest.permissions = ["tabs", "http://mochi.test/"];
+ }
+ let extension = ExtensionTestUtils.loadExtension({ manifest, background });
+
+ await extension.startup();
+ await extension.awaitFinish("finish");
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(win1);
+}
+
+add_task(async function test_pinned() {
+ await do_test_update(function background() {
+ // Create a new tab for testing update.
+ browser.tabs.create({}, function (tab) {
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ // Check callback
+ browser.test.assertEq(tabId, tab.id, "Check tab id");
+ browser.test.log("onUpdate: " + JSON.stringify(changeInfo));
+ if ("pinned" in changeInfo) {
+ browser.test.assertTrue(changeInfo.pinned, "Check changeInfo.pinned");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ // Remove created tab.
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ });
+ browser.tabs.update(tab.id, { pinned: true });
+ });
+ });
+});
+
+add_task(async function test_unpinned() {
+ await do_test_update(function background() {
+ // Create a new tab for testing update.
+ browser.tabs.create({ pinned: true }, function (tab) {
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ // Check callback
+ browser.test.assertEq(tabId, tab.id, "Check tab id");
+ browser.test.log("onUpdate: " + JSON.stringify(changeInfo));
+ if ("pinned" in changeInfo) {
+ browser.test.assertFalse(
+ changeInfo.pinned,
+ "Check changeInfo.pinned"
+ );
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ // Remove created tab.
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ });
+ browser.tabs.update(tab.id, { pinned: false });
+ });
+ });
+});
+
+add_task(async function test_url() {
+ await do_test_update(function background() {
+ // Create a new tab for testing update.
+ browser.tabs.create({ url: "about:blank?initial_url=1" }, function (tab) {
+ const expectedUpdatedURL = "about:blank?updated_url=1";
+
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ // Wait for the tabs.onUpdated events related to the updated url (because
+ // there is a good chance that we may still be receiving events related to
+ // the browser.tabs.create API call above before we are able to start
+ // loading the new url from the browser.tabs.update API call below).
+ if ("url" in changeInfo && changeInfo.url === expectedUpdatedURL) {
+ browser.test.assertEq(
+ expectedUpdatedURL,
+ changeInfo.url,
+ "Got tabs.onUpdated event for the expected url"
+ );
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ // Remove created tab.
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ // Check callback
+ browser.test.assertEq(tabId, tab.id, "Check tab id");
+ browser.test.log("onUpdate: " + JSON.stringify(changeInfo));
+ });
+
+ browser.tabs.update(tab.id, { url: expectedUpdatedURL });
+ });
+ });
+});
+
+add_task(async function test_title() {
+ await do_test_update(async function background() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html";
+ const tab = await browser.tabs.create({ url });
+
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ browser.test.assertEq(tabId, tab.id, "Check tab id");
+ browser.test.log(`onUpdated: ${JSON.stringify(changeInfo)}`);
+ if ("title" in changeInfo && changeInfo.title === "New Message (1)") {
+ browser.test.log("changeInfo.title is correct");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ });
+
+ browser.tabs.executeScript(tab.id, {
+ code: "document.title = 'New Message (1)'",
+ });
+ });
+});
+
+add_task(async function test_without_tabs_permission() {
+ await do_test_update(async function background() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html";
+ let tab = null;
+ let count = 0;
+
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ // An attention change can happen during tabs.create, so
+ // we can't compare against tab yet.
+ if (!("attention" in changeInfo)) {
+ browser.test.assertEq(tabId, tab.id, "Check tab id");
+ }
+ browser.test.log(`onUpdated: ${JSON.stringify(changeInfo)}`);
+
+ browser.test.assertFalse(
+ "url" in changeInfo,
+ "url should not be included without tabs permission"
+ );
+ browser.test.assertFalse(
+ "favIconUrl" in changeInfo,
+ "favIconUrl should not be included without tabs permission"
+ );
+ browser.test.assertFalse(
+ "title" in changeInfo,
+ "title should not be included without tabs permission"
+ );
+
+ if (changeInfo.status == "complete") {
+ count++;
+ if (count === 1) {
+ browser.tabs.reload(tabId);
+ } else {
+ browser.test.log("Reload complete");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ }
+ });
+
+ tab = await browser.tabs.create({ url });
+ }, false /* withPermissions */);
+});
+
+add_task(async function test_onUpdated_after_onRemoved() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html";
+ let removed = false;
+ let tab;
+
+ // If remove happens fast and we never receive onUpdated, that is ok, but
+ // we never want to receive onUpdated after onRemoved.
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ if (!tab || tab.id !== tabId) {
+ return;
+ }
+ browser.test.assertFalse(
+ removed,
+ "tab has not been removed before onUpdated"
+ );
+ });
+
+ browser.tabs.onRemoved.addListener((tabId, removedInfo) => {
+ if (!tab || tab.id !== tabId) {
+ return;
+ }
+ removed = true;
+ browser.test.notifyPass("onRemoved");
+ });
+
+ tab = await browser.tabs.create({ url });
+ browser.tabs.remove(tab.id);
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("onRemoved");
+ await extension.unload();
+});
+
+// Regression test for Bug 1852391.
+add_task(async function test_pin_discarded_tab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ const url = "http://mochi.test:8888";
+ const newTab = await browser.tabs.create({
+ url,
+ active: false,
+ discarded: true,
+ });
+ browser.tabs.onUpdated.addListener(
+ async (tabId, changeInfo) => {
+ browser.test.assertEq(
+ tabId,
+ newTab.id,
+ "Expect onUpdated to be fired for the expected tab"
+ );
+ browser.test.assertEq(
+ changeInfo.pinned,
+ true,
+ "Expect pinned to be set to true"
+ );
+ await browser.tabs.remove(newTab.id);
+ browser.test.notifyPass("onPinned");
+ },
+ { properties: ["pinned"] }
+ );
+ await browser.tabs.update(newTab.id, { pinned: true }).catch(err => {
+ browser.test.fail(`Got unexpected rejection from tabs.update: ${err}`);
+ browser.test.notifyFail("onPinned");
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("onPinned");
+ await extension.unload();
+});
+
+add_task(forceGC);
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated_filter.js b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated_filter.js
new file mode 100644
index 0000000000..83d305e491
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated_filter.js
@@ -0,0 +1,354 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_filter_url() {
+ let ext_fail = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ `received unexpected onUpdated event ${JSON.stringify(changeInfo)}`
+ );
+ },
+ { urls: ["*://*.mozilla.org/*"] }
+ );
+ },
+ });
+ await ext_fail.startup();
+
+ let ext_perm = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ `received unexpected onUpdated event without tabs permission`
+ );
+ },
+ { urls: ["*://mochi.test/*"] }
+ );
+ },
+ });
+ await ext_perm.startup();
+
+ let ext_ok = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.log(`got onUpdated ${JSON.stringify(changeInfo)}`);
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { urls: ["*://mochi.test/*"] }
+ );
+ },
+ });
+ await ext_ok.startup();
+ let ok1 = ext_ok.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ await ok1;
+
+ await ext_ok.unload();
+ await ext_fail.unload();
+ await ext_perm.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_url_activeTab() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ "should only have notification for activeTab, selectedTab is not activeTab"
+ );
+ },
+ { urls: ["*://mochi.test/*"] }
+ );
+ },
+ });
+ await ext.startup();
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { urls: ["*://mochi.test/*"] }
+ );
+ },
+ });
+ await ext2.startup();
+ let ok = ext2.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/#foreground"
+ );
+ await Promise.all([ok]);
+
+ await ext.unload();
+ await ext2.unload();
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_tabId() {
+ let ext_fail = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ `received unexpected onUpdated event ${JSON.stringify(changeInfo)}`
+ );
+ },
+ { tabId: 12345 }
+ );
+ },
+ });
+ await ext_fail.startup();
+
+ let ext_ok = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ });
+ },
+ });
+ await ext_ok.startup();
+ let ok = ext_ok.awaitFinish("onUpdated");
+
+ let ext_ok2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onCreated.addListener(tab => {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { tabId: tab.id }
+ );
+ browser.test.log(`Tab specific tab listener on tab ${tab.id}`);
+ });
+ },
+ });
+ await ext_ok2.startup();
+ let ok2 = ext_ok2.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ await Promise.all([ok, ok2]);
+
+ await ext_ok.unload();
+ await ext_ok2.unload();
+ await ext_fail.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_windowId() {
+ let ext_fail = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ `received unexpected onUpdated event ${JSON.stringify(changeInfo)}`
+ );
+ },
+ { windowId: 12345 }
+ );
+ },
+ });
+ await ext_fail.startup();
+
+ let ext_ok = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { windowId: browser.windows.WINDOW_ID_CURRENT }
+ );
+ },
+ });
+ await ext_ok.startup();
+ let ok = ext_ok.awaitFinish("onUpdated");
+
+ let ext_ok2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ let window = await browser.windows.getCurrent();
+ browser.test.log(`Window specific tab listener on window ${window.id}`);
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { windowId: window.id }
+ );
+ browser.test.sendMessage("ready");
+ },
+ });
+ await ext_ok2.startup();
+ await ext_ok2.awaitMessage("ready");
+ let ok2 = ext_ok2.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ await Promise.all([ok, ok2]);
+
+ await ext_ok.unload();
+ await ext_ok2.unload();
+ await ext_fail.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_isArticle() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ // We expect only status updates, anything else is a failure.
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.log(`got onUpdated ${JSON.stringify(changeInfo)}`);
+ if ("isArticle" in changeInfo) {
+ browser.test.notifyPass("isArticle");
+ }
+ },
+ { properties: ["isArticle"] }
+ );
+ },
+ });
+ await extension.startup();
+ let ok = extension.awaitFinish("isArticle");
+
+ const baseUrl = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://mochi.test:8888/"
+ );
+ const url = `${baseUrl}/readerModeArticle.html`;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await ok;
+
+ await extension.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_property() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ // We expect only status updates, anything else is a failure.
+ let properties = new Set([
+ "audible",
+ "discarded",
+ "favIconUrl",
+ "hidden",
+ "isArticle",
+ "mutedInfo",
+ "pinned",
+ "sharingState",
+ "title",
+ "url",
+ ]);
+
+ // Test that updated only happens after created.
+ let created = false;
+ let tabIds = (await browser.tabs.query({})).map(t => t.id);
+ browser.tabs.onCreated.addListener(tab => {
+ created = tab.id;
+ });
+
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ // ignore tabs created prior to extension startup
+ if (tabIds.includes(tabId)) {
+ return;
+ }
+ browser.test.assertEq(created, tabId, "tab created before updated");
+
+ browser.test.log(`got onUpdated ${JSON.stringify(changeInfo)}`);
+ browser.test.assertTrue(!!changeInfo.status, "changeInfo has status");
+ if (Object.keys(changeInfo).some(p => properties.has(p))) {
+ browser.test.fail(
+ `received unexpected onUpdated event ${JSON.stringify(
+ changeInfo
+ )}`
+ );
+ }
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { properties: ["status"] }
+ );
+ browser.test.sendMessage("ready");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ let ok = extension.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ await ok;
+
+ await extension.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_opener.js b/browser/components/extensions/test/browser/browser_ext_tabs_opener.js
new file mode 100644
index 0000000000..f5ea6c7a27
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_opener.js
@@ -0,0 +1,130 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank?1"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank?2"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background() {
+ let activeTab;
+ let tabId;
+ let tabIds;
+ browser.tabs
+ .query({ lastFocusedWindow: true })
+ .then(tabs => {
+ browser.test.assertEq(3, tabs.length, "We have three tabs");
+
+ browser.test.assertTrue(tabs[1].active, "Tab 1 is active");
+ activeTab = tabs[1];
+
+ tabIds = tabs.map(tab => tab.id);
+
+ return browser.tabs.create({
+ openerTabId: activeTab.id,
+ active: false,
+ });
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ activeTab.id,
+ tab.openerTabId,
+ "Tab opener ID is correct"
+ );
+ browser.test.assertEq(
+ activeTab.index + 1,
+ tab.index,
+ "Tab was inserted after the related current tab"
+ );
+
+ tabId = tab.id;
+ return browser.tabs.get(tabId);
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ activeTab.id,
+ tab.openerTabId,
+ "Tab opener ID is still correct"
+ );
+
+ return browser.tabs.update(tabId, { openerTabId: tabIds[0] });
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ tabIds[0],
+ tab.openerTabId,
+ "Updated tab opener ID is correct"
+ );
+
+ return browser.tabs.get(tabId);
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ tabIds[0],
+ tab.openerTabId,
+ "Updated tab opener ID is still correct"
+ );
+
+ return browser.tabs.create({ openerTabId: tabId, active: false });
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ tabId,
+ tab.openerTabId,
+ "New tab opener ID is correct"
+ );
+ browser.test.assertEq(
+ tabIds.length,
+ tab.index,
+ "New tab was not inserted after the unrelated current tab"
+ );
+
+ let promise = browser.tabs.remove(tabId);
+
+ tabId = tab.id;
+ return promise;
+ })
+ .then(() => {
+ return browser.tabs.get(tabId);
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ undefined,
+ tab.openerTabId,
+ "Tab opener ID was cleared after opener tab closed"
+ );
+
+ return browser.tabs.remove(tabId);
+ })
+ .then(() => {
+ browser.test.notifyPass("tab-opener");
+ })
+ .catch(e => {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tab-opener");
+ });
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("tab-opener");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_printPreview.js b/browser/components/extensions/test/browser/browser_ext_tabs_printPreview.js
new file mode 100644
index 0000000000..81dee72f93
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_printPreview.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testPrintPreview() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ await browser.tabs.printPreview();
+ browser.test.assertTrue(true, "print preview entered");
+ browser.test.notifyPass("tabs.printPreview");
+ },
+ });
+
+ is(
+ document.querySelector(".printPreviewBrowser"),
+ null,
+ "There shouldn't be any print preview browser"
+ );
+
+ await extension.startup();
+
+ // Ensure we're showing the preview...
+ await BrowserTestUtils.waitForCondition(() => {
+ let preview = document.querySelector(".printPreviewBrowser");
+ return preview && BrowserTestUtils.isVisible(preview);
+ });
+
+ gBrowser.getTabDialogBox(gBrowser.selectedBrowser).abortAllDialogs();
+ // Wait for the preview to go away
+ await BrowserTestUtils.waitForCondition(
+ () => !document.querySelector(".printPreviewBrowser")
+ );
+
+ await extension.awaitFinish("tabs.printPreview");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(gBrowser.tabs[1]);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_query.js b/browser/components/extensions/test/browser/browser_ext_tabs_query.js
new file mode 100644
index 0000000000..099588c701
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_query.js
@@ -0,0 +1,468 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:config"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ let tabs = await browser.tabs.query({ lastFocusedWindow: true });
+ browser.test.assertEq(tabs.length, 3, "should have three tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(tabs[0].url, "about:blank", "first tab blank");
+ tabs.shift();
+
+ browser.test.assertTrue(tabs[0].active, "tab 0 active");
+ browser.test.assertFalse(tabs[1].active, "tab 1 inactive");
+
+ browser.test.assertFalse(tabs[0].pinned, "tab 0 unpinned");
+ browser.test.assertFalse(tabs[1].pinned, "tab 1 unpinned");
+
+ browser.test.assertEq(tabs[0].url, "about:robots", "tab 0 url correct");
+ browser.test.assertEq(tabs[1].url, "about:config", "tab 1 url correct");
+
+ browser.test.assertEq(tabs[0].status, "complete", "tab 0 status correct");
+ browser.test.assertEq(tabs[1].status, "complete", "tab 1 status correct");
+
+ browser.test.assertEq(
+ tabs[0].title,
+ "Gort! Klaatu barada nikto!",
+ "tab 0 title correct"
+ );
+
+ tabs = await browser.tabs.query({ url: "about:blank" });
+ browser.test.assertEq(tabs.length, 1, "about:blank query finds one tab");
+ browser.test.assertEq(tabs[0].url, "about:blank", "with the correct url");
+
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://test1.example.org/MochiKit/"
+ );
+
+ // test simple queries
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.query(
+ {
+ url: "",
+ },
+ function (tabs) {
+ browser.test.assertEq(tabs.length, 3, "should have three tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(
+ tabs[0].url,
+ "http://example.com/",
+ "tab 0 url correct"
+ );
+ browser.test.assertEq(
+ tabs[1].url,
+ "http://example.net/",
+ "tab 1 url correct"
+ );
+ browser.test.assertEq(
+ tabs[2].url,
+ "http://test1.example.org/MochiKit/",
+ "tab 2 url correct"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // match pattern
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.query(
+ {
+ url: "http://*/MochiKit*",
+ },
+ function (tabs) {
+ browser.test.assertEq(tabs.length, 1, "should have one tab");
+
+ browser.test.assertEq(
+ tabs[0].url,
+ "http://test1.example.org/MochiKit/",
+ "tab 0 url correct"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // match array of patterns
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.query(
+ {
+ url: ["http://*/MochiKit*", "http://*.com/*"],
+ },
+ function (tabs) {
+ browser.test.assertEq(tabs.length, 2, "should have two tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(
+ tabs[0].url,
+ "http://example.com/",
+ "tab 0 url correct"
+ );
+ browser.test.assertEq(
+ tabs[1].url,
+ "http://test1.example.org/MochiKit/",
+ "tab 1 url correct"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // match title pattern
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ let tabs = await browser.tabs.query({
+ title: "mochitest index /",
+ });
+
+ browser.test.assertEq(tabs.length, 2, "should have two tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(
+ tabs[0].title,
+ "mochitest index /",
+ "tab 0 title correct"
+ );
+ browser.test.assertEq(
+ tabs[1].title,
+ "mochitest index /",
+ "tab 1 title correct"
+ );
+
+ tabs = await browser.tabs.query({
+ title: "?ochitest index /*",
+ });
+
+ browser.test.assertEq(tabs.length, 3, "should have three tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(
+ tabs[0].title,
+ "mochitest index /",
+ "tab 0 title correct"
+ );
+ browser.test.assertEq(
+ tabs[1].title,
+ "mochitest index /",
+ "tab 1 title correct"
+ );
+ browser.test.assertEq(
+ tabs[2].title,
+ "mochitest index /MochiKit/",
+ "tab 2 title correct"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // match highlighted
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs1 = await browser.tabs.query({ highlighted: false });
+ browser.test.assertEq(
+ 3,
+ tabs1.length,
+ "should have three non-highlighted tabs"
+ );
+
+ let tabs2 = await browser.tabs.query({ highlighted: true });
+ browser.test.assertEq(1, tabs2.length, "should have one highlighted tab");
+
+ for (let tab of [...tabs1, ...tabs2]) {
+ browser.test.assertEq(
+ tab.active,
+ tab.highlighted,
+ "highlighted and active are equal in tab " + tab.index
+ );
+ }
+
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // test width and height
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.test.onMessage.addListener(async msg => {
+ let tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(tabs.length, 1, "should have one tab");
+ browser.test.sendMessage("dims", {
+ width: tabs[0].width,
+ height: tabs[0].height,
+ });
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(RESOLUTION_PREF);
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ for (let resolution of [2, 1]) {
+ Services.prefs.setCharPref(RESOLUTION_PREF, String(resolution));
+ is(
+ window.devicePixelRatio,
+ resolution,
+ "window has the required resolution"
+ );
+
+ let { clientHeight, clientWidth } = gBrowser.selectedBrowser;
+
+ extension.sendMessage("check-size");
+ let dims = await extension.awaitMessage("dims");
+ is(dims.width, clientWidth, "tab reports expected width");
+ is(dims.height, clientHeight, "tab reports expected height");
+ }
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ Services.prefs.clearUserPref(RESOLUTION_PREF);
+});
+
+add_task(async function testQueryPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [],
+ },
+
+ async background() {
+ try {
+ let tabs = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ browser.test.assertEq(tabs.length, 1, "Expect query to return tabs");
+ browser.test.notifyPass("queryPermissions");
+ } catch (e) {
+ browser.test.notifyFail("queryPermissions");
+ }
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("queryPermissions");
+
+ await extension.unload();
+});
+
+add_task(async function testInvalidUrl() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.query({ url: "http://test1.net" }),
+ "Invalid url pattern: http://test1.net",
+ "Expected url to match pattern"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.query({ url: ["test2"] }),
+ "Invalid url pattern: test2",
+ "Expected an array with an invalid match pattern"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.query({ url: ["http://www.bbc.com/", "test3"] }),
+ "Invalid url pattern: test3",
+ "Expected an array with an invalid match pattern"
+ );
+ browser.test.notifyPass("testInvalidUrl");
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("testInvalidUrl");
+ await extension.unload();
+});
+
+add_task(async function test_query_index() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.onCreated.addListener(async function ({
+ index,
+ windowId,
+ id,
+ }) {
+ browser.test.assertThrows(
+ () => browser.tabs.query({ index: -1 }),
+ /-1 is too small \(must be at least 0\)/,
+ "tab indices must be non-negative"
+ );
+
+ let tabs = await browser.tabs.query({ index, windowId });
+ browser.test.assertEq(tabs.length, 1, `Got one tab at index ${index}`);
+ browser.test.assertEq(tabs[0].id, id, "The tab is the right one");
+
+ tabs = await browser.tabs.query({ index: 1e5, windowId });
+ browser.test.assertEq(tabs.length, 0, "There is no tab at this index");
+
+ browser.test.notifyPass("tabs.query");
+ });
+ },
+ });
+
+ await extension.startup();
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await extension.awaitFinish("tabs.query");
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_query_window() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let badWindowId = 0;
+ for (let { id } of await browser.windows.getAll()) {
+ badWindowId = Math.max(badWindowId, id + 1);
+ }
+
+ let tabs = await browser.tabs.query({ windowId: badWindowId });
+ browser.test.assertEq(
+ tabs.length,
+ 0,
+ "No tabs because there is no such window ID"
+ );
+
+ let { id: currentWindowId } = await browser.windows.getCurrent();
+ tabs = await browser.tabs.query({ currentWindow: true });
+ browser.test.assertEq(
+ tabs[0].windowId,
+ currentWindowId,
+ "Got tabs from the current window"
+ );
+
+ let { id: lastFocusedWindowId } = await browser.windows.getLastFocused();
+ tabs = await browser.tabs.query({ lastFocusedWindow: true });
+ browser.test.assertEq(
+ tabs[0].windowId,
+ lastFocusedWindowId,
+ "Got tabs from the last focused window"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js b/browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js
new file mode 100644
index 0000000000..1b86094611
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js
@@ -0,0 +1,138 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_reader_mode() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ let tab;
+ let tabId;
+ let expected = { isInReaderMode: false };
+ let testState = {};
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "updateUrl":
+ expected.isArticle = args[0];
+ expected.url = args[1];
+ tab = await browser.tabs.update({ url: expected.url });
+ tabId = tab.id;
+ break;
+ case "enterReaderMode":
+ expected.isArticle = !args[0];
+ expected.isInReaderMode = true;
+ tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(
+ false,
+ tab.isInReaderMode,
+ "The tab is not in reader mode."
+ );
+ if (args[0]) {
+ browser.tabs.toggleReaderMode(tabId);
+ } else {
+ await browser.test.assertRejects(
+ browser.tabs.toggleReaderMode(tabId),
+ /The specified tab cannot be placed into reader mode/,
+ "Toggle fails with an unreaderable document."
+ );
+ browser.test.assertEq(
+ false,
+ tab.isInReaderMode,
+ "The tab is still not in reader mode."
+ );
+ browser.test.sendMessage("enterFailed");
+ }
+ break;
+ case "leaveReaderMode":
+ expected.isInReaderMode = false;
+ tab = await browser.tabs.get(tabId);
+ browser.test.assertTrue(
+ tab.isInReaderMode,
+ "The tab is in reader mode."
+ );
+ browser.tabs.toggleReaderMode(tabId);
+ break;
+ }
+ });
+
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
+ if (changeInfo.status === "complete") {
+ testState.url = tab.url;
+ let urlOk = expected.isInReaderMode
+ ? testState.url.startsWith("about:reader")
+ : expected.url == testState.url;
+ if (urlOk && expected.isArticle == testState.isArticle) {
+ browser.test.sendMessage("tabUpdated", tab);
+ }
+ return;
+ }
+ if (
+ changeInfo.isArticle == expected.isArticle &&
+ changeInfo.isArticle != testState.isArticle
+ ) {
+ testState.isArticle = changeInfo.isArticle;
+ let urlOk = expected.isInReaderMode
+ ? testState.url.startsWith("about:reader")
+ : expected.url == testState.url;
+ if (urlOk && expected.isArticle == testState.isArticle) {
+ browser.test.sendMessage("isArticle", tab);
+ }
+ }
+ });
+ },
+ });
+
+ const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ );
+ const READER_MODE_PREFIX = "about:reader";
+
+ await extension.startup();
+ extension.sendMessage(
+ "updateUrl",
+ true,
+ `${TEST_PATH}readerModeArticle.html`
+ );
+ let tab = await extension.awaitMessage("isArticle");
+
+ ok(
+ !tab.url.startsWith(READER_MODE_PREFIX),
+ "Tab url does not indicate reader mode."
+ );
+ ok(tab.isArticle, "Tab is readerable.");
+
+ extension.sendMessage("enterReaderMode", true);
+ tab = await extension.awaitMessage("tabUpdated");
+ ok(tab.url.startsWith(READER_MODE_PREFIX), "Tab url indicates reader mode.");
+ ok(tab.isInReaderMode, "tab.isInReaderMode indicates reader mode.");
+
+ extension.sendMessage("leaveReaderMode");
+ tab = await extension.awaitMessage("tabUpdated");
+ ok(
+ !tab.url.startsWith(READER_MODE_PREFIX),
+ "Tab url does not indicate reader mode."
+ );
+ ok(!tab.isInReaderMode, "tab.isInReaderMode does not indicate reader mode.");
+
+ extension.sendMessage(
+ "updateUrl",
+ false,
+ `${TEST_PATH}readerModeNonArticle.html`
+ );
+ tab = await extension.awaitMessage("tabUpdated");
+ ok(
+ !tab.url.startsWith(READER_MODE_PREFIX),
+ "Tab url does not indicate reader mode."
+ );
+ ok(!tab.isArticle, "Tab is not readerable.");
+ ok(!tab.isInReaderMode, "tab.isInReaderMode does not indicate reader mode.");
+
+ extension.sendMessage("enterReaderMode", false);
+ await extension.awaitMessage("enterFailed");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_reload.js b/browser/components/extensions/test/browser/browser_ext_tabs_reload.js
new file mode 100644
index 0000000000..aed4a3822c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_reload.js
@@ -0,0 +1,53 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "tab.js": function () {
+ browser.runtime.sendMessage("tab-loaded");
+ },
+ "tab.html": `
+
+
+ `,
+ },
+
+ async background() {
+ let tabLoadedCount = 0;
+
+ let tab = await browser.tabs.create({ url: "tab.html", active: true });
+
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "tab-loaded") {
+ tabLoadedCount++;
+
+ if (tabLoadedCount == 1) {
+ // Reload the tab once passing no arguments.
+ return browser.tabs.reload();
+ }
+
+ if (tabLoadedCount == 2) {
+ // Reload the tab again with explicit arguments.
+ return browser.tabs.reload(tab.id, {
+ bypassCache: false,
+ });
+ }
+
+ if (tabLoadedCount == 3) {
+ browser.test.notifyPass("tabs.reload");
+ }
+ }
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.reload");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js b/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js
new file mode 100644
index 0000000000..ed3d8c7a14
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js
@@ -0,0 +1,89 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", ""],
+ },
+
+ async background() {
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const URL = BASE + "file_bypass_cache.sjs";
+
+ let tabId = null;
+ let loadPromise, resolveLoad;
+ function resetLoad() {
+ loadPromise = new Promise(resolve => {
+ resolveLoad = resolve;
+ });
+ }
+ function awaitLoad() {
+ return loadPromise.then(() => {
+ resetLoad();
+ });
+ }
+ resetLoad();
+
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId_,
+ changed,
+ tab
+ ) {
+ if (tabId == tabId_ && changed.status == "complete" && tab.url == URL) {
+ resolveLoad();
+ }
+ });
+
+ try {
+ let tab = await browser.tabs.create({ url: URL });
+ tabId = tab.id;
+ await awaitLoad();
+
+ await browser.tabs.reload(tab.id, { bypassCache: false });
+ await awaitLoad();
+
+ let [textContent] = await browser.tabs.executeScript(tab.id, {
+ code: "document.body.textContent",
+ });
+ browser.test.assertEq(
+ "",
+ textContent,
+ "`textContent` should be empty when bypassCache=false"
+ );
+
+ await browser.tabs.reload(tab.id, { bypassCache: true });
+ await awaitLoad();
+
+ [textContent] = await browser.tabs.executeScript(tab.id, {
+ code: "document.body.textContent",
+ });
+
+ let [pragma, cacheControl] = textContent.split(":");
+ browser.test.assertEq(
+ "no-cache",
+ pragma,
+ "`pragma` should be set to `no-cache` when bypassCache is true"
+ );
+ browser.test.assertEq(
+ "no-cache",
+ cacheControl,
+ "`cacheControl` should be set to `no-cache` when bypassCache is true"
+ );
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("tabs.reload_bypass_cache");
+ } catch (error) {
+ browser.test.fail(`${error} :: ${error.stack}`);
+ browser.test.notifyFail("tabs.reload_bypass_cache");
+ }
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.reload_bypass_cache");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_remove.js b/browser/components/extensions/test/browser/browser_ext_tabs_remove.js
new file mode 100644
index 0000000000..8e51494ed1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_remove.js
@@ -0,0 +1,258 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function undoCloseAfterExtRemovesOneTab() {
+ let initialTab = gBrowser.selectedTab;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({});
+
+ browser.test.assertEq(3, tabs.length, "Should have 3 tabs");
+
+ let tabIdsToRemove = (
+ await browser.tabs.query({
+ url: "https://example.com/closeme/*",
+ })
+ ).map(tab => tab.id);
+
+ await browser.tabs.remove(tabIdsToRemove);
+ browser.test.sendMessage("removedtabs");
+ },
+ });
+
+ await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/1"),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/closeme/2"
+ ),
+ ]);
+
+ await extension.startup();
+ await extension.awaitMessage("removedtabs");
+
+ is(
+ gBrowser.tabs.length,
+ 2,
+ "Once extension has closed a tab, there should be 2 tabs open"
+ );
+
+ // The tabs.remove API makes no promises about SessionStore's updates
+ // having been completed by the time it returns. So we need to wait separately
+ // for the closed tab count to be updated the correct value. This is OK because
+ // we can observe above that the tabs length has changed to reflect that
+ // some were closed.
+ await TestUtils.waitForCondition(
+ () => SessionStore.getLastClosedTabCount(window) == 1,
+ "SessionStore should know that one tab was closed"
+ );
+
+ undoCloseTab();
+
+ is(
+ gBrowser.tabs.length,
+ 3,
+ "All tabs should be restored for a total of 3 tabs"
+ );
+
+ await BrowserTestUtils.waitForEvent(gBrowser.tabs[2], "SSTabRestored");
+
+ is(
+ gBrowser.tabs[2].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/2",
+ "Restored tab at index 2 should have expected URL"
+ );
+
+ await extension.unload();
+ gBrowser.removeAllTabsBut(initialTab);
+});
+
+add_task(async function undoCloseAfterExtRemovesMultipleTabs() {
+ let initialTab = gBrowser.selectedTab;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabIds = (await browser.tabs.query({})).map(tab => tab.id);
+
+ browser.test.assertEq(
+ 8,
+ tabIds.length,
+ "Should have 8 total tabs (4 in each window: the initial blank tab and the 3 opened by this test)"
+ );
+
+ let tabIdsToRemove = (
+ await browser.tabs.query({
+ url: "https://example.com/closeme/*",
+ })
+ ).map(tab => tab.id);
+
+ await browser.tabs.remove(tabIdsToRemove);
+
+ browser.test.sendMessage("removedtabs");
+ },
+ });
+
+ await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/1"),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/closeme/2"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/closeme/3"
+ ),
+ ]);
+
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ "https://example.com/4"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ "https://example.com/closeme/5"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ "https://example.com/closeme/6"
+ ),
+ ]);
+
+ await extension.startup();
+ await extension.awaitMessage("removedtabs");
+
+ is(
+ gBrowser.tabs.length,
+ 2,
+ "Original window should have 2 tabs still open, after closing tabs"
+ );
+
+ is(
+ window2.gBrowser.tabs.length,
+ 2,
+ "Second window should have 2 tabs still open, after closing tabs"
+ );
+
+ // The tabs.remove API makes no promises about SessionStore's updates
+ // having been completed by the time it returns. So we need to wait separately
+ // for the closed tab count to be updated the correct value. This is OK because
+ // we can observe above that the tabs length has changed to reflect that
+ // some were closed.
+ await TestUtils.waitForCondition(
+ () => SessionStore.getLastClosedTabCount(window) == 2,
+ "Last closed tab count is 2"
+ );
+
+ await TestUtils.waitForCondition(
+ () => SessionStore.getLastClosedTabCount(window2) == 2,
+ "Last closed tab count is 2"
+ );
+
+ undoCloseTab();
+ window2.undoCloseTab();
+
+ is(
+ gBrowser.tabs.length,
+ 4,
+ "All tabs in original window should be restored for a total of 4 tabs"
+ );
+
+ is(
+ window2.gBrowser.tabs.length,
+ 4,
+ "All tabs in second window should be restored for a total of 4 tabs"
+ );
+
+ await Promise.all([
+ BrowserTestUtils.waitForEvent(gBrowser.tabs[2], "SSTabRestored"),
+ BrowserTestUtils.waitForEvent(gBrowser.tabs[3], "SSTabRestored"),
+ BrowserTestUtils.waitForEvent(window2.gBrowser.tabs[2], "SSTabRestored"),
+ BrowserTestUtils.waitForEvent(window2.gBrowser.tabs[3], "SSTabRestored"),
+ ]);
+
+ is(
+ gBrowser.tabs[2].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/2",
+ "Original window restored tab at index 2 should have expected URL"
+ );
+
+ is(
+ gBrowser.tabs[3].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/3",
+ "Original window restored tab at index 3 should have expected URL"
+ );
+
+ is(
+ window2.gBrowser.tabs[2].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/5",
+ "Second window restored tab at index 2 should have expected URL"
+ );
+
+ is(
+ window2.gBrowser.tabs[3].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/6",
+ "Second window restored tab at index 3 should have expected URL"
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(window2);
+ gBrowser.removeAllTabsBut(initialTab);
+});
+
+add_task(async function closeWindowIfExtClosesAllTabs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.closeWindowWithLastTab", true],
+ ["browser.tabs.warnOnClose", true],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async function () {
+ let tabsToRemove = await browser.tabs.query({ currentWindow: true });
+
+ let currentWindowId = tabsToRemove[0].windowId;
+
+ browser.test.assertEq(
+ 2,
+ tabsToRemove.length,
+ "Current window should have 2 tabs to remove"
+ );
+
+ await browser.tabs.remove(tabsToRemove.map(tab => tab.id));
+
+ await browser.test.assertRejects(
+ browser.windows.get(currentWindowId),
+ RegExp(`Invalid window ID: ${currentWindowId}`),
+ "After closing tabs, 2nd window should be closed and querying for it should be rejected"
+ );
+
+ browser.test.notifyPass("done");
+ },
+ });
+
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ "https://example.com/"
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js b/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js
new file mode 100644
index 0000000000..edaf2f61b4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js
@@ -0,0 +1,151 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScript() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ async function background() {
+ let tasks = [
+ // Insert CSS file.
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ file: "file2.css",
+ });
+ },
+ },
+ // Insert CSS code.
+ {
+ background: "rgb(42, 42, 42)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ });
+ },
+ },
+ // Remove CSS code again.
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.removeCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ });
+ },
+ },
+ // Remove CSS file again.
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 0, 0)",
+ promise: () => {
+ return browser.tabs.removeCSS({
+ file: "file2.css",
+ });
+ },
+ },
+ // Insert CSS code.
+ {
+ background: "rgb(42, 42, 42)",
+ foreground: "rgb(0, 0, 0)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ cssOrigin: "user",
+ });
+ },
+ },
+ // Remove CSS code again.
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 0, 0)",
+ promise: () => {
+ return browser.tabs.removeCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ cssOrigin: "user",
+ });
+ },
+ },
+ ];
+
+ function checkCSS() {
+ let computedStyle = window.getComputedStyle(document.body);
+ return [computedStyle.backgroundColor, computedStyle.color];
+ }
+
+ try {
+ for (let { promise, background, foreground } of tasks) {
+ let result = await promise();
+ browser.test.assertEq(undefined, result, "Expected callback result");
+
+ [result] = await browser.tabs.executeScript({
+ code: `(${checkCSS})()`,
+ });
+ browser.test.assertEq(
+ background,
+ result[0],
+ "Expected background color"
+ );
+ browser.test.assertEq(
+ foreground,
+ result[1],
+ "Expected foreground color"
+ );
+ }
+
+ browser.test.notifyPass("removeCSS");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("removeCSS");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+
+ background,
+
+ files: {
+ "file2.css": "* { color: rgb(0, 113, 4) }",
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("removeCSS");
+
+ // Verify that scripts created by tabs.removeCSS are not added to the content scripts
+ // that requires cleanup (Bug 1464711).
+ await SpecialPowers.spawn(tab.linkedBrowser, [extension.id], async extId => {
+ const { ExtensionContent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionContent.sys.mjs"
+ );
+
+ let contentScriptContext = ExtensionContent.getContextByExtensionId(
+ extId,
+ content.window
+ );
+
+ for (let script of contentScriptContext.scripts) {
+ if (script.matcher.removeCSS && script.requiresCleanup) {
+ throw new Error("tabs.removeCSS scripts should not require cleanup");
+ }
+ }
+ }).catch(err => {
+ // Log the error so that it is easy to see where the failure is coming from.
+ ok(false, err);
+ });
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js b/browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js
new file mode 100644
index 0000000000..fdff1dddbf
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js
@@ -0,0 +1,197 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testReturnStatus(expectedStatus) {
+ // Test that tabs.saveAsPDF() returns the correct status
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+
+ let saveDir = FileUtils.getDir("TmpD", [`testSaveDir-${Math.random()}`]);
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let saveFile = saveDir.clone();
+ saveFile.append("testSaveFile.pdf");
+ if (saveFile.exists()) {
+ saveFile.remove(false);
+ }
+
+ if (expectedStatus == "replaced") {
+ // Create file that can be replaced
+ saveFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ } else if (expectedStatus == "not_saved") {
+ // Create directory with same name as file - so that file cannot be saved
+ saveFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o666);
+ } else if (expectedStatus == "not_replaced") {
+ // Create file that cannot be replaced
+ saveFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o444);
+ }
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ if (expectedStatus == "replaced" || expectedStatus == "not_replaced") {
+ MockFilePicker.returnValue = MockFilePicker.returnReplace;
+ } else if (expectedStatus == "canceled") {
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ } else {
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ }
+
+ MockFilePicker.displayDirectory = saveDir;
+
+ MockFilePicker.showCallback = fp => {
+ MockFilePicker.setFiles([saveFile]);
+ MockFilePicker.filterIndex = 0; // *.* - all file extensions
+ };
+
+ let manifest = {
+ description: expectedStatus,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+
+ background: async function () {
+ let pageSettings = {};
+
+ let expected = chrome.runtime.getManifest().description;
+
+ let status = await browser.tabs.saveAsPDF(pageSettings);
+
+ browser.test.assertEq(expected, status, "Got expected status");
+
+ browser.test.notifyPass("tabs.saveAsPDF");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.saveAsPDF");
+ await extension.unload();
+
+ if (expectedStatus == "saved" || expectedStatus == "replaced") {
+ // Check that first four bytes of saved PDF file are "%PDF"
+ let text = await IOUtils.read(saveFile.path, { maxBytes: 4 });
+ text = new TextDecoder().decode(text);
+ is(text, "%PDF", "Got correct magic number - %PDF");
+ }
+
+ MockFilePicker.cleanup();
+
+ if (expectedStatus == "not_saved" || expectedStatus == "not_replaced") {
+ saveFile.permissions = 0o666;
+ }
+
+ saveDir.remove(true);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testSaveAsPDF_saved() {
+ await testReturnStatus("saved");
+});
+
+add_task(async function testSaveAsPDF_replaced() {
+ await testReturnStatus("replaced");
+});
+
+add_task(async function testSaveAsPDF_canceled() {
+ await testReturnStatus("canceled");
+});
+
+add_task(async function testSaveAsPDF_not_saved() {
+ await testReturnStatus("not_saved");
+});
+
+add_task(async function testSaveAsPDF_not_replaced() {
+ await testReturnStatus("not_replaced");
+});
+
+async function testFileName(expectedFileName) {
+ // Test that tabs.saveAsPDF() saves with the correct filename
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+
+ let saveDir = FileUtils.getDir("TmpD", [`testSaveDir-${Math.random()}`]);
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let saveFile = saveDir.clone();
+ saveFile.append(expectedFileName);
+ if (saveFile.exists()) {
+ saveFile.remove(false);
+ }
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ MockFilePicker.displayDirectory = saveDir;
+
+ MockFilePicker.showCallback = fp => {
+ is(
+ fp.defaultString,
+ expectedFileName,
+ "Got expected FilePicker defaultString"
+ );
+
+ is(fp.defaultExtension, "pdf", "Got expected FilePicker defaultExtension");
+
+ let file = saveDir.clone();
+ file.append(fp.defaultString);
+ MockFilePicker.setFiles([file]);
+ };
+
+ let manifest = {
+ description: expectedFileName,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+
+ background: async function () {
+ let pageSettings = {};
+
+ let expected = chrome.runtime.getManifest().description;
+
+ if (expected == "definedFileName") {
+ pageSettings.toFileName = expected;
+ }
+
+ let status = await browser.tabs.saveAsPDF(pageSettings);
+
+ browser.test.assertEq("saved", status, "Got expected status");
+
+ browser.test.notifyPass("tabs.saveAsPDF");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.saveAsPDF");
+ await extension.unload();
+
+ // Check that first four bytes of saved PDF file are "%PDF"
+ let text = await IOUtils.read(saveFile.path, { maxBytes: 4 });
+ text = new TextDecoder().decode(text);
+ is(text, "%PDF", "Got correct magic number - %PDF");
+
+ MockFilePicker.cleanup();
+
+ saveDir.remove(true);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testSaveAsPDF_defined_filename() {
+ await testFileName("definedFileName");
+});
+
+add_task(async function testSaveAsPDF_undefined_filename() {
+ // If pageSettings.toFileName is undefined, the expected filename will be
+ // the test page title "mochitest index /" with the "/" replaced by "_".
+ await testFileName("mochitest index _");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js b/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
new file mode 100644
index 0000000000..8c420c2821
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
@@ -0,0 +1,385 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function tabsSendMessageReply() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ content_scripts: [
+ {
+ matches: ["http://example.com/"],
+ js: ["content-script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+
+ background: async function () {
+ let firstTab;
+ let promiseResponse = new Promise(resolve => {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "content-script-ready") {
+ let tabId = sender.tab.id;
+
+ Promise.all([
+ promiseResponse,
+
+ browser.tabs.sendMessage(tabId, "respond-now"),
+ browser.tabs.sendMessage(tabId, "respond-now-2"),
+ new Promise(resolve =>
+ browser.tabs.sendMessage(tabId, "respond-soon", resolve)
+ ),
+ browser.tabs.sendMessage(tabId, "respond-promise"),
+ browser.tabs.sendMessage(tabId, "respond-promise-false"),
+ browser.tabs.sendMessage(tabId, "respond-false"),
+ browser.tabs.sendMessage(tabId, "respond-never"),
+ new Promise(resolve => {
+ browser.runtime.sendMessage("respond-never", response => {
+ resolve(response);
+ });
+ }),
+
+ browser.tabs
+ .sendMessage(tabId, "respond-error")
+ .catch(error => Promise.resolve({ error })),
+ browser.tabs
+ .sendMessage(tabId, "throw-error")
+ .catch(error => Promise.resolve({ error })),
+
+ browser.tabs
+ .sendMessage(tabId, "respond-uncloneable")
+ .catch(error => Promise.resolve({ error })),
+ browser.tabs
+ .sendMessage(tabId, "reject-uncloneable")
+ .catch(error => Promise.resolve({ error })),
+ browser.tabs
+ .sendMessage(tabId, "reject-undefined")
+ .catch(error => Promise.resolve({ error })),
+ browser.tabs
+ .sendMessage(tabId, "throw-undefined")
+ .catch(error => Promise.resolve({ error })),
+
+ browser.tabs
+ .sendMessage(firstTab, "no-listener")
+ .catch(error => Promise.resolve({ error })),
+ ])
+ .then(
+ ([
+ response,
+ respondNow,
+ respondNow2,
+ respondSoon,
+ respondPromise,
+ respondPromiseFalse,
+ respondFalse,
+ respondNever,
+ respondNever2,
+ respondError,
+ throwError,
+ respondUncloneable,
+ rejectUncloneable,
+ rejectUndefined,
+ throwUndefined,
+ noListener,
+ ]) => {
+ browser.test.assertEq(
+ "expected-response",
+ response,
+ "Content script got the expected response"
+ );
+
+ browser.test.assertEq(
+ "respond-now",
+ respondNow,
+ "Got the expected immediate response"
+ );
+ browser.test.assertEq(
+ "respond-now-2",
+ respondNow2,
+ "Got the expected immediate response from the second listener"
+ );
+ browser.test.assertEq(
+ "respond-soon",
+ respondSoon,
+ "Got the expected delayed response"
+ );
+ browser.test.assertEq(
+ "respond-promise",
+ respondPromise,
+ "Got the expected promise response"
+ );
+ browser.test.assertEq(
+ false,
+ respondPromiseFalse,
+ "Got the expected false value as a promise result"
+ );
+ browser.test.assertEq(
+ undefined,
+ respondFalse,
+ "Got the expected no-response when onMessage returns false"
+ );
+ browser.test.assertEq(
+ undefined,
+ respondNever,
+ "Got the expected no-response resolution"
+ );
+ browser.test.assertEq(
+ undefined,
+ respondNever2,
+ "Got the expected no-response resolution"
+ );
+
+ browser.test.assertEq(
+ "respond-error",
+ respondError.error.message,
+ "Got the expected error response"
+ );
+ browser.test.assertEq(
+ "throw-error",
+ throwError.error.message,
+ "Got the expected thrown error response"
+ );
+
+ browser.test.assertEq(
+ "Could not establish connection. Receiving end does not exist.",
+ respondUncloneable.error.message,
+ "An uncloneable response should be ignored"
+ );
+ browser.test.assertEq(
+ "An unexpected error occurred",
+ rejectUncloneable.error.message,
+ "Got the expected error for a rejection with an uncloneable value"
+ );
+ browser.test.assertEq(
+ "An unexpected error occurred",
+ rejectUndefined.error.message,
+ "Got the expected error for a void rejection"
+ );
+ browser.test.assertEq(
+ "An unexpected error occurred",
+ throwUndefined.error.message,
+ "Got the expected error for a void throw"
+ );
+
+ browser.test.assertEq(
+ "Could not establish connection. Receiving end does not exist.",
+ noListener.error.message,
+ "Got the expected no listener response"
+ );
+
+ return browser.tabs.remove(tabId);
+ }
+ )
+ .then(() => {
+ browser.test.notifyPass("sendMessage");
+ });
+
+ return Promise.resolve("expected-response");
+ } else if (msg[0] == "got-response") {
+ resolve(msg[1]);
+ }
+ });
+ });
+
+ let tabs = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ firstTab = tabs[0].id;
+ browser.tabs.create({ url: "http://example.com/" });
+ },
+
+ files: {
+ "content-script.js": async function () {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond(msg);
+ } else if (msg == "respond-soon") {
+ setTimeout(() => {
+ respond(msg);
+ }, 0);
+ return true;
+ } else if (msg == "respond-promise") {
+ return Promise.resolve(msg);
+ } else if (msg == "respond-promise-false") {
+ return Promise.resolve(false);
+ } else if (msg == "respond-false") {
+ // return false means that respond() is not expected to be called.
+ setTimeout(() => respond("should be ignored"));
+ return false;
+ } else if (msg == "respond-never") {
+ return undefined;
+ } else if (msg == "respond-error") {
+ return Promise.reject(new Error(msg));
+ } else if (msg === "respond-uncloneable") {
+ return Promise.resolve(window);
+ } else if (msg === "reject-uncloneable") {
+ return Promise.reject(window);
+ } else if (msg == "reject-undefined") {
+ return Promise.reject();
+ } else if (msg == "throw-undefined") {
+ throw undefined; // eslint-disable-line no-throw-literal
+ } else if (msg == "throw-error") {
+ throw new Error(msg);
+ }
+ });
+
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond("hello");
+ } else if (msg == "respond-now-2") {
+ respond(msg);
+ }
+ });
+
+ let response = await browser.runtime.sendMessage(
+ "content-script-ready"
+ );
+ browser.runtime.sendMessage(["got-response", response]);
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("sendMessage");
+
+ await extension.unload();
+});
+
+add_task(async function tabsSendHidden() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ content_scripts: [
+ {
+ matches: ["http://example.com/content*"],
+ js: ["content-script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+
+ background: async function () {
+ let resolveContent;
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg[0] == "content-ready") {
+ resolveContent(msg[1]);
+ }
+ });
+
+ let awaitContent = url => {
+ return new Promise(resolve => {
+ resolveContent = resolve;
+ }).then(result => {
+ browser.test.assertEq(url, result, "Expected content script URL");
+ });
+ };
+
+ try {
+ const URL1 = "http://example.com/content1.html";
+ const URL2 = "http://example.com/content2.html";
+
+ let tab = await browser.tabs.create({ url: URL1 });
+ await awaitContent(URL1);
+
+ let url = await browser.tabs.sendMessage(tab.id, URL1);
+ browser.test.assertEq(
+ URL1,
+ url,
+ "Should get response from expected content window"
+ );
+
+ await browser.tabs.update(tab.id, { url: URL2 });
+ await awaitContent(URL2);
+
+ url = await browser.tabs.sendMessage(tab.id, URL2);
+ browser.test.assertEq(
+ URL2,
+ url,
+ "Should get response from expected content window"
+ );
+
+ // Repeat once just to be sure the first message was processed by all
+ // listeners before we exit the test.
+ url = await browser.tabs.sendMessage(tab.id, URL2);
+ browser.test.assertEq(
+ URL2,
+ url,
+ "Should get response from expected content window"
+ );
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("contentscript-bfcache-window");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("contentscript-bfcache-window");
+ }
+ },
+
+ files: {
+ "content-script.js": function () {
+ // Store this in a local variable to make sure we don't touch any
+ // properties of the possibly-hidden content window.
+ let href = window.location.href;
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq(
+ href,
+ msg,
+ "Should be in the expected content window"
+ );
+
+ return Promise.resolve(href);
+ });
+
+ browser.runtime.sendMessage(["content-ready", href]);
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("contentscript-bfcache-window");
+
+ await extension.unload();
+});
+
+add_task(async function tabsSendMessageNoExceptionOnNonExistentTab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ let url =
+ "http://example.com/mochitest/browser/browser/components/extensions/test/browser/file_dummy.html";
+ let tab = await browser.tabs.create({ url });
+
+ await browser.test.assertRejects(
+ browser.tabs.sendMessage(tab.id, "message"),
+ /Could not establish connection. Receiving end does not exist./,
+ "exception should be raised on tabs.sendMessage to nonexistent tab"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.sendMessage(tab.id + 100, "message"),
+ /Could not establish connection. Receiving end does not exist./,
+ "exception should be raised on tabs.sendMessage to nonexistent tab"
+ );
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("tabs.sendMessage");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.sendMessage");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js b/browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js
new file mode 100644
index 0000000000..47f2006307
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js
@@ -0,0 +1,110 @@
+"use strict";
+
+add_task(async function test_tabs_mediaIndicators() {
+ let initialTab = gBrowser.selectedTab;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/#tab-sharing"
+ );
+
+ // Ensure that the tab to hide is not selected (otherwise
+ // it will not be hidden because it is selected).
+ gBrowser.selectedTab = initialTab;
+
+ // updateBrowserSharing is called when a request for media icons occurs. We're
+ // just testing that extension tabs get the info and are updated when it is
+ // called.
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, {
+ webRTC: {
+ sharing: "screen",
+ screen: "Window",
+ microphone: Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED,
+ camera: Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED,
+ },
+ });
+
+ async function background() {
+ let tabs = await browser.tabs.query({ url: "http://example.com/*" });
+ let testTab = tabs[0];
+
+ browser.test.assertEq(
+ testTab.url,
+ "http://example.com/#tab-sharing",
+ "Got the expected tab url"
+ );
+
+ browser.test.assertFalse(testTab.active, "test tab should not be selected");
+
+ let state = testTab.sharingState;
+ browser.test.assertTrue(state.camera, "sharing camera was turned on");
+ browser.test.assertTrue(state.microphone, "sharing mic was turned on");
+ browser.test.assertEq(state.screen, "Window", "sharing screen is window");
+
+ tabs = await browser.tabs.query({ screen: true });
+ browser.test.assertEq(tabs.length, 1, "screen sharing tab was found");
+
+ tabs = await browser.tabs.query({ screen: "Window" });
+ browser.test.assertEq(
+ tabs.length,
+ 1,
+ "screen sharing (window) tab was found"
+ );
+
+ tabs = await browser.tabs.query({ screen: "Screen" });
+ browser.test.assertEq(tabs.length, 0, "screen sharing tab was not found");
+
+ // Verify we cannot hide a sharing tab.
+ let hidden = await browser.tabs.hide(testTab.id);
+ browser.test.assertEq(hidden.length, 0, "unable to hide sharing tab");
+ tabs = await browser.tabs.query({ hidden: true });
+ browser.test.assertEq(tabs.length, 0, "unable to hide sharing tab");
+
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
+ if (testTab.id !== tabId) {
+ return;
+ }
+ let state = changeInfo.sharingState;
+
+ // Ignore tab update events unrelated to the sharing state.
+ if (!state) {
+ return;
+ }
+
+ browser.test.assertFalse(state.camera, "sharing camera was turned off");
+ browser.test.assertFalse(state.microphone, "sharing mic was turned off");
+ browser.test.assertFalse(state.screen, "sharing screen was turned off");
+
+ // Verify we can hide the tab once it is not shared over webRTC anymore.
+ let hidden = await browser.tabs.hide(testTab.id);
+ browser.test.assertEq(hidden.length, 1, "tab hidden successfully");
+ tabs = await browser.tabs.query({ hidden: true });
+ browser.test.assertEq(tabs.length, 1, "hidden tab found");
+
+ browser.test.notifyPass("done");
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extdata = {
+ manifest: { permissions: ["tabs", "tabHide"] },
+ useAddonManager: "temporary",
+ background,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extdata);
+ await extension.startup();
+
+ // Test that onUpdated is called after the sharing state is changed from
+ // chrome code.
+ await extension.awaitMessage("ready");
+
+ info("Updating browser sharing on the test tab");
+
+ // Clear only the webRTC part of the browser sharing state
+ // (used to test Bug 1577480 regression fix).
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { webRTC: null });
+
+ await extension.awaitFinish("done");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_successors.js b/browser/components/extensions/test/browser/browser_ext_tabs_successors.js
new file mode 100644
index 0000000000..77549c44d5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_successors.js
@@ -0,0 +1,396 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function background(tabCount, testFn) {
+ try {
+ const { TAB_ID_NONE } = browser.tabs;
+ const tabIds = await Promise.all(
+ Array.from({ length: tabCount }, () =>
+ browser.tabs.create({ url: "about:blank" }).then(t => t.id)
+ )
+ );
+
+ const toTabIds = i => tabIds[i];
+
+ const setSuccessors = mapping =>
+ Promise.all(
+ mapping.map((succ, i) =>
+ browser.tabs.update(tabIds[i], { successorTabId: tabIds[succ] })
+ )
+ );
+
+ const verifySuccessors = async function (mapping, name) {
+ const promises = [],
+ expected = [];
+ for (let i = 0; i < mapping.length; i++) {
+ if (mapping[i] !== undefined) {
+ promises.push(
+ browser.tabs.get(tabIds[i]).then(t => t.successorTabId)
+ );
+ expected.push(
+ mapping[i] === TAB_ID_NONE ? TAB_ID_NONE : tabIds[mapping[i]]
+ );
+ }
+ }
+ const results = await Promise.all(promises);
+ for (let i = 0; i < results.length; i++) {
+ browser.test.assertEq(
+ expected[i],
+ results[i],
+ `${name}: successorTabId of tab ${i} in mapping should be ${expected[i]}`
+ );
+ }
+ };
+
+ await testFn({
+ TAB_ID_NONE,
+ tabIds,
+ toTabIds,
+ setSuccessors,
+ verifySuccessors,
+ });
+
+ browser.test.notifyPass("background-script");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("background-script");
+ }
+}
+
+async function runTabTest(tabCount, testFn) {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background: `(${background})(${tabCount}, ${testFn});`,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("background-script");
+ await extension.unload();
+}
+
+add_task(function testTabSuccessors() {
+ return runTabTest(3, async function ({ TAB_ID_NONE, tabIds }) {
+ const anotherWindow = await browser.windows.create({ url: "about:blank" });
+
+ browser.test.assertEq(
+ TAB_ID_NONE,
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "Tabs default to an undefined successor"
+ );
+
+ // Basic getting and setting
+
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
+ browser.test.assertEq(
+ tabIds[1],
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "tabs.update assigned the correct successor"
+ );
+
+ await browser.tabs.update(tabIds[0], {
+ successorTabId: browser.tabs.TAB_ID_NONE,
+ });
+ browser.test.assertEq(
+ TAB_ID_NONE,
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "tabs.update cleared successor"
+ );
+
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[0] });
+ browser.test.assertEq(
+ TAB_ID_NONE,
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "Setting a tab as its own successor clears the successor instead"
+ );
+
+ // Validation tests
+
+ await browser.test.assertRejects(
+ browser.tabs.update(tabIds[0], { successorTabId: 1e8 }),
+ /Invalid successorTabId/,
+ "tabs.update should throw with an invalid successor tab ID"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.update(tabIds[0], {
+ successorTabId: anotherWindow.tabs[0].id,
+ }),
+ /Successor tab must be in the same window as the tab being updated/,
+ "tabs.update should throw with a successor tab ID from another window"
+ );
+
+ // Make sure the successor is truly being assigned
+
+ await browser.tabs.update(tabIds[0], {
+ successorTabId: tabIds[2],
+ active: true,
+ });
+ await browser.tabs.remove(tabIds[0]);
+ browser.test.assertEq(
+ tabIds[2],
+ (await browser.tabs.query({ active: true }))[0].id
+ );
+
+ return browser.tabs.remove([
+ tabIds[1],
+ tabIds[2],
+ anotherWindow.tabs[0].id,
+ ]);
+ });
+});
+
+add_task(function testMoveInSuccession_appendFalse() {
+ return runTabTest(
+ 8,
+ async function ({
+ TAB_ID_NONE,
+ tabIds,
+ toTabIds,
+ setSuccessors,
+ verifySuccessors,
+ }) {
+ await browser.tabs.moveInSuccession([1, 0].map(toTabIds), tabIds[0]);
+ await verifySuccessors([TAB_ID_NONE, 0], "scenario 1");
+
+ await browser.tabs.moveInSuccession(
+ [0, 1, 2, 3].map(toTabIds),
+ tabIds[0]
+ );
+ await verifySuccessors([1, 2, 3, 0], "scenario 2");
+
+ await browser.tabs.moveInSuccession([1, 0].map(toTabIds), tabIds[0]);
+ await verifySuccessors(
+ [TAB_ID_NONE, 0],
+ "scenario 1 after tab 0 has a successor"
+ );
+
+ await browser.tabs.update(tabIds[7], { successorTabId: tabIds[0] });
+ await browser.tabs.moveInSuccession([4, 5, 6, 7].map(toTabIds));
+ await verifySuccessors(
+ new Array(4).concat([5, 6, 7, TAB_ID_NONE]),
+ "scenario 4"
+ );
+
+ await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
+ await browser.tabs.moveInSuccession(
+ [4, 6, 3, 2].map(toTabIds),
+ tabIds[7]
+ );
+ await verifySuccessors([7, TAB_ID_NONE, 7, 2, 6, 7, 3, 5], "scenario 5");
+
+ await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
+ await browser.tabs.moveInSuccession(
+ [4, 6, 3, 2].map(toTabIds),
+ tabIds[7],
+ {
+ insert: true,
+ }
+ );
+ await verifySuccessors(
+ [4, TAB_ID_NONE, 7, 2, 6, 4, 3, 5],
+ "insert = true"
+ );
+
+ await setSuccessors([1, 2, 3, 4, 0]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[0], {
+ insert: true,
+ });
+ await verifySuccessors([4, 2, 0, 1, 3], "insert = true, part 2");
+
+ await browser.tabs.moveInSuccession([
+ tabIds[0],
+ tabIds[1],
+ 1e8,
+ tabIds[2],
+ ]);
+ await verifySuccessors([1, 2, TAB_ID_NONE], "unknown tab ID");
+
+ browser.test.assertTrue(
+ await browser.tabs.moveInSuccession([1e8]).then(
+ () => true,
+ () => false
+ ),
+ "When all tab IDs are unknown, tabs.moveInSuccession should not throw"
+ );
+
+ // Validation tests
+
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([tabIds[0], tabIds[1], tabIds[0]]),
+ /IDs must not occur more than once in tabIds/,
+ "tabs.moveInSuccession should throw when a tab is referenced more than once in tabIds"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([tabIds[0], tabIds[1]], tabIds[0], {
+ insert: true,
+ }),
+ /Value of tabId must not occur in tabIds if append or insert is true/,
+ "tabs.moveInSuccession should throw when tabId occurs in tabIds and insert is true"
+ );
+
+ return browser.tabs.remove(tabIds);
+ }
+ );
+});
+
+add_task(function testMoveInSuccession_appendTrue() {
+ return runTabTest(
+ 8,
+ async function ({
+ TAB_ID_NONE,
+ tabIds,
+ toTabIds,
+ setSuccessors,
+ verifySuccessors,
+ }) {
+ await browser.tabs.moveInSuccession([1].map(toTabIds), tabIds[0], {
+ append: true,
+ });
+ await verifySuccessors([1, TAB_ID_NONE], "scenario 1");
+
+ await browser.tabs.update(tabIds[3], { successorTabId: tabIds[4] });
+ await browser.tabs.moveInSuccession([1, 2, 3].map(toTabIds), tabIds[0], {
+ append: true,
+ });
+ await verifySuccessors([1, 2, 3, TAB_ID_NONE], "scenario 2");
+
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
+ await browser.tabs.moveInSuccession([1e8], tabIds[0], { append: true });
+ browser.test.assertEq(
+ TAB_ID_NONE,
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "If no tabs get appended after the reference tab, it should lose its successor"
+ );
+
+ await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
+ await browser.tabs.moveInSuccession(
+ [4, 6, 3, 2].map(toTabIds),
+ tabIds[7],
+ {
+ append: true,
+ }
+ );
+ await verifySuccessors(
+ [7, TAB_ID_NONE, TAB_ID_NONE, 2, 6, 7, 3, 4],
+ "scenario 3"
+ );
+
+ await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
+ await browser.tabs.moveInSuccession(
+ [4, 6, 3, 2].map(toTabIds),
+ tabIds[7],
+ {
+ append: true,
+ insert: true,
+ }
+ );
+ await verifySuccessors(
+ [7, TAB_ID_NONE, 5, 2, 6, 7, 3, 4],
+ "insert = true"
+ );
+
+ await browser.tabs.moveInSuccession([0, 4].map(toTabIds), tabIds[7], {
+ append: true,
+ insert: true,
+ });
+ await verifySuccessors(
+ [4, undefined, undefined, undefined, 6, undefined, undefined, 0],
+ "insert = true, part 2"
+ );
+
+ await setSuccessors([1, 2, 3, 4, 0]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[0], {
+ append: true,
+ insert: true,
+ });
+ await verifySuccessors([3, 2, 4, 1, 0], "insert = true, part 3");
+
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
+ await browser.tabs.moveInSuccession([1e8], tabIds[0], {
+ append: true,
+ insert: true,
+ });
+ browser.test.assertEq(
+ tabIds[1],
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "If no tabs get inserted after the reference tab, it should keep its successor"
+ );
+
+ // Validation tests
+
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([tabIds[0], tabIds[1]], tabIds[0], {
+ append: true,
+ }),
+ /Value of tabId must not occur in tabIds if append or insert is true/,
+ "tabs.moveInSuccession should throw when tabId occurs in tabIds and insert is true"
+ );
+
+ return browser.tabs.remove(tabIds);
+ }
+ );
+});
+
+add_task(function testMoveInSuccession_ignoreTabsInOtherWindows() {
+ return runTabTest(
+ 2,
+ async function ({
+ TAB_ID_NONE,
+ tabIds,
+ toTabIds,
+ setSuccessors,
+ verifySuccessors,
+ }) {
+ const anotherWindow = await browser.windows.create({
+ url: Array.from({ length: 3 }, () => "about:blank"),
+ });
+ tabIds.push(...anotherWindow.tabs.map(t => t.id));
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([1, 3, 2].map(toTabIds), tabIds[4]);
+ await verifySuccessors(
+ [1, 0, 4, 2, TAB_ID_NONE],
+ "first tab in another window"
+ );
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[4]);
+ await verifySuccessors(
+ [1, 0, 4, 2, TAB_ID_NONE],
+ "middle tab in another window"
+ );
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds));
+ await verifySuccessors(
+ [1, 0, TAB_ID_NONE, 2, TAB_ID_NONE],
+ "using the first tab to determine the window"
+ );
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([1, 3, 2].map(toTabIds), tabIds[4], {
+ append: true,
+ });
+ await verifySuccessors(
+ [1, 0, TAB_ID_NONE, 2, 3],
+ "first tab in another window, appending"
+ );
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[4], {
+ append: true,
+ });
+ await verifySuccessors(
+ [1, 0, TAB_ID_NONE, 2, 3],
+ "middle tab in another window, appending"
+ );
+
+ return browser.tabs.remove(tabIds);
+ }
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_update.js b/browser/components/extensions/test/browser/browser_ext_tabs_update.js
new file mode 100644
index 0000000000..7b16b24225
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update.js
@@ -0,0 +1,54 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:config"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.query(
+ {
+ lastFocusedWindow: true,
+ },
+ function (tabs) {
+ browser.test.assertEq(tabs.length, 3, "should have three tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(tabs[0].url, "about:blank", "first tab blank");
+ tabs.shift();
+
+ browser.test.assertTrue(tabs[0].active, "tab 0 active");
+ browser.test.assertFalse(tabs[1].active, "tab 1 inactive");
+
+ browser.tabs.update(tabs[1].id, { active: true }, function () {
+ browser.test.sendMessage("check");
+ });
+ }
+ );
+ },
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("check")]);
+
+ Assert.equal(gBrowser.selectedTab, tab2, "correct tab selected");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js b/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
new file mode 100644
index 0000000000..0adb05e827
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
@@ -0,0 +1,183 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_update_highlighted() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ const trackedEvents = ["onActivated", "onHighlighted"];
+ async function expectResults(fn, action) {
+ let resolve;
+ let reject;
+ let promise = new Promise((...args) => {
+ [resolve, reject] = args;
+ });
+ let expectedEvents;
+ let events = [];
+ let listeners = {};
+ for (let trackedEvent of trackedEvents) {
+ listeners[trackedEvent] = data => {
+ events.push([trackedEvent, data]);
+ if (expectedEvents && expectedEvents.length >= events.length) {
+ resolve();
+ }
+ };
+ browser.tabs[trackedEvent].addListener(listeners[trackedEvent]);
+ }
+ let expectedData = await fn();
+ let expectedHighlighted = expectedData.highlighted;
+ let expectedActive = expectedData.active;
+ expectedEvents = expectedData.events;
+ if (events.length < expectedEvents.length) {
+ // Wait up to 1000 ms for the expected number of events.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(reject, 1000);
+ await promise.catch(() => {
+ let numMissing = expectedEvents.length - events.length;
+ browser.test.fail(`${numMissing} missing events when ${action}`);
+ });
+ }
+ let [{ id: active }] = await browser.tabs.query({ active: true });
+ browser.test.assertEq(
+ expectedActive,
+ active,
+ `The expected tab is active when ${action}`
+ );
+ let highlighted = (await browser.tabs.query({ highlighted: true })).map(
+ ({ id }) => id
+ );
+ browser.test.assertEq(
+ JSON.stringify(expectedHighlighted),
+ JSON.stringify(highlighted),
+ `The expected tabs are highlighted when ${action}`
+ );
+ let unexpectedEvents = events.splice(expectedEvents.length);
+ browser.test.assertEq(
+ JSON.stringify(expectedEvents),
+ JSON.stringify(events),
+ `Should get expected events when ${action}`
+ );
+ if (unexpectedEvents.length) {
+ browser.test.fail(
+ `${unexpectedEvents.length} unexpected events when ${action}: ` +
+ JSON.stringify(unexpectedEvents)
+ );
+ }
+ for (let trackedEvent of trackedEvents) {
+ browser.tabs[trackedEvent].removeListener(listeners[trackedEvent]);
+ }
+ }
+
+ let { id: windowId } = await browser.windows.getCurrent();
+ let { id: tab1 } = await browser.tabs.create({ url: "about:blank?1" });
+ let { id: tab2 } = await browser.tabs.create({
+ url: "about:blank?2",
+ active: true,
+ });
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: true });
+ return { active: tab2, highlighted: [tab2], events: [] };
+ }, "highlighting active tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: false });
+ return { active: tab2, highlighted: [tab2], events: [] };
+ }, "unhighlighting active tab with no multiselection");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab1, { highlighted: true });
+ return {
+ active: tab1,
+ highlighted: [tab1, tab2],
+ events: [
+ ["onActivated", { tabId: tab1, previousTabId: tab2, windowId }],
+ ["onHighlighted", { tabIds: [tab1, tab2], windowId }],
+ ],
+ };
+ }, "highlighting non-highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: true });
+ return { active: tab1, highlighted: [tab1, tab2], events: [] };
+ }, "highlighting inactive highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab1, { highlighted: false });
+ return {
+ active: tab2,
+ highlighted: [tab2],
+ events: [
+ ["onActivated", { tabId: tab2, previousTabId: tab1, windowId }],
+ ["onHighlighted", { tabIds: [tab2], windowId }],
+ ],
+ };
+ }, "unhighlighting active tab with multiselection");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab1, { highlighted: true });
+ return {
+ active: tab1,
+ highlighted: [tab1, tab2],
+ events: [
+ ["onActivated", { tabId: tab1, previousTabId: tab2, windowId }],
+ ["onHighlighted", { tabIds: [tab1, tab2], windowId }],
+ ],
+ };
+ }, "highlighting non-highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: false });
+ return {
+ active: tab1,
+ highlighted: [tab1],
+ events: [["onHighlighted", { tabIds: [tab1], windowId }]],
+ };
+ }, "unhighlighting inactive highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: true, active: false });
+ return {
+ active: tab1,
+ highlighted: [tab1, tab2],
+ events: [["onHighlighted", { tabIds: [tab1, tab2], windowId }]],
+ };
+ }, "highlighting without activating non-highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: true, active: true });
+ return {
+ active: tab2,
+ highlighted: [tab2],
+ events: [
+ ["onActivated", { tabId: tab2, previousTabId: tab1, windowId }],
+ ["onHighlighted", { tabIds: [tab2], windowId }],
+ ],
+ };
+ }, "highlighting and activating inactive highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab1, { active: true, highlighted: true });
+ return {
+ active: tab1,
+ highlighted: [tab1],
+ events: [
+ ["onActivated", { tabId: tab1, previousTabId: tab2, windowId }],
+ ["onHighlighted", { tabIds: [tab1], windowId }],
+ ],
+ };
+ }, "highlighting and activating non-highlighted tab");
+
+ await browser.tabs.remove([tab1, tab2]);
+ browser.test.notifyPass("test-finished");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js b/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js
new file mode 100644
index 0000000000..f34a97c047
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js
@@ -0,0 +1,235 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+async function testTabsUpdateURL(
+ existentTabURL,
+ tabsUpdateURL,
+ isErrorExpected
+) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "tab.html": `
+
+
+
+
+
+
+ tab page
+
+
+ `.trim(),
+ },
+ background: function () {
+ browser.test.sendMessage("ready", browser.runtime.getURL("tab.html"));
+
+ browser.test.onMessage.addListener(
+ async (msg, tabsUpdateURL, isErrorExpected) => {
+ let tabs = await browser.tabs.query({ lastFocusedWindow: true });
+
+ try {
+ let tab = await browser.tabs.update(tabs[1].id, {
+ url: tabsUpdateURL,
+ });
+
+ browser.test.assertFalse(
+ isErrorExpected,
+ `tabs.update with URL ${tabsUpdateURL} should be rejected`
+ );
+ browser.test.assertTrue(
+ tab,
+ "on success the tab should be defined"
+ );
+ } catch (error) {
+ browser.test.assertTrue(
+ isErrorExpected,
+ `tabs.update with URL ${tabsUpdateURL} should not be rejected`
+ );
+ browser.test.assertTrue(
+ /^Illegal URL/.test(error.message),
+ "tabs.update should be rejected with the expected error message"
+ );
+ }
+
+ browser.test.sendMessage("done");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+
+ let mozExtTabURL = await extension.awaitMessage("ready");
+
+ if (tabsUpdateURL == "self") {
+ tabsUpdateURL = mozExtTabURL;
+ }
+
+ info(`tab.update URL "${tabsUpdateURL}" on tab with URL "${existentTabURL}"`);
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ existentTabURL
+ );
+
+ extension.sendMessage("start", tabsUpdateURL, isErrorExpected);
+ await extension.awaitMessage("done");
+
+ BrowserTestUtils.removeTab(tab1);
+ await extension.unload();
+}
+
+add_task(async function () {
+ info("Start testing tabs.update on javascript URLs");
+
+ let dataURLPage = `data:text/html,
+
+
+
+
+
+
+ data url page
+
+ `;
+
+ let checkList = [
+ {
+ tabsUpdateURL: "http://example.net",
+ isErrorExpected: false,
+ },
+ {
+ tabsUpdateURL: "self",
+ isErrorExpected: false,
+ },
+ {
+ tabsUpdateURL: "about:addons",
+ isErrorExpected: true,
+ },
+ {
+ tabsUpdateURL: "javascript:console.log('tabs.update execute javascript')",
+ isErrorExpected: true,
+ },
+ {
+ tabsUpdateURL: dataURLPage,
+ isErrorExpected: true,
+ },
+ ];
+
+ let testCases = checkList.map(check =>
+ Object.assign({}, check, { existentTabURL: "about:blank" })
+ );
+
+ for (let { existentTabURL, tabsUpdateURL, isErrorExpected } of testCases) {
+ await testTabsUpdateURL(existentTabURL, tabsUpdateURL, isErrorExpected);
+ }
+
+ info("done");
+});
+
+add_task(async function test_update_reload() {
+ const URL = "https://example.com/";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background() {
+ browser.test.onMessage.addListener(async (cmd, ...args) => {
+ const result = await browser.tabs[cmd](...args);
+ browser.test.sendMessage("result", result);
+ });
+
+ const filter = {
+ properties: ["status"],
+ };
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.sendMessage("historyAdded");
+ }
+ }, filter);
+ },
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tabBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(tabBrowser, URL);
+ await BrowserTestUtils.browserLoaded(tabBrowser, false, URL);
+ let tab = win.gBrowser.selectedTab;
+
+ async function getTabHistory() {
+ await TabStateFlusher.flush(tabBrowser);
+ return JSON.parse(SessionStore.getTabState(tab));
+ }
+
+ await extension.startup();
+ extension.sendMessage("query", { url: URL });
+ let tabs = await extension.awaitMessage("result");
+ let tabId = tabs[0].id;
+
+ let history = await getTabHistory();
+ is(
+ history.entries.length,
+ 1,
+ "Tab history contains the expected number of entries."
+ );
+ is(
+ history.entries[0].url,
+ URL,
+ `Tab history contains the expected entry: URL.`
+ );
+
+ extension.sendMessage("update", tabId, { url: `${URL}1/` });
+ await Promise.all([
+ extension.awaitMessage("result"),
+ extension.awaitMessage("historyAdded"),
+ ]);
+
+ history = await getTabHistory();
+ is(
+ history.entries.length,
+ 2,
+ "Tab history contains the expected number of entries."
+ );
+ is(
+ history.entries[1].url,
+ `${URL}1/`,
+ `Tab history contains the expected entry: ${URL}1/.`
+ );
+
+ extension.sendMessage("update", tabId, {
+ url: `${URL}2/`,
+ loadReplace: true,
+ });
+ await Promise.all([
+ extension.awaitMessage("result"),
+ extension.awaitMessage("historyAdded"),
+ ]);
+
+ history = await getTabHistory();
+ is(
+ history.entries.length,
+ 2,
+ "Tab history contains the expected number of entries."
+ );
+ is(
+ history.entries[1].url,
+ `${URL}2/`,
+ `Tab history contains the expected entry: ${URL}2/.`
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_warmup.js b/browser/components/extensions/test/browser/browser_ext_tabs_warmup.js
new file mode 100644
index 0000000000..e9a5382de8
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_warmup.js
@@ -0,0 +1,40 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWarmupTab() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ Assert.ok(!tab1.linkedBrowser.renderLayers, "tab is not warm yet");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let backgroundTab = (
+ await browser.tabs.query({
+ lastFocusedWindow: true,
+ url: "http://example.net/",
+ active: false,
+ })
+ )[0];
+ await browser.tabs.warmup(backgroundTab.id);
+ browser.test.notifyPass("tabs.warmup");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.warmup");
+ Assert.ok(tab1.linkedBrowser.renderLayers, "tab has been warmed up");
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
new file mode 100644
index 0000000000..5884a9163a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
@@ -0,0 +1,346 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const SITE_SPECIFIC_PREF = "browser.zoom.siteSpecific";
+const FULL_ZOOM_PREF = "browser.content.full-zoom";
+
+let gContentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+);
+
+// A single monitor for the tests. If it receives any
+// incognito data in event listeners it will fail.
+let monitor;
+add_task(async function startup() {
+ monitor = await startIncognitoMonitorExtension();
+});
+registerCleanupFunction(async function finish() {
+ await monitor.unload();
+});
+
+add_task(async function test_zoom_api() {
+ async function background() {
+ function promiseUpdated(tabId, attr) {
+ return new Promise(resolve => {
+ let onUpdated = (tabId_, changeInfo, tab) => {
+ if (tabId == tabId_ && attr in changeInfo) {
+ browser.tabs.onUpdated.removeListener(onUpdated);
+
+ resolve({ changeInfo, tab });
+ }
+ };
+ browser.tabs.onUpdated.addListener(onUpdated);
+ });
+ }
+
+ let deferred = {};
+ browser.test.onMessage.addListener((message, msg, result) => {
+ if (message == "msg-done" && deferred[msg]) {
+ deferred[msg].resolve(result);
+ }
+ });
+
+ let _id = 0;
+ function msg(...args) {
+ return new Promise((resolve, reject) => {
+ let id = ++_id;
+ deferred[id] = { resolve, reject };
+ browser.test.sendMessage("msg", id, ...args);
+ });
+ }
+
+ let lastZoomEvent = {};
+ let promiseZoomEvents = {};
+ browser.tabs.onZoomChange.addListener(info => {
+ lastZoomEvent[info.tabId] = info;
+ if (promiseZoomEvents[info.tabId]) {
+ promiseZoomEvents[info.tabId]();
+ promiseZoomEvents[info.tabId] = null;
+ }
+ });
+
+ let awaitZoom = async (tabId, newValue) => {
+ let listener;
+
+ // eslint-disable-next-line no-async-promise-executor
+ await new Promise(async resolve => {
+ listener = info => {
+ if (info.tabId == tabId && info.newZoomFactor == newValue) {
+ resolve();
+ }
+ };
+ browser.tabs.onZoomChange.addListener(listener);
+
+ let zoomFactor = await browser.tabs.getZoom(tabId);
+ if (zoomFactor == newValue) {
+ resolve();
+ }
+ });
+
+ browser.tabs.onZoomChange.removeListener(listener);
+ };
+
+ let checkZoom = async (tabId, newValue, oldValue = null) => {
+ let awaitEvent;
+ if (oldValue != null && !lastZoomEvent[tabId]) {
+ awaitEvent = new Promise(resolve => {
+ promiseZoomEvents[tabId] = resolve;
+ });
+ }
+
+ let [apiZoom, realZoom] = await Promise.all([
+ browser.tabs.getZoom(tabId),
+ msg("get-zoom", tabId),
+ awaitEvent,
+ ]);
+
+ browser.test.assertEq(
+ newValue,
+ apiZoom,
+ `Got expected zoom value from API`
+ );
+ browser.test.assertEq(
+ newValue,
+ realZoom,
+ `Got expected zoom value from parent`
+ );
+
+ if (oldValue != null) {
+ let event = lastZoomEvent[tabId];
+ lastZoomEvent[tabId] = null;
+ browser.test.assertEq(
+ tabId,
+ event.tabId,
+ `Got expected zoom event tab ID`
+ );
+ browser.test.assertEq(
+ newValue,
+ event.newZoomFactor,
+ `Got expected zoom event zoom factor`
+ );
+ browser.test.assertEq(
+ oldValue,
+ event.oldZoomFactor,
+ `Got expected zoom event old zoom factor`
+ );
+
+ browser.test.assertEq(
+ 3,
+ Object.keys(event.zoomSettings).length,
+ `Zoom settings should have 3 keys`
+ );
+ browser.test.assertEq(
+ "automatic",
+ event.zoomSettings.mode,
+ `Mode should be "automatic"`
+ );
+ browser.test.assertEq(
+ "per-origin",
+ event.zoomSettings.scope,
+ `Scope should be "per-origin"`
+ );
+ browser.test.assertEq(
+ 1,
+ event.zoomSettings.defaultZoomFactor,
+ `Default zoom should be 1`
+ );
+ }
+ };
+
+ try {
+ let tabs = await browser.tabs.query({});
+ browser.test.assertEq(tabs.length, 4, "We have 4 tabs");
+
+ let tabIds = tabs.splice(1).map(tab => tab.id);
+ await checkZoom(tabIds[0], 1);
+
+ await browser.tabs.setZoom(tabIds[0], 2);
+ await checkZoom(tabIds[0], 2, 1);
+
+ let zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);
+ browser.test.assertEq(
+ 3,
+ Object.keys(zoomSettings).length,
+ `Zoom settings should have 3 keys`
+ );
+ browser.test.assertEq(
+ "automatic",
+ zoomSettings.mode,
+ `Mode should be "automatic"`
+ );
+ browser.test.assertEq(
+ "per-origin",
+ zoomSettings.scope,
+ `Scope should be "per-origin"`
+ );
+ browser.test.assertEq(
+ 1,
+ zoomSettings.defaultZoomFactor,
+ `Default zoom should be 1`
+ );
+
+ browser.test.log(`Switch to tab 2`);
+ await browser.tabs.update(tabIds[1], { active: true });
+ await checkZoom(tabIds[1], 1);
+
+ browser.test.log(`Navigate tab 2 to origin of tab 1`);
+ browser.tabs.update(tabIds[1], { url: "http://example.com" });
+ await promiseUpdated(tabIds[1], "url");
+ await checkZoom(tabIds[1], 2, 1);
+
+ browser.test.log(`Update zoom in tab 2, expect changes in both tabs`);
+ await browser.tabs.setZoom(tabIds[1], 1.5);
+ await checkZoom(tabIds[1], 1.5, 2);
+
+ browser.test.log(`Switch to tab 3, expect zoom to affect private window`);
+ await browser.tabs.setZoom(tabIds[2], 3);
+ await checkZoom(tabIds[2], 3, 1);
+
+ browser.test.log(
+ `Switch to tab 1, expect asynchronous zoom change just after the switch`
+ );
+ await Promise.all([
+ awaitZoom(tabIds[0], 1.5),
+ browser.tabs.update(tabIds[0], { active: true }),
+ ]);
+ await checkZoom(tabIds[0], 1.5, 2);
+
+ browser.test.log("Set zoom to 0, expect it set to 1");
+ await browser.tabs.setZoom(tabIds[0], 0);
+ await checkZoom(tabIds[0], 1, 1.5);
+
+ browser.test.log("Change zoom externally, expect changes reflected");
+ await msg("enlarge");
+ await checkZoom(tabIds[0], 1.1, 1);
+
+ await Promise.all([
+ browser.tabs.setZoom(tabIds[0], 0),
+ browser.tabs.setZoom(tabIds[1], 0),
+ browser.tabs.setZoom(tabIds[2], 0),
+ ]);
+ await Promise.all([
+ checkZoom(tabIds[0], 1, 1.1),
+ checkZoom(tabIds[1], 1, 1.5),
+ checkZoom(tabIds[2], 1, 3),
+ ]);
+
+ browser.test.log("Check that invalid zoom values throw an error");
+ await browser.test.assertRejects(
+ browser.tabs.setZoom(tabIds[0], 42),
+ /Zoom value 42 out of range/,
+ "Expected an out of range error"
+ );
+
+ browser.test.log("Disable site-specific zoom, expect correct scope");
+ await msg("site-specific", false);
+ zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);
+
+ browser.test.assertEq(
+ "per-tab",
+ zoomSettings.scope,
+ `Scope should be "per-tab"`
+ );
+
+ await msg("site-specific", null);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "set-global-zoom-done") {
+ zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);
+
+ browser.test.assertEq(
+ 5,
+ zoomSettings.defaultZoomFactor,
+ `Default zoom should be 5 after being changed`
+ );
+
+ browser.test.notifyPass("tab-zoom");
+ }
+ });
+ await msg("set-global-zoom");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("tab-zoom");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ incognitoOverride: "spanning",
+ background,
+ });
+
+ extension.onMessage("msg", (id, msg, ...args) => {
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let resp;
+ if (msg == "get-zoom") {
+ let tab = tabTracker.getTab(args[0]);
+ resp = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
+ } else if (msg == "set-zoom") {
+ let tab = tabTracker.getTab(args[0]);
+ ZoomManager.setZoomForBrowser(tab.linkedBrowser);
+ } else if (msg == "set-global-zoom") {
+ resp = gContentPrefs.setGlobal(
+ FULL_ZOOM_PREF,
+ 5,
+ Cu.createLoadContext(),
+ {
+ handleCompletion() {
+ extension.sendMessage("set-global-zoom-done", id, resp);
+ },
+ }
+ );
+ } else if (msg == "enlarge") {
+ FullZoom.enlarge();
+ } else if (msg == "site-specific") {
+ if (args[0] == null) {
+ SpecialPowers.clearUserPref(SITE_SPECIFIC_PREF);
+ } else {
+ SpecialPowers.setBoolPref(SITE_SPECIFIC_PREF, args[0]);
+ }
+ }
+
+ extension.sendMessage("msg-done", id, resp);
+ });
+
+ let url = "https://example.com/";
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.org/"
+ );
+
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let selectedBrowser = privateWindow.gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(selectedBrowser, false, url);
+
+ gBrowser.selectedTab = tab1;
+
+ await extension.startup();
+
+ await extension.awaitFinish("tab-zoom");
+
+ await extension.unload();
+
+ await new Promise(resolve => {
+ gContentPrefs.setGlobal(FULL_ZOOM_PREF, null, Cu.createLoadContext(), {
+ handleCompletion() {
+ resolve();
+ },
+ });
+ });
+
+ privateWindow.close();
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_themes_validation.js b/browser/components/extensions/test/browser/browser_ext_themes_validation.js
new file mode 100644
index 0000000000..c004363a6b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_themes_validation.js
@@ -0,0 +1,55 @@
+"use strict";
+
+PromiseTestUtils.allowMatchingRejectionsGlobally(/packaging errors/);
+
+/**
+ * Helper function for testing a theme with invalid properties.
+ *
+ * @param {object} invalidProps The invalid properties to load the theme with.
+ */
+async function testThemeWithInvalidProperties(invalidProps) {
+ let manifest = {
+ theme: {},
+ };
+
+ invalidProps.forEach(prop => {
+ // Some properties require additional information:
+ switch (prop) {
+ case "background":
+ manifest[prop] = { scripts: ["background.js"] };
+ break;
+ case "permissions":
+ manifest[prop] = ["tabs"];
+ break;
+ case "omnibox":
+ manifest[prop] = { keyword: "test" };
+ break;
+ default:
+ manifest[prop] = {};
+ }
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({ manifest });
+ await Assert.rejects(
+ extension.startup(),
+ /startup failed/,
+ "Theme should fail to load if it contains invalid properties"
+ );
+}
+
+add_task(
+ async function test_that_theme_with_invalid_properties_fails_to_load() {
+ let invalidProps = [
+ "page_action",
+ "browser_action",
+ "background",
+ "permissions",
+ "omnibox",
+ "commands",
+ ];
+ for (let prop in invalidProps) {
+ await testThemeWithInvalidProperties([prop]);
+ }
+ await testThemeWithInvalidProperties(invalidProps);
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_topSites.js b/browser/components/extensions/test/browser/browser_ext_topSites.js
new file mode 100644
index 0000000000..fe0edf2b8d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_topSites.js
@@ -0,0 +1,413 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+const {
+ ExtensionUtils: { makeDataURI },
+} = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs");
+
+// A small 1x1 test png
+const IMAGE_1x1 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+
+async function updateTopSites(condition) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+async function loadExtension() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["topSites"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async options => {
+ let sites = await browser.topSites.get(options);
+ browser.test.sendMessage("sites", sites);
+ });
+ },
+ });
+ await extension.startup();
+ return extension;
+}
+
+async function getSites(extension, options) {
+ extension.sendMessage(options);
+ return extension.awaitMessage("sites");
+}
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // The pref for TopSites is empty by default.
+ [
+ "browser.newtabpage.activity-stream.default.sites",
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/",
+ ],
+ // Toggle the feed off and on as a workaround to read the new prefs.
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ true,
+ ],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:newtab",
+ false
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Tests newtab links with an empty history.
+add_task(async function test_topSites_newtab_emptyHistory() {
+ let extension = await loadExtension();
+
+ let expectedResults = [
+ {
+ type: "search",
+ url: "https://amazon.com",
+ title: "@amazon",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.youtube.com/",
+ title: "youtube",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.facebook.com/",
+ title: "facebook",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.reddit.com/",
+ title: "reddit",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.wikipedia.org/",
+ title: "wikipedia",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://twitter.com/",
+ title: "twitter",
+ favicon: null,
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true }),
+ "got topSites newtab links"
+ );
+
+ await extension.unload();
+});
+
+// Tests newtab links with some visits.
+add_task(async function test_topSites_newtab_visits() {
+ let extension = await loadExtension();
+
+ // Add some visits to a couple of URLs. We need to add at least two visits
+ // per URL for it to show up. Add some extra to be safe, and add one more to
+ // the first so that its frecency is larger.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ }
+ await PlacesTestUtils.addVisits("http://example-1.com/");
+
+ // Wait for example-1.com to be listed second, after the Amazon search link.
+ await updateTopSites(sites => {
+ return sites && sites[1] && sites[1].url == "http://example-1.com/";
+ });
+
+ let expectedResults = [
+ {
+ type: "search",
+ url: "https://amazon.com",
+ title: "@amazon",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "http://example-1.com/",
+ title: "test visit for http://example-1.com/",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "http://example-2.com/",
+ title: "test visit for http://example-2.com/",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.youtube.com/",
+ title: "youtube",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.facebook.com/",
+ title: "facebook",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.reddit.com/",
+ title: "reddit",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.wikipedia.org/",
+ title: "wikipedia",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://twitter.com/",
+ title: "twitter",
+ favicon: null,
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true }),
+ "got topSites newtab links"
+ );
+
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
+
+// Tests that the newtab parameter is ignored if newtab Top Sites are disabled.
+add_task(async function test_topSites_newtab_ignored() {
+ let extension = await loadExtension();
+ // Add some visits to a couple of URLs. We need to add at least two visits
+ // per URL for it to show up. Add some extra to be safe, and add one more to
+ // the first so that its frecency is larger.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ }
+ await PlacesTestUtils.addVisits("http://example-1.com/");
+
+ // Wait for example-1.com to be listed second, after the Amazon search link.
+ await updateTopSites(sites => {
+ return sites && sites[1] && sites[1].url == "http://example-1.com/";
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.feeds.system.topsites", false]],
+ });
+
+ let expectedResults = [
+ {
+ type: "url",
+ url: "http://example-1.com/",
+ title: "test visit for http://example-1.com/",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "http://example-2.com/",
+ title: "test visit for http://example-2.com/",
+ favicon: null,
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true }),
+ "Got top-frecency links from Places"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
+
+// Tests newtab links with some visits and favicons.
+add_task(async function test_topSites_newtab_visits_favicons() {
+ let extension = await loadExtension();
+
+ // Add some visits to a couple of URLs. We need to add at least two visits
+ // per URL for it to show up. Add some extra to be safe, and add one more to
+ // the first so that its frecency is larger.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ }
+ await PlacesTestUtils.addVisits("http://example-1.com/");
+
+ // Give the first URL a favicon but not the second so that we can test links
+ // both with and without favicons.
+ let faviconData = new Map();
+ faviconData.set("http://example-1.com", IMAGE_1x1);
+ await PlacesTestUtils.addFavicons(faviconData);
+
+ // Wait for example-1.com to be listed second, after the Amazon search link.
+ await updateTopSites(sites => {
+ return sites && sites[1] && sites[1].url == "http://example-1.com/";
+ });
+
+ let base = "chrome://activity-stream/content/data/content/tippytop/images/";
+
+ let expectedResults = [
+ {
+ type: "search",
+ url: "https://amazon.com",
+ title: "@amazon",
+ favicon: await makeDataURI(`${base}amazon@2x.png`),
+ },
+ {
+ type: "url",
+ url: "http://example-1.com/",
+ title: "test visit for http://example-1.com/",
+ favicon: IMAGE_1x1,
+ },
+ {
+ type: "url",
+ url: "http://example-2.com/",
+ title: "test visit for http://example-2.com/",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.youtube.com/",
+ title: "youtube",
+ favicon: await makeDataURI(`${base}youtube-com@2x.png`),
+ },
+ {
+ type: "url",
+ url: "https://www.facebook.com/",
+ title: "facebook",
+ favicon: await makeDataURI(`${base}facebook-com@2x.png`),
+ },
+ {
+ type: "url",
+ url: "https://www.reddit.com/",
+ title: "reddit",
+ favicon: await makeDataURI(`${base}reddit-com@2x.png`),
+ },
+ {
+ type: "url",
+ url: "https://www.wikipedia.org/",
+ title: "wikipedia",
+ favicon: await makeDataURI(`${base}wikipedia-org@2x.png`),
+ },
+ {
+ type: "url",
+ url: "https://twitter.com/",
+ title: "twitter",
+ favicon: await makeDataURI(`${base}twitter-com@2x.png`),
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true, includeFavicon: true }),
+ "got topSites newtab links"
+ );
+
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
+
+// Tests newtab links with some visits, favicons, and the `limit` option.
+add_task(async function test_topSites_newtab_visits_favicons_limit() {
+ let extension = await loadExtension();
+
+ // Add some visits to a couple of URLs. We need to add at least two visits
+ // per URL for it to show up. Add some extra to be safe, and add one more to
+ // the first so that its frecency is larger.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ }
+ await PlacesTestUtils.addVisits("http://example-1.com/");
+
+ // Give the first URL a favicon but not the second so that we can test links
+ // both with and without favicons.
+ let faviconData = new Map();
+ faviconData.set("http://example-1.com", IMAGE_1x1);
+ await PlacesTestUtils.addFavicons(faviconData);
+
+ // Wait for example-1.com to be listed second, after the Amazon search link.
+ await updateTopSites(sites => {
+ return sites && sites[1] && sites[1].url == "http://example-1.com/";
+ });
+
+ let expectedResults = [
+ {
+ type: "search",
+ url: "https://amazon.com",
+ title: "@amazon",
+ favicon: await makeDataURI(
+ "chrome://activity-stream/content/data/content/tippytop/images/amazon@2x.png"
+ ),
+ },
+ {
+ type: "url",
+ url: "http://example-1.com/",
+ title: "test visit for http://example-1.com/",
+ favicon: IMAGE_1x1,
+ },
+ {
+ type: "url",
+ url: "http://example-2.com/",
+ title: "test visit for http://example-2.com/",
+ favicon: null,
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true, includeFavicon: true, limit: 3 }),
+ "got topSites newtab links"
+ );
+
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
new file mode 100644
index 0000000000..988f44bd5d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
@@ -0,0 +1,794 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+requestLongerTimeout(4);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
+ ExtensionControlledPopup:
+ "resource:///modules/ExtensionControlledPopup.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+
+function getNotificationSetting(extensionId) {
+ return ExtensionSettingsStore.getSetting("newTabNotification", extensionId);
+}
+
+function getNewTabDoorhanger() {
+ ExtensionControlledPopup._getAndMaybeCreatePanel(document);
+ return document.getElementById("extension-new-tab-notification");
+}
+
+function clickKeepChanges(notification) {
+ notification.button.click();
+}
+
+function clickManage(notification) {
+ notification.secondaryButton.click();
+}
+
+async function promiseNewTab(expectUrl = AboutNewTab.newTabURL, win = window) {
+ let eventName = "browser-open-newtab-start";
+ let newTabStartPromise = new Promise(resolve => {
+ async function observer(subject) {
+ Services.obs.removeObserver(observer, eventName);
+ resolve(subject.wrappedJSObject);
+ }
+ Services.obs.addObserver(observer, eventName);
+ });
+
+ let newtabShown = TestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == expectUrl,
+ `Should open correct new tab url ${expectUrl}.`
+ );
+
+ win.BrowserOpenTab();
+ const newTabCreatedPromise = newTabStartPromise;
+ const browser = await newTabCreatedPromise;
+ await newtabShown;
+ const tab = win.gBrowser.selectedTab;
+
+ Assert.deepEqual(
+ browser,
+ tab.linkedBrowser,
+ "browser-open-newtab-start notified with the created browser"
+ );
+ return tab;
+}
+
+function waitForAddonDisabled(addon) {
+ return new Promise(resolve => {
+ let listener = {
+ onDisabled(disabledAddon) {
+ if (disabledAddon.id == addon.id) {
+ resolve();
+ AddonManager.removeAddonListener(listener);
+ }
+ },
+ };
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+function waitForAddonEnabled(addon) {
+ return new Promise(resolve => {
+ let listener = {
+ onEnabled(enabledAddon) {
+ if (enabledAddon.id == addon.id) {
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ },
+ };
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+// Default test extension data for newtab.
+const extensionData = {
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "newtaburl@mochi.test",
+ },
+ },
+ chrome_url_overrides: {
+ newtab: "newtab.html",
+ },
+ },
+ files: {
+ "newtab.html": "New tab! ",
+ },
+ useAddonManager: "temporary",
+};
+
+add_task(async function test_new_tab_opens() {
+ let panel = getNewTabDoorhanger().closest("panel");
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ // Simulate opening the newtab open as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab(extensionNewTabUrl);
+ await popupShown;
+
+ // This will show a confirmation doorhanger, make sure we don't leave it open.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_new_tab_ignore_settings() {
+ await ExtensionSettingsStore.initialize();
+ let notification = getNewTabDoorhanger();
+ let panel = notification.closest("panel");
+ let extensionId = "newtabignore@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: extensionId } },
+ browser_action: {
+ default_popup: "ignore.html",
+ default_area: "navbar",
+ },
+ chrome_url_overrides: { newtab: "ignore.html" },
+ },
+ files: { "ignore.html": 'New Tab! ' },
+ useAddonManager: "temporary",
+ });
+
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is initially closed"
+ );
+
+ await extension.startup();
+
+ // Simulate opening the New Tab as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab();
+ await popupShown;
+
+ // Ensure the doorhanger is shown and the setting isn't set yet.
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(gURLBar.focused, false, "The URL bar is not focused with a doorhanger");
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set for this extension"
+ );
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "newtabignore_mochi_test-BAP",
+ "The doorhanger is anchored to the browser action"
+ );
+
+ // Manually close the panel, as if the user ignored it.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ // Ensure panel is closed and the setting still isn't set.
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is closed"
+ );
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set after ignoring the doorhanger"
+ );
+
+ // Close the first tab and open another new tab.
+ BrowserTestUtils.removeTab(tab);
+ tab = await promiseNewTab();
+
+ // Verify the doorhanger is not shown a second time.
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel doesn't open after ignoring the doorhanger"
+ );
+ is(gURLBar.focused, true, "The URL bar is focused with no doorhanger");
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_new_tab_keep_settings() {
+ await ExtensionSettingsStore.initialize();
+ let notification = getNewTabDoorhanger();
+ let panel = notification.closest("panel");
+ let extensionId = "newtabkeep@mochi.test";
+ let manifest = {
+ version: "1.0",
+ name: "New Tab Add-on",
+ browser_specific_settings: { gecko: { id: extensionId } },
+ chrome_url_overrides: { newtab: "newtab.html" },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+ manifest,
+ useAddonManager: "permanent",
+ });
+
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is initially closed"
+ );
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ // Simulate opening the New Tab as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab(extensionNewTabUrl);
+ await popupShown;
+
+ // Ensure the panel is open and the setting isn't saved yet.
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set for this extension"
+ );
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "PanelUI-menu-button",
+ "The doorhanger is anchored to the menu icon"
+ );
+ is(
+ panel.querySelector("#extension-new-tab-notification-description")
+ .textContent,
+ "An extension, New Tab Add-on, changed the page you see when you open a new tab.Learn more",
+ "The description includes the add-on name"
+ );
+
+ // Click the Keep Changes button.
+ let confirmationSaved = TestUtils.waitForCondition(() => {
+ return ExtensionSettingsStore.getSetting(
+ "newTabNotification",
+ extensionId,
+ extensionId
+ ).value;
+ });
+ clickKeepChanges(notification);
+ await confirmationSaved;
+
+ // Ensure panel is closed and setting is updated.
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is closed after click"
+ );
+ is(
+ getNotificationSetting(extensionId).value,
+ true,
+ "The New Tab notification is set after keeping the changes"
+ );
+
+ // Close the first tab and open another new tab.
+ BrowserTestUtils.removeTab(tab);
+ tab = await promiseNewTab(extensionNewTabUrl);
+
+ // Verify the doorhanger is not shown a second time.
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is not opened after keeping the changes"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ let upgradedExtension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+ manifest: Object.assign({}, manifest, { version: "2.0" }),
+ useAddonManager: "permanent",
+ });
+
+ await upgradedExtension.startup();
+ extensionNewTabUrl = `moz-extension://${upgradedExtension.uuid}/newtab.html`;
+
+ tab = await promiseNewTab(extensionNewTabUrl);
+
+ // Ensure panel is closed and setting is still set.
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is closed after click"
+ );
+ is(
+ getNotificationSetting(extensionId).value,
+ true,
+ "The New Tab notification is set after keeping the changes"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await upgradedExtension.unload();
+ await extension.unload();
+
+ let confirmation = ExtensionSettingsStore.getSetting(
+ "newTabNotification",
+ extensionId,
+ extensionId
+ );
+ is(confirmation, null, "The confirmation has been cleaned up");
+});
+
+add_task(async function test_new_tab_restore_settings() {
+ await ExtensionSettingsStore.initialize();
+ let notification = getNewTabDoorhanger();
+ let panel = notification.closest("panel");
+ let extensionId = "newtabrestore@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: extensionId } },
+ chrome_url_overrides: { newtab: "restore.html" },
+ },
+ files: { "restore.html": 'New Tab! ' },
+ useAddonManager: "temporary",
+ });
+
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is initially closed"
+ );
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not initially set for this extension"
+ );
+
+ await extension.startup();
+
+ // Simulate opening the newtab open as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab();
+ await popupShown;
+
+ // Verify that the panel is open and add-on is enabled.
+ let addon = await AddonManager.getAddonByID(extensionId);
+ is(addon.userDisabled, false, "The add-on is enabled at first");
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set for this extension"
+ );
+
+ // Click the Manage button.
+ let preferencesShown = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:preferences#home",
+ "Should open about:preferences."
+ );
+
+ let popupHidden = promisePopupHidden(panel);
+ clickManage(notification);
+ await popupHidden;
+ await preferencesShown;
+
+ // Ensure panel is closed, settings haven't changed and add-on is disabled.
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is closed after click"
+ );
+
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set after clicking manage"
+ );
+
+ // Reopen a browser tab and verify that there's no doorhanger.
+ BrowserTestUtils.removeTab(tab);
+ tab = await promiseNewTab();
+
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is not opened after keeping the changes"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_new_tab_restore_settings_multiple() {
+ await ExtensionSettingsStore.initialize();
+ let notification = getNewTabDoorhanger();
+ let panel = notification.closest("panel");
+ let extensionOneId = "newtabrestoreone@mochi.test";
+ let extensionOne = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: extensionOneId } },
+ chrome_url_overrides: { newtab: "restore-one.html" },
+ },
+ files: {
+ "restore-one.html": `
+ New Tab!
+ `,
+ },
+ useAddonManager: "temporary",
+ });
+ let extensionTwoId = "newtabrestoretwo@mochi.test";
+ let extensionTwo = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: extensionTwoId } },
+ chrome_url_overrides: { newtab: "restore-two.html" },
+ },
+ files: { "restore-two.html": 'New Tab! ' },
+ useAddonManager: "temporary",
+ });
+
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is initially closed"
+ );
+ is(
+ getNotificationSetting(extensionOneId),
+ null,
+ "The New Tab notification is not initially set for this extension"
+ );
+ is(
+ getNotificationSetting(extensionTwoId),
+ null,
+ "The New Tab notification is not initially set for this extension"
+ );
+
+ await extensionOne.startup();
+ await extensionTwo.startup();
+
+ // Simulate opening the newtab open as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab1 = await promiseNewTab();
+ await popupShown;
+
+ // Verify that the panel is open and add-on is enabled.
+ let addonTwo = await AddonManager.getAddonByID(extensionTwoId);
+ is(addonTwo.userDisabled, false, "The add-on is enabled at first");
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(
+ getNotificationSetting(extensionTwoId),
+ null,
+ "The New Tab notification is not set for this extension"
+ );
+
+ // Click the Manage button.
+ let popupHidden = promisePopupHidden(panel);
+ let preferencesShown = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:preferences#home",
+ "Should open about:preferences."
+ );
+ clickManage(notification);
+ await popupHidden;
+ await preferencesShown;
+
+ // Disable the second addon then refresh the new tab expect to see a new addon dropdown.
+ let addonDisabled = waitForAddonDisabled(addonTwo);
+ addonTwo.disable();
+ await addonDisabled;
+
+ // Ensure the panel opens again for the next add-on.
+ popupShown = promisePopupShown(panel);
+ let newtabShown = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == AboutNewTab.newTabURL,
+ "Should open correct new tab url."
+ );
+ let tab2 = await promiseNewTab();
+ await newtabShown;
+ await popupShown;
+
+ is(
+ getNotificationSetting(extensionTwoId),
+ null,
+ "The New Tab notification is not set after restoring the settings"
+ );
+ let addonOne = await AddonManager.getAddonByID(extensionOneId);
+ is(
+ addonOne.userDisabled,
+ false,
+ "The extension is enabled before making a choice"
+ );
+ is(
+ getNotificationSetting(extensionOneId),
+ null,
+ "The New Tab notification is not set before making a choice"
+ );
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(
+ gBrowser.currentURI.spec,
+ AboutNewTab.newTabURL,
+ "The user is now on the next extension's New Tab page"
+ );
+
+ preferencesShown = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:preferences#home",
+ "Should open about:preferences."
+ );
+ popupHidden = promisePopupHidden(panel);
+ clickManage(notification);
+ await popupHidden;
+ await preferencesShown;
+ // remove the extra preferences tab.
+ BrowserTestUtils.removeTab(tab2);
+
+ addonDisabled = waitForAddonDisabled(addonOne);
+ addonOne.disable();
+ await addonDisabled;
+ tab2 = await promiseNewTab();
+
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is closed after restoring the second time"
+ );
+ is(
+ getNotificationSetting(extensionOneId),
+ null,
+ "The New Tab notification is not set after restoring the settings"
+ );
+ is(
+ gBrowser.currentURI.spec,
+ "about:newtab",
+ "The user is now on the original New Tab URL since all extensions are disabled"
+ );
+
+ // Reopen a browser tab and verify that there's no doorhanger.
+ BrowserTestUtils.removeTab(tab2);
+ tab2 = await promiseNewTab();
+
+ Assert.notEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is not opened after keeping the changes"
+ );
+
+ // FIXME: We need to enable the add-on so it gets cleared from the
+ // ExtensionSettingsStore for now. See bug 1408226.
+ let addonsEnabled = Promise.all([
+ waitForAddonEnabled(addonOne),
+ waitForAddonEnabled(addonTwo),
+ ]);
+ await addonOne.enable();
+ await addonTwo.enable();
+ await addonsEnabled;
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ await extensionOne.unload();
+ await extensionTwo.unload();
+});
+
+/**
+ * Ensure we don't show the extension URL in the URL bar temporarily in new tabs
+ * while we're switching remoteness (when the URL we're loading and the
+ * default content principal are different).
+ */
+add_task(async function dontTemporarilyShowAboutExtensionPath() {
+ await ExtensionSettingsStore.initialize();
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ let wpl = {
+ onLocationChange() {
+ is(gURLBar.value, "", "URL bar value should stay empty.");
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: extensionNewTabUrl,
+ });
+
+ gBrowser.removeProgressListener(wpl);
+ is(gURLBar.value, "", "URL bar value should be empty.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ is(
+ content.document.body.textContent,
+ "New tab!",
+ "New tab page is loaded."
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_overriding_newtab_incognito_not_allowed() {
+ let panel = getNewTabDoorhanger().closest("panel");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab(extensionNewTabUrl);
+ await popupShown;
+
+ // This will show a confirmation doorhanger, make sure we don't leave it open.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Verify a private window does not open the extension page. We would
+ // get an extra notification that we don't listen for if it gets loaded.
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow({ private: true });
+ await windowOpenedPromise;
+
+ await promiseNewTab("about:privatebrowsing", win);
+
+ is(win.gURLBar.value, "", "newtab not used in private window");
+
+ // Verify setting the pref directly doesn't bypass permissions.
+ let origUrl = AboutNewTab.newTabURL;
+ AboutNewTab.newTabURL = extensionNewTabUrl;
+ await promiseNewTab("about:privatebrowsing", win);
+
+ is(win.gURLBar.value, "", "directly set newtab not used in private window");
+
+ AboutNewTab.newTabURL = origUrl;
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_overriding_newtab_incognito_spanning() {
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+ useAddonManager: "permanent",
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow({ private: true });
+ await windowOpenedPromise;
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(win.document);
+ let popupShown = promisePopupShown(panel);
+ await promiseNewTab(extensionNewTabUrl, win);
+ await popupShown;
+
+ // This will show a confirmation doorhanger, make sure we don't leave it open.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Test that prefs set by the newtab override code are
+// properly unset when all newtab extensions are gone.
+add_task(async function testNewTabPrefsReset() {
+ function isUndefinedPref(pref) {
+ try {
+ Services.prefs.getBoolPref(pref);
+ return false;
+ } catch (e) {
+ return true;
+ }
+ }
+
+ ok(
+ isUndefinedPref("browser.newtab.extensionControlled"),
+ "extensionControlled pref is not set"
+ );
+ ok(
+ isUndefinedPref("browser.newtab.privateAllowed"),
+ "privateAllowed pref is not set"
+ );
+});
+
+// This test ensures that an extension provided newtab
+// can be opened by another extension (e.g. tab manager)
+// regardless of whether the newtab url is made available
+// in web_accessible_resources.
+add_task(async function test_newtab_from_extension() {
+ let panel = getNewTabDoorhanger().closest("panel");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "newtaburl@mochi.test",
+ },
+ },
+ chrome_url_overrides: {
+ newtab: "newtab.html",
+ },
+ },
+ files: {
+ "newtab.html": `New tab! `,
+ "newtab.js": () => {
+ browser.test.sendMessage("newtab-loaded");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab(extensionNewTabUrl);
+ await popupShown;
+
+ // This will show a confirmation doorhanger, make sure we don't leave it open.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ BrowserTestUtils.removeTab(tab);
+
+ // extension to open the newtab
+ let opener = ExtensionTestUtils.loadExtension({
+ async background() {
+ let newtab = await browser.tabs.create({});
+ browser.test.assertTrue(
+ newtab.id !== browser.tabs.TAB_ID_NONE,
+ "New tab was created."
+ );
+ await browser.tabs.remove(newtab.id);
+ browser.test.sendMessage("complete");
+ },
+ });
+
+ function listener(msg) {
+ Assert.ok(!/may not load or link to moz-extension/.test(msg.message));
+ }
+ Services.console.registerListener(listener);
+ registerCleanupFunction(() => {
+ Services.console.unregisterListener(listener);
+ });
+
+ await opener.startup();
+ await opener.awaitMessage("complete");
+ await extension.awaitMessage("newtab-loaded");
+ await opener.unload();
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_user_events.js b/browser/components/extensions/test/browser/browser_ext_user_events.js
new file mode 100644
index 0000000000..4852ffd124
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_user_events.js
@@ -0,0 +1,271 @@
+"use strict";
+
+// Test that different types of events are all considered
+// "handling user input".
+add_task(async function testSources() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ async function request(perm) {
+ try {
+ let result = await browser.permissions.request({
+ permissions: [perm],
+ });
+ browser.test.sendMessage("request", { success: true, result, perm });
+ } catch (err) {
+ browser.test.sendMessage("request", {
+ success: false,
+ errmsg: err.message,
+ perm,
+ });
+ }
+ }
+
+ let tabs = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tabs[0].id);
+
+ browser.pageAction.onClicked.addListener(() => request("bookmarks"));
+ browser.browserAction.onClicked.addListener(() => request("tabs"));
+ browser.commands.onCommand.addListener(() => request("downloads"));
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "contextMenus.update") {
+ browser.contextMenus.onClicked.addListener(() =>
+ request("webNavigation")
+ );
+ browser.contextMenus.update(
+ "menu",
+ {
+ title: "test user events in onClicked",
+ onclick: null,
+ },
+ () => browser.test.sendMessage("contextMenus.update-done")
+ );
+ }
+ if (msg === "openOptionsPage") {
+ browser.runtime.openOptionsPage();
+ }
+ });
+
+ browser.contextMenus.create(
+ {
+ id: "menu",
+ title: "test user events in onclick",
+ contexts: ["page"],
+ onclick() {
+ request("cookies");
+ },
+ },
+ () => {
+ browser.test.sendMessage("actions-ready");
+ }
+ );
+ },
+
+ files: {
+ "options.html": `
+
+
+
+
+
+
+
+ Link
+
+ `,
+
+ "options.js"() {
+ addEventListener("load", async () => {
+ let link = document.getElementById("link");
+ link.onclick = async event => {
+ link.onclick = null;
+ event.preventDefault();
+
+ browser.test.log("Calling permission.request from options page.");
+
+ let perm = "history";
+ try {
+ let result = await browser.permissions.request({
+ permissions: [perm],
+ });
+ browser.test.sendMessage("request", {
+ success: true,
+ result,
+ perm,
+ });
+ } catch (err) {
+ browser.test.sendMessage("request", {
+ success: false,
+ errmsg: err.message,
+ perm,
+ });
+ }
+ };
+
+ // Make a few trips through the event loop to make sure the
+ // options browser is fully visible. This is a bit dodgy, but
+ // we don't really have a reliable way to detect this from the
+ // options page side, and synthetic click events won't work
+ // until it is.
+ do {
+ browser.test.log(
+ "Waiting for the options browser to be visible..."
+ );
+ await new Promise(resolve => setTimeout(resolve, 0));
+ synthesizeMouseAtCenter(link, {});
+ } while (link.onclick !== null);
+ });
+ },
+ },
+
+ manifest: {
+ browser_action: {
+ default_title: "test",
+ default_area: "navbar",
+ },
+ page_action: { default_title: "test" },
+ permissions: ["contextMenus"],
+ optional_permissions: [
+ "bookmarks",
+ "tabs",
+ "webNavigation",
+ "history",
+ "cookies",
+ "downloads",
+ ],
+ options_ui: { page: "options.html" },
+ content_security_policy:
+ "script-src 'self' https://example.com; object-src 'none';",
+ commands: {
+ command: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ async function testPermissionRequest(
+ { requestPermission, expectPrompt, perm },
+ what
+ ) {
+ info(`check request permission from '${what}'`);
+
+ let promptPromise = null;
+ if (expectPrompt) {
+ promptPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+ }
+
+ await requestPermission();
+ await promptPromise;
+
+ let result = await extension.awaitMessage("request");
+ ok(result.success, `request() did not throw when called from ${what}`);
+ is(result.result, true, `request() succeeded when called from ${what}`);
+ is(result.perm, perm, `requested permission ${what}`);
+ await promptPromise;
+ }
+
+ // Remove Sidebar button to prevent pushing extension button to overflow menu
+ CustomizableUI.removeWidgetFromArea("sidebar-button");
+
+ await extension.startup();
+ await extension.awaitMessage("actions-ready");
+
+ await testPermissionRequest(
+ {
+ requestPermission: () => clickPageAction(extension),
+ expectPrompt: true,
+ perm: "bookmarks",
+ },
+ "page action click"
+ );
+
+ await testPermissionRequest(
+ {
+ requestPermission: () => clickBrowserAction(extension),
+ expectPrompt: true,
+ perm: "tabs",
+ },
+ "browser action click"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gBrowser.selectedTab = tab;
+
+ await testPermissionRequest(
+ {
+ requestPermission: async () => {
+ let menu = await openContextMenu("body");
+ let items = menu.getElementsByAttribute(
+ "label",
+ "test user events in onclick"
+ );
+ is(items.length, 1, "Found context menu item");
+ menu.activateItem(items[0]);
+ },
+ expectPrompt: false, // cookies permission has no prompt.
+ perm: "cookies",
+ },
+ "context menu in onclick"
+ );
+
+ extension.sendMessage("contextMenus.update");
+ await extension.awaitMessage("contextMenus.update-done");
+
+ await testPermissionRequest(
+ {
+ requestPermission: async () => {
+ let menu = await openContextMenu("body");
+ let items = menu.getElementsByAttribute(
+ "label",
+ "test user events in onClicked"
+ );
+ is(items.length, 1, "Found context menu item again");
+ menu.activateItem(items[0]);
+ },
+ expectPrompt: true,
+ perm: "webNavigation",
+ },
+ "context menu in onClicked"
+ );
+
+ await testPermissionRequest(
+ {
+ requestPermission: () => {
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ },
+ expectPrompt: true,
+ perm: "downloads",
+ },
+ "commands shortcut"
+ );
+
+ await testPermissionRequest(
+ {
+ requestPermission: () => {
+ extension.sendMessage("openOptionsPage");
+ },
+ expectPrompt: true,
+ perm: "history",
+ },
+ "options page link click"
+ );
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+
+ registerCleanupFunction(() => CustomizableUI.reset());
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_containerIsolation.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_containerIsolation.js
new file mode 100644
index 0000000000..3bf849b518
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_containerIsolation.js
@@ -0,0 +1,169 @@
+"use strict";
+
+add_task(async function containerIsolation_restricted() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.userContextIsolation.enabled", true],
+ ["privacy.userContext.enabled", true],
+ ],
+ });
+
+ let helperExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies", "webNavigation"],
+ },
+
+ async background() {
+ browser.webNavigation.onCompleted.addListener(details => {
+ browser.test.sendMessage("tabCreated", details.tabId);
+ });
+ browser.test.onMessage.addListener(async message => {
+ switch (message.subject) {
+ case "createTab": {
+ await browser.tabs.create({
+ url: message.data.url,
+ cookieStoreId: message.data.cookieStoreId,
+ });
+ break;
+ }
+ }
+ });
+ },
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+
+ async background() {
+ let eventNames = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ ];
+
+ const initialEmptyTabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(
+ 1,
+ initialEmptyTabs.length,
+ `Got one initial empty tab as expected: ${JSON.stringify(
+ initialEmptyTabs
+ )}`
+ );
+
+ for (let eventName of eventNames) {
+ browser.webNavigation[eventName].addListener(details => {
+ if (details.tabId === initialEmptyTabs[0].id) {
+ // Ignore webNavigation related to the initial about:blank tab, it may be technically
+ // still being loading when we start this test extension to run the test scenario.
+ return;
+ }
+ browser.test.assertEq(
+ "http://www.example.com/?allowed",
+ details.url,
+ `expected ${eventName} event`
+ );
+ browser.test.sendMessage(eventName, details.tabId);
+ });
+ }
+
+ const [restrictedTab, unrestrictedTab, noContainerTab] =
+ await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => resolve(message));
+ });
+
+ await browser.test.assertRejects(
+ browser.webNavigation.getFrame({
+ tabId: restrictedTab,
+ frameId: 0,
+ }),
+ `Invalid tab ID: ${restrictedTab}`,
+ "getFrame rejected Promise should pass the expected error"
+ );
+
+ await browser.test.assertRejects(
+ browser.webNavigation.getAllFrames({ tabId: restrictedTab }),
+ `Invalid tab ID: ${restrictedTab}`,
+ "getAllFrames rejected Promise should pass the expected error"
+ );
+
+ await browser.tabs.remove(unrestrictedTab);
+ await browser.tabs.remove(noContainerTab);
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [`extensions.userContextIsolation.${extension.id}.restricted`, "[1]"],
+ ],
+ });
+
+ await helperExtension.startup();
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/?restricted",
+ cookieStoreId: "firefox-container-1",
+ },
+ });
+
+ const restrictedTab = await helperExtension.awaitMessage("tabCreated");
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/?allowed",
+ cookieStoreId: "firefox-container-2",
+ },
+ });
+
+ const unrestrictedTab = await helperExtension.awaitMessage("tabCreated");
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/?allowed",
+ },
+ });
+
+ const noContainerTab = await helperExtension.awaitMessage("tabCreated");
+
+ let eventNames = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ ];
+ for (let eventName of eventNames) {
+ let recTabId1 = await extension.awaitMessage(eventName);
+ let recTabId2 = await extension.awaitMessage(eventName);
+
+ Assert.equal(
+ recTabId1,
+ unrestrictedTab,
+ `Expected unrestricted tab with tabId: ${unrestrictedTab} from ${eventName} event`
+ );
+
+ Assert.equal(
+ recTabId2,
+ noContainerTab,
+ `Expected noContainer tab with tabId: ${noContainerTab} from ${eventName} event`
+ );
+ }
+
+ extension.sendMessage([restrictedTab, unrestrictedTab, noContainerTab]);
+
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+ await helperExtension.unload();
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js
new file mode 100644
index 0000000000..c5b2c72778
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js
@@ -0,0 +1,43 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function webNavigation_getFrameId_of_existing_main_frame() {
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const DUMMY_URL = BASE + "file_dummy.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ DUMMY_URL,
+ true
+ );
+
+ async function background(DUMMY_URL) {
+ let tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ let frames = await browser.webNavigation.getAllFrames({
+ tabId: tabs[0].id,
+ });
+ browser.test.assertEq(1, frames.length, "The dummy page has one frame");
+ browser.test.assertEq(0, frames[0].frameId, "Main frame's ID must be 0");
+ browser.test.assertEq(
+ DUMMY_URL,
+ frames[0].url,
+ "Main frame URL must match"
+ );
+ browser.test.notifyPass("frameId checked");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+
+ background: `(${background})(${JSON.stringify(DUMMY_URL)});`,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("frameId checked");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
new file mode 100644
index 0000000000..b60940bae7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
@@ -0,0 +1,323 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWebNavigationGetNonExistentTab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async function () {
+ // There is no "tabId = 0" because the id assigned by tabTracker (defined in ext-browser.js)
+ // starts from 1.
+ await browser.test.assertRejects(
+ browser.webNavigation.getAllFrames({ tabId: 0 }),
+ "Invalid tab ID: 0",
+ "getAllFrames rejected Promise should pass the expected error"
+ );
+
+ // There is no "tabId = 0" because the id assigned by tabTracker (defined in ext-browser.js)
+ // starts from 1, processId is currently marked as optional and it is ignored.
+ await browser.test.assertRejects(
+ browser.webNavigation.getFrame({
+ tabId: 0,
+ frameId: 15,
+ processId: 20,
+ }),
+ "Invalid tab ID: 0",
+ "getFrame rejected Promise should pass the expected error"
+ );
+
+ browser.test.sendMessage("getNonExistentTab.done");
+ },
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("getNonExistentTab.done");
+
+ await extension.unload();
+});
+
+add_task(async function testWebNavigationFrames() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async function () {
+ let tabId;
+ let collectedDetails = [];
+
+ browser.webNavigation.onCompleted.addListener(async details => {
+ collectedDetails.push(details);
+
+ if (details.frameId !== 0) {
+ // wait for the top level iframe to be complete
+ return;
+ }
+
+ let getAllFramesDetails = await browser.webNavigation.getAllFrames({
+ tabId,
+ });
+
+ let getFramePromises = getAllFramesDetails.map(({ frameId }) => {
+ // processId is currently marked as optional and it is ignored.
+ return browser.webNavigation.getFrame({
+ tabId,
+ frameId,
+ processId: 0,
+ });
+ });
+
+ let getFrameResults = await Promise.all(getFramePromises);
+ browser.test.sendMessage("webNavigationFrames.done", {
+ collectedDetails,
+ getAllFramesDetails,
+ getFrameResults,
+ });
+
+ // Pick a random frameId.
+ let nonExistentFrameId = Math.floor(Math.random() * 10000);
+
+ // Increment the picked random nonExistentFrameId until it doesn't exists.
+ while (
+ getAllFramesDetails.filter(
+ details => details.frameId == nonExistentFrameId
+ ).length
+ ) {
+ nonExistentFrameId += 1;
+ }
+
+ // Check that getFrame Promise is rejected with the expected error message on nonexistent frameId.
+ await browser.test.assertRejects(
+ browser.webNavigation.getFrame({
+ tabId,
+ frameId: nonExistentFrameId,
+ processId: 20,
+ }),
+ `No frame found with frameId: ${nonExistentFrameId}`,
+ "getFrame promise should be rejected with the expected error message on unexistent frameId"
+ );
+
+ await browser.tabs.remove(tabId);
+ browser.test.sendMessage("webNavigationFrames.done");
+ });
+
+ let tab = await browser.tabs.create({ url: "tab.html" });
+ tabId = tab.id;
+ },
+ manifest: {
+ permissions: ["webNavigation", "tabs"],
+ },
+ files: {
+ "tab.html": `
+
+
+
+
+
+
+
+
+
+
+ `,
+ "subframe.html": `
+
+
+
+
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ let { collectedDetails, getAllFramesDetails, getFrameResults } =
+ await extension.awaitMessage("webNavigationFrames.done");
+
+ is(getAllFramesDetails.length, 3, "expected number of frames found");
+ is(
+ getAllFramesDetails.length,
+ collectedDetails.length,
+ "number of frames found should equal the number onCompleted events collected"
+ );
+
+ is(
+ getAllFramesDetails[0].frameId,
+ 0,
+ "the root frame has the expected frameId"
+ );
+ is(
+ getAllFramesDetails[0].parentFrameId,
+ -1,
+ "the root frame has the expected parentFrameId"
+ );
+
+ // ordered by frameId
+ let sortByFrameId = (el1, el2) => {
+ let val1 = el1 ? el1.frameId : -1;
+ let val2 = el2 ? el2.frameId : -1;
+ return val1 - val2;
+ };
+
+ collectedDetails = collectedDetails.sort(sortByFrameId);
+ getAllFramesDetails = getAllFramesDetails.sort(sortByFrameId);
+ getFrameResults = getFrameResults.sort(sortByFrameId);
+
+ info("check frame details content");
+
+ is(
+ getFrameResults.length,
+ getAllFramesDetails.length,
+ "getFrame and getAllFrames should return the same number of results"
+ );
+
+ Assert.deepEqual(
+ getFrameResults,
+ getAllFramesDetails,
+ "getFrame and getAllFrames should return the same results"
+ );
+
+ info(`check frame details collected and retrieved with getAllFrames`);
+
+ for (let [i, collected] of collectedDetails.entries()) {
+ let getAllFramesDetail = getAllFramesDetails[i];
+
+ is(getAllFramesDetail.frameId, collected.frameId, "frameId");
+ is(
+ getAllFramesDetail.parentFrameId,
+ collected.parentFrameId,
+ "parentFrameId"
+ );
+ is(getAllFramesDetail.tabId, collected.tabId, "tabId");
+
+ // This can be uncommented once Bug 1246125 has been fixed
+ // is(getAllFramesDetail.url, collected.url, "url");
+ }
+
+ info("frame details content checked");
+
+ await extension.awaitMessage("webNavigationFrames.done");
+
+ await extension.unload();
+});
+
+add_task(async function testWebNavigationGetFrameOnDiscardedTab() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+ async background() {
+ let tabs = await browser.tabs.query({ currentWindow: true });
+ browser.test.assertEq(2, tabs.length, "Expect 2 tabs open");
+
+ const tabId = tabs[1].id;
+
+ await browser.tabs.discard(tabId);
+ let tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(true, tab.discarded, "Expect a discarded tab");
+
+ const allFrames = await browser.webNavigation.getAllFrames({ tabId });
+ browser.test.assertEq(
+ null,
+ allFrames,
+ "Expect null from calling getAllFrames on discarded tab"
+ );
+
+ tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(
+ true,
+ tab.discarded,
+ "Expect tab to stay discarded"
+ );
+
+ const topFrame = await browser.webNavigation.getFrame({
+ tabId,
+ frameId: 0,
+ });
+ browser.test.assertEq(
+ null,
+ topFrame,
+ "Expect null from calling getFrame on discarded tab"
+ );
+
+ tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(
+ true,
+ tab.discarded,
+ "Expect tab to stay discarded"
+ );
+
+ browser.test.sendMessage("get-frames-done");
+ },
+ });
+
+ const initialTab = gBrowser.selectedTab;
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/?toBeDiscarded=true"
+ );
+ // Switch back to the initial tab to allow the new tab
+ // to be discarded.
+ await BrowserTestUtils.switchTab(gBrowser, initialTab);
+
+ ok(!!tab.linkedPanel, "Tab not initially discarded");
+
+ await extension.startup();
+ await extension.awaitMessage("get-frames-done");
+
+ ok(!tab.linkedPanel, "Tab should be discarded");
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
+
+add_task(async function testWebNavigationCrossOriginFrames() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ async background() {
+ let url =
+ "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html";
+ let tab = await browser.tabs.create({ url });
+
+ await new Promise(resolve => {
+ browser.webNavigation.onCompleted.addListener(details => {
+ if (details.tabId === tab.id && details.frameId === 0) {
+ resolve();
+ }
+ });
+ });
+
+ let frames = await browser.webNavigation.getAllFrames({ tabId: tab.id });
+ browser.test.assertEq(frames[0].url, url, "Top is from mochi.test");
+
+ await browser.tabs.remove(tab.id);
+ browser.test.sendMessage("webNavigation.CrossOriginFrames", frames);
+ },
+ });
+
+ await extension.startup();
+
+ let frames = await extension.awaitMessage("webNavigation.CrossOriginFrames");
+ is(frames.length, 2, "getAllFrames() returns both frames.");
+
+ is(frames[0].frameId, 0, "Top frame has correct frameId.");
+ is(frames[0].parentFrameId, -1, "Top parentFrameId is correct.");
+
+ Assert.greater(
+ frames[1].frameId,
+ 0,
+ "Cross-origin iframe has non-zero frameId."
+ );
+ is(frames[1].parentFrameId, 0, "Iframe parentFrameId is correct.");
+ is(
+ frames[1].url,
+ "http://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_img.html",
+ "Irame is from example.org"
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js
new file mode 100644
index 0000000000..efe847c2b4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js
@@ -0,0 +1,194 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+SpecialPowers.pushPrefEnv({
+ set: [["security.allow_eval_with_system_principal", true]],
+});
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_on_created_navigation_target_from_mouse_click() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("Open link in a new tab using Ctrl-click");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-tab-from-mouse-click",
+ { ctrlKey: true, metaKey: true },
+ tab.linkedBrowser
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-mouse-click`,
+ },
+ });
+
+ info("Open link in a new window using Shift-click");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-window-from-mouse-click",
+ { shiftKey: true },
+ tab.linkedBrowser
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-window-from-mouse-click`,
+ },
+ });
+
+ info('Open link with target="_blank" in a new tab using click');
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-tab-from-targetblank-click",
+ {},
+ tab.linkedBrowser
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-targetblank-click`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_on_created_navigation_target_from_mouse_click_subframe() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("Open a subframe link in a new tab using Ctrl-click");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-tab-from-mouse-click-subframe",
+ { ctrlKey: true, metaKey: true },
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-mouse-click-subframe`,
+ },
+ });
+
+ info("Open a subframe link in a new window using Shift-click");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-window-from-mouse-click-subframe",
+ { shiftKey: true },
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-window-from-mouse-click-subframe`,
+ },
+ });
+
+ info('Open a subframe link with target="_blank" in a new tab using click');
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-tab-from-targetblank-click-subframe",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-targetblank-click-subframe`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_contextmenu.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_contextmenu.js
new file mode 100644
index 0000000000..8fd94af4f1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_contextmenu.js
@@ -0,0 +1,182 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+SpecialPowers.pushPrefEnv({
+ set: [["security.allow_eval_with_system_principal", true]],
+});
+
+async function clickContextMenuItem({
+ pageElementSelector,
+ contextMenuItemLabel,
+ frameIndex,
+}) {
+ let contentAreaContextMenu;
+ if (frameIndex == null) {
+ contentAreaContextMenu = await openContextMenu(pageElementSelector);
+ } else {
+ contentAreaContextMenu = await openContextMenuInFrame(
+ pageElementSelector,
+ frameIndex
+ );
+ }
+ const item = contentAreaContextMenu.getElementsByAttribute(
+ "label",
+ contextMenuItemLabel
+ );
+ is(item.length, 1, `found contextMenu item for "${contextMenuItemLabel}"`);
+ const closed = promiseContextMenuClosed(contentAreaContextMenu);
+ contentAreaContextMenu.activateItem(item[0]);
+ await closed;
+}
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_on_created_navigation_target_from_context_menu() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("Open link in a new tab from the context menu");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ async openNavTarget() {
+ await clickContextMenuItem({
+ pageElementSelector: "#test-create-new-tab-from-context-menu",
+ contextMenuItemLabel: "Open Link in New Tab",
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-context-menu`,
+ },
+ });
+
+ info("Open link in a new window from the context menu");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ async openNavTarget() {
+ await clickContextMenuItem({
+ pageElementSelector: "#test-create-new-window-from-context-menu",
+ contextMenuItemLabel: "Open Link in New Window",
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-window-from-context-menu`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_on_created_navigation_target_from_context_menu_subframe() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("Open a subframe link in a new tab from the context menu");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ async openNavTarget() {
+ await clickContextMenuItem({
+ pageElementSelector:
+ "#test-create-new-tab-from-context-menu-subframe",
+ contextMenuItemLabel: "Open Link in New Tab",
+ frameIndex: 0,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-context-menu-subframe`,
+ },
+ });
+
+ info("Open a subframe link in a new window from the context menu");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ async openNavTarget() {
+ await clickContextMenuItem({
+ pageElementSelector:
+ "#test-create-new-window-from-context-menu-subframe",
+ contextMenuItemLabel: "Open Link in New Window",
+ frameIndex: 0,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-window-from-context-menu-subframe`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_named_window.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_named_window.js
new file mode 100644
index 0000000000..3a0a950319
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_named_window.js
@@ -0,0 +1,100 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.onMessage.addListener(({ type, code }) => {
+ if (type === "execute-contentscript") {
+ browser.tabs.executeScript(sourceTabId, { code: code });
+ }
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_window_open_in_named_win() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("open a url in a new named window from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-named-window-open", "TestWinName"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-named-window-open`,
+ },
+ });
+
+ info("open a url in an existent named window from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#existent-named-window-open", "TestWinName"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#existent-named-window-open`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_subframe_window_open.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_subframe_window_open.js
new file mode 100644
index 0000000000..15439460a5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_subframe_window_open.js
@@ -0,0 +1,168 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.onMessage.addListener(({ type, code }) => {
+ if (type === "execute-contentscript") {
+ browser.tabs.executeScript(sourceTabId, { code: code });
+ }
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_window_open_from_subframe() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("open a url in a new tab from subframe window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `document.querySelector('iframe').contentWindow.open("${OPENED_PAGE}#new-tab-from-window-open-subframe"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-window-open-subframe`,
+ },
+ });
+
+ info("open a url in a new window from subframe window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `document.querySelector('iframe').contentWindow.open("${OPENED_PAGE}#new-win-from-window-open-subframe", "_blank", "toolbar=0"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-win-from-window-open-subframe`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
+
+add_task(async function test_window_open_close_from_browserAction_popup() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ function popup() {
+ window.open("", "_self").close();
+
+ browser.test.sendMessage("browserAction_popup_executed");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ files: {
+ "popup.html": `
+
+
+
+
+
+
+
+
+ `,
+ "popup.js": popup,
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ clickBrowserAction(extension);
+
+ await extension.awaitMessage("browserAction_popup_executed");
+
+ info("open a url in a new tab from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-tab-from-window-open"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-window-open`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js
new file mode 100644
index 0000000000..8a1c5ee82d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js
@@ -0,0 +1,168 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.onMessage.addListener(({ type, code }) => {
+ if (type === "execute-contentscript") {
+ browser.tabs.executeScript(sourceTabId, { code: code });
+ }
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_window_open() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("open a url in a new tab from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-tab-from-window-open"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-window-open`,
+ },
+ });
+
+ info("open a url in a new window from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-win-from-window-open", "_blank", "toolbar=0"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-win-from-window-open`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
+
+add_task(async function test_window_open_close_from_browserAction_popup() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ function popup() {
+ window.open("", "_self").close();
+
+ browser.test.sendMessage("browserAction_popup_executed");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ files: {
+ "popup.html": `
+
+
+
+
+
+
+
+
+ `,
+ "popup.js": popup,
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ clickBrowserAction(extension);
+
+ await extension.awaitMessage("browserAction_popup_executed");
+
+ info("open a url in a new tab from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-tab-from-window-open"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-window-open`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
new file mode 100644
index 0000000000..07c6e70a93
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
@@ -0,0 +1,314 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+SearchTestUtils.init(this);
+
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+function promiseAutocompleteResultPopup(value) {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value,
+ });
+}
+
+async function addBookmark(bookmark) {
+ if (bookmark.keyword) {
+ await PlacesUtils.keywords.insert({
+ keyword: bookmark.keyword,
+ url: bookmark.url,
+ });
+ }
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: bookmark.url,
+ title: bookmark.title,
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+}
+
+async function prepareSearchEngine() {
+ let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+
+ registerCleanupFunction(async function () {
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+
+ // Make sure the popup is closed for the next test.
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Clicking suggestions causes visits to search results pages, so clear that
+ // history now.
+ await PlacesUtils.history.clear();
+ });
+}
+
+add_task(async function test_webnavigation_urlbar_typed_transitions() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ "http://example.com/?q=typed",
+ msg.url,
+ "Got the expected url"
+ );
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "typed",
+ msg.transitionType,
+ "Got the expected transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.typed");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+
+ gURLBar.focus();
+ gURLBar.value = "";
+ const inputValue = "http://example.com/?q=typed";
+ await EventUtils.sendString(inputValue);
+ await EventUtils.synthesizeKey("VK_RETURN", { altKey: true });
+
+ await extension.awaitFinish("webNavigation.from_address_bar.typed");
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_webnavigation_urlbar_typed_closed_popup_transitions() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ "http://example.com/?q=typedClosed",
+ msg.url,
+ "Got the expected url"
+ );
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "typed",
+ msg.transitionType,
+ "Got the expected transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.typed");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+ await promiseAutocompleteResultPopup("http://example.com/?q=typedClosed");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ // Closing the popup forces a different code route that handles no results
+ // being displayed.
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ await extension.awaitFinish("webNavigation.from_address_bar.typed");
+
+ await extension.unload();
+ }
+);
+
+add_task(async function test_webnavigation_urlbar_bookmark_transitions() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ "http://example.com/?q=bookmark",
+ msg.url,
+ "Got the expected url"
+ );
+
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "auto_bookmark",
+ msg.transitionType,
+ "Got the expected transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.auto_bookmark");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await addBookmark({
+ title: "Bookmark To Click",
+ url: "http://example.com/?q=bookmark",
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+
+ await promiseAutocompleteResultPopup("Bookmark To Click");
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+ await extension.awaitFinish("webNavigation.from_address_bar.auto_bookmark");
+
+ await extension.unload();
+});
+
+add_task(async function test_webnavigation_urlbar_keyword_transition() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ `http://example.com/?q=search`,
+ msg.url,
+ "Got the expected url"
+ );
+
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "keyword",
+ msg.transitionType,
+ "Got the expected transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.keyword");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await addBookmark({
+ title: "Test Keyword",
+ url: "http://example.com/?q=%s",
+ keyword: "testkw",
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+
+ await promiseAutocompleteResultPopup("testkw search");
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+
+ await extension.awaitFinish("webNavigation.from_address_bar.keyword");
+
+ await extension.unload();
+});
+
+add_task(async function test_webnavigation_urlbar_search_transitions() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ "http://mochi.test:8888/",
+ msg.url,
+ "Got the expected url"
+ );
+
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "generated",
+ msg.transitionType,
+ "Got the expected 'generated' transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.generated");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+
+ await prepareSearchEngine();
+ await promiseAutocompleteResultPopup("foo");
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+
+ await extension.awaitFinish("webNavigation.from_address_bar.generated");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webRequest.js b/browser/components/extensions/test/browser/browser_ext_webRequest.js
new file mode 100644
index 0000000000..c2ff1d6c64
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webRequest.js
@@ -0,0 +1,142 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* import-globals-from ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js */
+loadTestSubscript("head_webrequest.js");
+
+const { HiddenFrame } = ChromeUtils.importESModule(
+ "resource://gre/modules/HiddenFrame.sys.mjs"
+);
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+SimpleTest.requestCompleteLog();
+
+function createHiddenBrowser(url) {
+ let frame = new HiddenFrame();
+ return new Promise(resolve =>
+ frame.get().then(subframe => {
+ let doc = subframe.document;
+ let browser = doc.createElementNS(XUL_NS, "browser");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("remote", "true");
+ browser.setAttribute("disableglobalhistory", "true");
+ browser.setAttribute("src", url);
+
+ doc.documentElement.appendChild(browser);
+ resolve({ frame: frame, browser: browser });
+ })
+ );
+}
+
+let extension;
+let dummy =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_dummy.html";
+let headers = {
+ request: {
+ add: {
+ "X-WebRequest-request": "text",
+ "X-WebRequest-request-binary": "binary",
+ },
+ modify: {
+ "user-agent": "WebRequest",
+ },
+ remove: ["accept-encoding"],
+ },
+ response: {
+ add: {
+ "X-WebRequest-response": "text",
+ "X-WebRequest-response-binary": "binary",
+ },
+ modify: {
+ server: "WebRequest",
+ "content-type": "text/html; charset=utf-8",
+ },
+ remove: ["connection"],
+ },
+};
+
+let urls = ["http://mochi.test/browser/*"];
+let events = {
+ onBeforeRequest: [{ urls }, ["blocking"]],
+ onBeforeSendHeaders: [{ urls }, ["blocking", "requestHeaders"]],
+ onSendHeaders: [{ urls }, ["requestHeaders"]],
+ onHeadersReceived: [{ urls }, ["blocking", "responseHeaders"]],
+ onCompleted: [{ urls }, ["responseHeaders"]],
+};
+
+add_setup(async function () {
+ extension = makeExtension(events);
+ await extension.startup();
+});
+
+add_task(async function test_newWindow() {
+ let expect = {
+ "file_dummy.html": {
+ type: "main_frame",
+ headers,
+ },
+ };
+ // NOTE: When running solo, favicon will be loaded at some point during
+ // the tests in this file, so all tests ignore it. When running with
+ // other tests in this directory, favicon gets loaded at some point before
+ // we run, and we never see the request, thus it cannot be handled as part
+ // of expect above.
+ extension.sendMessage("set-expected", { expect, ignore: ["favicon.ico"] });
+ await extension.awaitMessage("continue");
+
+ let openedWindow = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(
+ openedWindow.gBrowser,
+ `${dummy}?newWindow=${Math.random()}`
+ );
+
+ await extension.awaitMessage("done");
+ await BrowserTestUtils.closeWindow(openedWindow);
+});
+
+add_task(async function test_newTab() {
+ // again, in this window
+ let expect = {
+ "file_dummy.html": {
+ type: "main_frame",
+ headers,
+ },
+ };
+ extension.sendMessage("set-expected", { expect, ignore: ["favicon.ico"] });
+ await extension.awaitMessage("continue");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ `${dummy}?newTab=${Math.random()}`
+ );
+
+ await extension.awaitMessage("done");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_subframe() {
+ let expect = {
+ "file_dummy.html": {
+ type: "main_frame",
+ headers,
+ },
+ };
+ // test a content subframe attached to hidden window
+ extension.sendMessage("set-expected", { expect, ignore: ["favicon.ico"] });
+ info("*** waiting to continue");
+ await extension.awaitMessage("continue");
+ info("*** creating hidden browser");
+ let frameInfo = await createHiddenBrowser(
+ `${dummy}?subframe=${Math.random()}`
+ );
+ info("*** waiting for finish");
+ await extension.awaitMessage("done");
+ info("*** destroying hidden browser");
+ // cleanup
+ frameInfo.browser.remove();
+ frameInfo.frame.destroy();
+});
+
+add_task(async function teardown() {
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webRequest_error_after_stopped_or_closed.js b/browser/components/extensions/test/browser/browser_ext_webRequest_error_after_stopped_or_closed.js
new file mode 100644
index 0000000000..75d85dd3bf
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webRequest_error_after_stopped_or_closed.js
@@ -0,0 +1,110 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const SLOW_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://www.example.com"
+ ) + "file_slowed_document.sjs";
+
+async function runTest(stopLoadFunc) {
+ async function background() {
+ let urls = ["https://www.example.com/*"];
+ browser.webRequest.onCompleted.addListener(
+ details => {
+ browser.test.sendMessage("done", {
+ msg: "onCompleted",
+ requestId: details.requestId,
+ });
+ },
+ { urls }
+ );
+
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ browser.test.sendMessage("onBeforeRequest", {
+ requestId: details.requestId,
+ });
+ },
+ { urls },
+ ["blocking"]
+ );
+
+ browser.webRequest.onErrorOccurred.addListener(
+ details => {
+ browser.test.sendMessage("done", {
+ msg: "onErrorOccurred",
+ requestId: details.requestId,
+ });
+ },
+ { urls }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "https://www.example.com/*",
+ ],
+ },
+ background,
+ });
+ await extension.startup();
+
+ // Open a SLOW_PAGE and don't wait for it to load
+ let slowTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SLOW_PAGE,
+ false
+ );
+
+ stopLoadFunc(slowTab);
+
+ // Retrieve the requestId from onBeforeRequest
+ let requestIdOnBeforeRequest = await extension.awaitMessage(
+ "onBeforeRequest"
+ );
+
+ // Now verify that we got the correct event and request id
+ let doneMessage = await extension.awaitMessage("done");
+
+ // We shouldn't get the onCompleted message here
+ is(doneMessage.msg, "onErrorOccurred", "received onErrorOccurred message");
+ is(
+ requestIdOnBeforeRequest.requestId,
+ doneMessage.requestId,
+ "request Ids match"
+ );
+
+ BrowserTestUtils.removeTab(slowTab);
+ await extension.unload();
+}
+
+/**
+ * Check that after we cancel a slow page load, we get an error associated with
+ * our request.
+ */
+add_task(async function test_click_stop_button() {
+ await runTest(async slowTab => {
+ // Stop the load
+ let stopButton = document.getElementById("stop-button");
+ await TestUtils.waitForCondition(() => {
+ return !stopButton.disabled;
+ });
+ stopButton.click();
+ });
+});
+
+/**
+ * Check that after we close the tab corresponding to a slow page load,
+ * that we get an error associated with our request.
+ */
+add_task(async function test_remove_tab() {
+ await runTest(slowTab => {
+ // Remove the tab
+ BrowserTestUtils.removeTab(slowTab);
+ });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webrtc.js b/browser/components/extensions/test/browser/browser_ext_webrtc.js
new file mode 100644
index 0000000000..520cb9cd69
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webrtc.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.navigator.permission.fake", true]],
+ });
+});
+
+add_task(async function test_background_request() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {},
+ async background() {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg.type != "testGUM") {
+ browser.test.fail("unknown message");
+ }
+
+ await browser.test.assertRejects(
+ navigator.mediaDevices.getUserMedia({ audio: true }),
+ /The request is not allowed/,
+ "Calling gUM in background pages throws an error"
+ );
+ browser.test.notifyPass("done");
+ });
+ },
+ });
+
+ await extension.startup();
+
+ let policy = WebExtensionPolicy.getByID(extension.id);
+ let principal = policy.extension.principal;
+ // Add a permission for the extension to make sure that we throw even
+ // if permission was given.
+ PermissionTestUtils.add(principal, "microphone", Services.perms.ALLOW_ACTION);
+
+ let finished = extension.awaitFinish("done");
+ extension.sendMessage({ type: "testGUM" });
+ await finished;
+
+ PermissionTestUtils.remove(principal, "microphone");
+ await extension.unload();
+});
+
+let scriptPage = url =>
+ ` ${url}`;
+
+add_task(async function test_popup_request() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": function () {
+ browser.test
+ .assertRejects(
+ navigator.mediaDevices.getUserMedia({ audio: true }),
+ /The request is not allowed/,
+ "Calling gUM in popup pages without permission throws an error"
+ )
+ .then(function () {
+ browser.test.notifyPass("done");
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ clickBrowserAction(extension);
+ await extension.awaitFinish("done");
+ await extension.unload();
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ // Use the same url for background page and browserAction popup,
+ // to double-check that the page url is not being used to decide
+ // if webRTC requests should be allowed or not.
+ background: { page: "page.html" },
+ browser_action: {
+ default_popup: "page.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "page.html": scriptPage("page.js"),
+ "page.js": async function () {
+ const isBackgroundPage =
+ window == (await browser.runtime.getBackgroundPage());
+
+ if (isBackgroundPage) {
+ await browser.test.assertRejects(
+ navigator.mediaDevices.getUserMedia({ audio: true }),
+ /The request is not allowed/,
+ "Calling gUM in background pages throws an error"
+ );
+ } else {
+ try {
+ await navigator.mediaDevices.getUserMedia({ audio: true });
+ browser.test.notifyPass("done");
+ } catch (err) {
+ browser.test.fail(`Failed with error ${err.message}`);
+ browser.test.notifyFail("done");
+ }
+ }
+ },
+ },
+ });
+
+ // Add a permission for the extension to make sure that we throw even
+ // if permission was given.
+ await extension.startup();
+
+ let policy = WebExtensionPolicy.getByID(extension.id);
+ let principal = policy.extension.principal;
+
+ PermissionTestUtils.add(principal, "microphone", Services.perms.ALLOW_ACTION);
+ clickBrowserAction(extension);
+
+ await extension.awaitFinish("done");
+ PermissionTestUtils.remove(principal, "microphone");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows.js b/browser/components/extensions/test/browser/browser_ext_windows.js
new file mode 100644
index 0000000000..8d6cff25b4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows.js
@@ -0,0 +1,348 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Since we apply title localization asynchronously,
+// we'll use this helper to wait for the title to match
+// the condition and then test against it.
+async function verifyTitle(win, test, desc) {
+ await TestUtils.waitForCondition(test);
+ ok(true, desc);
+}
+
+add_task(async function testWindowGetAll() {
+ let raisedWin = Services.ww.openWindow(
+ null,
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,dialog=no,all,alwaysRaised",
+ null
+ );
+
+ await TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == raisedWin
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async function () {
+ let wins = await browser.windows.getAll();
+ browser.test.assertEq(2, wins.length, "Expect two windows");
+
+ browser.test.assertEq(
+ false,
+ wins[0].alwaysOnTop,
+ "Expect first window not to be always on top"
+ );
+ browser.test.assertEq(
+ true,
+ wins[1].alwaysOnTop,
+ "Expect first window to be always on top"
+ );
+
+ let win = await browser.windows.create({
+ url: "http://example.com",
+ type: "popup",
+ });
+
+ wins = await browser.windows.getAll();
+ browser.test.assertEq(3, wins.length, "Expect three windows");
+
+ wins = await browser.windows.getAll({ windowTypes: ["popup"] });
+ browser.test.assertEq(1, wins.length, "Expect one window");
+ browser.test.assertEq("popup", wins[0].type, "Expect type to be popup");
+
+ wins = await browser.windows.getAll({ windowTypes: ["normal"] });
+ browser.test.assertEq(2, wins.length, "Expect two windows");
+ browser.test.assertEq("normal", wins[0].type, "Expect type to be normal");
+ browser.test.assertEq("normal", wins[1].type, "Expect type to be normal");
+
+ wins = await browser.windows.getAll({ windowTypes: ["popup", "normal"] });
+ browser.test.assertEq(3, wins.length, "Expect three windows");
+
+ await browser.windows.remove(win.id);
+
+ browser.test.notifyPass("getAll");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("getAll");
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(raisedWin);
+});
+
+add_task(async function testWindowTitle() {
+ const PREFACE1 = "My prefix1 - ";
+ const PREFACE2 = "My prefix2 - ";
+ const START_URL =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+ const START_TITLE = "Dummy test page";
+ const NEW_URL =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_title.html";
+ const NEW_TITLE = "Different title test page";
+
+ async function background() {
+ browser.test.onMessage.addListener(
+ async (msg, options, windowId, expected) => {
+ if (msg === "create") {
+ let win = await browser.windows.create(options);
+ browser.test.sendMessage("created", win);
+ }
+ if (msg === "update") {
+ let win = await browser.windows.get(windowId);
+ browser.test.assertTrue(
+ win.title.startsWith(expected.before.preface),
+ "Window has the expected title preface before update."
+ );
+ browser.test.assertTrue(
+ win.title.includes(expected.before.text),
+ "Window has the expected title text before update."
+ );
+ win = await browser.windows.update(windowId, options);
+ browser.test.assertTrue(
+ win.title.startsWith(expected.after.preface),
+ "Window has the expected title preface after update."
+ );
+ browser.test.assertTrue(
+ win.title.includes(expected.after.text),
+ "Window has the expected title text after update."
+ );
+ browser.test.sendMessage("updated", win);
+ }
+ }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["tabs"],
+ },
+ });
+
+ await extension.startup();
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ async function createApiWin(options) {
+ let promiseLoaded = BrowserTestUtils.waitForNewWindow({ url: START_URL });
+ extension.sendMessage("create", options);
+ let apiWin = await extension.awaitMessage("created");
+ let realWin = windowTracker.getWindow(apiWin.id);
+ await promiseLoaded;
+ let expectedPreface = options.titlePreface ? options.titlePreface : "";
+ await verifyTitle(
+ realWin,
+ () => {
+ return (
+ realWin.document.title.startsWith(expectedPreface || START_TITLE) &&
+ realWin.document.title.includes(START_TITLE)
+ );
+ },
+ "Created window starts with the expected preface and includes the right title text."
+ );
+ return apiWin;
+ }
+
+ async function updateWindow(options, apiWin, expected) {
+ extension.sendMessage("update", options, apiWin.id, expected);
+ await extension.awaitMessage("updated");
+ let realWin = windowTracker.getWindow(apiWin.id);
+ await verifyTitle(
+ realWin,
+ () => {
+ return (
+ realWin.document.title.startsWith(
+ expected.after.preface || expected.after.text
+ ) && realWin.document.title.includes(expected.after.text)
+ );
+ },
+ "Updated window starts with the expected preface and includes the right title text."
+ );
+ await BrowserTestUtils.closeWindow(realWin);
+ }
+
+ // Create a window without a preface.
+ let apiWin = await createApiWin({ url: START_URL });
+
+ // Add a titlePreface to the window.
+ let expected = {
+ before: {
+ preface: "",
+ text: START_TITLE,
+ },
+ after: {
+ preface: PREFACE1,
+ text: START_TITLE,
+ },
+ };
+ await updateWindow({ titlePreface: PREFACE1 }, apiWin, expected);
+
+ // Create a window with a preface.
+ apiWin = await createApiWin({ url: START_URL, titlePreface: PREFACE1 });
+
+ // Navigate to a different url and check that title is reflected.
+ let realWin = windowTracker.getWindow(apiWin.id);
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ realWin.gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.startLoadingURIString(
+ realWin.gBrowser.selectedBrowser,
+ NEW_URL
+ );
+ await promiseLoaded;
+ await verifyTitle(
+ realWin,
+ () => {
+ return (
+ realWin.document.title.startsWith(PREFACE1) &&
+ realWin.document.title.includes(NEW_TITLE)
+ );
+ },
+ "Updated window starts with the expected preface and includes the expected title."
+ );
+
+ // Update the titlePreface of the window.
+ expected = {
+ before: {
+ preface: PREFACE1,
+ text: NEW_TITLE,
+ },
+ after: {
+ preface: PREFACE2,
+ text: NEW_TITLE,
+ },
+ };
+ await updateWindow({ titlePreface: PREFACE2 }, apiWin, expected);
+
+ // Create a window with a preface.
+ apiWin = await createApiWin({ url: START_URL, titlePreface: PREFACE1 });
+ realWin = windowTracker.getWindow(apiWin.id);
+
+ // Update the titlePreface of the window with an empty string.
+ expected = {
+ before: {
+ preface: PREFACE1,
+ text: START_TITLE,
+ },
+ after: {
+ preface: "",
+ text: START_TITLE,
+ },
+ };
+ await verifyTitle(
+ realWin,
+ () => realWin.document.title.startsWith(expected.before.preface),
+ "Updated window has the expected title preface."
+ );
+ await updateWindow({ titlePreface: "" }, apiWin, expected);
+ await verifyTitle(
+ realWin,
+ () => !realWin.document.title.startsWith(expected.before.preface),
+ "Updated window doesn't not contain the preface after update."
+ );
+
+ // Create a window with a preface.
+ apiWin = await createApiWin({ url: START_URL, titlePreface: PREFACE1 });
+ realWin = windowTracker.getWindow(apiWin.id);
+
+ // Update the window without a titlePreface.
+ expected = {
+ before: {
+ preface: PREFACE1,
+ text: START_TITLE,
+ },
+ after: {
+ preface: PREFACE1,
+ text: START_TITLE,
+ },
+ };
+ await updateWindow({}, apiWin, expected);
+
+ await extension.unload();
+});
+
+// Test that the window title is only available with the correct tab
+// permissions.
+add_task(async function testWindowTitlePermissions() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ function awaitMessage(name) {
+ return new Promise(resolve => {
+ browser.test.onMessage.addListener(function listener(...msg) {
+ if (msg[0] === name) {
+ browser.test.onMessage.removeListener(listener);
+ resolve(msg[1]);
+ }
+ });
+ });
+ }
+
+ let window = await browser.windows.getCurrent();
+
+ browser.test.assertEq(
+ undefined,
+ window.title,
+ "Window title should be null without tab permission"
+ );
+
+ browser.test.sendMessage("grant-activeTab");
+ let expectedTitle = await awaitMessage("title");
+
+ window = await browser.windows.getCurrent();
+ browser.test.assertEq(
+ expectedTitle,
+ window.title,
+ "Window should have the expected title with tab permission granted"
+ );
+
+ await browser.test.notifyPass("window-title-permissions");
+ },
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("grant-activeTab");
+ await clickBrowserAction(extension);
+ extension.sendMessage("title", document.title);
+
+ await extension.awaitFinish("window-title-permissions");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testInvalidWindowId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.test.assertRejects(
+ // Assuming that this windowId does not exist.
+ browser.windows.get(123456789),
+ /Invalid window/,
+ "Should receive invalid window"
+ );
+ browser.test.notifyPass("windows.get.invalid");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("windows.get.invalid");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js b/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js
new file mode 100644
index 0000000000..c89bcfce77
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js
@@ -0,0 +1,69 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Tests allowScriptsToClose option
+add_task(async function test_allowScriptsToClose() {
+ const files = {
+ "dummy.html": " ",
+ "close.js": function () {
+ window.close();
+ if (!window.closed) {
+ browser.test.sendMessage("close-failed");
+ }
+ },
+ };
+
+ function background() {
+ browser.test.onMessage.addListener((msg, options) => {
+ function listener(_, { status }, { url }) {
+ if (status == "complete" && url == options.url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ browser.tabs.executeScript({ file: "close.js" });
+ }
+ }
+ options.url = browser.runtime.getURL(options.url);
+ browser.windows.create(options);
+ if (msg === "create+execute") {
+ browser.tabs.onUpdated.addListener(listener);
+ }
+ });
+ browser.test.notifyPass();
+ }
+
+ const example = "http://example.com/";
+ const manifest = { permissions: ["tabs", example] };
+
+ const extension = ExtensionTestUtils.loadExtension({
+ files,
+ background,
+ manifest,
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.allow_scripts_to_close_windows", false]],
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+
+ extension.sendMessage("create", { url: "dummy.html" });
+ let win = await BrowserTestUtils.waitForNewWindow();
+ await BrowserTestUtils.windowClosed(win);
+ info("script allowed to close the window");
+
+ extension.sendMessage("create+execute", { url: example });
+ win = await BrowserTestUtils.waitForNewWindow();
+ await BrowserTestUtils.windowClosed(win);
+ info("script allowed to close the window");
+
+ extension.sendMessage("create+execute", {
+ url: example,
+ allowScriptsToClose: true,
+ });
+ win = await BrowserTestUtils.waitForNewWindow();
+ await BrowserTestUtils.windowClosed(win);
+ info("script allowed to close the window");
+
+ await SpecialPowers.popPrefEnv();
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create.js b/browser/components/extensions/test/browser/browser_ext_windows_create.js
new file mode 100644
index 0000000000..6c41abcd3e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create.js
@@ -0,0 +1,205 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWindowCreate() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let _checkWindowPromise;
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "checked-window") {
+ _checkWindowPromise.resolve();
+ _checkWindowPromise = null;
+ }
+ });
+
+ let os;
+
+ function checkWindow(expected) {
+ return new Promise(resolve => {
+ _checkWindowPromise = { resolve };
+ browser.test.sendMessage("check-window", expected);
+ });
+ }
+
+ async function createWindow(params, expected, keep = false) {
+ let window = await browser.windows.create(...params);
+ // params is null when testing create without createData
+ params = params[0] || {};
+
+ // Prevent frequent intermittent failures on macos where the newly created window
+ // may have not always got into the fullscreen state before browser.window.create
+ // resolves the windows details.
+ if (
+ os === "mac" &&
+ params.state === "fullscreen" &&
+ window.state !== params.state
+ ) {
+ browser.test.log(
+ "Wait for window.state for the newly create window to be set to fullscreen"
+ );
+ while (window.state !== params.state) {
+ window = await browser.windows.get(window.id, { populate: true });
+ }
+ browser.test.log(
+ "Newly created browser window got into fullscreen state"
+ );
+ }
+
+ for (let key of Object.keys(params)) {
+ if (key == "state" && os == "mac" && params.state == "normal") {
+ // OS-X doesn't have a hard distinction between "normal" and
+ // "maximized" states.
+ browser.test.assertTrue(
+ window.state == "normal" || window.state == "maximized",
+ `Expected window.state (currently ${window.state}) to be "normal" but will accept "maximized"`
+ );
+ } else {
+ browser.test.assertEq(
+ params[key],
+ window[key],
+ `Got expected value for window.${key}`
+ );
+ }
+ }
+
+ browser.test.assertEq(
+ 1,
+ window.tabs.length,
+ "tabs property got populated"
+ );
+
+ await checkWindow(expected);
+ if (keep) {
+ return window;
+ }
+
+ if (params.state == "fullscreen" && os == "win") {
+ // FIXME: Closing a fullscreen window causes a window leak in
+ // Windows tests.
+ await browser.windows.update(window.id, { state: "normal" });
+ }
+ await browser.windows.remove(window.id);
+ }
+
+ try {
+ ({ os } = await browser.runtime.getPlatformInfo());
+
+ // Set the current window to state: "normal" because the test is failing on Windows
+ // where the current window is maximized.
+ let currentWindow = await browser.windows.getCurrent();
+ await browser.windows.update(currentWindow.id, { state: "normal" });
+
+ await createWindow([], { state: "STATE_NORMAL" });
+ await createWindow([{ state: "maximized" }], {
+ state: "STATE_MAXIMIZED",
+ });
+ await createWindow([{ state: "minimized" }], {
+ state: "STATE_MINIMIZED",
+ });
+ await createWindow([{ state: "normal" }], {
+ state: "STATE_NORMAL",
+ hiddenChrome: [],
+ });
+ await createWindow([{ state: "fullscreen" }], {
+ state: "STATE_FULLSCREEN",
+ });
+
+ let window = await createWindow(
+ [{ type: "popup" }],
+ {
+ hiddenChrome: [
+ "menubar",
+ "toolbar",
+ "location",
+ "directories",
+ "status",
+ "extrachrome",
+ ],
+ chromeFlags: ["CHROME_OPENAS_DIALOG"],
+ },
+ true
+ );
+
+ let tabs = await browser.tabs.query({
+ windowType: "popup",
+ active: true,
+ });
+
+ browser.test.assertEq(1, tabs.length, "Expected only one popup");
+ browser.test.assertEq(
+ window.id,
+ tabs[0].windowId,
+ "Expected new window to be returned in query"
+ );
+
+ await browser.windows.remove(window.id);
+
+ browser.test.notifyPass("window-create");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create");
+ }
+ },
+ });
+
+ let latestWindow;
+ let windowListener = (window, topic) => {
+ if (topic == "domwindowopened") {
+ latestWindow = window;
+ }
+ };
+ Services.ww.registerNotification(windowListener);
+
+ extension.onMessage("check-window", expected => {
+ if (expected.state != null) {
+ let { windowState } = latestWindow;
+ if (latestWindow.fullScreen) {
+ windowState = latestWindow.STATE_FULLSCREEN;
+ }
+
+ if (expected.state == "STATE_NORMAL") {
+ ok(
+ windowState == window.STATE_NORMAL ||
+ windowState == window.STATE_MAXIMIZED,
+ `Expected windowState (currently ${windowState}) to be STATE_NORMAL but will accept STATE_MAXIMIZED`
+ );
+ } else {
+ is(
+ windowState,
+ window[expected.state],
+ `Expected window state to be ${expected.state}`
+ );
+ }
+ }
+ if (expected.hiddenChrome) {
+ let chromeHidden =
+ latestWindow.document.documentElement.getAttribute("chromehidden");
+ is(
+ chromeHidden.trim().split(/\s+/).sort().join(" "),
+ expected.hiddenChrome.sort().join(" "),
+ "Got expected hidden chrome"
+ );
+ }
+ if (expected.chromeFlags) {
+ let { chromeFlags } = latestWindow.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow);
+ for (let flag of expected.chromeFlags) {
+ ok(
+ chromeFlags & Ci.nsIWebBrowserChrome[flag],
+ `Expected window to have the ${flag} flag`
+ );
+ }
+ }
+
+ extension.sendMessage("checked-window");
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-create");
+ await extension.unload();
+
+ Services.ww.unregisterNotification(windowListener);
+ latestWindow = null;
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js b/browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js
new file mode 100644
index 0000000000..5eda338fe5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js
@@ -0,0 +1,345 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function no_cookies_permission() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-container-1" }),
+ /No permission for cookieStoreId/,
+ "cookieStoreId requires cookies permission"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function invalid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "not-firefox-container-1" }),
+ /Illegal cookieStoreId/,
+ "cookieStoreId must be valid"
+ );
+
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-private" }),
+ /Illegal to set private cookieStoreId in a non-private window/,
+ "cookieStoreId cannot be private in a non-private window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-default",
+ incognito: true,
+ }),
+ /Illegal to set non-private cookieStoreId in a private window/,
+ "cookieStoreId cannot be non-private in an private window"
+ );
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-container-1",
+ incognito: true,
+ }),
+ /Illegal to set non-private cookieStoreId in a private window/,
+ "cookieStoreId cannot be a container tab ID in a private window"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function perma_private_browsing_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are unavailable in permanent private browsing mode/,
+ "cookieStoreId cannot be a container tab ID in perma-private browsing mode"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function userContext_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are currently disabled/,
+ "cookieStoreId cannot be a container tab ID when contextual identities are disabled"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function valid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ const testCases = [
+ {
+ description: "no explicit URL",
+ createParams: {
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1"],
+ expectedExecuteScriptResult: [
+ // Default URL is about:home, and extensions cannot run scripts in it.
+ "Missing host permission for the tab",
+ ],
+ },
+ {
+ description: "one URL",
+ createParams: {
+ url: "about:blank",
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1"],
+ expectedExecuteScriptResult: ["about:blank - null"],
+ },
+ {
+ description: "one URL in an array",
+ createParams: {
+ url: ["about:blank"],
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1"],
+ expectedExecuteScriptResult: ["about:blank - null"],
+ },
+ {
+ description: "two URLs in an array",
+ createParams: {
+ url: ["about:blank", "about:blank"],
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1", "firefox-container-1"],
+ expectedExecuteScriptResult: ["about:blank - null", "about:blank - null"],
+ },
+ ];
+
+ async function background(testCases) {
+ let readyTabs = new Map();
+ let tabReadyCheckers = new Set();
+ browser.webNavigation.onCompleted.addListener(({ url, tabId, frameId }) => {
+ if (frameId === 0) {
+ readyTabs.set(tabId, url);
+ browser.test.log(`Detected navigation in tab ${tabId} to ${url}.`);
+
+ for (let check of tabReadyCheckers) {
+ check(tabId, url);
+ }
+ }
+ });
+ async function awaitTabReady(tabId, expectedUrl) {
+ if (readyTabs.get(tabId) === expectedUrl) {
+ browser.test.log(`Tab ${tabId} was ready with URL ${expectedUrl}.`);
+ return;
+ }
+ await new Promise(resolve => {
+ browser.test.log(
+ `Waiting for tab ${tabId} to load URL ${expectedUrl}...`
+ );
+ tabReadyCheckers.add(function check(completedTabId, completedUrl) {
+ if (completedTabId === tabId && completedUrl === expectedUrl) {
+ tabReadyCheckers.delete(check);
+ resolve();
+ }
+ });
+ });
+ browser.test.log(`Tab ${tabId} is ready with URL ${expectedUrl}.`);
+ }
+
+ async function executeScriptAndGetResult(tabId) {
+ try {
+ return (
+ await browser.tabs.executeScript(tabId, {
+ matchAboutBlank: true,
+ code: "`${document.URL} - ${origin}`",
+ })
+ )[0];
+ } catch (e) {
+ return e.message;
+ }
+ }
+ for (let {
+ description,
+ createParams,
+ expectedCookieStoreIds,
+ expectedExecuteScriptResult,
+ } of testCases) {
+ let win = await browser.windows.create(createParams);
+
+ browser.test.assertEq(
+ expectedCookieStoreIds.length,
+ win.tabs.length,
+ "Expected number of tabs"
+ );
+
+ for (let [i, expectedCookieStoreId] of Object.entries(
+ expectedCookieStoreIds
+ )) {
+ browser.test.assertEq(
+ expectedCookieStoreId,
+ win.tabs[i].cookieStoreId,
+ `expected cookieStoreId for tab ${i} (${description})`
+ );
+ }
+
+ for (let [i, expectedResult] of Object.entries(
+ expectedExecuteScriptResult
+ )) {
+ // Wait until the the tab can process the tabs.executeScript calls.
+ // TODO: Remove this when bug 1418655 and bug 1397667 are fixed.
+ let expectedUrl = Array.isArray(createParams.url)
+ ? createParams.url[i]
+ : createParams.url || "about:home";
+ await awaitTabReady(win.tabs[i].id, expectedUrl);
+
+ let result = await executeScriptAndGetResult(win.tabs[i].id);
+ browser.test.assertEq(
+ expectedResult,
+ result,
+ `expected executeScript result for tab ${i} (${description})`
+ );
+ }
+
+ await browser.windows.remove(win.id);
+ }
+ browser.test.sendMessage("done");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ host_permissions: ["*://*/*"], // allows script in top-level about:blank.
+ permissions: ["cookies", "webNavigation"],
+ },
+ background: `(${background})(${JSON.stringify(testCases)})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function cookieStoreId_and_tabId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["cookies"],
+ },
+ async background() {
+ for (let cookieStoreId of ["firefox-default", "firefox-container-1"]) {
+ let { id: normalTabId } = await browser.tabs.create({ cookieStoreId });
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-private",
+ tabId: normalTabId,
+ }),
+ /`cookieStoreId` must match the tab's cookieStoreId/,
+ "Cannot use cookieStoreId for pre-existing tabs with a different cookieStoreId"
+ );
+
+ let win = await browser.windows.create({
+ cookieStoreId,
+ tabId: normalTabId,
+ });
+ browser.test.assertEq(
+ cookieStoreId,
+ win.tabs[0].cookieStoreId,
+ "Adopted tab"
+ );
+ await browser.windows.remove(win.id);
+ }
+
+ {
+ let privateWindow = await browser.windows.create({ incognito: true });
+ let privateTabId = privateWindow.tabs[0].id;
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-default",
+ tabId: privateTabId,
+ }),
+ /`cookieStoreId` must match the tab's cookieStoreId/,
+ "Cannot use cookieStoreId for pre-existing tab in a private window"
+ );
+ let win = await browser.windows.create({
+ cookieStoreId: "firefox-private",
+ tabId: privateTabId,
+ });
+ browser.test.assertEq(
+ "firefox-private",
+ win.tabs[0].cookieStoreId,
+ "Adopted private tab"
+ );
+ await browser.windows.remove(win.id);
+
+ await browser.test.assertRejects(
+ browser.windows.remove(privateWindow.id),
+ /Invalid window ID:/,
+ "The original private window should have been closed when its only tab was adopted."
+ );
+ }
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_params.js b/browser/components/extensions/test/browser/browser_ext_windows_create_params.js
new file mode 100644
index 0000000000..6d80085433
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_params.js
@@ -0,0 +1,249 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+// Tests that incompatible parameters can't be used together.
+add_task(async function testWindowCreateParams() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ try {
+ for (let state of ["minimized", "maximized", "fullscreen"]) {
+ for (let param of ["left", "top", "width", "height"]) {
+ let expected = `"state": "${state}" may not be combined with "left", "top", "width", or "height"`;
+
+ await browser.test.assertRejects(
+ browser.windows.create({ state, [param]: 100 }),
+ RegExp(expected),
+ `Got expected error from create(${param}=100)`
+ );
+ }
+ }
+
+ browser.test.notifyPass("window-create-params");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create-params");
+ }
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-create-params");
+ await extension.unload();
+});
+
+// We do not support the focused param, however we do not want
+// to fail despite an error when it is passed. This provides
+// better code level compatibility with chrome.
+add_task(async function testWindowCreateFocused() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ async function doWaitForWindow(createOpts, resolve) {
+ let created;
+ browser.windows.onFocusChanged.addListener(async function listener(
+ wid
+ ) {
+ if (wid == browser.windows.WINDOW_ID_NONE) {
+ return;
+ }
+ let win = await created;
+ if (win.id !== wid) {
+ return;
+ }
+ browser.windows.onFocusChanged.removeListener(listener);
+ // update the window object
+ let window = await browser.windows.get(wid);
+ resolve(window);
+ });
+ created = browser.windows.create(createOpts);
+ }
+ async function awaitNewFocusedWindow(createOpts) {
+ return new Promise(resolve => {
+ // eslint doesn't like an async promise function, so
+ // we need to wrap it like this.
+ doWaitForWindow(createOpts, resolve);
+ });
+ }
+ try {
+ let window = await awaitNewFocusedWindow({});
+ browser.test.assertEq(
+ window.focused,
+ true,
+ "window is focused without focused param"
+ );
+ browser.test.log("removeWindow");
+ await browser.windows.remove(window.id);
+ window = await awaitNewFocusedWindow({ focused: true });
+ browser.test.assertEq(
+ window.focused,
+ true,
+ "window is focused with focused: true"
+ );
+ browser.test.log("removeWindow");
+ await browser.windows.remove(window.id);
+ window = await awaitNewFocusedWindow({ focused: false });
+ browser.test.assertEq(
+ window.focused,
+ true,
+ "window is focused with focused: false"
+ );
+ browser.test.log("removeWindow");
+ await browser.windows.remove(window.id);
+ browser.test.notifyPass("window-create-params");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create-params");
+ }
+ },
+ });
+
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ await extension.startup();
+ await extension.awaitFinish("window-create-params");
+ await extension.unload();
+ });
+
+ AddonTestUtils.checkMessages(
+ messages,
+ {
+ expected: [
+ {
+ message:
+ /Warning processing focused: Opening inactive windows is not supported/,
+ },
+ ],
+ },
+ "Expected warning processing focused"
+ );
+
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+});
+
+add_task(async function testPopupTypeWithDimension() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.windows.create({
+ type: "popup",
+ left: 123,
+ top: 123,
+ width: 151,
+ height: 152,
+ });
+ await browser.windows.create({
+ type: "popup",
+ left: 123,
+ width: 152,
+ height: 153,
+ });
+ await browser.windows.create({
+ type: "popup",
+ top: 123,
+ width: 153,
+ height: 154,
+ });
+ await browser.windows.create({
+ type: "popup",
+ left: screen.availWidth * 100,
+ top: screen.availHeight * 100,
+ width: 154,
+ height: 155,
+ });
+ await browser.windows.create({
+ type: "popup",
+ left: -screen.availWidth * 100,
+ top: -screen.availHeight * 100,
+ width: 155,
+ height: 156,
+ });
+ browser.test.sendMessage("windows-created");
+ },
+ });
+
+ const baseWindow = await BrowserTestUtils.openNewBrowserWindow();
+ baseWindow.resizeTo(150, 150);
+ baseWindow.moveTo(50, 50);
+
+ let windows = [];
+ let windowListener = (window, topic) => {
+ if (topic == "domwindowopened") {
+ windows.push(window);
+ }
+ };
+ Services.ww.registerNotification(windowListener);
+
+ await extension.startup();
+ await extension.awaitMessage("windows-created");
+ await extension.unload();
+
+ const regularScreen = getScreenAt(0, 0, 150, 150);
+ const roundedX = roundCssPixcel(123, regularScreen);
+ const roundedY = roundCssPixcel(123, regularScreen);
+
+ const availRectLarge = getCssAvailRect(
+ getScreenAt(screen.width * 100, screen.height * 100, 150, 150)
+ );
+ const maxRight = availRectLarge.right;
+ const maxBottom = availRectLarge.bottom;
+
+ const availRectSmall = getCssAvailRect(
+ getScreenAt(-screen.width * 100, -screen.height * 100, 150, 150150)
+ );
+ const minLeft = availRectSmall.left;
+ const minTop = availRectSmall.top;
+
+ const actualCoordinates = windows
+ .slice(0, 3)
+ .map(window => `${window.screenX},${window.screenY}`);
+ const offsetFromBase = 10;
+ const expectedCoordinates = [
+ `${roundedX},${roundedY}`,
+ // Missing top should be +10 from the last browser window.
+ `${roundedX},${baseWindow.screenY + offsetFromBase}`,
+ // Missing left should be +10 from the last browser window.
+ `${baseWindow.screenX + offsetFromBase},${roundedY}`,
+ ];
+ is(
+ actualCoordinates.join(" / "),
+ expectedCoordinates.join(" / "),
+ "expected popup type windows are opened at given coordinates"
+ );
+
+ const actualSizes = windows
+ .slice(0, 3)
+ .map(window => `${window.outerWidth}x${window.outerHeight}`);
+ const expectedSizes = [`151x152`, `152x153`, `153x154`];
+ is(
+ actualSizes.join(" / "),
+ expectedSizes.join(" / "),
+ "expected popup type windows are opened with given size"
+ );
+
+ const actualRect = {
+ top: windows[4].screenY,
+ bottom: windows[3].screenY + windows[3].outerHeight,
+ left: windows[4].screenX,
+ right: windows[3].screenX + windows[3].outerWidth,
+ };
+ const maxRect = {
+ top: minTop,
+ bottom: maxBottom,
+ left: minLeft,
+ right: maxRight,
+ };
+ isRectContained(actualRect, maxRect);
+
+ for (const window of windows) {
+ window.close();
+ }
+
+ Services.ww.unregisterNotification(windowListener);
+ windows = null;
+ await BrowserTestUtils.closeWindow(baseWindow);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js b/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js
new file mode 100644
index 0000000000..83fda199b7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js
@@ -0,0 +1,387 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function assertNoLeaksInTabTracker() {
+ // Check that no tabs have been leaked by the internal tabTracker helper class.
+ const { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+ );
+ const { tabTracker } = ExtensionParent.apiManager.global;
+
+ for (const [tabId, nativeTab] of tabTracker._tabIds) {
+ if (!nativeTab.ownerGlobal) {
+ ok(
+ false,
+ `A tab with tabId ${tabId} has been leaked in the tabTracker ("${nativeTab.title}")`
+ );
+ }
+ }
+}
+
+add_task(async function testWindowCreate() {
+ async function background() {
+ let promiseTabAttached = () => {
+ return new Promise(resolve => {
+ browser.tabs.onAttached.addListener(function listener() {
+ browser.tabs.onAttached.removeListener(listener);
+ resolve();
+ });
+ });
+ };
+
+ let promiseTabUpdated = expected => {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId,
+ changeInfo,
+ tab
+ ) {
+ if (changeInfo.url === expected) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ };
+
+ try {
+ let window = await browser.windows.getCurrent();
+ let windowId = window.id;
+
+ browser.test.log("Create additional tab in window 1");
+ let tab = await browser.tabs.create({ windowId, url: "about:blank" });
+ let tabId = tab.id;
+
+ browser.test.log("Create a new window, adopting the new tab");
+
+ // Note that we want to check against actual boolean values for
+ // all of the `incognito` property tests.
+ browser.test.assertEq(false, tab.incognito, "Tab is not private");
+
+ {
+ let [, window] = await Promise.all([
+ promiseTabAttached(),
+ browser.windows.create({ tabId: tabId }),
+ ]);
+ browser.test.assertEq(
+ false,
+ window.incognito,
+ "New window is not private"
+ );
+ browser.test.assertEq(
+ tabId,
+ window.tabs[0].id,
+ "tabs property populated correctly"
+ );
+
+ browser.test.log("Close the new window");
+ await browser.windows.remove(window.id);
+ }
+
+ {
+ browser.test.log("Create a new private window");
+ let privateWindow = await browser.windows.create({ incognito: true });
+ browser.test.assertEq(
+ true,
+ privateWindow.incognito,
+ "Private window is private"
+ );
+
+ browser.test.log("Create additional tab in private window");
+ let privateTab = await browser.tabs.create({
+ windowId: privateWindow.id,
+ });
+ browser.test.assertEq(
+ true,
+ privateTab.incognito,
+ "Private tab is private"
+ );
+
+ browser.test.log("Create a new window, adopting the new private tab");
+ let [, newWindow] = await Promise.all([
+ promiseTabAttached(),
+ browser.windows.create({ tabId: privateTab.id }),
+ ]);
+ browser.test.assertEq(
+ true,
+ newWindow.incognito,
+ "New private window is private"
+ );
+
+ browser.test.log("Close the new private window");
+ await browser.windows.remove(newWindow.id);
+
+ browser.test.log("Close the private window");
+ await browser.windows.remove(privateWindow.id);
+ }
+
+ browser.test.log("Try to create a window with both a tab and a URL");
+ [tab] = await browser.tabs.query({ windowId, active: true });
+ await browser.test.assertRejects(
+ browser.windows.create({ tabId: tab.id, url: "http://example.com/" }),
+ /`tabId` may not be used in conjunction with `url`/,
+ "Create call failed as expected"
+ );
+
+ browser.test.log(
+ "Try to create a window with both a tab and an invalid incognito setting"
+ );
+ await browser.test.assertRejects(
+ browser.windows.create({ tabId: tab.id, incognito: true }),
+ /`incognito` property must match the incognito state of tab/,
+ "Create call failed as expected"
+ );
+
+ browser.test.log("Try to create a window with an invalid tabId");
+ await browser.test.assertRejects(
+ browser.windows.create({ tabId: 0 }),
+ /Invalid tab ID: 0/,
+ "Create call failed as expected"
+ );
+
+ browser.test.log("Try to create a window with two URLs");
+ let readyPromise = Promise.all([
+ // tabs.onUpdated can be invoked between the call of windows.create and
+ // the invocation of its callback/promise, so set up the listeners
+ // before creating the window.
+ promiseTabUpdated("http://example.com/"),
+ promiseTabUpdated("http://example.org/"),
+ ]);
+
+ window = await browser.windows.create({
+ url: ["http://example.com/", "http://example.org/"],
+ });
+ await readyPromise;
+
+ browser.test.assertEq(
+ 2,
+ window.tabs.length,
+ "2 tabs were opened in new window"
+ );
+ browser.test.assertEq(
+ "about:blank",
+ window.tabs[0].url,
+ "about:blank, page not loaded yet"
+ );
+ browser.test.assertEq(
+ "about:blank",
+ window.tabs[1].url,
+ "about:blank, page not loaded yet"
+ );
+
+ window = await browser.windows.get(window.id, { populate: true });
+
+ browser.test.assertEq(
+ 2,
+ window.tabs.length,
+ "2 tabs were opened in new window"
+ );
+ browser.test.assertEq(
+ "http://example.com/",
+ window.tabs[0].url,
+ "Correct URL was loaded in tab 1"
+ );
+ browser.test.assertEq(
+ "http://example.org/",
+ window.tabs[1].url,
+ "Correct URL was loaded in tab 2"
+ );
+
+ await browser.windows.remove(window.id);
+
+ browser.test.notifyPass("window-create");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-create");
+ await extension.unload();
+
+ assertNoLeaksInTabTracker();
+});
+
+add_task(async function testWebNavigationOnWindowCreateTabId() {
+ async function background() {
+ const webNavEvents = [];
+ const onceTabsAttached = [];
+
+ let promiseTabAttached = tab => {
+ return new Promise(resolve => {
+ browser.tabs.onAttached.addListener(function listener(tabId) {
+ if (tabId !== tab.id) {
+ return;
+ }
+ browser.tabs.onAttached.removeListener(listener);
+ resolve();
+ });
+ });
+ };
+
+ // Listen to webNavigation.onCompleted events to ensure that
+ // it is not going to be fired when we move the existent tabs
+ // to new windows.
+ browser.webNavigation.onCompleted.addListener(data => {
+ webNavEvents.push(data);
+ });
+
+ // Wait for the list of urls needed to select the test tabs,
+ // and then move these tabs to a new window and assert that
+ // no webNavigation.onCompleted events should be received
+ // while the tabs are being adopted into the new windows.
+ browser.test.onMessage.addListener(async (msg, testTabURLs) => {
+ if (msg !== "testTabURLs") {
+ return;
+ }
+
+ // Retrieve the tabs list and filter out the tabs that should
+ // not be moved into a new window.
+ let allTabs = await browser.tabs.query({});
+ let testTabs = allTabs.filter(tab => {
+ return testTabURLs.includes(tab.url);
+ });
+
+ browser.test.assertEq(
+ 2,
+ testTabs.length,
+ "Got the expected number of test tabs"
+ );
+
+ for (let tab of testTabs) {
+ onceTabsAttached.push(promiseTabAttached(tab));
+ await browser.windows.create({ tabId: tab.id });
+ }
+
+ // Wait the tabs to have been attached to the new window and then assert that no
+ // webNavigation.onCompleted event has been received.
+ browser.test.log("Waiting tabs move to new window to be attached");
+ await Promise.all(onceTabsAttached);
+
+ browser.test.assertEq(
+ "[]",
+ JSON.stringify(webNavEvents),
+ "No webNavigation.onCompleted event should have been received"
+ );
+
+ // Remove all the test tabs before exiting the test successfully.
+ for (let tab of testTabs) {
+ await browser.tabs.remove(tab.id);
+ }
+
+ browser.test.notifyPass("webNavigation-on-window-create-tabId");
+ });
+ }
+
+ const testURLs = ["http://example.com/", "http://example.org/"];
+
+ for (let url of testURLs) {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.sendMessage("testTabURLs", testURLs);
+
+ await extension.awaitFinish("webNavigation-on-window-create-tabId");
+ await extension.unload();
+
+ assertNoLeaksInTabTracker();
+});
+
+add_task(async function testGetLastFocusedDoesNotLeakDuringTabAdoption() {
+ async function background() {
+ const allTabs = await browser.tabs.query({});
+
+ browser.test.onMessage.addListener(async (msg, testTabURL) => {
+ if (msg !== "testTabURL") {
+ return;
+ }
+
+ let tab = allTabs.filter(tab => tab.url === testTabURL).pop();
+
+ // Keep calling getLastFocused while browser.windows.create is creating
+ // a new window to adopt the test tab, so that the test recreates
+ // conditions similar to the extension that has been triggered this leak
+ // (See Bug 1458918 for a rationale).
+ // The while loop is explicited exited right before the notifyPass
+ // (but unloading the extension will stop it in any case).
+ let stopGetLastFocusedLoop = false;
+ Promise.resolve().then(async () => {
+ while (!stopGetLastFocusedLoop) {
+ browser.windows.getLastFocused({ populate: true });
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 50));
+ }
+ });
+
+ // Create a new window which adopt an existent tab and wait the tab to
+ // be fully attached to the new window.
+ await Promise.all([
+ new Promise(resolve => {
+ const listener = () => {
+ browser.tabs.onAttached.removeListener(listener);
+ resolve();
+ };
+ browser.tabs.onAttached.addListener(listener);
+ }),
+ browser.windows.create({ tabId: tab.id }),
+ ]);
+
+ // Check that getLastFocused populate the tabs property once the tab adoption
+ // has been completed.
+ const lastFocusedPopulate = await browser.windows.getLastFocused({
+ populate: true,
+ });
+ browser.test.assertEq(
+ 1,
+ lastFocusedPopulate.tabs.length,
+ "Got the expected number of tabs from windows.getLastFocused"
+ );
+
+ // Remove the test tab.
+ await browser.tabs.remove(tab.id);
+
+ stopGetLastFocusedLoop = true;
+
+ browser.test.notifyPass("tab-adopted");
+ });
+ }
+
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ extension.sendMessage("testTabURL", "http://example.com/");
+
+ await extension.awaitFinish("tab-adopted");
+
+ await extension.unload();
+
+ assertNoLeaksInTabTracker();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_url.js b/browser/components/extensions/test/browser/browser_ext_windows_create_url.js
new file mode 100644
index 0000000000..7c847382c5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_url.js
@@ -0,0 +1,253 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWindowCreate() {
+ let pageExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "page@mochitest" } },
+ protocol_handlers: [
+ {
+ protocol: "ext+foo",
+ name: "a foo protocol handler",
+ uriTemplate: "page.html?val=%s",
+ },
+ ],
+ },
+ files: {
+ "page.html": `
+
+ `,
+ },
+ });
+ await pageExt.startup();
+
+ async function background(OTHER_PAGE) {
+ browser.test.log(`== using ${OTHER_PAGE}`);
+ const EXTENSION_URL = browser.runtime.getURL("test.html");
+ const EXT_PROTO = "ext+bar:foo";
+ const OTHER_PROTO = "ext+foo:bar";
+
+ let windows = new (class extends Map {
+ // eslint-disable-line new-parens
+ get(id) {
+ if (!this.has(id)) {
+ let window = {
+ tabs: new Map(),
+ };
+ window.promise = new Promise(resolve => {
+ window.resolvePromise = resolve;
+ });
+
+ this.set(id, window);
+ }
+
+ return super.get(id);
+ }
+ })();
+
+ browser.tabs.onUpdated.addListener((tabId, changed, tab) => {
+ if (changed.status == "complete" && tab.url !== "about:blank") {
+ let window = windows.get(tab.windowId);
+ window.tabs.set(tab.index, tab);
+
+ if (window.tabs.size === window.expectedTabs) {
+ browser.test.log("resolving a window load");
+ window.resolvePromise(window);
+ }
+ }
+ });
+
+ async function create(options) {
+ browser.test.log(`creating window for ${options.url}`);
+ // Note: may reject
+ let window = await browser.windows.create(options);
+ let win = windows.get(window.id);
+ win.id = window.id;
+
+ win.expectedTabs = Array.isArray(options.url) ? options.url.length : 1;
+
+ return win.promise;
+ }
+
+ let TEST_SETS = [
+ {
+ name: "Single protocol URL in this extension",
+ url: EXT_PROTO,
+ expect: [`${EXTENSION_URL}?val=ext%2Bbar%3Afoo`],
+ },
+ {
+ name: "Single, relative URL",
+ url: "test.html",
+ expect: [EXTENSION_URL],
+ },
+ {
+ name: "Single, absolute, extension URL",
+ url: EXTENSION_URL,
+ expect: [EXTENSION_URL],
+ },
+ {
+ // This is primarily for backwards-compatibility, to allow extensions
+ // to open other home pages. This test case opens the home page
+ // explicitly; the implicit case (windows.create({}) without URL) is at
+ // browser_ext_chrome_settings_overrides_home.js.
+ name: "Single, absolute, other extension URL",
+ url: OTHER_PAGE,
+ expect: [OTHER_PAGE],
+ },
+ {
+ // This is oddly inconsistent with the non-array case, but here we are
+ // intentionally stricter because of lesser backwards-compatibility
+ // concerns.
+ name: "Array, absolute, other extension URL",
+ url: [OTHER_PAGE],
+ expectError: `Illegal URL: ${OTHER_PAGE}`,
+ },
+ {
+ name: "Single protocol URL in other extension",
+ url: OTHER_PROTO,
+ expect: [`${OTHER_PAGE}?val=ext%2Bfoo%3Abar`],
+ },
+ {
+ name: "Single, about:blank",
+ // Added "?" after "about:blank" because the test's tab load detection
+ // ignores about:blank.
+ url: "about:blank?",
+ expect: ["about:blank?"],
+ },
+ {
+ name: "multiple urls",
+ url: [EXT_PROTO, "test.html", EXTENSION_URL, OTHER_PROTO],
+ expect: [
+ `${EXTENSION_URL}?val=ext%2Bbar%3Afoo`,
+ EXTENSION_URL,
+ EXTENSION_URL,
+ `${OTHER_PAGE}?val=ext%2Bfoo%3Abar`,
+ ],
+ },
+ {
+ name: "Reject array of own allowed URLs and other moz-extension:-URL",
+ url: [EXTENSION_URL, EXT_PROTO, "about:blank?#", OTHER_PAGE],
+ expectError: `Illegal URL: ${OTHER_PAGE}`,
+ },
+ {
+ name: "Single, about:robots",
+ url: "about:robots",
+ expectError: "Illegal URL: about:robots",
+ },
+ {
+ name: "Array containing about:robots",
+ url: ["about:robots"],
+ expectError: "Illegal URL: about:robots",
+ },
+ ];
+ async function checkCreateResult({ status, value, reason }, testCase) {
+ const window = status === "fulfilled" ? value : null;
+ try {
+ if (testCase.expectError) {
+ let error = reason?.message;
+ browser.test.assertEq(testCase.expectError, error, testCase.name);
+ } else {
+ let tabUrls = [];
+ for (let [tabIndex, tab] of window.tabs) {
+ tabUrls[tabIndex] = tab.url;
+ }
+ browser.test.assertDeepEq(testCase.expect, tabUrls, testCase.name);
+ }
+ } catch (e) {
+ browser.test.fail(`Unexpected failure in ${testCase.name} :: ${e}`);
+ } finally {
+ // Close opened windows, whether they were expected or not.
+ if (window) {
+ await browser.windows.remove(window.id);
+ }
+ }
+ }
+ try {
+ let promises = [];
+ for (let testSet of TEST_SETS) {
+ try {
+ let testPromise = create({ url: testSet.url });
+ promises.push(testPromise);
+ // Bug 1869385 - we need to await each window opening sequentially.
+ // The events we check for depend on paint finishing,
+ // which won't happen if the window is occluded before it finishes
+ // loading.
+ await testPromise;
+ } catch (e) {
+ // Some of these calls are expected to fail,
+ // which we verify when calling checkCreateResult below.
+ }
+ }
+ const results = await Promise.allSettled(promises);
+ await Promise.all(
+ TEST_SETS.map((t, i) => checkCreateResult(results[i], t))
+ );
+ browser.test.notifyPass("window-create-url");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create-url");
+ }
+ }
+
+ // Watch for any permission prompts to show up and accept them.
+ let dialogCount = 0;
+ let windowObserver = window => {
+ // This listener will go away when the window is closed so there is no need
+ // to explicitely remove it.
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener("dialogopen", event => {
+ dialogCount++;
+ let { dialog } = event.detail;
+ Assert.equal(
+ dialog?._openedURL,
+ "chrome://mozapps/content/handling/permissionDialog.xhtml",
+ "Should only ever see the permission dialog"
+ );
+ let dialogEl = dialog._frame.contentDocument.querySelector("dialog");
+ Assert.ok(dialogEl, "Dialog element should exist");
+ dialogEl.setAttribute("buttondisabledaccept", false);
+ dialogEl.acceptDialog();
+ });
+ };
+ Services.obs.addObserver(windowObserver, "browser-delayed-startup-finished");
+ registerCleanupFunction(() => {
+ Services.obs.removeObserver(
+ windowObserver,
+ "browser-delayed-startup-finished"
+ );
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ protocol_handlers: [
+ {
+ protocol: "ext+bar",
+ name: "a bar protocol handler",
+ uriTemplate: "test.html?val=%s",
+ },
+ ],
+ },
+
+ background: `(${background})("moz-extension://${pageExt.uuid}/page.html")`,
+
+ files: {
+ "test.html": ` `,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-create-url");
+ await extension.unload();
+ await pageExt.unload();
+
+ Assert.equal(
+ dialogCount,
+ 2,
+ "Expected to see the right number of permission prompts."
+ );
+
+ // Make sure windows have been released before finishing.
+ Cu.forceGC();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_events.js b/browser/components/extensions/test/browser/browser_ext_windows_events.js
new file mode 100644
index 0000000000..aa8a2655ce
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_events.js
@@ -0,0 +1,222 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+SimpleTest.requestCompleteLog();
+
+add_task(async function test_windows_events_not_allowed() {
+ let monitor = await startIncognitoMonitorExtension();
+
+ function background() {
+ browser.windows.onCreated.addListener(window => {
+ browser.test.log(`onCreated: windowId=${window.id}`);
+
+ browser.test.assertTrue(
+ Number.isInteger(window.id),
+ "Window object's id is an integer"
+ );
+ browser.test.assertEq(
+ "normal",
+ window.type,
+ "Window object returned with the correct type"
+ );
+ browser.test.sendMessage("window-created", window.id);
+ });
+
+ let lastWindowId;
+ browser.windows.onFocusChanged.addListener(async eventWindowId => {
+ browser.test.log(
+ `onFocusChange: windowId=${eventWindowId} lastWindowId=${lastWindowId}`
+ );
+
+ browser.test.assertTrue(
+ lastWindowId !== eventWindowId,
+ "onFocusChanged fired once for the given window"
+ );
+ lastWindowId = eventWindowId;
+
+ browser.test.assertTrue(
+ Number.isInteger(eventWindowId),
+ "windowId is an integer"
+ );
+ let window = await browser.windows.getLastFocused();
+ browser.test.sendMessage("window-focus-changed", {
+ winId: eventWindowId,
+ lastFocusedWindowId: window.id,
+ });
+ });
+
+ browser.windows.onRemoved.addListener(windowId => {
+ browser.test.log(`onRemoved: windowId=${windowId}`);
+
+ browser.test.assertTrue(
+ Number.isInteger(windowId),
+ "windowId is an integer"
+ );
+ browser.test.sendMessage("window-removed", windowId);
+ browser.test.notifyPass("windows.events");
+ });
+
+ browser.test.sendMessage("ready", browser.windows.WINDOW_ID_NONE);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {},
+ background,
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+ const WINDOW_ID_NONE = await extension.awaitMessage("ready");
+
+ async function awaitFocusChanged() {
+ let windowInfo = await extension.awaitMessage("window-focus-changed");
+ if (windowInfo.winId === WINDOW_ID_NONE) {
+ info("Ignoring a superfluous WINDOW_ID_NONE (blur) event.");
+ windowInfo = await extension.awaitMessage("window-focus-changed");
+ }
+ is(
+ windowInfo.winId,
+ windowInfo.lastFocusedWindowId,
+ "Last focused window has the correct id"
+ );
+ return windowInfo.winId;
+ }
+
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let currentWindow = window;
+ let currentWindowId = windowTracker.getId(currentWindow);
+ info(`Current window ID: ${currentWindowId}`);
+
+ info("Create browser window 1");
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win1Id = await extension.awaitMessage("window-created");
+ info(`Window 1 ID: ${win1Id}`);
+
+ // This shouldn't be necessary, but tests intermittently fail, so let's give
+ // it a try.
+ win1.focus();
+
+ let winId = await awaitFocusChanged();
+ is(winId, win1Id, "Got focus change event for the correct window ID.");
+
+ info("Create browser window 2");
+ let win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ let win2Id = await extension.awaitMessage("window-created");
+ info(`Window 2 ID: ${win2Id}`);
+
+ win2.focus();
+
+ winId = await awaitFocusChanged();
+ is(winId, win2Id, "Got focus change event for the correct window ID.");
+
+ info("Focus browser window 1");
+ await focusWindow(win1);
+
+ winId = await awaitFocusChanged();
+ is(winId, win1Id, "Got focus change event for the correct window ID.");
+
+ info("Close browser window 2");
+ await BrowserTestUtils.closeWindow(win2);
+
+ winId = await extension.awaitMessage("window-removed");
+ is(winId, win2Id, "Got removed event for the correct window ID.");
+
+ info("Close browser window 1");
+ await BrowserTestUtils.closeWindow(win1);
+
+ currentWindow.focus();
+
+ winId = await extension.awaitMessage("window-removed");
+ is(winId, win1Id, "Got removed event for the correct window ID.");
+
+ winId = await awaitFocusChanged();
+ is(
+ winId,
+ currentWindowId,
+ "Got focus change event for the correct window ID."
+ );
+
+ await extension.awaitFinish("windows.events");
+ await extension.unload();
+ await monitor.unload();
+});
+
+add_task(async function test_windows_event_page() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@windows" } },
+ background: { persistent: false },
+ },
+ background() {
+ let removed;
+ browser.windows.onCreated.addListener(window => {
+ browser.test.sendMessage("onCreated", window.id);
+ });
+ browser.windows.onRemoved.addListener(wid => {
+ removed = wid;
+ browser.test.sendMessage("onRemoved", wid);
+ });
+ browser.windows.onFocusChanged.addListener(wid => {
+ if (wid != browser.windows.WINDOW_ID_NONE && wid != removed) {
+ browser.test.sendMessage("onFocusChanged", wid);
+ }
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = ["onCreated", "onRemoved", "onFocusChanged"];
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "windows", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "windows", event, {
+ primed: true,
+ });
+ }
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await extension.awaitMessage("ready");
+ let windowId = await extension.awaitMessage("onCreated");
+ ok(true, "persistent event woke background");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "windows", event, {
+ primed: false,
+ });
+ }
+ // focus returns the new window
+ let focusedId = await extension.awaitMessage("onFocusChanged");
+ Assert.equal(windowId, focusedId, "new window was focused");
+ await extension.terminateBackground();
+
+ await BrowserTestUtils.closeWindow(win);
+ await extension.awaitMessage("ready");
+ let removedId = await extension.awaitMessage("onRemoved");
+ Assert.equal(windowId, removedId, "window was removed");
+ // focus returns the window focus was passed to
+ focusedId = await extension.awaitMessage("onFocusChanged");
+ Assert.notEqual(windowId, focusedId, "old window was focused");
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_incognito.js b/browser/components/extensions/test/browser/browser_ext_windows_incognito.js
new file mode 100644
index 0000000000..ef6d8a8eae
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_incognito.js
@@ -0,0 +1,84 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_window_incognito() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_iframe_document.html";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+ background() {
+ let lastFocusedWindowId = null;
+ // Catch focus change events to power the test below.
+ browser.windows.onFocusChanged.addListener(function listener(
+ eventWindowId
+ ) {
+ lastFocusedWindowId = eventWindowId;
+ browser.windows.onFocusChanged.removeListener(listener);
+ });
+
+ browser.test.onMessage.addListener(async pbw => {
+ browser.test.assertEq(
+ browser.windows.WINDOW_ID_NONE,
+ lastFocusedWindowId,
+ "Focus on private window sends the event, but doesn't reveal windowId (without permissions)"
+ );
+
+ await browser.test.assertRejects(
+ browser.windows.get(pbw.windowId),
+ /Invalid window ID/,
+ "should not be able to get incognito window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.remove(pbw.windowId),
+ /Invalid window ID/,
+ "should not be able to remove incognito window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.getCurrent(),
+ /Invalid window/,
+ "should not be able to get incognito top window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.getLastFocused(),
+ /Invalid window/,
+ "should not be able to get incognito focused window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.create({ incognito: true }),
+ /Extension does not have permission for incognito mode/,
+ "should not be able to create incognito window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.update(pbw.windowId, { focused: true }),
+ /Invalid window ID/,
+ "should not be able to update incognito window"
+ );
+
+ let windows = await browser.windows.getAll();
+ browser.test.assertEq(
+ 1,
+ windows.length,
+ "unable to get incognito window"
+ );
+
+ browser.test.notifyPass("pass");
+ });
+ },
+ });
+
+ await extension.startup();
+
+ // The tests expect the incognito window to be
+ // created after the extension is started, so think
+ // carefully when moving this line.
+ let winData = await getIncognitoWindow(url);
+
+ extension.sendMessage(winData.details);
+ await extension.awaitFinish("pass");
+ await BrowserTestUtils.closeWindow(winData.win);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_remove.js b/browser/components/extensions/test/browser/browser_ext_windows_remove.js
new file mode 100644
index 0000000000..455987a908
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_remove.js
@@ -0,0 +1,53 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWindowRemove() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ async function closeWindow(id) {
+ let window = await browser.windows.get(id);
+ return new Promise(function (resolve) {
+ browser.windows.onRemoved.addListener(async function listener(
+ windowId
+ ) {
+ browser.windows.onRemoved.removeListener(listener);
+ await browser.test.assertEq(
+ windowId,
+ window.id,
+ "The right window was closed"
+ );
+ await browser.test.assertRejects(
+ browser.windows.get(windowId),
+ new RegExp(`Invalid window ID: ${windowId}`),
+ "The window was really closed."
+ );
+ resolve();
+ });
+ browser.windows.remove(id);
+ });
+ }
+
+ browser.test.log("Create a new window and close it by its ID");
+ let newWindow = await browser.windows.create();
+ await closeWindow(newWindow.id);
+
+ browser.test.log("Create a new window and close it by WINDOW_ID_CURRENT");
+ await browser.windows.create();
+ await closeWindow(browser.windows.WINDOW_ID_CURRENT);
+
+ browser.test.log("Assert failure for bad parameter.");
+ await browser.test.assertThrows(
+ () => browser.windows.remove(-3),
+ /-3 is too small \(must be at least -2\)/,
+ "Invalid windowId throws"
+ );
+
+ browser.test.notifyPass("window-remove");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-remove");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_size.js b/browser/components/extensions/test/browser/browser_ext_windows_size.js
new file mode 100644
index 0000000000..4a4f0d8a0c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_size.js
@@ -0,0 +1,122 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function testWindowCreate() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let _checkWindowPromise;
+ browser.test.onMessage.addListener((msg, arg) => {
+ if (msg == "checked-window") {
+ _checkWindowPromise.resolve(arg);
+ _checkWindowPromise = null;
+ }
+ });
+
+ let getWindowSize = () => {
+ return new Promise(resolve => {
+ _checkWindowPromise = { resolve };
+ browser.test.sendMessage("check-window");
+ });
+ };
+
+ const KEYS = ["left", "top", "width", "height"];
+ function checkGeom(expected, actual) {
+ for (let key of KEYS) {
+ browser.test.assertEq(
+ expected[key],
+ actual[key],
+ `Expected '${key}' value`
+ );
+ }
+ }
+
+ let windowId;
+ async function checkWindow(expected, retries = 5) {
+ let geom = await getWindowSize();
+
+ if (retries && KEYS.some(key => expected[key] != geom[key])) {
+ browser.test.log(
+ `Got mismatched size (${JSON.stringify(
+ expected
+ )} != ${JSON.stringify(geom)}). Retrying after a short delay.`
+ );
+
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ return checkWindow(expected, retries - 1);
+ }
+
+ browser.test.log(`Check actual window size`);
+ checkGeom(expected, geom);
+
+ browser.test.log("Check API-reported window size");
+
+ geom = await browser.windows.get(windowId);
+
+ checkGeom(expected, geom);
+ }
+
+ try {
+ let geom = { left: 100, top: 100, width: 500, height: 300 };
+
+ let window = await browser.windows.create(geom);
+ windowId = window.id;
+
+ await checkWindow(geom);
+
+ let update = { left: 150, width: 600 };
+ Object.assign(geom, update);
+ await browser.windows.update(windowId, update);
+ await checkWindow(geom);
+
+ update = { top: 150, height: 400 };
+ Object.assign(geom, update);
+ await browser.windows.update(windowId, update);
+ await checkWindow(geom);
+
+ geom = { left: 200, top: 200, width: 800, height: 600 };
+ await browser.windows.update(windowId, geom);
+ await checkWindow(geom);
+
+ let platformInfo = await browser.runtime.getPlatformInfo();
+ if (platformInfo.os != "linux") {
+ geom = { left: -50, top: -50, width: 800, height: 600 };
+ await browser.windows.update(windowId, geom);
+ await checkWindow({ ...geom, left: 0, top: 0 });
+ }
+
+ await browser.windows.remove(windowId);
+ browser.test.notifyPass("window-size");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-size");
+ }
+ },
+ });
+
+ let latestWindow;
+ let windowListener = (window, topic) => {
+ if (topic == "domwindowopened") {
+ latestWindow = window;
+ }
+ };
+ Services.ww.registerNotification(windowListener);
+
+ extension.onMessage("check-window", () => {
+ extension.sendMessage("checked-window", {
+ top: latestWindow.screenY,
+ left: latestWindow.screenX,
+ width: latestWindow.outerWidth,
+ height: latestWindow.outerHeight,
+ });
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-size");
+ await extension.unload();
+
+ Services.ww.unregisterNotification(windowListener);
+ latestWindow = null;
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_update.js b/browser/components/extensions/test/browser/browser_ext_windows_update.js
new file mode 100644
index 0000000000..7331b2c0cc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_update.js
@@ -0,0 +1,390 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ function promiseWaitForFocus(window) {
+ return new Promise(resolve => {
+ waitForFocus(function () {
+ Assert.strictEqual(
+ Services.focus.activeWindow,
+ window,
+ "correct window focused"
+ );
+ resolve();
+ }, window);
+ });
+ }
+
+ let window1 = window;
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ window2.focus();
+ await promiseWaitForFocus(window2);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function () {
+ browser.windows.getAll(undefined, function (wins) {
+ browser.test.assertEq(wins.length, 2, "should have two windows");
+
+ // Sort the unfocused window to the lower index.
+ wins.sort(function (win1, win2) {
+ if (win1.focused === win2.focused) {
+ return 0;
+ }
+
+ return win1.focused ? 1 : -1;
+ });
+
+ browser.windows.update(wins[0].id, { focused: true }, function () {
+ browser.test.sendMessage("check");
+ });
+ });
+ },
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("check")]);
+
+ await promiseWaitForFocus(window1);
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(window2);
+});
+
+add_task(async function testWindowUpdate() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let _checkWindowPromise;
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "checked-window") {
+ _checkWindowPromise.resolve();
+ _checkWindowPromise = null;
+ }
+ });
+
+ let os;
+ function checkWindow(expected) {
+ return new Promise(resolve => {
+ _checkWindowPromise = { resolve };
+ browser.test.sendMessage("check-window", expected);
+ });
+ }
+
+ let currentWindowId;
+ async function updateWindow(windowId, params, expected, otherChecks) {
+ let window = await browser.windows.update(windowId, params);
+
+ browser.test.assertEq(
+ currentWindowId,
+ window.id,
+ "Expected WINDOW_ID_CURRENT to refer to the same window"
+ );
+ for (let key of Object.keys(params)) {
+ if (key == "state" && os == "mac" && params.state == "normal") {
+ // OS-X doesn't have a hard distinction between "normal" and
+ // "maximized" states.
+ browser.test.assertTrue(
+ window.state == "normal" || window.state == "maximized",
+ `Expected window.state (currently ${window.state}) to be "normal" but will accept "maximized"`
+ );
+ } else {
+ browser.test.assertEq(
+ params[key],
+ window[key],
+ `Got expected value for window.${key}`
+ );
+ }
+ }
+ if (otherChecks) {
+ for (let key of Object.keys(otherChecks)) {
+ browser.test.assertEq(
+ otherChecks[key],
+ window[key],
+ `Got expected value for window.${key}`
+ );
+ }
+ }
+
+ return checkWindow(expected);
+ }
+
+ try {
+ let windowId = browser.windows.WINDOW_ID_CURRENT;
+
+ ({ os } = await browser.runtime.getPlatformInfo());
+
+ let window = await browser.windows.getCurrent();
+ currentWindowId = window.id;
+
+ // Store current, "normal" width and height to compare against
+ // window width and height after updating to "normal" state.
+ let normalWidth = window.width;
+ let normalHeight = window.height;
+
+ await updateWindow(
+ windowId,
+ { state: "maximized" },
+ { state: "STATE_MAXIMIZED" }
+ );
+ await updateWindow(
+ windowId,
+ { state: "normal" },
+ { state: "STATE_NORMAL" },
+ { width: normalWidth, height: normalHeight }
+ );
+ await updateWindow(
+ windowId,
+ { state: "minimized" },
+ { state: "STATE_MINIMIZED" }
+ );
+ await updateWindow(
+ windowId,
+ { state: "normal" },
+ { state: "STATE_NORMAL" },
+ { width: normalWidth, height: normalHeight }
+ );
+ await updateWindow(
+ windowId,
+ { state: "fullscreen" },
+ { state: "STATE_FULLSCREEN" }
+ );
+ await updateWindow(
+ windowId,
+ { state: "normal" },
+ { state: "STATE_NORMAL" },
+ { width: normalWidth, height: normalHeight }
+ );
+
+ browser.test.notifyPass("window-update");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-update");
+ }
+ },
+ });
+
+ extension.onMessage("check-window", expected => {
+ if (expected.state != null) {
+ let { windowState } = window;
+ if (window.fullScreen) {
+ windowState = window.STATE_FULLSCREEN;
+ }
+
+ // Temporarily accepting STATE_MAXIMIZED on Linux because of bug 1307759.
+ if (
+ expected.state == "STATE_NORMAL" &&
+ (AppConstants.platform == "macosx" || AppConstants.platform == "linux")
+ ) {
+ ok(
+ windowState == window.STATE_NORMAL ||
+ windowState == window.STATE_MAXIMIZED,
+ `Expected windowState (currently ${windowState}) to be STATE_NORMAL but will accept STATE_MAXIMIZED`
+ );
+ } else {
+ is(
+ windowState,
+ window[expected.state],
+ `Expected window state to be ${expected.state}`
+ );
+ }
+ }
+
+ extension.sendMessage("checked-window");
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-update");
+ await extension.unload();
+});
+
+add_task(async function () {
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function () {
+ browser.windows.getAll(undefined, function (wins) {
+ browser.test.assertEq(wins.length, 2, "should have two windows");
+
+ let unfocused = wins.find(win => !win.focused);
+ browser.windows.update(
+ unfocused.id,
+ { drawAttention: true },
+ function () {
+ browser.test.sendMessage("check");
+ }
+ );
+ });
+ },
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("check")]);
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(window2);
+});
+
+// Tests that incompatible parameters can't be used together.
+add_task(async function testWindowUpdateParams() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ try {
+ for (let state of ["minimized", "maximized", "fullscreen"]) {
+ for (let param of ["left", "top", "width", "height"]) {
+ let expected = `"state": "${state}" may not be combined with "left", "top", "width", or "height"`;
+
+ let windowId = browser.windows.WINDOW_ID_CURRENT;
+ await browser.test.assertRejects(
+ browser.windows.update(windowId, { state, [param]: 100 }),
+ RegExp(expected),
+ `Got expected error for create(${param}=100`
+ );
+ }
+ }
+
+ browser.test.notifyPass("window-update-params");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-update-params");
+ }
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-update-params");
+ await extension.unload();
+});
+
+add_task(async function testPositionBoundaryCheck() {
+ const extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ function waitMessage() {
+ return new Promise((resolve, reject) => {
+ const onMessage = message => {
+ if (message == "continue") {
+ browser.test.onMessage.removeListener(onMessage);
+ resolve();
+ }
+ };
+ browser.test.onMessage.addListener(onMessage);
+ });
+ }
+ const win = await browser.windows.create({
+ type: "popup",
+ left: 50,
+ top: 50,
+ width: 150,
+ height: 150,
+ });
+ await browser.test.sendMessage("ready");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ left: 123,
+ top: 123,
+ });
+ await browser.test.sendMessage("regular");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ left: 123,
+ });
+ await browser.test.sendMessage("only-left");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ top: 123,
+ });
+ await browser.test.sendMessage("only-top");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ left: screen.availWidth * 100,
+ top: screen.availHeight * 100,
+ });
+ await browser.test.sendMessage("too-large");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ left: -screen.availWidth * 100,
+ top: -screen.availHeight * 100,
+ });
+ await browser.test.sendMessage("too-small");
+ },
+ });
+
+ const promisedWin = new Promise((resolve, reject) => {
+ const windowListener = (window, topic) => {
+ if (topic == "domwindowopened") {
+ Services.ww.unregisterNotification(windowListener);
+ resolve(window);
+ }
+ };
+ Services.ww.registerNotification(windowListener);
+ });
+
+ await extension.startup();
+
+ const win = await promisedWin;
+
+ const regularScreen = getScreenAt(0, 0, 150, 150);
+ const roundedX = roundCssPixcel(123, regularScreen);
+ const roundedY = roundCssPixcel(123, regularScreen);
+
+ const availRectLarge = getCssAvailRect(
+ getScreenAt(screen.width * 100, screen.height * 100, 150, 150)
+ );
+ const maxRight = availRectLarge.right;
+ const maxBottom = availRectLarge.bottom;
+
+ const availRectSmall = getCssAvailRect(
+ getScreenAt(-screen.width * 100, -screen.height * 100, 150, 150)
+ );
+ const minLeft = availRectSmall.left;
+ const minTop = availRectSmall.top;
+
+ const expectedCoordinates = [
+ `${roundedX},${roundedY}`,
+ `${roundedX},${win.screenY}`,
+ `${win.screenX},${roundedY}`,
+ ];
+
+ await extension.awaitMessage("ready");
+
+ const actualCoordinates = [];
+ extension.sendMessage("continue");
+ await extension.awaitMessage("regular");
+ actualCoordinates.push(`${win.screenX},${win.screenY}`);
+ win.moveTo(50, 50);
+ extension.sendMessage("continue");
+ await extension.awaitMessage("only-left");
+ actualCoordinates.push(`${win.screenX},${win.screenY}`);
+ win.moveTo(50, 50);
+ extension.sendMessage("continue");
+ await extension.awaitMessage("only-top");
+ actualCoordinates.push(`${win.screenX},${win.screenY}`);
+ is(
+ actualCoordinates.join(" / "),
+ expectedCoordinates.join(" / "),
+ "expected window is placed at given coordinates"
+ );
+
+ const actualRect = {};
+ const maxRect = {
+ top: minTop,
+ bottom: maxBottom,
+ left: minLeft,
+ right: maxRight,
+ };
+
+ extension.sendMessage("continue");
+ await extension.awaitMessage("too-large");
+ actualRect.right = win.screenX + win.outerWidth;
+ actualRect.bottom = win.screenY + win.outerHeight;
+
+ extension.sendMessage("continue");
+ await extension.awaitMessage("too-small");
+ actualRect.top = win.screenY;
+ actualRect.left = win.screenX;
+
+ isRectContained(actualRect, maxRect);
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_legacy_recent_tabs.toml b/browser/components/extensions/test/browser/browser_legacy_recent_tabs.toml
new file mode 100644
index 0000000000..ae23831a0d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_legacy_recent_tabs.toml
@@ -0,0 +1,34 @@
+[DEFAULT]
+#
+# This manifest lists tests that use cover the session API and recently-closed tabs behavior with the legacy pref values
+#
+tags = "webextensions"
+dupe-manifest = true
+prefs = [
+ "browser.sessionstore.closedTabsFromAllWindows=false",
+ "browser.sessionstore.closedTabsFromClosedWindows=false",
+]
+support-files = [
+ "head.js",
+ "empty.xpi",
+]
+
+["browser_ext_sessions_forgetClosedTab.js"]
+
+["browser_ext_sessions_forgetClosedWindow.js"]
+
+["browser_ext_sessions_getRecentlyClosed.js"]
+
+["browser_ext_sessions_getRecentlyClosed_private.js"]
+
+["browser_ext_sessions_getRecentlyClosed_tabs.js"]
+
+["browser_ext_sessions_incognito.js"]
+
+["browser_ext_sessions_restore.js"]
+
+["browser_ext_sessions_restoreTab.js"]
+
+["browser_ext_sessions_restore_private.js"]
+
+["browser_ext_sessions_window_tab_value.js"]
diff --git a/browser/components/extensions/test/browser/browser_toolbar_prefers_color_scheme.js b/browser/components/extensions/test/browser/browser_toolbar_prefers_color_scheme.js
new file mode 100644
index 0000000000..ae7f488f0a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_toolbar_prefers_color_scheme.js
@@ -0,0 +1,266 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kDark = 0;
+const kLight = 1;
+const kSystem = 2;
+
+// The above tests should be enough to make sure that the prefs behave as
+// expected, the following ones test various edge cases in a simpler way.
+async function testTheme(description, toolbar, content, themeManifestData) {
+ info(description);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "dummy@mochi.test",
+ },
+ },
+ ...themeManifestData,
+ },
+ });
+
+ await Promise.all([
+ TestUtils.topicObserved("lightweight-theme-styling-update"),
+ extension.startup(),
+ ]);
+
+ is(
+ SpecialPowers.getIntPref("browser.theme.toolbar-theme"),
+ toolbar,
+ "Toolbar theme expected"
+ );
+ is(
+ SpecialPowers.getIntPref("browser.theme.content-theme"),
+ content,
+ "Content theme expected"
+ );
+
+ await Promise.all([
+ TestUtils.topicObserved("lightweight-theme-styling-update"),
+ extension.unload(),
+ ]);
+}
+
+add_task(async function test_dark_toolbar_dark_text() {
+ // Bug 1743010
+ await testTheme(
+ "Dark toolbar color, dark toolbar background",
+ kDark,
+ kSystem,
+ {
+ theme: {
+ colors: {
+ toolbar: "rgb(20, 17, 26)",
+ toolbar_text: "rgb(251, 29, 78)",
+ },
+ },
+ }
+ );
+
+ // Dark frame text is ignored as it might be overlaid with an image,
+ // see bug 1741931.
+ await testTheme("Dark frame is ignored", kLight, kSystem, {
+ theme: {
+ colors: {
+ frame: "#000000",
+ tab_background_text: "#000000",
+ },
+ },
+ });
+
+ await testTheme(
+ "Semi-transparent toolbar backgrounds are ignored.",
+ kLight,
+ kSystem,
+ {
+ theme: {
+ colors: {
+ toolbar: "rgba(0, 0, 0, .2)",
+ toolbar_text: "#000",
+ },
+ },
+ }
+ );
+});
+
+add_task(async function dark_theme_presence_overrides_heuristics() {
+ const systemScheme = window.matchMedia("(-moz-system-dark-theme)").matches
+ ? kDark
+ : kLight;
+ await testTheme(
+ "darkTheme presence overrides heuristics",
+ systemScheme,
+ kSystem,
+ {
+ theme: {
+ colors: {
+ toolbar: "#000",
+ toolbar_text: "#fff",
+ },
+ },
+ dark_theme: {
+ colors: {
+ toolbar: "#000",
+ toolbar_text: "#fff",
+ },
+ },
+ }
+ );
+});
+
+add_task(async function color_scheme_override() {
+ await testTheme(
+ "color_scheme overrides toolbar / toolbar_text pair (dark)",
+ kDark,
+ kDark,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ },
+ properties: {
+ color_scheme: "dark",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "color_scheme overrides toolbar / toolbar_text pair (light)",
+ kLight,
+ kLight,
+ {
+ theme: {
+ colors: {
+ toolbar: "#000",
+ toolbar_text: "#fff",
+ },
+ properties: {
+ color_scheme: "light",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "content_color_scheme overrides ntp_text / ntp_background (dark)",
+ kLight,
+ kDark,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#fff",
+ ntp_text: "#000",
+ },
+ properties: {
+ content_color_scheme: "dark",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "content_color_scheme overrides ntp_text / ntp_background (light)",
+ kLight,
+ kLight,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#000",
+ ntp_text: "#fff",
+ },
+ properties: {
+ content_color_scheme: "light",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "content_color_scheme overrides color_scheme only for content",
+ kLight,
+ kDark,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#fff",
+ ntp_text: "#000",
+ },
+ properties: {
+ content_color_scheme: "dark",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "content_color_scheme sytem overrides color_scheme only for content",
+ kLight,
+ kSystem,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#fff",
+ ntp_text: "#000",
+ },
+ properties: {
+ content_color_scheme: "system",
+ },
+ },
+ }
+ );
+
+ await testTheme("color_scheme: sytem override", kSystem, kSystem, {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#fff",
+ ntp_text: "#000",
+ },
+ properties: {
+ color_scheme: "system",
+ content_color_scheme: "system",
+ },
+ },
+ });
+});
+
+add_task(async function unified_theme() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.theme.unified-color-scheme", true]],
+ });
+
+ await testTheme("Dark toolbar color", kDark, kDark, {
+ theme: {
+ colors: {
+ toolbar: "rgb(20, 17, 26)",
+ toolbar_text: "rgb(251, 29, 78)",
+ },
+ },
+ });
+
+ await testTheme("Light toolbar color", kLight, kLight, {
+ theme: {
+ colors: {
+ toolbar: "white",
+ toolbar_text: "black",
+ },
+ },
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions.js b/browser/components/extensions/test/browser/browser_unified_extensions.js
new file mode 100644
index 0000000000..7ab7753c0e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions.js
@@ -0,0 +1,1545 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+/* import-globals-from ../../../../../toolkit/mozapps/extensions/test/browser/head.js */
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+loadTestSubscript("head_unified_extensions.js");
+
+const openCustomizationUI = async () => {
+ const customizationReady = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gCustomizeMode.enter();
+ await customizationReady;
+ ok(
+ CustomizationHandler.isCustomizing(),
+ "expected customizing mode to be enabled"
+ );
+};
+
+const closeCustomizationUI = async () => {
+ const afterCustomization = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "aftercustomization"
+ );
+ gCustomizeMode.exit();
+ await afterCustomization;
+ ok(
+ !CustomizationHandler.isCustomizing(),
+ "expected customizing mode to be disabled"
+ );
+};
+
+add_setup(async function () {
+ // Make sure extension buttons added to the navbar will not overflow in the
+ // panel, which could happen when a previous test file resizes the current
+ // window.
+ await ensureMaximizedWindow(window);
+});
+
+add_task(async function test_button_enabled_by_pref() {
+ const { button } = gUnifiedExtensions;
+ is(button.hidden, false, "expected button to be visible");
+ is(
+ document
+ .getElementById("nav-bar")
+ .getAttribute("unifiedextensionsbuttonshown"),
+ "true",
+ "expected attribute on nav-bar"
+ );
+});
+
+add_task(async function test_open_panel_on_button_click() {
+ const extensions = createExtensions([
+ { name: "Extension #1" },
+ { name: "Another extension", icons: { 16: "test-icon-16.png" } },
+ {
+ name: "Yet another extension with an icon",
+ icons: {
+ 32: "test-icon-32.png",
+ },
+ },
+ ]);
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ await openExtensionsPanel();
+
+ let item = getUnifiedExtensionsItem(extensions[0].id);
+ is(
+ item.querySelector(".unified-extensions-item-name").textContent,
+ "Extension #1",
+ "expected name of the first extension"
+ );
+ is(
+ item.querySelector(".unified-extensions-item-icon").src,
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg",
+ "expected generic icon for the first extension"
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(
+ item.querySelector(".unified-extensions-item-menu-button")
+ ),
+ {
+ id: "unified-extensions-item-open-menu",
+ args: { extensionName: "Extension #1" },
+ },
+ "expected l10n attributes for the first extension"
+ );
+
+ item = getUnifiedExtensionsItem(extensions[1].id);
+ is(
+ item.querySelector(".unified-extensions-item-name").textContent,
+ "Another extension",
+ "expected name of the second extension"
+ );
+ ok(
+ item
+ .querySelector(".unified-extensions-item-icon")
+ .src.endsWith("/test-icon-16.png"),
+ "expected custom icon for the second extension"
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(
+ item.querySelector(".unified-extensions-item-menu-button")
+ ),
+ {
+ id: "unified-extensions-item-open-menu",
+ args: { extensionName: "Another extension" },
+ },
+ "expected l10n attributes for the second extension"
+ );
+
+ item = getUnifiedExtensionsItem(extensions[2].id);
+ is(
+ item.querySelector(".unified-extensions-item-name").textContent,
+ "Yet another extension with an icon",
+ "expected name of the third extension"
+ );
+ ok(
+ item
+ .querySelector(".unified-extensions-item-icon")
+ .src.endsWith("/test-icon-32.png"),
+ "expected custom icon for the third extension"
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(
+ item.querySelector(".unified-extensions-item-menu-button")
+ ),
+ {
+ id: "unified-extensions-item-open-menu",
+ args: { extensionName: "Yet another extension with an icon" },
+ },
+ "expected l10n attributes for the third extension"
+ );
+
+ await closeExtensionsPanel();
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+// Verify that the context click doesn't open the panel in addition to the
+// context menu.
+add_task(async function test_clicks_on_unified_extension_button() {
+ const extensions = createExtensions([{ name: "Extension #1" }]);
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ const { button, panel } = gUnifiedExtensions;
+ ok(button, "expected button");
+ ok(panel, "expected panel");
+
+ info("open panel with primary click");
+ await openExtensionsPanel();
+ Assert.strictEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "expected panel to be visible"
+ );
+ await closeExtensionsPanel();
+ ok(!panel.hasAttribute("panelopen"), "expected panel to be hidden");
+
+ info("open context menu with non-primary click");
+ const contextMenu = document.getElementById("toolbar-context-menu");
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+ ok(!panel.hasAttribute("panelopen"), "expected panel to remain hidden");
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ // On MacOS, ctrl-click shouldn't open the panel because this normally opens
+ // the context menu. We can't test anything on MacOS...
+ if (AppConstants.platform !== "macosx") {
+ info("open panel with ctrl-click");
+ const listView = getListView();
+ const viewShown = BrowserTestUtils.waitForEvent(listView, "ViewShown");
+ EventUtils.synthesizeMouseAtCenter(button, { ctrlKey: true });
+ await viewShown;
+ Assert.strictEqual(
+ panel.getAttribute("panelopen"),
+ "true",
+ "expected panel to be visible"
+ );
+ await closeExtensionsPanel();
+ ok(!panel.hasAttribute("panelopen"), "expected panel to be hidden");
+ }
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+add_task(async function test_item_shows_the_best_addon_icon() {
+ const extensions = createExtensions([
+ {
+ name: "Extension with different icons",
+ icons: {
+ 16: "test-icon-16.png",
+ 32: "test-icon-32.png",
+ 64: "test-icon-64.png",
+ 96: "test-icon-96.png",
+ 128: "test-icon-128.png",
+ },
+ },
+ ]);
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ for (const { resolution, expectedIcon } of [
+ { resolution: 2, expectedIcon: "test-icon-64.png" },
+ { resolution: 1, expectedIcon: "test-icon-32.png" },
+ ]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.css.devPixelsPerPx", String(resolution)]],
+ });
+ is(
+ window.devicePixelRatio,
+ resolution,
+ "window has the required resolution"
+ );
+
+ await openExtensionsPanel();
+
+ const item = getUnifiedExtensionsItem(extensions[0].id);
+ const iconSrc = item.querySelector(".unified-extensions-item-icon").src;
+ ok(
+ iconSrc.endsWith(expectedIcon),
+ `expected ${expectedIcon}, got: ${iconSrc}`
+ );
+
+ await closeExtensionsPanel();
+ await SpecialPowers.popPrefEnv();
+ }
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+add_task(async function test_panel_has_a_manage_extensions_button() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots" },
+ async () => {
+ await openExtensionsPanel();
+
+ const manageExtensionsButton = getListView().querySelector(
+ "#unified-extensions-manage-extensions"
+ );
+ ok(manageExtensionsButton, "expected a 'manage extensions' button");
+
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+ const popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popuphidden",
+ true
+ );
+
+ manageExtensionsButton.click();
+
+ const [tab] = await Promise.all([tabPromise, popupHiddenPromise]);
+ is(
+ gBrowser.currentURI.spec,
+ "about:addons",
+ "Manage opened about:addons"
+ );
+ is(
+ gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
+ "addons://list/extension",
+ "expected about:addons to show the list of extensions"
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_list_active_extensions_only() {
+ const arrayOfManifestData = [
+ {
+ name: "hidden addon",
+ browser_specific_settings: { gecko: { id: "ext1@test" } },
+ hidden: true,
+ },
+ {
+ name: "regular addon",
+ browser_specific_settings: { gecko: { id: "ext2@test" } },
+ hidden: false,
+ },
+ {
+ name: "disabled addon",
+ browser_specific_settings: { gecko: { id: "ext3@test" } },
+ hidden: false,
+ },
+ {
+ name: "regular addon with browser action",
+ browser_specific_settings: { gecko: { id: "ext4@test" } },
+ hidden: false,
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ {
+ manifest_version: 3,
+ name: "regular mv3 addon with browser action",
+ browser_specific_settings: { gecko: { id: "ext5@test" } },
+ hidden: false,
+ action: {
+ default_area: "navbar",
+ },
+ },
+ {
+ name: "regular addon with page action",
+ browser_specific_settings: { gecko: { id: "ext6@test" } },
+ hidden: false,
+ page_action: {},
+ },
+ ];
+ const extensions = createExtensions(arrayOfManifestData, {
+ useAddonManager: "temporary",
+ // Allow all extensions in PB mode by default.
+ incognitoOverride: "spanning",
+ });
+ // This extension is loaded with a different `incognitoOverride` value to
+ // make sure it won't show up in a private window.
+ extensions.push(
+ ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "ext7@test" } },
+ name: "regular addon with private browsing disabled",
+ },
+ useAddonManager: "temporary",
+ incognitoOverride: "not_allowed",
+ })
+ );
+
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ // Disable the "disabled addon".
+ let addon2 = await AddonManager.getAddonByID(extensions[2].id);
+ await addon2.disable();
+
+ for (const isPrivate of [false, true]) {
+ info(
+ `verifying extensions listed in the panel with private browsing ${
+ isPrivate ? "enabled" : "disabled"
+ }`
+ );
+ const aWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: isPrivate,
+ });
+ // Make sure extension buttons added to the navbar will not overflow in the
+ // panel, which could happen when a previous test file resizes the current
+ // window.
+ await ensureMaximizedWindow(aWin);
+
+ await openExtensionsPanel(aWin);
+
+ ok(
+ aWin.gUnifiedExtensions._button.open,
+ "Expected unified extension panel to be open"
+ );
+
+ const hiddenAddonItem = getUnifiedExtensionsItem(extensions[0].id, aWin);
+ is(hiddenAddonItem, null, "didn't expect an item for a hidden add-on");
+
+ const regularAddonItem = getUnifiedExtensionsItem(extensions[1].id, aWin);
+ is(
+ regularAddonItem.querySelector(".unified-extensions-item-name")
+ .textContent,
+ "regular addon",
+ "expected an item for a regular add-on"
+ );
+
+ const disabledAddonItem = getUnifiedExtensionsItem(extensions[2].id, aWin);
+ is(disabledAddonItem, null, "didn't expect an item for a disabled add-on");
+
+ const browserActionItem = getUnifiedExtensionsItem(extensions[3].id, aWin);
+ is(
+ browserActionItem,
+ null,
+ "didn't expect an item for an add-on with browser action placed in the navbar"
+ );
+
+ const mv3BrowserActionItem = getUnifiedExtensionsItem(
+ extensions[4].id,
+ aWin
+ );
+ is(
+ mv3BrowserActionItem,
+ null,
+ "didn't expect an item for a MV3 add-on with browser action placed in the navbar"
+ );
+
+ const pageActionItem = getUnifiedExtensionsItem(extensions[5].id, aWin);
+ is(
+ pageActionItem.querySelector(".unified-extensions-item-name").textContent,
+ "regular addon with page action",
+ "expected an item for a regular add-on with page action"
+ );
+
+ const privateBrowsingDisabledItem = getUnifiedExtensionsItem(
+ extensions[6].id,
+ aWin
+ );
+ if (isPrivate) {
+ is(
+ privateBrowsingDisabledItem,
+ null,
+ "didn't expect an item for a regular add-on with private browsing enabled"
+ );
+ } else {
+ is(
+ privateBrowsingDisabledItem.querySelector(
+ ".unified-extensions-item-name"
+ ).textContent,
+ "regular addon with private browsing disabled",
+ "expected an item for a regular add-on with private browsing disabled"
+ );
+ }
+
+ await closeExtensionsPanel(aWin);
+
+ await BrowserTestUtils.closeWindow(aWin);
+ }
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+add_task(async function test_button_opens_discopane_when_no_extension() {
+ // The test harness registers regular extensions so we need to mock the
+ // `getActivePolicies` extension to simulate zero extensions installed.
+ const origGetActivePolicies = gUnifiedExtensions.getActivePolicies;
+ gUnifiedExtensions.getActivePolicies = () => [];
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots" },
+ async () => {
+ const { button } = gUnifiedExtensions;
+ ok(button, "expected button");
+
+ // Primary click should open about:addons.
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+
+ button.click();
+
+ const tab = await tabPromise;
+ is(
+ gBrowser.currentURI.spec,
+ "about:addons",
+ "expected about:addons to be open"
+ );
+ is(
+ gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
+ "addons://discover/",
+ "expected about:addons to show the recommendations"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ // "Right-click" should open the context menu only.
+ const contextMenu = document.getElementById("toolbar-context-menu");
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+ await closeChromeContextMenu(contextMenu.id, null);
+ }
+ );
+
+ gUnifiedExtensions.getActivePolicies = origGetActivePolicies;
+});
+
+add_task(
+ async function test_button_opens_extlist_when_no_extension_and_pane_disabled() {
+ // If extensions.getAddons.showPane is set to false, there is no "Recommended" tab,
+ // so we need to make sure we don't navigate to it.
+
+ // The test harness registers regular extensions so we need to mock the
+ // `getActivePolicies` extension to simulate zero extensions installed.
+ const origGetActivePolicies = gUnifiedExtensions.getActivePolicies;
+ gUnifiedExtensions.getActivePolicies = () => [];
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set this to another value to make sure not to "accidentally" land on the right page
+ ["extensions.ui.lastCategory", "addons://list/theme"],
+ ["extensions.getAddons.showPane", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots" },
+ async () => {
+ const { button } = gUnifiedExtensions;
+ ok(button, "expected button");
+
+ // Primary click should open about:addons.
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+
+ button.click();
+
+ const tab = await tabPromise;
+ is(
+ gBrowser.currentURI.spec,
+ "about:addons",
+ "expected about:addons to be open"
+ );
+ is(
+ gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
+ "addons://list/extension",
+ "expected about:addons to show the extension list"
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+
+ gUnifiedExtensions.getActivePolicies = origGetActivePolicies;
+ }
+);
+
+add_task(
+ async function test_unified_extensions_panel_not_open_in_customization_mode() {
+ const listView = getListView();
+ ok(listView, "expected list view");
+ const throwIfExecuted = () => {
+ throw new Error("panel should not have been shown");
+ };
+ listView.addEventListener("ViewShown", throwIfExecuted);
+
+ await openCustomizationUI();
+
+ const unifiedExtensionsButtonToggled = BrowserTestUtils.waitForEvent(
+ window,
+ "UnifiedExtensionsTogglePanel"
+ );
+ const button = document.getElementById("unified-extensions-button");
+
+ button.click();
+ await unifiedExtensionsButtonToggled;
+
+ await closeCustomizationUI();
+
+ listView.removeEventListener("ViewShown", throwIfExecuted);
+ }
+);
+
+const NO_ACCESS = { id: "origin-controls-state-no-access", args: null };
+const QUARANTINED = { id: "origin-controls-state-quarantined", args: null };
+
+const ALWAYS_ON = { id: "origin-controls-state-always-on", args: null };
+const WHEN_CLICKED = { id: "origin-controls-state-when-clicked", args: null };
+const TEMP_ACCESS = {
+ id: "origin-controls-state-temporary-access",
+ args: null,
+};
+
+const HOVER_RUN_VISIT_ONLY = {
+ id: "origin-controls-state-hover-run-visit-only",
+ args: null,
+};
+const HOVER_RUNNABLE_RUN_EXT = {
+ id: "origin-controls-state-runnable-hover-run",
+ args: null,
+};
+const HOVER_RUNNABLE_OPEN_EXT = {
+ id: "origin-controls-state-runnable-hover-open",
+ args: null,
+};
+
+add_task(async function test_messages_origin_controls() {
+ const TEST_CASES = [
+ {
+ title: "MV2 - no access",
+ manifest: {
+ manifest_version: 2,
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - always on",
+ manifest: {
+ manifest_version: 2,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - content script",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - non-matching content script",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://foobar.net/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - all_urls content script",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: [""],
+ },
+ ],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - activeTab without browser action",
+ manifest: {
+ manifest_version: 2,
+ permissions: ["activeTab"],
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - when clicked: activeTab with browser action",
+ manifest: {
+ manifest_version: 2,
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV3 - when clicked: activeTab with action",
+ manifest: {
+ manifest_version: 3,
+ permissions: ["activeTab"],
+ action: {},
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV2 - browser action - click event - always on",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV2 - browser action - popup - always on",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV2 - browser action - click event - content script",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV2 - browser action - popup - content script",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "no access",
+ manifest: {
+ manifest_version: 3,
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "when clicked with host permissions",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "when clicked with host permissions already granted",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ grantHostPermissions: true,
+ },
+ {
+ title: "when clicked",
+ manifest: {
+ manifest_version: 3,
+ permissions: ["activeTab"],
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "page action - no access",
+ manifest: {
+ manifest_version: 3,
+ page_action: {},
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "page action - when clicked with host permissions",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ page_action: {},
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "page action - when clicked with host permissions already granted",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ page_action: {},
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ grantHostPermissions: true,
+ },
+ {
+ title: "page action - when clicked",
+ manifest: {
+ manifest_version: 3,
+ permissions: ["activeTab"],
+ page_action: {},
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "browser action - click event - no access",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "browser action - popup - no access",
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_popup: "popup.html",
+ },
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "browser action - click event - when clicked",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "browser action - popup - when clicked",
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_popup: "popup.html",
+ },
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title:
+ "browser action - click event - when clicked with host permissions already granted",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ grantHostPermissions: true,
+ },
+ {
+ title:
+ "browser action - popup - when clicked with host permissions already granted",
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_popup: "popup.html",
+ },
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT,
+ expectedActionButtonDisabled: false,
+ grantHostPermissions: true,
+ },
+ ];
+
+ async function runTestCases(testCases) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "https://example.com/" },
+ async () => {
+ let count = 0;
+
+ for (const {
+ title,
+ manifest,
+ expectedDefaultMessage,
+ expectedHoverMessage,
+ expectedActionButtonDisabled,
+ grantHostPermissions,
+ } of testCases) {
+ info(`case: ${title}`);
+
+ const id = `test-origin-controls-${count++}@ext`;
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: title,
+ browser_specific_settings: { gecko: { id } },
+ ...manifest,
+ },
+ files: {
+ "script.js": "",
+ "popup.html": "",
+ },
+ useAddonManager: "permanent",
+ });
+
+ if (grantHostPermissions) {
+ info("Granting initial permissions.");
+ await ExtensionPermissions.add(id, {
+ permissions: [],
+ origins: manifest.host_permissions,
+ });
+ }
+
+ await extension.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ const item = getUnifiedExtensionsItem(extension.id);
+ ok(item, `expected item for ${extension.id}`);
+
+ const messageDeck = item.querySelector(
+ ".unified-extensions-item-message-deck"
+ );
+ ok(messageDeck, "expected a message deck element");
+
+ // 1. Verify the default message displayed below the extension's name.
+ const defaultMessage = item.querySelector(
+ ".unified-extensions-item-message-default"
+ );
+ ok(defaultMessage, "expected a default message element");
+
+ Assert.deepEqual(
+ document.l10n.getAttributes(defaultMessage),
+ expectedDefaultMessage,
+ "expected l10n attributes for the default message"
+ );
+
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ // 2. Verify the action button state.
+ const actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(actionButton, "expected an action button");
+ is(
+ actionButton.disabled,
+ expectedActionButtonDisabled,
+ `expected action button to be ${
+ expectedActionButtonDisabled ? "disabled" : "enabled"
+ }`
+ );
+
+ // 3. Verify the message displayed on hover but only when the action
+ // button isn't disabled to avoid some test failures.
+ if (!expectedActionButtonDisabled) {
+ const hovered = BrowserTestUtils.waitForEvent(
+ actionButton,
+ "mouseover"
+ );
+ EventUtils.synthesizeMouseAtCenter(actionButton, {
+ type: "mouseover",
+ });
+ await hovered;
+
+ const hoverMessage = item.querySelector(
+ ".unified-extensions-item-message-hover"
+ );
+ ok(hoverMessage, "expected a hover message element");
+
+ Assert.deepEqual(
+ document.l10n.getAttributes(hoverMessage),
+ expectedHoverMessage,
+ "expected l10n attributes for the message on hover"
+ );
+
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+ }
+
+ await closeExtensionsPanel();
+
+ // Move cursor elsewhere to avoid issues with previous "hovering".
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {});
+
+ await extension.unload();
+ }
+ }
+ );
+ }
+
+ await runTestCases(TEST_CASES);
+
+ info("Testing again with example.com quarantined.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.quarantinedDomains.list", "example.com"]],
+ });
+
+ await runTestCases([
+ {
+ title: "MV2 - no access",
+ manifest: {
+ manifest_version: 2,
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - host permission but quarantined",
+ manifest: {
+ manifest_version: 2,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: QUARANTINED,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - content script but quarantined",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: QUARANTINED,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - non-matching content script",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://foobar.net/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV3 - content script but quarantined",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: QUARANTINED,
+ expectedActionButtonDisabled: true,
+ grantHostPermissions: true,
+ },
+ {
+ title: "MV3 host permissions already granted but quarantined",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: QUARANTINED,
+ expectedActionButtonDisabled: true,
+ grantHostPermissions: true,
+ },
+ {
+ title: "browser action, host permissions already granted, quarantined",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ grantHostPermissions: true,
+ },
+ ]);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_hover_message_when_button_updates_itself() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ name: "an extension that refreshes its title",
+ action: {},
+ },
+ background() {
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.assertEq(
+ "update-button",
+ msg,
+ "expected 'update-button' message"
+ );
+
+ browser.action.setTitle({ title: "a title" });
+
+ browser.test.sendMessage(`${msg}-done`);
+ });
+
+ browser.test.sendMessage("background-ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ await openExtensionsPanel();
+
+ const item = getUnifiedExtensionsItem(extension.id);
+ ok(item, "expected item in the panel");
+
+ const actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(actionButton, "expected an action button");
+
+ const menuButton = item.querySelector(".unified-extensions-item-menu-button");
+ ok(menuButton, "expected a menu button");
+
+ const hovered = BrowserTestUtils.waitForEvent(actionButton, "mouseover");
+ EventUtils.synthesizeMouseAtCenter(actionButton, { type: "mouseover" });
+ await hovered;
+
+ const messageDeck = item.querySelector(
+ ".unified-extensions-item-message-deck"
+ );
+ ok(messageDeck, "expected a message deck element");
+
+ const hoverMessage = item.querySelector(
+ ".unified-extensions-item-message-hover"
+ );
+ ok(hoverMessage, "expected a hover message element");
+
+ const expectedL10nAttributes = {
+ id: "origin-controls-state-runnable-hover-run",
+ args: null,
+ };
+ Assert.deepEqual(
+ document.l10n.getAttributes(hoverMessage),
+ expectedL10nAttributes,
+ "expected l10n attributes for the hover message"
+ );
+
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ extension.sendMessage("update-button");
+ await extension.awaitMessage("update-button-done");
+
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to remain the same"
+ );
+
+ const menuButtonHovered = BrowserTestUtils.waitForEvent(
+ menuButton,
+ "mouseover"
+ );
+ EventUtils.synthesizeMouseAtCenter(menuButton, { type: "mouseover" });
+ await menuButtonHovered;
+
+ await closeExtensionsPanel();
+
+ // Move cursor to the center of the entire browser UI to avoid issues with
+ // other focus/hover checks. We do this to avoid intermittent test failures.
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {});
+
+ await extension.unload();
+});
+
+// Test the temporary access state messages and attention indicator.
+add_task(async function test_temporary_access() {
+ const TEST_CASES = [
+ {
+ title: "mv3 with active scripts and browser action",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ before: {
+ attention: true,
+ state: WHEN_CLICKED,
+ disabled: false,
+ },
+ messages: ["action-onClicked", "cs-injected"],
+ after: {
+ attention: false,
+ state: TEMP_ACCESS,
+ disabled: false,
+ },
+ },
+ {
+ title: "mv3 with active scripts and no browser action",
+ manifest: {
+ manifest_version: 3,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ before: {
+ attention: true,
+ state: WHEN_CLICKED,
+ disabled: false,
+ },
+ messages: ["cs-injected"],
+ after: {
+ attention: false,
+ state: TEMP_ACCESS,
+ // TODO: This will need updating for bug 1807835.
+ disabled: false,
+ },
+ },
+ {
+ title: "mv3 with browser action and host_permission",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ before: {
+ attention: true,
+ state: WHEN_CLICKED,
+ disabled: false,
+ },
+ messages: ["action-onClicked"],
+ after: {
+ attention: false,
+ state: TEMP_ACCESS,
+ disabled: false,
+ },
+ },
+ {
+ title: "mv3 with browser action no host_permissions",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ before: {
+ attention: false,
+ state: NO_ACCESS,
+ disabled: false,
+ },
+ messages: ["action-onClicked"],
+ after: {
+ attention: false,
+ state: NO_ACCESS,
+ disabled: false,
+ },
+ },
+ // MV2 tests.
+ {
+ title: "mv2 with content scripts and browser action",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ before: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: false,
+ },
+ messages: ["action-onClicked", "cs-injected"],
+ after: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: false,
+ },
+ },
+ {
+ title: "mv2 with content scripts and no browser action",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ before: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: true,
+ },
+ messages: ["cs-injected"],
+ after: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: true,
+ },
+ },
+ {
+ title: "mv2 with browser action and host_permission",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ before: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: false,
+ },
+ messages: ["action-onClicked"],
+ after: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: false,
+ },
+ },
+ {
+ title: "mv2 with browser action no host_permissions",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ },
+ before: {
+ attention: false,
+ state: NO_ACCESS,
+ disabled: false,
+ },
+ messages: ["action-onClicked"],
+ after: {
+ attention: false,
+ state: NO_ACCESS,
+ disabled: false,
+ },
+ },
+ ];
+
+ let count = 1;
+ await Promise.all(
+ TEST_CASES.map(test => {
+ let id = `test-temp-access-${count++}@ext`;
+ test.extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: test.title,
+ browser_specific_settings: { gecko: { id } },
+ ...test.manifest,
+ },
+ files: {
+ "popup.html": "",
+ "script.js"() {
+ browser.test.sendMessage("cs-injected");
+ },
+ },
+ background() {
+ let action = browser.action ?? browser.browserAction;
+ action?.onClicked.addListener(() => {
+ browser.test.sendMessage("action-onClicked");
+ });
+ },
+ useAddonManager: "temporary",
+ });
+
+ return test.extension.startup();
+ })
+ );
+
+ async function checkButton(extension, expect, click = false) {
+ await openExtensionsPanel();
+
+ let item = getUnifiedExtensionsItem(extension.id);
+ ok(item, `Expected item for ${extension.id}.`);
+
+ let state = item.querySelector(".unified-extensions-item-message-default");
+ ok(state, "Expected a default state message element.");
+
+ is(
+ item.hasAttribute("attention"),
+ !!expect.attention,
+ "Expected attention badge."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(state),
+ expect.state,
+ "Expected l10n attributes for the message."
+ );
+
+ let button = item.querySelector(".unified-extensions-item-action-button");
+ is(button.disabled, !!expect.disabled, "Expect disabled item.");
+
+ // If we should click, and button is not disabled.
+ if (click && !expect.disabled) {
+ let onClick = BrowserTestUtils.waitForEvent(button, "click");
+ button.click();
+ await onClick;
+ } else {
+ // Otherwise, just close the panel.
+ await closeExtensionsPanel();
+ }
+ }
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "https://example.com/" },
+ async () => {
+ for (let { title, extension, before, messages, after } of TEST_CASES) {
+ info(`Test case: ${title}`);
+ await checkButton(extension, before, true);
+
+ await Promise.all(
+ messages.map(msg => {
+ info(`Waiting for ${msg} from clicking the button.`);
+ return extension.awaitMessage(msg);
+ })
+ );
+
+ await checkButton(extension, after);
+ await extension.unload();
+ }
+ }
+ );
+});
+
+add_task(
+ async function test_action_and_menu_buttons_css_class_with_new_window() {
+ const [extension] = createExtensions([
+ {
+ name: "an extension placed in the extensions panel",
+ browser_action: {
+ default_area: "menupanel",
+ },
+ },
+ ]);
+ await extension.startup();
+
+ let aSecondWindow = await BrowserTestUtils.openNewBrowserWindow();
+ await ensureMaximizedWindow(aSecondWindow);
+
+ // Open and close the extensions panel in the newly created window to build
+ // the extensions panel and add the extension widget(s) to it.
+ await openExtensionsPanel(aSecondWindow);
+ await closeExtensionsPanel(aSecondWindow);
+
+ for (const { title, win } of [
+ { title: "current window", win: window },
+ { title: "second window", win: aSecondWindow },
+ ]) {
+ const node = CustomizableUI.getWidget(
+ AppUiTestInternals.getBrowserActionWidgetId(extension.id)
+ ).forWindow(win).node;
+
+ let actionButton = node.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ `${title} - expected .subviewbutton CSS class on the action button`
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ `${title} - expected no .toolbarbutton-1 CSS class on the action button`
+ );
+ let menuButton = node.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ `${title} - expected .subviewbutton CSS class on the menu button`
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ `${title} - expected no .toolbarbutton-1 CSS class on the menu button`
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(aSecondWindow);
+
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js b/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js
new file mode 100644
index 0000000000..44d861d97c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js
@@ -0,0 +1,302 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+add_task(async function test_keyboard_navigation_activeScript() {
+ const extension1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ name: "1",
+ content_scripts: [
+ {
+ matches: ["*://*/*"],
+ js: ["script.js"],
+ },
+ ],
+ },
+ files: {
+ "script.js": () => {
+ browser.test.fail("this script should NOT have been executed");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ const extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ name: "2",
+ content_scripts: [
+ {
+ matches: ["*://*/*"],
+ js: ["script.js"],
+ },
+ ],
+ },
+ files: {
+ "script.js": () => {
+ browser.test.sendMessage("script executed");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "https://example.org/"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ await Promise.all([extension1.startup(), extension2.startup()]);
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ let item = getUnifiedExtensionsItem(extension1.id);
+ ok(item, `expected item for ${extension1.id}`);
+
+ info("moving focus to first item in the unified extensions panel");
+ let actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ let focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+ is(
+ actionButton,
+ document.activeElement,
+ "expected action button of first extension item to be focused"
+ );
+
+ item = getUnifiedExtensionsItem(extension2.id);
+ ok(item, `expected item for ${extension2.id}`);
+
+ info("moving focus to second item in the unified extensions panel");
+ actionButton = item.querySelector(".unified-extensions-item-action-button");
+ focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ await focused;
+ is(
+ actionButton,
+ document.activeElement,
+ "expected action button of second extension item to be focused"
+ );
+
+ info("granting permission");
+ const popupHidden = BrowserTestUtils.waitForEvent(
+ document,
+ "popuphidden",
+ true
+ );
+ EventUtils.synthesizeKey(" ", {});
+ await Promise.all([popupHidden, extension2.awaitMessage("script executed")]);
+
+ await Promise.all([extension1.unload(), extension2.unload()]);
+});
+
+add_task(async function test_keyboard_navigation_opens_menu() {
+ const extension1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "1",
+ // activeTab and browser_action needed to enable the action button in mv2.
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ useAddonManager: "temporary",
+ });
+ const extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "2",
+ },
+ useAddonManager: "temporary",
+ });
+ const extension3 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ name: "3",
+ // activeTab enables the action button without a browser action in mv3.
+ permissions: ["activeTab"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension1.startup();
+ await extension2.startup();
+ await extension3.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ let item = getUnifiedExtensionsItem(extension1.id);
+ ok(item, `expected item for ${extension1.id}`);
+
+ let messageDeck = item.querySelector(".unified-extensions-item-message-deck");
+ ok(messageDeck, "expected a message deck element");
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ info("moving focus to first item in the unified extensions panel");
+ let actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ let focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+ is(
+ actionButton,
+ document.activeElement,
+ "expected action button of the first extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ info(
+ "moving focus to menu button of the first item in the unified extensions panel"
+ );
+ let menuButton = item.querySelector(".unified-extensions-item-menu-button");
+ focused = BrowserTestUtils.waitForEvent(menuButton, "focus");
+ ok(menuButton, "expected menu button");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+ is(
+ menuButton,
+ document.activeElement,
+ "expected menu button in first extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when hovering the menu button"
+ );
+
+ info("opening menu of the first item");
+ const contextMenu = document.getElementById(
+ "unified-extensions-context-menu"
+ );
+ ok(contextMenu, "expected menu");
+ const shown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeKey(" ", {});
+ await shown;
+
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ info("moving focus back to the action button of the first item");
+ focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ await focused;
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ // Moving to the third extension directly because the second extension cannot
+ // do anything on the current page and its action button is disabled. Note
+ // that this third extension does not have a browser action but it has
+ // "activeTab", which makes the extension "clickable". This allows us to
+ // verify the focus/blur behavior of custom elments.
+ info("moving focus to third item in the panel");
+ item = getUnifiedExtensionsItem(extension3.id);
+ ok(item, `expected item for ${extension3.id}`);
+ actionButton = item.querySelector(".unified-extensions-item-action-button");
+ ok(actionButton, `expected action button for ${extension3.id}`);
+ messageDeck = item.querySelector(".unified-extensions-item-message-deck");
+ ok(messageDeck, `expected message deck for ${extension3.id}`);
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+ // Now that we checked everything on this third extension, let's actually
+ // focus it with the arrow down key.
+ focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ await focused;
+ is(
+ actionButton,
+ document.activeElement,
+ "expected action button of the third extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ info(
+ "moving focus to menu button of the third item in the unified extensions panel"
+ );
+ menuButton = item.querySelector(".unified-extensions-item-menu-button");
+ focused = BrowserTestUtils.waitForEvent(menuButton, "focus");
+ ok(menuButton, "expected menu button");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+ is(
+ menuButton,
+ document.activeElement,
+ "expected menu button in third extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when hovering the menu button"
+ );
+
+ info("moving focus back to the action button of the third item");
+ focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ await focused;
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ await closeExtensionsPanel();
+
+ await extension1.unload();
+ await extension2.unload();
+ await extension3.unload();
+});
+
+add_task(async function test_open_panel_with_keyboard_navigation() {
+ const { button, panel } = gUnifiedExtensions;
+ ok(button, "expected button");
+ ok(panel, "expected panel");
+
+ const listView = getListView();
+ ok(listView, "expected list view");
+
+ // Force focus on the unified extensions button.
+ const forceFocusUnifiedExtensionsButton = () => {
+ button.setAttribute("tabindex", "-1");
+ button.focus();
+ button.removeAttribute("tabindex");
+ };
+ forceFocusUnifiedExtensionsButton();
+
+ // Use the "space" key to open the panel.
+ let viewShown = BrowserTestUtils.waitForEvent(listView, "ViewShown");
+ EventUtils.synthesizeKey(" ", {});
+ await viewShown;
+
+ await closeExtensionsPanel();
+
+ // Force focus on the unified extensions button again.
+ forceFocusUnifiedExtensionsButton();
+
+ // Use the "return" key to open the panel.
+ viewShown = BrowserTestUtils.waitForEvent(listView, "ViewShown");
+ EventUtils.synthesizeKey("KEY_Enter", {});
+ await viewShown;
+
+ await closeExtensionsPanel();
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js b/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js
new file mode 100644
index 0000000000..d04d85e535
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js
@@ -0,0 +1,1006 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
+});
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+loadTestSubscript("head_unified_extensions.js");
+
+// We expect this rejection when the abuse report dialog window is
+// being forcefully closed as part of the related test task.
+PromiseTestUtils.allowMatchingRejectionsGlobally(/report dialog closed/);
+
+const promiseExtensionUninstalled = extensionId => {
+ return new Promise(resolve => {
+ let listener = {};
+ listener.onUninstalled = addon => {
+ if (addon.id == extensionId) {
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ });
+};
+
+function waitClosedWindow(win) {
+ return new Promise(resolve => {
+ function onWindowClosed() {
+ if (win && !win.closed) {
+ // If a specific window reference has been passed, then check
+ // that the window is closed before resolving the promise.
+ return;
+ }
+ Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
+ resolve();
+ }
+ Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
+ });
+}
+
+function assertVisibleContextMenuItems(contextMenu, expected) {
+ let visibleItems = contextMenu.querySelectorAll(
+ ":is(menuitem, menuseparator):not([hidden])"
+ );
+ is(visibleItems.length, expected, `expected ${expected} visible menu items`);
+}
+
+function assertOrderOfWidgetsInPanel(extensions, win = window) {
+ const widgetIds = CustomizableUI.getWidgetIdsInArea(
+ CustomizableUI.AREA_ADDONS
+ ).filter(
+ widgetId => !!CustomizableUI.getWidget(widgetId).forWindow(win).node
+ );
+ const widgetIdsFromExtensions = extensions.map(ext =>
+ AppUiTestInternals.getBrowserActionWidgetId(ext.id)
+ );
+
+ Assert.deepEqual(
+ widgetIds,
+ widgetIdsFromExtensions,
+ "expected extensions to be ordered"
+ );
+}
+
+async function moveWidgetUp(extension, win = window) {
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id, win);
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(
+ contextMenu.querySelector(".unified-extensions-context-menu-move-widget-up")
+ );
+ await hidden;
+}
+
+async function moveWidgetDown(extension, win = window) {
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id, win);
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(
+ contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-down"
+ )
+ );
+ await hidden;
+}
+
+async function pinToToolbar(extension, win = window) {
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id, win);
+ const pinToToolbarItem = contextMenu.querySelector(
+ ".unified-extensions-context-menu-pin-to-toolbar"
+ );
+ ok(pinToToolbarItem, "expected 'pin to toolbar' menu item");
+
+ const hidden = BrowserTestUtils.waitForEvent(
+ win.gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ contextMenu.activateItem(pinToToolbarItem);
+ await hidden;
+}
+
+async function assertMoveContextMenuItems(
+ ext,
+ { expectMoveUpHidden, expectMoveDownHidden, expectOrder },
+ win = window
+) {
+ const extName = WebExtensionPolicy.getByID(ext.id).name;
+ info(`Assert Move context menu items visibility for ${extName}`);
+ const contextMenu = await openUnifiedExtensionsContextMenu(ext.id, win);
+ const moveUp = contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-up"
+ );
+ const moveDown = contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-down"
+ );
+ ok(moveUp, "expected 'move up' item in the context menu");
+ ok(moveDown, "expected 'move down' item in the context menu");
+
+ is(
+ BrowserTestUtils.isHidden(moveUp),
+ expectMoveUpHidden,
+ `expected 'move up' item to be ${expectMoveUpHidden ? "hidden" : "visible"}`
+ );
+ is(
+ BrowserTestUtils.isHidden(moveDown),
+ expectMoveDownHidden,
+ `expected 'move down' item to be ${
+ expectMoveDownHidden ? "hidden" : "visible"
+ }`
+ );
+ const expectedVisibleItems =
+ 5 + (+(expectMoveUpHidden ? 0 : 1) + (expectMoveDownHidden ? 0 : 1));
+ assertVisibleContextMenuItems(contextMenu, expectedVisibleItems);
+ if (expectOrder) {
+ assertOrderOfWidgetsInPanel(expectOrder, win);
+ }
+ await closeChromeContextMenu(contextMenu.id, null, win);
+}
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.abuseReport.enabled", true],
+ [
+ "extensions.abuseReport.amoFormURL",
+ "https://example.org/%LOCALE%/%APP%/feedback/addon/%addonID%/",
+ ],
+ ],
+ });
+});
+
+add_task(async function test_context_menu() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ // Get the menu button of the extension and verify the mouseover/mouseout
+ // behavior. We expect a help message (in the message deck) to be selected
+ // (and therefore displayed) when the menu button is hovered/focused.
+ const item = getUnifiedExtensionsItem(extension.id);
+ ok(item, "expected an item for the extension");
+
+ const messageDeck = item.querySelector(
+ ".unified-extensions-item-message-deck"
+ );
+ ok(messageDeck, "expected message deck");
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ const hoverMenuButtonMessage = item.querySelector(
+ ".unified-extensions-item-message-hover-menu-button"
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(hoverMenuButtonMessage),
+ { id: "unified-extensions-item-message-manage", args: null },
+ "expected correct l10n attributes for the hover message"
+ );
+
+ const menuButton = item.querySelector(".unified-extensions-item-menu-button");
+ ok(menuButton, "expected menu button");
+
+ let hovered = BrowserTestUtils.waitForEvent(menuButton, "mouseover");
+ EventUtils.synthesizeMouseAtCenter(menuButton, { type: "mouseover" });
+ await hovered;
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when hovering the menu button"
+ );
+
+ let notHovered = BrowserTestUtils.waitForEvent(menuButton, "mouseout");
+ // Move mouse somewhere else...
+ EventUtils.synthesizeMouseAtCenter(item, { type: "mouseover" });
+ await notHovered;
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ // Open the context menu for the extension.
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+ const doc = contextMenu.ownerDocument;
+
+ const manageButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-manage-extension"
+ );
+ ok(manageButton, "expected manage button");
+ is(manageButton.hidden, false, "expected manage button to be visible");
+ is(manageButton.disabled, false, "expected manage button to be enabled");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(manageButton),
+ { id: "unified-extensions-context-menu-manage-extension", args: null },
+ "expected correct l10n attributes for manage button"
+ );
+
+ const removeButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-remove-extension"
+ );
+ ok(removeButton, "expected remove button");
+ is(removeButton.hidden, false, "expected remove button to be visible");
+ is(removeButton.disabled, false, "expected remove button to be enabled");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(removeButton),
+ { id: "unified-extensions-context-menu-remove-extension", args: null },
+ "expected correct l10n attributes for remove button"
+ );
+
+ const reportButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-report-extension"
+ );
+ ok(reportButton, "expected report button");
+ is(reportButton.hidden, false, "expected report button to be visible");
+ is(reportButton.disabled, false, "expected report button to be enabled");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(reportButton),
+ { id: "unified-extensions-context-menu-report-extension", args: null },
+ "expected correct l10n attributes for report button"
+ );
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_context_menu_report_button_hidden_when_abuse_report_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.enabled", false]],
+ });
+
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel, then open the contextMenu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const reportButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-report-extension"
+ );
+ ok(reportButton, "expected report button");
+ is(reportButton.hidden, true, "expected report button to be hidden");
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+ }
+);
+
+add_task(
+ async function test_context_menu_remove_button_disabled_when_extension_cannot_be_uninstalled() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Extensions: {
+ Locked: [extension.id],
+ },
+ },
+ });
+
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const removeButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-remove-extension"
+ );
+ ok(removeButton, "expected remove button");
+ is(removeButton.disabled, true, "expected remove button to be disabled");
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ }
+);
+
+add_task(async function test_manage_extension() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots" },
+ async () => {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const manageButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-manage-extension"
+ );
+ ok(manageButton, "expected manage button");
+
+ // Click the "manage extension" context menu item, and wait until the menu is
+ // closed and about:addons is open.
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ const aboutAddons = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+ contextMenu.activateItem(manageButton);
+ const [aboutAddonsTab] = await Promise.all([aboutAddons, hidden]);
+
+ // Close the tab containing about:addons because we don't need it anymore.
+ BrowserTestUtils.removeTab(aboutAddonsTab);
+
+ await extension.unload();
+ }
+ );
+});
+
+add_task(async function test_report_extension() {
+ function runReportTest(extension) {
+ return BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const reportButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-report-extension"
+ );
+ ok(reportButton, "expected report button");
+
+ // Click the "report extension" context menu item, and wait until the menu is
+ // closed and about:addons is open with the "abuse report dialog".
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+
+ if (AbuseReporter.amoFormEnabled) {
+ const reportURL = Services.urlFormatter
+ .formatURLPref("extensions.abuseReport.amoFormURL")
+ .replace("%addonID%", extension.id);
+
+ const promiseReportTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ reportURL,
+ /* waitForLoad */ false,
+ // Do not expect it to be the next tab opened
+ /* waitForAnyTab */ true
+ );
+ contextMenu.activateItem(reportButton);
+ const [reportTab] = await Promise.all([promiseReportTab, hidden]);
+ // Remove the report tab and expect the selected tab
+ // to become the about:addons tab.
+ BrowserTestUtils.removeTab(reportTab);
+ if (AbuseReporter.amoFormEnabled) {
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:blank",
+ "Expect about:addons tab to have not been opened (amoFormEnabled=true)"
+ );
+ } else {
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:addons",
+ "Got about:addons tab selected (amoFormEnabled=false)"
+ );
+ }
+ return;
+ }
+
+ const abuseReportOpen = BrowserTestUtils.waitForCondition(
+ () => AbuseReporter.getOpenDialog(),
+ "wait for the abuse report dialog to have been opened"
+ );
+ contextMenu.activateItem(reportButton);
+ const [reportDialogWindow] = await Promise.all([abuseReportOpen, hidden]);
+
+ const reportDialogParams =
+ reportDialogWindow.arguments[0].wrappedJSObject;
+ is(
+ reportDialogParams.report.addon.id,
+ extension.id,
+ "abuse report dialog has the expected addon id"
+ );
+ is(
+ reportDialogParams.report.reportEntryPoint,
+ "unified_context_menu",
+ "abuse report dialog has the expected reportEntryPoint"
+ );
+
+ let promiseClosedWindow = waitClosedWindow();
+ reportDialogWindow.close();
+ // Wait for the report dialog window to be completely closed
+ // (to prevent an intermittent failure due to a race between
+ // the dialog window being closed and the test tasks that follows
+ // opening the unified extensions button panel to not lose the
+ // focus and be suddently closed before the task has done with
+ // its assertions, see Bug 1782304).
+ await promiseClosedWindow;
+ });
+ }
+
+ const [ext] = createExtensions([{ name: "an extension" }]);
+ await ext.startup();
+
+ info("Test report with amoFormEnabled=true");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amoFormEnabled", true]],
+ });
+ await runReportTest(ext);
+ await SpecialPowers.popPrefEnv();
+
+ info("Test report with amoFormEnabled=false");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amoFormEnabled", false]],
+ });
+ await runReportTest(ext);
+ await SpecialPowers.popPrefEnv();
+
+ await ext.unload();
+});
+
+add_task(async function test_remove_extension() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const removeButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-remove-extension"
+ );
+ ok(removeButton, "expected remove button");
+
+ // Set up a mock prompt service that returns 0 to indicate that the user
+ // pressed the OK button.
+ const { prompt } = Services;
+ const promptService = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx() {
+ return 0;
+ },
+ };
+ Services.prompt = promptService;
+ registerCleanupFunction(() => {
+ Services.prompt = prompt;
+ });
+
+ // Click the "remove extension" context menu item, and wait until the menu is
+ // closed and the extension is uninstalled.
+ const uninstalled = promiseExtensionUninstalled(extension.id);
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(removeButton);
+ await Promise.all([uninstalled, hidden]);
+
+ await extension.unload();
+ // Restore prompt service.
+ Services.prompt = prompt;
+});
+
+add_task(async function test_remove_extension_cancelled() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const removeButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-remove-extension"
+ );
+ ok(removeButton, "expected remove button");
+
+ // Set up a mock prompt service that returns 1 to indicate that the user
+ // refused to uninstall the extension.
+ const { prompt } = Services;
+ const promptService = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx() {
+ return 1;
+ },
+ };
+ Services.prompt = promptService;
+ registerCleanupFunction(() => {
+ Services.prompt = prompt;
+ });
+
+ // Click the "remove extension" context menu item, and wait until the menu is
+ // closed.
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(removeButton);
+ await hidden;
+
+ // Re-open the panel to make sure the extension is still there.
+ await openExtensionsPanel();
+ const item = getUnifiedExtensionsItem(extension.id);
+ is(
+ item.querySelector(".unified-extensions-item-name").textContent,
+ "an extension",
+ "expected extension to still be listed"
+ );
+ await closeExtensionsPanel();
+
+ await extension.unload();
+ // Restore prompt service.
+ Services.prompt = prompt;
+});
+
+add_task(async function test_open_context_menu_on_click() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ const button = getUnifiedExtensionsItem(extension.id).querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(button, "expected menu button");
+
+ const contextMenu = document.getElementById(
+ "unified-extensions-context-menu"
+ );
+ ok(contextMenu, "expected menu");
+
+ // Open the context menu with a "right-click".
+ const shown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(button, { type: "contextmenu" });
+ await shown;
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+});
+
+add_task(async function test_open_context_menu_with_keyboard() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ const button = getUnifiedExtensionsItem(extension.id).querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(button, "expected menu button");
+ // Make this button focusable because those (toolbar) buttons are only made
+ // focusable when a user is navigating with the keyboard, which isn't exactly
+ // what we are doing in this test.
+ button.setAttribute("tabindex", "-1");
+
+ const contextMenu = document.getElementById(
+ "unified-extensions-context-menu"
+ );
+ ok(contextMenu, "expected menu");
+
+ // Open the context menu by focusing the button and pressing the SPACE key.
+ let shown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ button.focus();
+ is(button, document.activeElement, "expected button to be focused");
+ EventUtils.synthesizeKey(" ", {});
+ await shown;
+
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ if (AppConstants.platform != "macosx") {
+ // Open the context menu by focusing the button and pressing the ENTER key.
+ // TODO(emilio): Maybe we should harmonize this behavior across platforms,
+ // we're inconsistent right now.
+ shown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ button.focus();
+ is(button, document.activeElement, "expected button to be focused");
+ EventUtils.synthesizeKey("KEY_Enter", {});
+ await shown;
+ await closeChromeContextMenu(contextMenu.id, null);
+ }
+
+ await closeExtensionsPanel();
+
+ await extension.unload();
+});
+
+add_task(async function test_context_menu_without_browserActionFor_global() {
+ const { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+ );
+ const { browserActionFor } = ExtensionParent.apiManager.global;
+ const cleanup = () => {
+ ExtensionParent.apiManager.global.browserActionFor = browserActionFor;
+ };
+ registerCleanupFunction(cleanup);
+ // This is needed to simulate the case where the browserAction API hasn't
+ // been loaded yet (since it is lazy-loaded). That could happen when only
+ // extensions without browser actions are installed. In which case, the
+ // `global.browserActionFor()` function would not be defined yet.
+ delete ExtensionParent.apiManager.global.browserActionFor;
+
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel and then the context menu for the extension that
+ // has been loaded above. We expect the context menu to be displayed and no
+ // error caused by the lack of `global.browserActionFor()`.
+ await openExtensionsPanel();
+ // This promise rejects with an error if the implementation does not handle
+ // the case where `global.browserActionFor()` is undefined.
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+ assertVisibleContextMenuItems(contextMenu, 3);
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+
+ cleanup();
+});
+
+add_task(async function test_page_action_context_menu() {
+ const extWithMenuPageAction = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {},
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create(
+ {
+ id: "some-menu-id",
+ title: "Click me!",
+ contexts: ["all"],
+ },
+ () => browser.test.sendMessage("menu-created")
+ );
+ },
+ useAddonManager: "temporary",
+ });
+ const extWithoutMenu1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "extension without any menu",
+ },
+ useAddonManager: "temporary",
+ });
+
+ const extensions = [extWithMenuPageAction, extWithoutMenu1];
+
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ await extWithMenuPageAction.awaitMessage("menu-created");
+
+ await openExtensionsPanel();
+
+ info("extension with page action and a menu");
+ // This extension declares a page action so its menu shouldn't be added to
+ // the unified extensions context menu.
+ let contextMenu = await openUnifiedExtensionsContextMenu(
+ extWithMenuPageAction.id
+ );
+ assertVisibleContextMenuItems(contextMenu, 3);
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ info("extension with no browser action and no menu");
+ // There is no context menu created by this extension, so there should only
+ // be 3 menu items corresponding to the default manage/remove/report items.
+ contextMenu = await openUnifiedExtensionsContextMenu(extWithoutMenu1.id);
+ assertVisibleContextMenuItems(contextMenu, 3);
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ await closeExtensionsPanel();
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+add_task(async function test_pin_to_toolbar() {
+ const [extension] = createExtensions([
+ { name: "an extension", browser_action: {} },
+ ]);
+ await extension.startup();
+
+ // Open the extension panel, then open the context menu for the extension and
+ // pin the extension to the toolbar.
+ await openExtensionsPanel();
+ await pinToToolbar(extension);
+
+ // Undo the 'pin to toolbar' action.
+ await CustomizableUI.reset();
+ await extension.unload();
+});
+
+add_task(async function test_contextmenu_command_closes_panel() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "an extension",
+ browser_action: {},
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create(
+ {
+ id: "some-menu-id",
+ title: "Click me!",
+ contexts: ["all"],
+ },
+ () => browser.test.sendMessage("menu-created")
+ );
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ await extension.awaitMessage("menu-created");
+
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const firstMenuItem = contextMenu.querySelector("menuitem");
+ is(
+ firstMenuItem?.getAttribute("label"),
+ "Click me!",
+ "expected custom menu item as first child"
+ );
+
+ const hidden = BrowserTestUtils.waitForEvent(
+ gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ contextMenu.activateItem(firstMenuItem);
+ await hidden;
+
+ await extension.unload();
+});
+
+add_task(async function test_contextmenu_reorder_extensions() {
+ const [ext1, ext2, ext3] = createExtensions([
+ { name: "ext1", browser_action: {} },
+ { name: "ext2", browser_action: {} },
+ { name: "ext3", browser_action: {} },
+ ]);
+ // Start the test extensions in sequence to reduce chance of
+ // intermittent failures when asserting the order of the
+ // entries in the panel in the rest of this test task.
+ await ext1.startup();
+ await ext2.startup();
+ await ext3.startup();
+
+ await openExtensionsPanel();
+
+ // First extension in the list should only have "Move Down".
+ await assertMoveContextMenuItems(ext1, {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: false,
+ });
+
+ // Second extension in the list should have "Move Up" and "Move Down".
+ await assertMoveContextMenuItems(ext2, {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: false,
+ });
+
+ // Third extension in the list should only have "Move Up".
+ await assertMoveContextMenuItems(ext3, {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: true,
+ expectOrder: [ext1, ext2, ext3],
+ });
+
+ // Let's move some extensions now. We'll start by moving ext1 down until it
+ // is positioned at the end of the list.
+ info("Move down ext1 action to the bottom of the list");
+ await moveWidgetDown(ext1);
+ assertOrderOfWidgetsInPanel([ext2, ext1, ext3]);
+ await moveWidgetDown(ext1);
+
+ // Verify that the extension 1 has the right context menu items now that it
+ // is located at the end of the list.
+ await assertMoveContextMenuItems(ext1, {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: true,
+ expectOrder: [ext2, ext3, ext1],
+ });
+
+ info("Move up ext1 action to the top of the list");
+ await moveWidgetUp(ext1);
+ assertOrderOfWidgetsInPanel([ext2, ext1, ext3]);
+
+ await moveWidgetUp(ext1);
+ assertOrderOfWidgetsInPanel([ext1, ext2, ext3]);
+
+ // Move the last extension up.
+ info("Move up ext3 action");
+ await moveWidgetUp(ext3);
+ assertOrderOfWidgetsInPanel([ext1, ext3, ext2]);
+
+ // Move the last extension up (again).
+ info("Move up ext2 action to the top of the list");
+ await moveWidgetUp(ext2);
+ assertOrderOfWidgetsInPanel([ext1, ext2, ext3]);
+
+ // Move the second extension up.
+ await moveWidgetUp(ext2);
+ assertOrderOfWidgetsInPanel([ext2, ext1, ext3]);
+
+ // Pin an extension to the toolbar, which should remove it from the panel.
+ info("Pin ext1 action to the toolbar");
+ await pinToToolbar(ext1);
+ await openExtensionsPanel();
+ assertOrderOfWidgetsInPanel([ext2, ext3]);
+ await closeExtensionsPanel();
+
+ await Promise.all([ext1.unload(), ext2.unload(), ext3.unload()]);
+ await CustomizableUI.reset();
+});
+
+add_task(async function test_contextmenu_only_one_widget() {
+ const [extension] = createExtensions([{ name: "ext1", browser_action: {} }]);
+ await extension.startup();
+
+ await openExtensionsPanel();
+ await assertMoveContextMenuItems(extension, {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: true,
+ });
+ await closeExtensionsPanel();
+
+ await extension.unload();
+ await CustomizableUI.reset();
+});
+
+add_task(
+ async function test_contextmenu_reorder_extensions_with_private_window() {
+ // We want a panel in private mode that looks like this one (ext2 is not
+ // allowed in PB mode):
+ //
+ // - ext1
+ // - ext3
+ //
+ // But if we ask CUI to list the widgets in the panel, it would list:
+ //
+ // - ext1
+ // - ext2
+ // - ext3
+ //
+ const ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "ext1",
+ browser_specific_settings: { gecko: { id: "ext1@reorder-private" } },
+ browser_action: {},
+ },
+ useAddonManager: "temporary",
+ incognitoOverride: "spanning",
+ });
+ await ext1.startup();
+
+ const ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "ext2",
+ browser_specific_settings: { gecko: { id: "ext2@reorder-private" } },
+ browser_action: {},
+ },
+ useAddonManager: "temporary",
+ incognitoOverride: "not_allowed",
+ });
+ await ext2.startup();
+
+ const ext3 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "ext3",
+ browser_specific_settings: { gecko: { id: "ext3@reorder-private" } },
+ browser_action: {},
+ },
+ useAddonManager: "temporary",
+ incognitoOverride: "spanning",
+ });
+ await ext3.startup();
+
+ // Make sure all extension widgets are in the correct order.
+ assertOrderOfWidgetsInPanel([ext1, ext2, ext3]);
+
+ const privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await openExtensionsPanel(privateWin);
+
+ // First extension in the list should only have "Move Down".
+ await assertMoveContextMenuItems(
+ ext1,
+ {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: false,
+ expectOrder: [ext1, ext3],
+ },
+ privateWin
+ );
+
+ // Second extension in the list (which is ext3) should only have "Move Up".
+ await assertMoveContextMenuItems(
+ ext3,
+ {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: true,
+ expectOrder: [ext1, ext3],
+ },
+ privateWin
+ );
+
+ // In private mode, we should only have two CUI widget nodes in the panel.
+ assertOrderOfWidgetsInPanel([ext1, ext3], privateWin);
+
+ info("Move ext1 down");
+ await moveWidgetDown(ext1, privateWin);
+ // The new order in a regular window should be:
+ assertOrderOfWidgetsInPanel([ext2, ext3, ext1]);
+ // ... while the order in the private window should be:
+ assertOrderOfWidgetsInPanel([ext3, ext1], privateWin);
+
+ // Verify that the extension 1 has the right context menu items now that it
+ // is located at the end of the list in PB mode.
+ await assertMoveContextMenuItems(
+ ext1,
+ {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: true,
+ expectOrder: [ext3, ext1],
+ },
+ privateWin
+ );
+
+ // Verify that the extension 3 has the right context menu items now that it
+ // is located at the top of the list in PB mode.
+ await assertMoveContextMenuItems(
+ ext3,
+ {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: false,
+ expectOrder: [ext3, ext1],
+ },
+ privateWin
+ );
+
+ info("Move ext3 extension down");
+ await moveWidgetDown(ext3, privateWin);
+ // The new order in a regular window should be:
+ assertOrderOfWidgetsInPanel([ext2, ext1, ext3]);
+ // ... while the order in the private window should be:
+ assertOrderOfWidgetsInPanel([ext1, ext3], privateWin);
+
+ // Pin an extension to the toolbar, which should remove it from the panel.
+ info("Pin ext1 to the toolbar");
+ await pinToToolbar(ext1, privateWin);
+ await openExtensionsPanel(privateWin);
+
+ // The new order in a regular window should be:
+ assertOrderOfWidgetsInPanel([ext2, ext3]);
+ await assertMoveContextMenuItems(
+ ext3,
+ {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: true,
+ // ... while the order in the private window should be:
+ expectOrder: [ext3],
+ },
+ privateWin
+ );
+
+ await closeExtensionsPanel(privateWin);
+
+ await Promise.all([ext1.unload(), ext2.unload(), ext3.unload()]);
+ await CustomizableUI.reset();
+
+ await BrowserTestUtils.closeWindow(privateWin);
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_cui.js b/browser/components/extensions/test/browser/browser_unified_extensions_cui.js
new file mode 100644
index 0000000000..dc02623452
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_cui.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+/**
+ * Tests that if the addons panel is somehow open when customization mode is
+ * invoked, that the panel is hidden.
+ */
+add_task(async function test_hide_panel_when_customizing() {
+ await openExtensionsPanel();
+
+ let panel = gUnifiedExtensions.panel;
+ Assert.equal(panel.state, "open");
+
+ let panelHidden = BrowserTestUtils.waitForPopupEvent(panel, "hidden");
+ CustomizableUI.dispatchToolboxEvent("customizationstarting", {});
+ await panelHidden;
+ Assert.equal(panel.state, "closed");
+ CustomizableUI.dispatchToolboxEvent("aftercustomization", {});
+});
+
+/**
+ * Tests that if a browser action is in a collapsed toolbar area, like the
+ * bookmarks toolbar, that its DOM node is overflowed in the extensions panel.
+ */
+add_task(async function test_extension_in_collapsed_area() {
+ const extensions = createExtensions(
+ [
+ {
+ name: "extension1",
+ browser_action: { default_area: "navbar", default_popup: "popup.html" },
+ browser_specific_settings: {
+ gecko: { id: "unified-extensions-cui@ext-1" },
+ },
+ },
+ {
+ name: "extension2",
+ browser_action: { default_area: "navbar" },
+ browser_specific_settings: {
+ gecko: { id: "unified-extensions-cui@ext-2" },
+ },
+ },
+ ],
+ {
+ files: {
+ "popup.html": `
+
+
+ test popup
+
+
+
+ `,
+ "popup.js": function () {
+ browser.test.sendMessage("test-popup-opened");
+ },
+ },
+ }
+ );
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ await openExtensionsPanel();
+ for (const extension of extensions) {
+ let item = getUnifiedExtensionsItem(extension.id);
+ Assert.ok(
+ !item,
+ `extension with ID=${extension.id} should not appear in the panel`
+ );
+ }
+ await closeExtensionsPanel();
+
+ // Move an extension to the bookmarks toolbar.
+ const bookmarksToolbar = document.getElementById(
+ CustomizableUI.AREA_BOOKMARKS
+ );
+ const firstExtensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensions[0].id
+ );
+ CustomizableUI.addWidgetToArea(
+ firstExtensionWidgetID,
+ CustomizableUI.AREA_BOOKMARKS
+ );
+
+ // Ensure that the toolbar is currently collapsed.
+ await promiseSetToolbarVisibility(bookmarksToolbar, false);
+
+ await openExtensionsPanel();
+ let item = getUnifiedExtensionsItem(extensions[0].id);
+ Assert.ok(
+ item,
+ "extension placed in the collapsed toolbar should appear in the panel"
+ );
+
+ // NOTE: ideally we would simply call `AppUiTestDelegate.clickBrowserAction()`
+ // but, unfortunately, that does internally call `showBrowserAction()`, which
+ // explicitly assert the group areaType that would hit a failure in this test
+ // because we are moving it to AREA_BOOKMARKS.
+ let widget = getBrowserActionWidget(extensions[0]).forWindow(window);
+ ok(widget, "Got a widget for the extension button overflowed into the panel");
+ widget.node.firstElementChild.click();
+
+ const promisePanelBrowser = AppUiTestDelegate.awaitExtensionPanel(
+ window,
+ extensions[0].id,
+ true
+ );
+ await extensions[0].awaitMessage("test-popup-opened");
+ const extPanelBrowser = await promisePanelBrowser;
+ ok(extPanelBrowser, "Got a action panel browser");
+ closeBrowserAction(extensions[0]);
+
+ // Now, make the toolbar visible.
+ await promiseSetToolbarVisibility(bookmarksToolbar, true);
+
+ await openExtensionsPanel();
+ for (const extension of extensions) {
+ let item = getUnifiedExtensionsItem(extension.id);
+ Assert.ok(
+ !item,
+ `extension with ID=${extension.id} should not appear in the panel`
+ );
+ }
+ await closeExtensionsPanel();
+
+ // Hide the bookmarks toolbar again.
+ await promiseSetToolbarVisibility(bookmarksToolbar, false);
+
+ await openExtensionsPanel();
+ item = getUnifiedExtensionsItem(extensions[0].id);
+ Assert.ok(item, "extension should reappear in the panel");
+ await closeExtensionsPanel();
+
+ // We now empty the bookmarks toolbar but we keep the extension widget.
+ for (const widgetId of CustomizableUI.getWidgetIdsInArea(
+ CustomizableUI.AREA_BOOKMARKS
+ ).filter(widgetId => widgetId !== firstExtensionWidgetID)) {
+ CustomizableUI.removeWidgetFromArea(widgetId);
+ }
+
+ // We make the bookmarks toolbar visible again. At this point, the extension
+ // widget should be re-inserted in this toolbar.
+ await promiseSetToolbarVisibility(bookmarksToolbar, true);
+
+ await openExtensionsPanel();
+ for (const extension of extensions) {
+ let item = getUnifiedExtensionsItem(extension.id);
+ Assert.ok(
+ !item,
+ `extension with ID=${extension.id} should not appear in the panel`
+ );
+ }
+ await closeExtensionsPanel();
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+ await CustomizableUI.reset();
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js b/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js
new file mode 100644
index 0000000000..8603928894
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+const verifyPermissionsPrompt = async expectedAnchorID => {
+ const ext = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "some search name",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ optional_permissions: ["history"],
+ },
+
+ background: () => {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "create-tab") {
+ return;
+ }
+
+ await browser.tabs.create({
+ url: browser.runtime.getURL("content.html"),
+ active: true,
+ });
+ });
+ },
+
+ files: {
+ "content.html": ``,
+ "content.js": async () => {
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.assertEq(
+ msg,
+ "grant-permission",
+ "expected message to grant permission"
+ );
+
+ const granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({ permissions: ["history"] })
+ );
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+
+ browser.test.sendMessage("ok");
+ });
+
+ browser.test.sendMessage("ready");
+ },
+ },
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ const defaultSearchPopupPromise = promisePopupNotificationShown(
+ "addon-webext-defaultsearch"
+ );
+ let [panel] = await Promise.all([defaultSearchPopupPromise, ext.startup()]);
+ ok(panel, "expected panel");
+ let notification = PopupNotifications.getNotification(
+ "addon-webext-defaultsearch"
+ );
+ ok(notification, "expected notification");
+ // We always want the defaultsearch popup to be anchored on the urlbar (the
+ // ID below) because the post-install popup would be displayed on top of
+ // this one otherwise, see Bug 1789407.
+ is(
+ notification?.anchorElement?.id,
+ "addons-notification-icon",
+ "expected the right anchor ID for the defaultsearch popup"
+ );
+ // Accept to override the search.
+ panel.button.click();
+ await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");
+
+ ext.sendMessage("create-tab");
+ await ext.awaitMessage("ready");
+
+ const popupPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ );
+ ext.sendMessage("grant-permission");
+ panel = await popupPromise;
+ ok(panel, "expected panel");
+ notification = PopupNotifications.getNotification(
+ "addon-webext-permissions"
+ );
+ ok(notification, "expected notification");
+ is(
+ // We access the parent element because the anchor is on the icon (inside
+ // the button), not on the unified extensions button itself.
+ notification.anchorElement.id ||
+ notification.anchorElement.parentElement.id,
+ expectedAnchorID,
+ "expected the right anchor ID"
+ );
+
+ panel.button.click();
+ await ext.awaitMessage("ok");
+
+ await ext.unload();
+ });
+};
+
+add_task(async function test_permissions_prompt() {
+ await verifyPermissionsPrompt("unified-extensions-button");
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_messages.js b/browser/components/extensions/test/browser/browser_unified_extensions_messages.js
new file mode 100644
index 0000000000..10ff3c9c73
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_messages.js
@@ -0,0 +1,222 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+const verifyMessageBar = message => {
+ Assert.equal(
+ message.getAttribute("type"),
+ "warning",
+ "expected warning message"
+ );
+ Assert.ok(
+ !message.hasAttribute("dismissable"),
+ "expected message to not be dismissable"
+ );
+
+ const supportLink = message.querySelector("a");
+ Assert.equal(
+ supportLink.getAttribute("support-page"),
+ "quarantined-domains",
+ "expected the correct support page ID"
+ );
+ Assert.equal(
+ supportLink.getAttribute("aria-label"),
+ "Learn more: Some extensions are not allowed",
+ "expected the correct aria-labelledby value"
+ );
+};
+
+add_task(async function test_quarantined_domain_message_disabled() {
+ const quarantinedDomain = "example.org";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.quarantinedDomains.enabled", false],
+ ["extensions.quarantinedDomains.list", quarantinedDomain],
+ ],
+ });
+
+ // Load an extension that will have access to all domains, including the
+ // quarantined domain.
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ });
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+ Assert.equal(getMessageBars().length, 0, "expected no message");
+ await closeExtensionsPanel();
+ }
+ );
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_quarantined_domain_message() {
+ const quarantinedDomain = "example.org";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.quarantinedDomains.enabled", true],
+ ["extensions.quarantinedDomains.list", quarantinedDomain],
+ ],
+ });
+
+ // Load an extension that will have access to all domains, including the
+ // quarantined domain.
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ });
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+
+ const [message] = messages;
+ verifyMessageBar(message);
+
+ await closeExtensionsPanel();
+ }
+ );
+
+ // Navigating to a different tab/domain shouldn't show any message.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `http://mochi.test:8888/` },
+ async () => {
+ await openExtensionsPanel();
+ Assert.equal(getMessageBars().length, 0, "expected no message");
+ await closeExtensionsPanel();
+ }
+ );
+
+ // Back to a quarantined domain, if we update the list, we expect the message
+ // to be gone when we re-open the panel (and not before because we don't
+ // listen to the pref currently).
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+
+ const [message] = messages;
+ verifyMessageBar(message);
+
+ await closeExtensionsPanel();
+
+ // Clear the list of quarantined domains.
+ Services.prefs.setStringPref("extensions.quarantinedDomains.list", "");
+
+ await openExtensionsPanel();
+ Assert.equal(getMessageBars().length, 0, "expected no message");
+ await closeExtensionsPanel();
+ }
+ );
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_quarantined_domain_message_learn_more_link() {
+ const quarantinedDomain = "example.org";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.quarantinedDomains.enabled", true],
+ ["extensions.quarantinedDomains.list", quarantinedDomain],
+ ],
+ });
+
+ // Load an extension that will have access to all domains, including the
+ // quarantined domain.
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ });
+ await extension.startup();
+
+ const expectedSupportURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "quarantined-domains";
+
+ // We expect the SUMO page to be open in a new tab and the panel to be closed
+ // when the user clicks on the "learn more" link.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+
+ const [message] = messages;
+ verifyMessageBar(message);
+
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ expectedSupportURL
+ );
+ const hidden = BrowserTestUtils.waitForEvent(
+ gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ message.querySelector("a").click();
+ const [tab] = await Promise.all([tabPromise, hidden]);
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+
+ // Same as above but with keyboard navigation.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+
+ const [message] = messages;
+ verifyMessageBar(message);
+
+ const supportLink = message.querySelector("a");
+
+ // Focus the "learn more" (support) link.
+ const focused = BrowserTestUtils.waitForEvent(supportLink, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ expectedSupportURL
+ );
+ const hidden = BrowserTestUtils.waitForEvent(
+ gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ EventUtils.synthesizeKey("KEY_Enter", {});
+ const [tab] = await Promise.all([tabPromise, hidden]);
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_overflowable_toolbar.js b/browser/components/extensions/test/browser/browser_unified_extensions_overflowable_toolbar.js
new file mode 100644
index 0000000000..9758a96636
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_overflowable_toolbar.js
@@ -0,0 +1,1389 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the behaviour of the overflowable nav-bar with Unified
+ * Extensions enabled and disabled.
+ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+requestLongerTimeout(2);
+
+const NUM_EXTENSIONS = 5;
+const OVERFLOW_WINDOW_WIDTH_PX = 450;
+const DEFAULT_WIDGET_IDS = [
+ "home-button",
+ "library-button",
+ "zoom-controls",
+ "search-container",
+ "sidebar-button",
+];
+const OVERFLOWED_EXTENSIONS_LIST_ID = "overflowed-extensions-list";
+
+add_setup(async function () {
+ // To make it easier to control things that will overflow, we'll start by
+ // removing that's removable out of the nav-bar and adding just a fixed
+ // set of items (DEFAULT_WIDGET_IDS) at the end of the nav-bar.
+ let existingWidgetIDs = CustomizableUI.getWidgetIdsInArea(
+ CustomizableUI.AREA_NAVBAR
+ );
+ for (let widgetID of existingWidgetIDs) {
+ if (CustomizableUI.isWidgetRemovable(widgetID)) {
+ CustomizableUI.removeWidgetFromArea(widgetID);
+ }
+ }
+ for (const widgetID of DEFAULT_WIDGET_IDS) {
+ CustomizableUI.addWidgetToArea(widgetID, CustomizableUI.AREA_NAVBAR);
+ }
+
+ registerCleanupFunction(async () => {
+ await CustomizableUI.reset();
+ });
+});
+
+/**
+ * Returns the IDs of the children of parent.
+ *
+ * @param {Element} parent
+ * @returns {string[]} the IDs of the children
+ */
+function getChildrenIDs(parent) {
+ return Array.from(parent.children).map(child => child.id);
+}
+
+/**
+ * Returns a NodeList of all non-hidden menu, menuitem and menuseparators
+ * that are direct descendants of popup.
+ *
+ * @param {Element} popup
+ * @returns {NodeList} the visible items.
+ */
+function getVisibleMenuItems(popup) {
+ return popup.querySelectorAll(
+ ":scope > :is(menu, menuitem, menuseparator):not([hidden])"
+ );
+}
+
+/**
+ * This helper function does most of the heavy lifting for these tests.
+ * It does the following in order:
+ *
+ * 1. Registers and enables NUM_EXTENSIONS test WebExtensions that add
+ * browser_action buttons to the nav-bar.
+ * 2. Resizes the window to force things after the URL bar to overflow.
+ * 3. Calls an async test function to analyze the overflow lists.
+ * 4. Restores the window's original width, ensuring that the IDs of the
+ * nav-bar match the original set.
+ * 5. Unloads all of the test WebExtensions
+ *
+ * @param {DOMWindow} win The browser window to perform the test on.
+ * @param {object} options Additional options when running this test.
+ * @param {Function} options.beforeOverflowed This optional async function will
+ * be run after the extensions are created and added to the toolbar, but
+ * before the toolbar overflows. The function is called with the following
+ * arguments:
+ *
+ * {string[]} extensionIDs: The IDs of the test WebExtensions.
+ *
+ * The return value of the function is ignored.
+ * @param {Function} options.whenOverflowed This optional async function will
+ * run once the window is in the overflow state. The function is called
+ * with the following arguments:
+ *
+ * {Element} defaultList: The DOM element that holds overflowed default
+ * items.
+ * {Element} unifiedExtensionList: The DOM element that holds overflowed
+ * WebExtension browser_actions when Unified Extensions is enabled.
+ * {string[]} extensionIDs: The IDs of the test WebExtensions.
+ *
+ * The return value of the function is ignored.
+ * @param {Function} options.afterUnderflowed This optional async function will
+ * be run after the window is expanded and the toolbar has underflowed, but
+ * before the extensions are removed. This function is not passed any
+ * arguments. The return value of the function is ignored.
+ *
+ */
+async function withWindowOverflowed(
+ win,
+ {
+ beforeOverflowed = async () => {},
+ whenOverflowed = async () => {},
+ afterUnderflowed = async () => {},
+ } = {}
+) {
+ const doc = win.document;
+ doc.documentElement.removeAttribute("persist");
+ const navbar = doc.getElementById(CustomizableUI.AREA_NAVBAR);
+
+ await ensureMaximizedWindow(win);
+
+ // The OverflowableToolbar operates asynchronously at times, so we will
+ // poll a widget's overflowedItem attribute to detect whether or not the
+ // widgets have finished being moved. We'll use the first widget that
+ // we added to the nav-bar, as this should be the left-most item in the
+ // set that we added.
+ const signpostWidgetID = "home-button";
+ // We'll also force the signpost widget to be extra-wide to ensure that it
+ // overflows after we shrink the window.
+ CustomizableUI.getWidget(signpostWidgetID).forWindow(win).node.style =
+ "width: 150px";
+
+ const extWithMenuBrowserAction = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Extension #0",
+ browser_specific_settings: {
+ gecko: { id: "unified-extensions-overflowable-toolbar@ext-0" },
+ },
+ browser_action: {
+ default_area: "navbar",
+ },
+ // We pass `activeTab` to have a different permission message when
+ // hovering the primary/action button.
+ permissions: ["activeTab", "contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create(
+ {
+ id: "some-menu-id",
+ title: "Click me!",
+ contexts: ["all"],
+ },
+ () => browser.test.sendMessage("menu-created")
+ );
+ },
+ useAddonManager: "temporary",
+ });
+
+ const extWithSubMenuBrowserAction = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Extension #1",
+ browser_specific_settings: {
+ gecko: { id: "unified-extensions-overflowable-toolbar@ext-1" },
+ },
+ browser_action: {
+ default_area: "navbar",
+ },
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create({
+ id: "some-menu-id",
+ title: "Open sub-menu",
+ contexts: ["all"],
+ });
+ browser.contextMenus.create(
+ {
+ id: "some-sub-menu-id",
+ parentId: "some-menu-id",
+ title: "Click me!",
+ contexts: ["all"],
+ },
+ () => browser.test.sendMessage("menu-created")
+ );
+ },
+ useAddonManager: "temporary",
+ });
+
+ const manifests = [];
+ for (let i = 2; i < NUM_EXTENSIONS; ++i) {
+ manifests.push({
+ name: `Extension #${i}`,
+ browser_action: {
+ default_area: "navbar",
+ },
+ browser_specific_settings: {
+ gecko: { id: `unified-extensions-overflowable-toolbar@ext-${i}` },
+ },
+ });
+ }
+
+ const extensions = [
+ extWithMenuBrowserAction,
+ extWithSubMenuBrowserAction,
+ ...createExtensions(manifests),
+ ];
+
+ // Adding browser actions is asynchronous, so this CustomizableUI listener
+ // is used to make sure that the browser action widgets have finished getting
+ // added.
+ let listener = {
+ _remainingBrowserActions: NUM_EXTENSIONS,
+ _deferred: Promise.withResolvers(),
+
+ get promise() {
+ return this._deferred.promise;
+ },
+
+ onWidgetAdded(widgetID, area) {
+ if (widgetID.endsWith("-browser-action")) {
+ this._remainingBrowserActions--;
+ }
+ if (!this._remainingBrowserActions) {
+ this._deferred.resolve();
+ }
+ },
+ };
+ CustomizableUI.addListener(listener);
+ // Start all the extensions sequentially.
+ for (const extension of extensions) {
+ await extension.startup();
+ }
+ await Promise.all([
+ extWithMenuBrowserAction.awaitMessage("menu-created"),
+ extWithSubMenuBrowserAction.awaitMessage("menu-created"),
+ ]);
+ await listener.promise;
+ CustomizableUI.removeListener(listener);
+
+ const extensionIDs = extensions.map(extension => extension.id);
+
+ try {
+ info("Running beforeOverflowed task");
+ await beforeOverflowed(extensionIDs);
+ } finally {
+ // The beforeOverflowed task may have moved some items out from the navbar,
+ // so only listen for overflows for items still in there.
+ const browserActionIDs = extensionIDs.map(id =>
+ AppUiTestInternals.getBrowserActionWidgetId(id)
+ );
+ const browserActionsInNavBar = browserActionIDs.filter(widgetID => {
+ let placement = CustomizableUI.getPlacementOfWidget(widgetID);
+ return placement.area == CustomizableUI.AREA_NAVBAR;
+ });
+
+ let widgetOverflowListener = {
+ _remainingOverflowables:
+ browserActionsInNavBar.length + DEFAULT_WIDGET_IDS.length,
+ _deferred: Promise.withResolvers(),
+
+ get promise() {
+ return this._deferred.promise;
+ },
+
+ onWidgetOverflow(widgetNode, areaNode) {
+ this._remainingOverflowables--;
+ if (!this._remainingOverflowables) {
+ this._deferred.resolve();
+ }
+ },
+ };
+ CustomizableUI.addListener(widgetOverflowListener);
+
+ win.resizeTo(OVERFLOW_WINDOW_WIDTH_PX, win.outerHeight);
+ await widgetOverflowListener.promise;
+ CustomizableUI.removeListener(widgetOverflowListener);
+
+ Assert.ok(
+ navbar.hasAttribute("overflowing"),
+ "Should have an overflowing toolbar."
+ );
+
+ const defaultList = doc.getElementById(
+ navbar.getAttribute("default-overflowtarget")
+ );
+
+ const unifiedExtensionList = doc.getElementById(
+ navbar.getAttribute("addon-webext-overflowtarget")
+ );
+
+ try {
+ info("Running whenOverflowed task");
+ await whenOverflowed(defaultList, unifiedExtensionList, extensionIDs);
+ } finally {
+ await ensureMaximizedWindow(win);
+
+ // Notably, we don't wait for the nav-bar to not have the "overflowing"
+ // attribute. This is because we might be running in an environment
+ // where the nav-bar was overflowing to begin with. Let's just hope that
+ // our sign-post widget has stopped overflowing.
+ await TestUtils.waitForCondition(() => {
+ return !doc
+ .getElementById(signpostWidgetID)
+ .hasAttribute("overflowedItem");
+ });
+
+ try {
+ info("Running afterUnderflowed task");
+ await afterUnderflowed();
+ } finally {
+ await Promise.all(extensions.map(extension => extension.unload()));
+ }
+ }
+ }
+}
+
+async function verifyExtensionWidget(widget, win = window) {
+ Assert.ok(widget, "expected widget");
+
+ let actionButton = widget.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ Assert.ok(
+ actionButton.classList.contains("unified-extensions-item-action-button"),
+ "expected action class on the button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+
+ let menuButton = widget.lastElementChild;
+ Assert.ok(
+ menuButton.classList.contains("unified-extensions-item-menu-button"),
+ "expected class on the button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+
+ let contents = actionButton.querySelector(
+ ".unified-extensions-item-contents"
+ );
+
+ Assert.ok(contents, "expected contents element");
+ // This is needed to correctly position the contents (vbox) element in the
+ // toolbarbutton.
+ Assert.equal(
+ contents.getAttribute("move-after-stack"),
+ "true",
+ "expected move-after-stack attribute to be set"
+ );
+ // Make sure the contents element is inserted after the stack one (which is
+ // automagically created by the toolbarbutton element).
+ Assert.deepEqual(
+ Array.from(actionButton.childNodes.values()).map(
+ child => child.classList[0]
+ ),
+ [
+ // The stack (which contains the extension icon) should be the first
+ // child.
+ "toolbarbutton-badge-stack",
+ // This is the widget label, which is hidden with CSS.
+ "toolbarbutton-text",
+ // This is the contents element, which displays the extension name and
+ // messages.
+ "unified-extensions-item-contents",
+ ],
+ "expected the correct order for the children of the action button"
+ );
+
+ let name = contents.querySelector(".unified-extensions-item-name");
+ Assert.ok(name, "expected name element");
+ Assert.ok(
+ name.textContent.startsWith("Extension "),
+ "expected name to not be empty"
+ );
+ Assert.ok(
+ contents.querySelector(".unified-extensions-item-message-default"),
+ "expected message default element"
+ );
+ Assert.ok(
+ contents.querySelector(".unified-extensions-item-message-hover"),
+ "expected message hover element"
+ );
+
+ Assert.equal(
+ win.document.l10n.getAttributes(menuButton).id,
+ "unified-extensions-item-open-menu",
+ "expected l10n id attribute for the extension"
+ );
+ Assert.deepEqual(
+ Object.keys(win.document.l10n.getAttributes(menuButton).args),
+ ["extensionName"],
+ "expected l10n args attribute for the extension"
+ );
+ Assert.ok(
+ win.document.l10n
+ .getAttributes(menuButton)
+ .args.extensionName.startsWith("Extension "),
+ "expected l10n args attribute to start with the correct name"
+ );
+ Assert.ok(
+ menuButton.getAttribute("aria-label") !== "",
+ "expected menu button to have non-empty localized content"
+ );
+}
+
+/**
+ * Tests that overflowed browser actions go to the Unified Extensions
+ * panel, and default toolbar items go into the default overflow
+ * panel.
+ */
+add_task(async function test_overflowable_toolbar() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let movedNode;
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ // Ensure that there are 5 items in the Unified Extensions overflow
+ // list, and the default widgets should all be in the default overflow
+ // list (though there might be more items from the nav-bar in there that
+ // already existed in the nav-bar before we put the default widgets in
+ // there as well).
+ let defaultListIDs = getChildrenIDs(defaultList);
+ for (const widgetID of DEFAULT_WIDGET_IDS) {
+ Assert.ok(
+ defaultListIDs.includes(widgetID),
+ `Default overflow list should have ${widgetID}`
+ );
+ }
+
+ Assert.ok(
+ unifiedExtensionList.children.length,
+ "Should have items in the Unified Extension list."
+ );
+
+ for (const child of Array.from(unifiedExtensionList.children)) {
+ Assert.ok(
+ extensionIDs.includes(child.dataset.extensionid),
+ `Unified Extensions overflow list should have ${child.dataset.extensionid}`
+ );
+ await verifyExtensionWidget(child, win);
+ }
+
+ let extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensionIDs.at(-1)
+ );
+ movedNode =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node;
+ Assert.equal(movedNode.getAttribute("cui-areatype"), "toolbar");
+
+ CustomizableUI.addWidgetToArea(
+ extensionWidgetID,
+ CustomizableUI.AREA_ADDONS
+ );
+
+ Assert.equal(
+ movedNode.getAttribute("cui-areatype"),
+ "panel",
+ "The moved browser action button should have the right cui-areatype set."
+ );
+ },
+ afterUnderflowed: async () => {
+ // Ensure that the moved node's parent is still the add-ons panel.
+ Assert.equal(
+ movedNode.parentElement.id,
+ CustomizableUI.AREA_ADDONS,
+ "The browser action should still be in the addons panel"
+ );
+ CustomizableUI.addWidgetToArea(movedNode.id, CustomizableUI.AREA_NAVBAR);
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_context_menu() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ Assert.ok(
+ unifiedExtensionList.children.length,
+ "Should have items in the Unified Extension list."
+ );
+
+ // Open the extension panel.
+ await openExtensionsPanel(win);
+
+ // Let's verify the context menus for the following extensions:
+ //
+ // - the first one defines a menu in the background script
+ // - the second one defines a menu with submenu
+ // - the third extension has no menu
+
+ info("extension with browser action and a menu");
+ const firstExtensionWidget = unifiedExtensionList.children[0];
+ Assert.ok(firstExtensionWidget, "expected extension widget");
+ let contextMenu = await openUnifiedExtensionsContextMenu(
+ firstExtensionWidget.dataset.extensionid,
+ win
+ );
+ Assert.ok(contextMenu, "expected a context menu");
+ let visibleItems = getVisibleMenuItems(contextMenu);
+
+ // The context menu for the extension that declares a browser action menu
+ // should have the menu item created by the extension, a menu separator, the control
+ // for pinning the browser action to the toolbar, a menu separator and the 3 default menu items.
+ is(
+ visibleItems.length,
+ 7,
+ "expected a custom context menu item, a menu separator, the pin to " +
+ "toolbar menu item, a menu separator, and the 3 default menu items"
+ );
+
+ const [item, separator] = visibleItems;
+ is(
+ item.getAttribute("label"),
+ "Click me!",
+ "expected menu item as first child"
+ );
+ is(
+ separator.tagName,
+ "menuseparator",
+ "expected separator after last menu item created by the extension"
+ );
+
+ await closeChromeContextMenu(contextMenu.id, null, win);
+
+ info("extension with browser action and a menu with submenu");
+ const secondExtensionWidget = unifiedExtensionList.children[1];
+ Assert.ok(secondExtensionWidget, "expected extension widget");
+ contextMenu = await openUnifiedExtensionsContextMenu(
+ secondExtensionWidget.dataset.extensionid,
+ win
+ );
+ visibleItems = getVisibleMenuItems(contextMenu);
+ is(visibleItems.length, 7, "expected 7 menu items");
+ const popup = await openSubmenu(visibleItems[0]);
+ is(popup.children.length, 1, "expected 1 submenu item");
+ is(
+ popup.children[0].getAttribute("label"),
+ "Click me!",
+ "expected menu item"
+ );
+ // The number of items in the (main) context menu should remain the same.
+ visibleItems = getVisibleMenuItems(contextMenu);
+ is(visibleItems.length, 7, "expected 7 menu items");
+ await closeChromeContextMenu(contextMenu.id, null, win);
+
+ info("extension with no browser action and no menu");
+ // There is no context menu created by this extension, so there should
+ // only be 3 menu items corresponding to the default manage/remove/report
+ // items.
+ const thirdExtensionWidget = unifiedExtensionList.children[2];
+ Assert.ok(thirdExtensionWidget, "expected extension widget");
+ contextMenu = await openUnifiedExtensionsContextMenu(
+ thirdExtensionWidget.dataset.extensionid,
+ win
+ );
+ Assert.ok(contextMenu, "expected a context menu");
+ visibleItems = getVisibleMenuItems(contextMenu);
+ is(visibleItems.length, 5, "expected 5 menu items");
+
+ await closeChromeContextMenu(contextMenu.id, null, win);
+
+ // We can close the unified extensions panel now.
+ await closeExtensionsPanel(win);
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_message_deck() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ Assert.ok(
+ unifiedExtensionList.children.length,
+ "Should have items in the Unified Extension list."
+ );
+
+ const firstExtensionWidget = unifiedExtensionList.children[0];
+ Assert.ok(firstExtensionWidget, "expected extension widget");
+ Assert.ok(
+ firstExtensionWidget.dataset.extensionid,
+ "expected data attribute for extension ID"
+ );
+
+ // Navigate to a page where `activeTab` is useful.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "https://example.com/" },
+ async () => {
+ // Open the extension panel.
+ await openExtensionsPanel(win);
+
+ info("verify message when focusing the action button");
+ const item = getUnifiedExtensionsItem(
+ firstExtensionWidget.dataset.extensionid,
+ win
+ );
+ Assert.ok(item, "expected an item for the extension");
+
+ const actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ Assert.ok(actionButton, "expected action button");
+
+ const menuButton = item.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ Assert.ok(menuButton, "expected menu button");
+
+ const messageDeck = item.querySelector(
+ ".unified-extensions-item-message-deck"
+ );
+ Assert.ok(messageDeck, "expected message deck");
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ const defaultMessage = item.querySelector(
+ ".unified-extensions-item-message-default"
+ );
+ Assert.deepEqual(
+ win.document.l10n.getAttributes(defaultMessage),
+ { id: "origin-controls-state-when-clicked", args: null },
+ "expected correct l10n attributes for the default message"
+ );
+ Assert.ok(
+ defaultMessage.textContent !== "",
+ "expected default message to not be empty"
+ );
+
+ const hoverMessage = item.querySelector(
+ ".unified-extensions-item-message-hover"
+ );
+ Assert.deepEqual(
+ win.document.l10n.getAttributes(hoverMessage),
+ { id: "origin-controls-state-hover-run-visit-only", args: null },
+ "expected correct l10n attributes for the hover message"
+ );
+ Assert.ok(
+ hoverMessage.textContent !== "",
+ "expected hover message to not be empty"
+ );
+
+ const hoverMenuButtonMessage = item.querySelector(
+ ".unified-extensions-item-message-hover-menu-button"
+ );
+ Assert.deepEqual(
+ win.document.l10n.getAttributes(hoverMenuButtonMessage),
+ { id: "unified-extensions-item-message-manage", args: null },
+ "expected correct l10n attributes for the message when hovering the menu button"
+ );
+ Assert.ok(
+ hoverMenuButtonMessage.textContent !== "",
+ "expected message for when the menu button is hovered to not be empty"
+ );
+
+ // 1. Focus the action button of the first extension in the panel.
+ let focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ await focused;
+ is(
+ actionButton,
+ win.document.activeElement,
+ "expected action button of the first extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ // 2. Focus the menu button, causing the action button to lose focus.
+ focused = BrowserTestUtils.waitForEvent(menuButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ await focused;
+ is(
+ menuButton,
+ win.document.activeElement,
+ "expected menu button of the first extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when focusing the menu button"
+ );
+
+ await closeExtensionsPanel(win);
+
+ info("verify message when hovering the action button");
+ await openExtensionsPanel(win);
+
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ // 1. Hover the action button of the first extension in the panel.
+ let hovered = BrowserTestUtils.waitForEvent(
+ actionButton,
+ "mouseover"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ actionButton,
+ { type: "mouseover" },
+ win
+ );
+ await hovered;
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ // 2. Hover the menu button, causing the action button to no longer
+ // be hovered.
+ hovered = BrowserTestUtils.waitForEvent(menuButton, "mouseover");
+ EventUtils.synthesizeMouseAtCenter(
+ menuButton,
+ { type: "mouseover" },
+ win
+ );
+ await hovered;
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when hovering the menu button"
+ );
+
+ await closeExtensionsPanel(win);
+ }
+ );
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests that if we pin a browser action button listed in the addons panel
+ * to the toolbar when that button would immediately overflow, that the
+ * button is put into the addons panel overflow list.
+ */
+add_task(async function test_pinning_to_toolbar_when_overflowed() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ let movedNode;
+ let extensionWidgetID;
+ let actionButton;
+ let menuButton;
+
+ await withWindowOverflowed(win, {
+ beforeOverflowed: async extensionIDs => {
+ // Before we overflow the toolbar, let's move the last item to the addons
+ // panel.
+ extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensionIDs.at(-1)
+ );
+
+ movedNode =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node;
+
+ actionButton = movedNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the navbar"
+ );
+ ok(
+ !actionButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the navbar"
+ );
+
+ menuButton = movedNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the navbar"
+ );
+ ok(
+ !menuButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the navbar"
+ );
+
+ CustomizableUI.addWidgetToArea(
+ extensionWidgetID,
+ CustomizableUI.AREA_ADDONS
+ );
+
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+ },
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+
+ // Now that the window is overflowed, let's move the widget in the addons
+ // panel back to the navbar. This should cause the widget to overflow back
+ // into the addons panel.
+ CustomizableUI.addWidgetToArea(
+ extensionWidgetID,
+ CustomizableUI.AREA_NAVBAR
+ );
+ await TestUtils.waitForCondition(() => {
+ return movedNode.hasAttribute("overflowedItem");
+ });
+ Assert.equal(
+ movedNode.parentElement,
+ unifiedExtensionList,
+ "Should have overflowed the extension button to the right list."
+ );
+
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * This test verifies that, when an extension placed in the toolbar is
+ * overflowed into the addons panel and context-clicked, it shows the "Pin to
+ * Toolbar" item as checked, and that unchecking this menu item inserts the
+ * extension into the dedicated addons area of the panel, and that the item
+ * then does not underflow.
+ */
+add_task(async function test_unpin_overflowed_widget() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let extensionID;
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ const firstExtensionWidget = unifiedExtensionList.children[0];
+ Assert.ok(firstExtensionWidget, "expected an extension widget");
+ extensionID = firstExtensionWidget.dataset.extensionid;
+
+ let movedNode = CustomizableUI.getWidget(
+ firstExtensionWidget.id
+ ).forWindow(win).node;
+ Assert.equal(
+ movedNode.getAttribute("cui-areatype"),
+ "toolbar",
+ "expected extension widget to be in the toolbar"
+ );
+ Assert.ok(
+ movedNode.hasAttribute("overflowedItem"),
+ "expected extension widget to be overflowed"
+ );
+ let actionButton = movedNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ let menuButton = movedNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+
+ // Open the panel, then the context menu of the extension widget, verify
+ // the 'Pin to Toolbar' menu item, then click on this menu item to
+ // uncheck it (i.e. unpin the extension).
+ await openExtensionsPanel(win);
+ const contextMenu = await openUnifiedExtensionsContextMenu(
+ extensionID,
+ win
+ );
+ Assert.ok(contextMenu, "expected a context menu");
+
+ const pinToToolbar = contextMenu.querySelector(
+ ".unified-extensions-context-menu-pin-to-toolbar"
+ );
+ Assert.ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item");
+ Assert.ok(
+ !pinToToolbar.hidden,
+ "expected 'Pin to Toolbar' to be visible"
+ );
+ Assert.equal(
+ pinToToolbar.getAttribute("checked"),
+ "true",
+ "expected 'Pin to Toolbar' to be checked"
+ );
+
+ // Uncheck "Pin to Toolbar" menu item. Clicking a menu item in the
+ // context menu closes the unified extensions panel automatically.
+ const hidden = BrowserTestUtils.waitForEvent(
+ win.gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ contextMenu.activateItem(pinToToolbar);
+ await hidden;
+
+ // We expect the widget to no longer be overflowed.
+ await TestUtils.waitForCondition(() => {
+ return !movedNode.hasAttribute("overflowedItem");
+ });
+
+ Assert.equal(
+ movedNode.parentElement.id,
+ CustomizableUI.AREA_ADDONS,
+ "expected extension widget to have been unpinned and placed in the addons area"
+ );
+ Assert.equal(
+ movedNode.getAttribute("cui-areatype"),
+ "panel",
+ "expected extension widget to be in the unified extensions panel"
+ );
+ },
+ afterUnderflowed: async () => {
+ await openExtensionsPanel(win);
+
+ const item = getUnifiedExtensionsItem(extensionID, win);
+ Assert.ok(
+ item,
+ "expected extension widget to be listed in the unified extensions panel"
+ );
+ let actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ let menuButton = item.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+
+ await closeExtensionsPanel(win);
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_overflow_with_a_second_window() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ // Open a second window that will stay maximized. We want to be sure that
+ // overflowing a widget in one window isn't going to affect the other window
+ // since we have an instance (of a CUI widget) per window.
+ let secondWin = await BrowserTestUtils.openNewBrowserWindow();
+ await ensureMaximizedWindow(secondWin);
+ await BrowserTestUtils.openNewForegroundTab(
+ secondWin.gBrowser,
+ "https://example.com/"
+ );
+
+ // Make sure the first window is the active window.
+ let windowActivePromise = new Promise(resolve => {
+ if (Services.focus.activeWindow == win) {
+ resolve();
+ } else {
+ win.addEventListener(
+ "activate",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ }
+ });
+ win.focus();
+ await windowActivePromise;
+
+ let extensionWidgetID;
+ let aNode;
+ let aNodeInSecondWindow;
+
+ await withWindowOverflowed(win, {
+ beforeOverflowed: async extensionIDs => {
+ extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensionIDs.at(-1)
+ );
+
+ // This is the DOM node for the current window that is overflowed.
+ aNode = CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node;
+ Assert.ok(
+ !aNode.hasAttribute("overflowedItem"),
+ "expected extension widget to NOT be overflowed"
+ );
+
+ let actionButton = aNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button"
+ );
+ ok(
+ !actionButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button"
+ );
+
+ let menuButton = aNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button"
+ );
+ ok(
+ !menuButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button"
+ );
+
+ // This is the DOM node of the same CUI widget but in the maximized
+ // window opened before.
+ aNodeInSecondWindow =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(secondWin).node;
+
+ let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the second window"
+ );
+ ok(
+ !actionButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the second window"
+ );
+
+ let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the second window"
+ );
+ ok(
+ !menuButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the second window"
+ );
+ },
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ // The DOM node should have been overflowed.
+ Assert.ok(
+ aNode.hasAttribute("overflowedItem"),
+ "expected extension widget to be overflowed"
+ );
+ Assert.equal(
+ aNode.getAttribute("widget-id"),
+ extensionWidgetID,
+ "expected the CUI widget ID to be set on the DOM node"
+ );
+
+ // When the node is overflowed, we swap the CSS class on the action
+ // and menu buttons since the node is now placed in the extensions panel.
+ let actionButton = aNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the action button"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button"
+ );
+ let menuButton = aNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the menu button"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button"
+ );
+
+ // The DOM node in the other window should not have been overflowed.
+ Assert.ok(
+ !aNodeInSecondWindow.hasAttribute("overflowedItem"),
+ "expected extension widget to NOT be overflowed in the other window"
+ );
+ Assert.equal(
+ aNodeInSecondWindow.getAttribute("widget-id"),
+ extensionWidgetID,
+ "expected the CUI widget ID to be set on the DOM node"
+ );
+
+ // We expect no CSS class changes for the node in the other window.
+ let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the second window"
+ );
+ ok(
+ !actionButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the second window"
+ );
+ let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the second window"
+ );
+ ok(
+ !menuButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the second window"
+ );
+ },
+ afterUnderflowed: async () => {
+ // After underflow, we expect the CSS class on the action and menu
+ // buttons of the DOM node of the current window to be updated.
+ let actionButton = aNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the panel"
+ );
+ let menuButton = aNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the panel"
+ );
+
+ // The DOM node of the other window should not be changed.
+ let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the second window"
+ );
+ ok(
+ !actionButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the second window"
+ );
+ let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the second window"
+ );
+ ok(
+ !menuButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the second window"
+ );
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+ await BrowserTestUtils.closeWindow(secondWin);
+});
+
+add_task(async function test_overflow_with_extension_in_collapsed_area() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const bookmarksToolbar = win.document.getElementById(
+ CustomizableUI.AREA_BOOKMARKS
+ );
+
+ let movedNode;
+ let extensionWidgetID;
+ let extensionWidgetPosition;
+
+ await withWindowOverflowed(win, {
+ beforeOverflowed: async extensionIDs => {
+ // Before we overflow the toolbar, let's move the last item to the
+ // (visible) bookmarks toolbar.
+ extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensionIDs.at(-1)
+ );
+
+ movedNode =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node;
+
+ // Ensure that the toolbar is currently visible.
+ await promiseSetToolbarVisibility(bookmarksToolbar, true);
+
+ // Move an extension to the bookmarks toolbar.
+ CustomizableUI.addWidgetToArea(
+ extensionWidgetID,
+ CustomizableUI.AREA_BOOKMARKS
+ );
+
+ Assert.equal(
+ movedNode.parentElement.id,
+ CustomizableUI.AREA_BOOKMARKS,
+ "expected extension widget to be in the bookmarks toolbar"
+ );
+ Assert.ok(
+ !movedNode.hasAttribute("artificallyOverflowed"),
+ "expected node to not have any artificallyOverflowed prop"
+ );
+
+ extensionWidgetPosition =
+ CustomizableUI.getPlacementOfWidget(extensionWidgetID).position;
+
+ // At this point we have an extension in the bookmarks toolbar, and this
+ // toolbar is visible. We are going to resize the window (width) AND
+ // collapse the toolbar to verify that the extension placed in the
+ // bookmarks toolbar is overflowed in the panel without any side effects.
+ },
+ whenOverflowed: async () => {
+ // Ensure that the toolbar is currently collapsed.
+ await promiseSetToolbarVisibility(bookmarksToolbar, false);
+
+ Assert.equal(
+ movedNode.parentElement.id,
+ OVERFLOWED_EXTENSIONS_LIST_ID,
+ "expected extension widget to be in the extensions panel"
+ );
+ Assert.ok(
+ movedNode.getAttribute("artificallyOverflowed"),
+ "expected node to be artifically overflowed"
+ );
+
+ // At this point the extension is in the panel because it was overflowed
+ // after the bookmarks toolbar has been collapsed. The window is also
+ // narrow, but we are going to restore the initial window size. Since the
+ // visibility of the bookmarks toolbar hasn't changed, the extension
+ // should still be in the panel.
+ },
+ afterUnderflowed: async () => {
+ Assert.equal(
+ movedNode.parentElement.id,
+ OVERFLOWED_EXTENSIONS_LIST_ID,
+ "expected extension widget to still be in the extensions panel"
+ );
+ Assert.ok(
+ movedNode.getAttribute("artificallyOverflowed"),
+ "expected node to still be artifically overflowed"
+ );
+
+ // Ensure that the toolbar is visible again, which should move the
+ // extension back to where it was initially.
+ await promiseSetToolbarVisibility(bookmarksToolbar, true);
+
+ Assert.equal(
+ movedNode.parentElement.id,
+ CustomizableUI.AREA_BOOKMARKS,
+ "expected extension widget to be in the bookmarks toolbar"
+ );
+ Assert.ok(
+ !movedNode.hasAttribute("artificallyOverflowed"),
+ "expected node to not have any artificallyOverflowed prop"
+ );
+ Assert.equal(
+ CustomizableUI.getPlacementOfWidget(extensionWidgetID).position,
+ extensionWidgetPosition,
+ "expected the extension to be back at the same position in the bookmarks toolbar"
+ );
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_overflowed_extension_cannot_be_moved() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let extensionID;
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ const secondExtensionWidget = unifiedExtensionList.children[1];
+ Assert.ok(secondExtensionWidget, "expected an extension widget");
+ extensionID = secondExtensionWidget.dataset.extensionid;
+
+ await openExtensionsPanel(win);
+ const contextMenu = await openUnifiedExtensionsContextMenu(
+ extensionID,
+ win
+ );
+ Assert.ok(contextMenu, "expected a context menu");
+
+ const moveUp = contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-up"
+ );
+ Assert.ok(moveUp, "expected 'move up' item in the context menu");
+ Assert.ok(moveUp.hidden, "expected 'move up' item to be hidden");
+
+ const moveDown = contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-down"
+ );
+ Assert.ok(moveDown, "expected 'move down' item in the context menu");
+ Assert.ok(moveDown.hidden, "expected 'move down' item to be hidden");
+
+ await closeChromeContextMenu(contextMenu.id, null, win);
+ await closeExtensionsPanel(win);
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/context.html b/browser/components/extensions/test/browser/context.html
new file mode 100644
index 0000000000..cd1a3db904
--- /dev/null
+++ b/browser/components/extensions/test/browser/context.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ just some text 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
+
+
+
+ Some link
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit
+ voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque
+ ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta
+ sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
+ odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem
+ sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit
+ amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora
+ incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad
+ minima veniam, quis nostrum exercitationem ullam corporis suscipit
+ laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum
+ iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae
+ consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
+
+
+
+
diff --git a/browser/components/extensions/test/browser/context_frame.html b/browser/components/extensions/test/browser/context_frame.html
new file mode 100644
index 0000000000..39ed37674f
--- /dev/null
+++ b/browser/components/extensions/test/browser/context_frame.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+ Just some text
+
+
diff --git a/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html b/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html
new file mode 100644
index 0000000000..0e9b54b523
--- /dev/null
+++ b/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html
@@ -0,0 +1,19 @@
+
+
+ test iframe
+
+
+
diff --git a/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html b/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html
new file mode 100644
index 0000000000..0f2ce1e8fe
--- /dev/null
+++ b/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html
@@ -0,0 +1,18 @@
+
+
+ test page
+
+
+
+
diff --git a/browser/components/extensions/test/browser/context_with_redirect.html b/browser/components/extensions/test/browser/context_with_redirect.html
new file mode 100644
index 0000000000..cbf676729b
--- /dev/null
+++ b/browser/components/extensions/test/browser/context_with_redirect.html
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/browser/components/extensions/test/browser/ctxmenu-image.png b/browser/components/extensions/test/browser/ctxmenu-image.png
new file mode 100644
index 0000000000..4c3be50847
Binary files /dev/null and b/browser/components/extensions/test/browser/ctxmenu-image.png differ
diff --git a/browser/components/extensions/test/browser/empty.xpi b/browser/components/extensions/test/browser/empty.xpi
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/browser/components/extensions/test/browser/file_bypass_cache.sjs b/browser/components/extensions/test/browser/file_bypass_cache.sjs
new file mode 100644
index 0000000000..eed8a6ef49
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_bypass_cache.sjs
@@ -0,0 +1,13 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain; charset=UTF-8", false);
+
+ if (request.hasHeader("pragma") && request.hasHeader("cache-control")) {
+ response.write(
+ `${request.getHeader("pragma")}:${request.getHeader("cache-control")}`
+ );
+ }
+}
diff --git a/browser/components/extensions/test/browser/file_dataTransfer_files.html b/browser/components/extensions/test/browser/file_dataTransfer_files.html
new file mode 100644
index 0000000000..553196a942
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_dataTransfer_files.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/browser/file_dummy.html b/browser/components/extensions/test/browser/file_dummy.html
new file mode 100644
index 0000000000..966e0fd5d0
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_dummy.html
@@ -0,0 +1,10 @@
+
+
+Dummy test page
+
+
+
+Dummy test page
+link
+
+
diff --git a/browser/components/extensions/test/browser/file_find_frames.html b/browser/components/extensions/test/browser/file_find_frames.html
new file mode 100644
index 0000000000..cb93ae484e
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_find_frames.html
@@ -0,0 +1,19 @@
+
+
+
+
+Bánana 0
+
+bAnana 1
+fru itca ke
+ang
elf
ood
+This is an example of an example in the same node.
+This is an example of an example of ranges in separate nodes.
+
+
+
diff --git a/browser/components/extensions/test/browser/file_has_non_web_controlled_blank_page_link.html b/browser/components/extensions/test/browser/file_has_non_web_controlled_blank_page_link.html
new file mode 100644
index 0000000000..6c118fdb85
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_has_non_web_controlled_blank_page_link.html
@@ -0,0 +1,5 @@
+
+
+wait-a-bit - _blank target
diff --git a/browser/components/extensions/test/browser/file_iframe_document.html b/browser/components/extensions/test/browser/file_iframe_document.html
new file mode 100644
index 0000000000..7b65ce17cc
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_iframe_document.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/browser/file_inspectedwindow_eval.html b/browser/components/extensions/test/browser/file_inspectedwindow_eval.html
new file mode 100644
index 0000000000..04128d9ef3
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_inspectedwindow_eval.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/browser/file_inspectedwindow_reload_target.sjs b/browser/components/extensions/test/browser/file_inspectedwindow_reload_target.sjs
new file mode 100644
index 0000000000..2a7a401360
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_inspectedwindow_reload_target.sjs
@@ -0,0 +1,130 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */
+"use strict";
+
+Cu.importGlobalProperties(["URLSearchParams"]);
+
+function handleRequest(request, response) {
+ let params = new URLSearchParams(request.queryString);
+
+ switch (params.get("test")) {
+ case "cache":
+ /* eslint-disable-next-line no-use-before-define */
+ handleCacheTestRequest(request, response);
+ break;
+
+ case "user-agent":
+ /* eslint-disable-next-line no-use-before-define */
+ handleUserAgentTestRequest(request, response);
+ break;
+
+ case "injected-script":
+ /* eslint-disable-next-line no-use-before-define */
+ handleInjectedScriptTestRequest(request, response, params);
+ break;
+ }
+}
+
+function handleCacheTestRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain; charset=UTF-8", false);
+
+ if (request.hasHeader("pragma") && request.hasHeader("cache-control")) {
+ response.write(
+ `${request.getHeader("pragma")}:${request.getHeader("cache-control")}`
+ );
+ } else {
+ response.write("empty cache headers");
+ }
+}
+
+function handleUserAgentTestRequest(request, response) {
+ response.setHeader("Content-Type", "text/html", false);
+
+ const userAgentHeader = request.hasHeader("user-agent")
+ ? request.getHeader("user-agent")
+ : null;
+
+ const query = new URLSearchParams(request.queryString);
+ if (query.get("crossOriginIsolated") === "true") {
+ response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false);
+ }
+
+ const IFRAME_HTML = `
+
+
+
+
+
+
+
+ Iframe
+
+ `;
+ // We always want the iframe to have a different host from the top-level document.
+ const iframeHost =
+ request.host === "example.com" ? "example.org" : "example.com";
+ const iframeOrigin = `${request.scheme}://${iframeHost}`;
+ const iframeUrl = `${iframeOrigin}/document-builder.sjs?html=${encodeURI(
+ IFRAME_HTML
+ )}`;
+
+ const HTML = `
+
+
+
+
+ test
+
+
+
+ Top-level
+ ${userAgentHeader ?? "no user-agent header"}
+
+
+ `;
+
+ response.write(HTML);
+}
+
+function handleInjectedScriptTestRequest(request, response, params) {
+ response.setHeader("Content-Type", "text/html; charset=UTF-8", false);
+
+ let content = "";
+ const frames = parseInt(params.get("frames"), 10);
+ if (frames > 0) {
+ // Output an iframe in seamless mode, so that there is an higher chance that in case
+ // of test failures we get a screenshot where the nested iframes are all visible.
+ content = ``;
+ }
+
+ response.write(`
+
+
+
+
+
+
+ IFRAME ${frames}
+ injected script NOT executed
+
+ ${content}
+
+
+ `);
+}
diff --git a/browser/components/extensions/test/browser/file_popup_api_injection_a.html b/browser/components/extensions/test/browser/file_popup_api_injection_a.html
new file mode 100644
index 0000000000..750ff1db37
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_popup_api_injection_a.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/browser/file_popup_api_injection_b.html b/browser/components/extensions/test/browser/file_popup_api_injection_b.html
new file mode 100644
index 0000000000..b8c287e55c
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_popup_api_injection_b.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/browser/file_slowed_document.sjs b/browser/components/extensions/test/browser/file_slowed_document.sjs
new file mode 100644
index 0000000000..8c42fcc966
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_slowed_document.sjs
@@ -0,0 +1,49 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */
+"use strict";
+
+// This script slows the load of an HTML document so that we can reliably test
+// all phases of the load cycle supported by the extension API.
+
+/* eslint-disable no-unused-vars */
+
+const URL = "file_slowed_document.sjs";
+
+const DELAY = 2 * 1000; // Delay two seconds before completing the request.
+
+let nsTimer = Components.Constructor(
+ "@mozilla.org/timer;1",
+ "nsITimer",
+ "initWithCallback"
+);
+
+let timer;
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(`
+
+
+
+
+
+
+ `);
+
+ // Note: We need to store a reference to the timer to prevent it from being
+ // canceled when it's GCed.
+ timer = new nsTimer(
+ () => {
+ if (request.queryString.includes("with-iframe")) {
+ response.write(``);
+ }
+ response.write(``);
+ response.finish();
+ },
+ DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/browser/components/extensions/test/browser/file_title.html b/browser/components/extensions/test/browser/file_title.html
new file mode 100644
index 0000000000..2a5d0bca30
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_title.html
@@ -0,0 +1,9 @@
+
+
+Different title test page
+
+
+
+A page with a different title
+
+
diff --git a/browser/components/extensions/test/browser/file_with_example_com_frame.html b/browser/components/extensions/test/browser/file_with_example_com_frame.html
new file mode 100644
index 0000000000..a4263b3315
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_with_example_com_frame.html
@@ -0,0 +1,5 @@
+
+
+
+Load an iframe from example.com
+
diff --git a/browser/components/extensions/test/browser/file_with_xorigin_frame.html b/browser/components/extensions/test/browser/file_with_xorigin_frame.html
new file mode 100644
index 0000000000..cee430a387
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_with_xorigin_frame.html
@@ -0,0 +1,5 @@
+
+
+
+Load a cross-origin iframe from example.net
+
diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js
new file mode 100644
index 0000000000..02f905d05c
--- /dev/null
+++ b/browser/components/extensions/test/browser/head.js
@@ -0,0 +1,1046 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported CustomizableUI makeWidgetId focusWindow forceGC
+ * getBrowserActionWidget assertPersistentListeners
+ * clickBrowserAction clickPageAction clickPageActionInPanel
+ * triggerPageActionWithKeyboard triggerPageActionWithKeyboardInPanel
+ * triggerBrowserActionWithKeyboard
+ * getBrowserActionPopup getPageActionPopup getPageActionButton
+ * openBrowserActionPanel
+ * closeBrowserAction closePageAction
+ * promisePopupShown promisePopupHidden promisePopupNotificationShown
+ * toggleBookmarksToolbar
+ * openContextMenu closeContextMenu promiseContextMenuClosed
+ * openContextMenuInSidebar openContextMenuInPopup
+ * openExtensionContextMenu closeExtensionContextMenu
+ * openActionContextMenu openSubmenu closeActionContextMenu
+ * openTabContextMenu closeTabContextMenu
+ * openToolsMenu closeToolsMenu
+ * imageBuffer imageBufferFromDataURI
+ * getInlineOptionsBrowser
+ * getListStyleImage getRawListStyleImage getPanelForNode
+ * awaitExtensionPanel awaitPopupResize
+ * promiseContentDimensions alterContent
+ * promisePrefChangeObserved openContextMenuInFrame
+ * promiseAnimationFrame getCustomizableUIPanelID
+ * awaitEvent BrowserWindowIterator
+ * navigateTab historyPushState promiseWindowRestored
+ * getIncognitoWindow startIncognitoMonitorExtension
+ * loadTestSubscript awaitBrowserLoaded
+ * getScreenAt roundCssPixcel getCssAvailRect isRectContained
+ * getToolboxBackgroundColor
+ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// This bug should be fixed, but for the moment all tests in this directory
+// allow various classes of promise rejections.
+//
+// NOTE: Allowing rejections on an entire directory should be avoided.
+// Normally you should use "expectUncaughtRejection" to flag individual
+// failures.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/No matching message handler/);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Receiving end does not exist/
+);
+
+const { AppUiTestDelegate, AppUiTestInternals } = ChromeUtils.importESModule(
+ "resource://testing-common/AppUiTestDelegate.sys.mjs"
+);
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+var { makeWidgetId, promisePopupShown, getPanelForNode, awaitBrowserLoaded } =
+ AppUiTestInternals;
+
+// The extension tests can run a lot slower under ASAN.
+if (AppConstants.ASAN) {
+ requestLongerTimeout(5);
+}
+
+function loadTestSubscript(filePath) {
+ Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
+}
+
+// Ensure when we turn off topsites in the next few lines,
+// we don't hit any remote endpoints.
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setStringPref("discoverystream.endpointSpocsClear", "");
+// Leaving Top Sites enabled during these tests would create site screenshots
+// and update pinned Top Sites unnecessarily.
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setBoolPref("feeds.topsites", false);
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setBoolPref("feeds.system.topsites", false);
+
+{
+ // Touch the recipeParentPromise lazy getter so we don't get
+ // `this._recipeManager is undefined` errors during tests.
+ const { LoginManagerParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerParent.sys.mjs"
+ );
+ void LoginManagerParent.recipeParentPromise;
+}
+
+// Persistent Listener test functionality
+const { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
+
+// Bug 1239884: Our tests occasionally hit a long GC pause at unpredictable
+// times in debug builds, which results in intermittent timeouts. Until we have
+// a better solution, we force a GC after certain strategic tests, which tend to
+// accumulate a high number of unreaped windows.
+function forceGC() {
+ if (AppConstants.DEBUG) {
+ Cu.forceGC();
+ }
+}
+
+var focusWindow = async function focusWindow(win) {
+ if (Services.focus.activeWindow == win) {
+ return;
+ }
+
+ let promise = new Promise(resolve => {
+ win.addEventListener(
+ "focus",
+ function () {
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+
+ win.focus();
+ await promise;
+};
+
+function imageBufferFromDataURI(encodedImageData) {
+ let decodedImageData = atob(encodedImageData);
+ return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
+}
+
+let img =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==";
+var imageBuffer = imageBufferFromDataURI(img);
+
+function getInlineOptionsBrowser(aboutAddonsBrowser) {
+ let { contentDocument } = aboutAddonsBrowser;
+ return contentDocument.getElementById("addon-inline-options");
+}
+
+function getRawListStyleImage(button) {
+ // Ensure popups are initialized so that the elements are rendered and
+ // getComputedStyle works.
+ for (
+ let popup = button.closest("panel,menupopup");
+ popup;
+ popup = popup.parentElement?.closest("panel,menupopup")
+ ) {
+ popup.ensureInitialized();
+ }
+
+ return button.ownerGlobal.getComputedStyle(button).listStyleImage;
+}
+
+function getListStyleImage(button) {
+ let match = /url\("([^"]*)"\)/.exec(getRawListStyleImage(button));
+ return match && match[1];
+}
+
+function promiseAnimationFrame(win = window) {
+ return AppUiTestInternals.promiseAnimationFrame(win);
+}
+
+function promisePopupHidden(popup) {
+ return new Promise(resolve => {
+ let onPopupHidden = event => {
+ popup.removeEventListener("popuphidden", onPopupHidden);
+ resolve();
+ };
+ popup.addEventListener("popuphidden", onPopupHidden);
+ });
+}
+
+/**
+ * Wait for the given PopupNotification to display
+ *
+ * @param {string} name
+ * The name of the notification to wait for.
+ * @param {Window} [win]
+ * The chrome window in which to wait for the notification.
+ *
+ * @returns {Promise}
+ * Resolves with the notification window.
+ */
+function promisePopupNotificationShown(name, win = window) {
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = win.PopupNotifications.getNotification(name);
+ if (!notification) {
+ return;
+ }
+
+ ok(notification, `${name} notification shown`);
+ ok(win.PopupNotifications.isPanelOpen, "notification panel open");
+
+ win.PopupNotifications.panel.removeEventListener(
+ "popupshown",
+ popupshown
+ );
+ resolve(win.PopupNotifications.panel.firstElementChild);
+ }
+
+ win.PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function promisePossiblyInaccurateContentDimensions(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ function copyProps(obj, props) {
+ let res = {};
+ for (let prop of props) {
+ res[prop] = obj[prop];
+ }
+ return res;
+ }
+
+ return {
+ window: copyProps(content, [
+ "innerWidth",
+ "innerHeight",
+ "outerWidth",
+ "outerHeight",
+ "scrollX",
+ "scrollY",
+ "scrollMaxX",
+ "scrollMaxY",
+ ]),
+ body: copyProps(content.document.body, [
+ "clientWidth",
+ "clientHeight",
+ "scrollWidth",
+ "scrollHeight",
+ ]),
+ root: copyProps(content.document.documentElement, [
+ "clientWidth",
+ "clientHeight",
+ "scrollWidth",
+ "scrollHeight",
+ ]),
+ isStandards: content.document.compatMode !== "BackCompat",
+ };
+ });
+}
+
+function delay(ms = 0) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Retrieve the content dimensions (and wait until the content gets to the.
+ * size of the browser element they are loaded into, optionally tollerating
+ * size differences to prevent intermittent failures).
+ *
+ * @param {BrowserElement} browser
+ * The browser element where the content has been loaded.
+ * @param {number} [tolleratedWidthSizeDiff]
+ * width size difference to tollerate in pixels (defaults to 1).
+ *
+ * @returns {Promise}
+ * An object with the dims retrieved from the content.
+ */
+async function promiseContentDimensions(browser, tolleratedWidthSizeDiff = 1) {
+ // For remote browsers, each resize operation requires an asynchronous
+ // round-trip to resize the content window. Since there's a certain amount of
+ // unpredictability in the timing, mainly due to the unpredictability of
+ // reflows, we need to wait until the content window dimensions match the
+ // dimensions before returning data.
+
+ let dims = await promisePossiblyInaccurateContentDimensions(browser);
+ while (
+ Math.abs(browser.clientWidth - dims.window.innerWidth) >
+ tolleratedWidthSizeDiff ||
+ browser.clientHeight !== Math.round(dims.window.innerHeight)
+ ) {
+ const diffWidth = Math.abs(browser.clientWidth - dims.window.innerWidth);
+ const diffHeight = Math.abs(browser.clientHeight - dims.window.innerHeight);
+ info(
+ `Content dimension did not reached the expected size yet (diff: ${diffWidth}x${diffHeight}). Wait further.`
+ );
+ await delay(50);
+ dims = await promisePossiblyInaccurateContentDimensions(browser);
+ }
+
+ return dims;
+}
+
+async function awaitPopupResize(browser) {
+ await BrowserTestUtils.waitForEvent(
+ browser,
+ "WebExtPopupResized",
+ event => event.detail === "delayed"
+ );
+
+ return promiseContentDimensions(browser);
+}
+
+function alterContent(browser, task, arg = null) {
+ return Promise.all([
+ SpecialPowers.spawn(browser, [arg], task),
+ awaitPopupResize(browser),
+ ]).then(([, dims]) => dims);
+}
+
+async function focusButtonAndPressKey(key, elem, modifiers) {
+ let focused = BrowserTestUtils.waitForEvent(elem, "focus", true);
+
+ elem.setAttribute("tabindex", "-1");
+ elem.focus();
+ elem.removeAttribute("tabindex");
+ await focused;
+
+ EventUtils.synthesizeKey(key, modifiers);
+ elem.blur();
+}
+
+var awaitExtensionPanel = function (extension, win = window, awaitLoad = true) {
+ return AppUiTestDelegate.awaitExtensionPanel(win, extension.id, awaitLoad);
+};
+
+function getCustomizableUIPanelID(win = window) {
+ return CustomizableUI.AREA_ADDONS;
+}
+
+function getBrowserActionWidget(extension) {
+ return AppUiTestInternals.getBrowserActionWidget(extension.id);
+}
+
+function getBrowserActionPopup(extension, win = window) {
+ let group = getBrowserActionWidget(extension);
+
+ if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
+ return win.document.getElementById("customizationui-widget-panel");
+ }
+
+ return win.gUnifiedExtensions.panel;
+}
+
+var showBrowserAction = function (extension, win = window) {
+ return AppUiTestInternals.showBrowserAction(win, extension.id);
+};
+
+function clickBrowserAction(extension, win = window, modifiers) {
+ return AppUiTestDelegate.clickBrowserAction(win, extension.id, modifiers);
+}
+
+async function triggerBrowserActionWithKeyboard(
+ extension,
+ key = "KEY_Enter",
+ modifiers = {},
+ win = window
+) {
+ await promiseAnimationFrame(win);
+ await showBrowserAction(extension, win);
+
+ let group = getBrowserActionWidget(extension);
+ let node = group.forWindow(win).node.firstElementChild;
+
+ if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
+ await focusButtonAndPressKey(key, node, modifiers);
+ } else if (group.areaType == CustomizableUI.TYPE_PANEL) {
+ // Use key navigation so that the PanelMultiView doesn't ignore key events.
+ let panel = win.gUnifiedExtensions.panel;
+ while (win.document.activeElement != node) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ panel.contains(win.document.activeElement),
+ "Focus is inside the panel"
+ );
+ }
+ EventUtils.synthesizeKey(key, modifiers);
+ }
+}
+
+function closeBrowserAction(extension, win = window) {
+ return AppUiTestDelegate.closeBrowserAction(win, extension.id);
+}
+
+function openBrowserActionPanel(extension, win = window, awaitLoad = false) {
+ clickBrowserAction(extension, win);
+
+ return awaitExtensionPanel(extension, win, awaitLoad);
+}
+
+async function toggleBookmarksToolbar(visible = true) {
+ let bookmarksToolbar = document.getElementById("PersonalToolbar");
+ // Third parameter is 'persist' and true is the default.
+ // Fourth parameter is 'animated' and we want no animation.
+ setToolbarVisibility(bookmarksToolbar, visible, true, false);
+ if (!visible) {
+ return BrowserTestUtils.waitForMutationCondition(
+ bookmarksToolbar,
+ { attributes: true },
+ () => bookmarksToolbar.collapsed
+ );
+ }
+
+ return BrowserTestUtils.waitForEvent(
+ bookmarksToolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+}
+
+async function openContextMenuInPopup(
+ extension,
+ selector = "body",
+ win = window
+) {
+ let doc = win.document;
+ let contentAreaContextMenu = doc.getElementById("contentAreaContextMenu");
+ let browser = await awaitExtensionPanel(extension, win);
+
+ // Ensure that the document layout has been flushed before triggering the mouse event
+ // (See Bug 1519808 for a rationale).
+ await browser.ownerGlobal.promiseDocumentFlushed(() => {});
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "mousedown", button: 2 },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu" },
+ browser
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+async function openContextMenuInSidebar(selector = "body") {
+ let contentAreaContextMenu = SidebarUI.browser.contentDocument.getElementById(
+ "contentAreaContextMenu"
+ );
+ let browser = SidebarUI.browser.contentDocument.getElementById(
+ "webext-panels-browser"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+
+ // Wait for the layout to be flushed, otherwise this test may
+ // fail intermittently if synthesizeMouseAtCenter is being called
+ // while the sidebar is still opening and the browser window layout
+ // being recomputed.
+ await SidebarUI.browser.contentWindow.promiseDocumentFlushed(() => {});
+
+ info("Opening context menu in sidebarAction panel");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "mousedown", button: 2 },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu" },
+ browser
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+// `selector` should refer to the content in the frame. If invalid the test can
+// fail intermittently because the click could inadvertently be registered on
+// the upper-left corner of the frame (instead of inside the frame).
+async function openContextMenuInFrame(selector = "body", frameIndex = 0) {
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu" },
+ gBrowser.selectedBrowser.browsingContext.children[frameIndex]
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+async function openContextMenu(selector = "#img1", win = window) {
+ let contentAreaContextMenu = win.document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "mousedown", button: 2 },
+ win.gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu" },
+ win.gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+async function promiseContextMenuClosed(contextMenu) {
+ let contentAreaContextMenu =
+ contextMenu || document.getElementById("contentAreaContextMenu");
+ return BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
+}
+
+async function closeContextMenu(contextMenu, win = window) {
+ let contentAreaContextMenu =
+ contextMenu || win.document.getElementById("contentAreaContextMenu");
+ let closed = promiseContextMenuClosed(contentAreaContextMenu);
+ contentAreaContextMenu.hidePopup();
+ await closed;
+}
+
+async function openExtensionContextMenu(selector = "#img1") {
+ let contextMenu = await openContextMenu(selector);
+ let topLevelMenu = contextMenu.getElementsByAttribute(
+ "ext-type",
+ "top-level-menu"
+ );
+
+ // Return null if the extension only has one item and therefore no extension menu.
+ if (!topLevelMenu.length) {
+ return null;
+ }
+
+ let extensionMenu = topLevelMenu[0];
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ extensionMenu.openMenu(true);
+ await popupShownPromise;
+ return extensionMenu;
+}
+
+async function closeExtensionContextMenu(itemToSelect, modifiers = {}) {
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupHiddenPromise = promiseContextMenuClosed(contentAreaContextMenu);
+ if (itemToSelect) {
+ itemToSelect.closest("menupopup").activateItem(itemToSelect, modifiers);
+ } else {
+ contentAreaContextMenu.hidePopup();
+ }
+ await popupHiddenPromise;
+
+ // Bug 1351638: parent menu fails to close intermittently, make sure it does.
+ contentAreaContextMenu.hidePopup();
+}
+
+async function openToolsMenu(win = window) {
+ const node = win.document.getElementById("tools-menu");
+ const menu = win.document.getElementById("menu_ToolsPopup");
+ const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ if (AppConstants.platform === "macosx") {
+ // We can't open menubar items on OSX, so mocking instead.
+ menu.dispatchEvent(new MouseEvent("popupshowing"));
+ menu.dispatchEvent(new MouseEvent("popupshown"));
+ } else {
+ node.open = true;
+ }
+ await shown;
+ return menu;
+}
+
+function closeToolsMenu(itemToSelect, win = window) {
+ const menu = win.document.getElementById("menu_ToolsPopup");
+ const hidden = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ if (AppConstants.platform === "macosx") {
+ // Mocking on OSX, see above.
+ if (itemToSelect) {
+ itemToSelect.doCommand();
+ }
+ menu.dispatchEvent(new MouseEvent("popuphiding"));
+ menu.dispatchEvent(new MouseEvent("popuphidden"));
+ } else if (itemToSelect) {
+ EventUtils.synthesizeMouseAtCenter(itemToSelect, {}, win);
+ } else {
+ menu.hidePopup();
+ }
+ return hidden;
+}
+
+async function openChromeContextMenu(menuId, target, win = window) {
+ const node = win.document.querySelector(target);
+ const menu = win.document.getElementById(menuId);
+ const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(node, { type: "contextmenu" }, win);
+ await shown;
+ return menu;
+}
+
+async function openSubmenu(submenuItem, win = window) {
+ const submenu = submenuItem.menupopup;
+ const shown = BrowserTestUtils.waitForEvent(submenu, "popupshown");
+ submenuItem.openMenu(true);
+ await shown;
+ return submenu;
+}
+
+function closeChromeContextMenu(menuId, itemToSelect, win = window) {
+ const menu = win.document.getElementById(menuId);
+ const hidden = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ if (itemToSelect) {
+ itemToSelect.closest("menupopup").activateItem(itemToSelect);
+ } else {
+ menu.hidePopup();
+ }
+ return hidden;
+}
+
+async function openActionContextMenu(extension, kind, win = window) {
+ // See comment from getPageActionButton below.
+ win.gURLBar.setPageProxyState("valid");
+ await promiseAnimationFrame(win);
+ let buttonID;
+ let menuID;
+ if (kind == "page") {
+ buttonID =
+ "#" +
+ BrowserPageActions.urlbarButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+ menuID = "pageActionContextMenu";
+ } else {
+ buttonID = `#${makeWidgetId(extension.id)}-${kind}-action`;
+ menuID = "toolbar-context-menu";
+ }
+ return openChromeContextMenu(menuID, buttonID, win);
+}
+
+function closeActionContextMenu(itemToSelect, kind, win = window) {
+ let menuID =
+ kind == "page" ? "pageActionContextMenu" : "toolbar-context-menu";
+ return closeChromeContextMenu(menuID, itemToSelect, win);
+}
+
+function openTabContextMenu(tab = gBrowser.selectedTab) {
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu before opening.
+ tab.focus();
+ let indexOfTab = Array.prototype.indexOf.call(tab.parentNode.children, tab);
+ return openChromeContextMenu(
+ "tabContextMenu",
+ `.tabbrowser-tab:nth-child(${indexOfTab + 1})`,
+ tab.ownerGlobal
+ );
+}
+
+function closeTabContextMenu(itemToSelect, win = window) {
+ return closeChromeContextMenu("tabContextMenu", itemToSelect, win);
+}
+
+function getPageActionPopup(extension, win = window) {
+ return AppUiTestInternals.getPageActionPopup(win, extension.id);
+}
+
+function getPageActionButton(extension, win = window) {
+ return AppUiTestInternals.getPageActionButton(win, extension.id);
+}
+
+function clickPageAction(extension, win = window, modifiers = {}) {
+ return AppUiTestDelegate.clickPageAction(win, extension.id, modifiers);
+}
+
+// Shows the popup for the page action which for lists
+// all available page actions
+async function showPageActionsPanel(win = window) {
+ // See the comment at getPageActionButton
+ win.gURLBar.setPageProxyState("valid");
+ await promiseAnimationFrame(win);
+
+ let pageActionsPopup = win.document.getElementById("pageActionPanel");
+
+ let popupShownPromise = promisePopupShown(pageActionsPopup);
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.getElementById("pageActionButton"),
+ {},
+ win
+ );
+ await popupShownPromise;
+
+ return pageActionsPopup;
+}
+
+async function clickPageActionInPanel(extension, win = window, modifiers = {}) {
+ let pageActionsPopup = await showPageActionsPanel(win);
+
+ let pageActionId = BrowserPageActions.panelButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+
+ let popupHiddenPromise = promisePopupHidden(pageActionsPopup);
+ let widgetButton = win.document.getElementById(pageActionId);
+ EventUtils.synthesizeMouseAtCenter(widgetButton, modifiers, win);
+ if (widgetButton.disabled) {
+ pageActionsPopup.hidePopup();
+ }
+ await popupHiddenPromise;
+
+ return new Promise(SimpleTest.executeSoon);
+}
+
+async function triggerPageActionWithKeyboard(
+ extension,
+ modifiers = {},
+ win = window
+) {
+ let elem = await getPageActionButton(extension, win);
+ await focusButtonAndPressKey("KEY_Enter", elem, modifiers);
+ return new Promise(SimpleTest.executeSoon);
+}
+
+async function triggerPageActionWithKeyboardInPanel(
+ extension,
+ modifiers = {},
+ win = window
+) {
+ let pageActionsPopup = await showPageActionsPanel(win);
+
+ let pageActionId = BrowserPageActions.panelButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+
+ let popupHiddenPromise = promisePopupHidden(pageActionsPopup);
+ let widgetButton = win.document.getElementById(pageActionId);
+ if (widgetButton.disabled) {
+ pageActionsPopup.hidePopup();
+ return new Promise(SimpleTest.executeSoon);
+ }
+
+ // Use key navigation so that the PanelMultiView doesn't ignore key events
+ while (win.document.activeElement != widgetButton) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ pageActionsPopup.contains(win.document.activeElement),
+ "Focus is inside of the panel"
+ );
+ }
+ EventUtils.synthesizeKey("KEY_Enter", modifiers);
+ await popupHiddenPromise;
+
+ return new Promise(SimpleTest.executeSoon);
+}
+
+function closePageAction(extension, win = window) {
+ return AppUiTestDelegate.closePageAction(win, extension.id);
+}
+
+function promisePrefChangeObserved(pref) {
+ return new Promise((resolve, reject) =>
+ Preferences.observe(pref, function prefObserver() {
+ Preferences.ignore(pref, prefObserver);
+ resolve();
+ })
+ );
+}
+
+function promiseWindowRestored(window) {
+ return new Promise(resolve =>
+ window.addEventListener("SSWindowRestored", resolve, { once: true })
+ );
+}
+
+function awaitEvent(eventName, id) {
+ return new Promise(resolve => {
+ let listener = (_eventName, ...args) => {
+ let extension = args[0];
+ if (_eventName === eventName && extension.id == id) {
+ Management.off(eventName, listener);
+ resolve();
+ }
+ };
+
+ Management.on(eventName, listener);
+ });
+}
+
+function* BrowserWindowIterator() {
+ for (let currentWindow of Services.wm.getEnumerator("navigator:browser")) {
+ if (!currentWindow.closed) {
+ yield currentWindow;
+ }
+ }
+}
+
+async function locationChange(tab, url, task) {
+ let locationChanged = BrowserTestUtils.waitForLocationChange(gBrowser, url);
+ await SpecialPowers.spawn(tab.linkedBrowser, [url], task);
+ return locationChanged;
+}
+
+function navigateTab(tab, url) {
+ return locationChange(tab, url, url => {
+ content.location.href = url;
+ });
+}
+
+function historyPushState(tab, url) {
+ return locationChange(tab, url, url => {
+ content.history.pushState(null, null, url);
+ });
+}
+
+// This monitor extension runs with incognito: not_allowed, if it receives any
+// events with incognito data it fails.
+async function startIncognitoMonitorExtension() {
+ function background() {
+ // Bug 1513220 - We're unable to get the tab during onRemoved, so we track
+ // valid tabs in "seen" so we can at least validate tabs that we have "seen"
+ // during onRemoved. This means that the monitor extension must be started
+ // prior to creating any tabs that will be removed.
+
+ // Map tab>
+ let seenTabs = new Map();
+ function getTabById(tabId) {
+ return seenTabs.has(tabId)
+ ? seenTabs.get(tabId)
+ : browser.tabs.get(tabId);
+ }
+
+ async function testTab(tabOrId, eventName) {
+ let tab = tabOrId;
+ if (typeof tabOrId == "number") {
+ let tabId = tabOrId;
+ try {
+ tab = await getTabById(tabId);
+ } catch (e) {
+ browser.test.fail(
+ `tabs.${eventName} for id ${tabOrId} unexpected failure ${e}\n`
+ );
+ return;
+ }
+ }
+ browser.test.assertFalse(
+ tab.incognito,
+ `tabs.${eventName} ${tab.id}: monitor extension got expected incognito value`
+ );
+ seenTabs.set(tab.id, tab);
+ }
+ async function testTabInfo(tabInfo, eventName) {
+ if (typeof tabInfo == "number") {
+ await testTab(tabInfo, eventName);
+ } else if (typeof tabInfo == "object") {
+ if (tabInfo.id !== undefined) {
+ await testTab(tabInfo, eventName);
+ } else if (tabInfo.tab !== undefined) {
+ await testTab(tabInfo.tab, eventName);
+ } else if (tabInfo.tabIds !== undefined) {
+ await Promise.all(
+ tabInfo.tabIds.map(tabId => testTab(tabId, eventName))
+ );
+ } else if (tabInfo.tabId !== undefined) {
+ await testTab(tabInfo.tabId, eventName);
+ }
+ }
+ }
+ let tabEvents = [
+ "onUpdated",
+ "onCreated",
+ "onAttached",
+ "onDetached",
+ "onRemoved",
+ "onMoved",
+ "onZoomChange",
+ "onHighlighted",
+ ];
+ for (let eventName of tabEvents) {
+ browser.tabs[eventName].addListener(async details => {
+ await testTabInfo(details, eventName);
+ });
+ }
+ browser.tabs.onReplaced.addListener(async (addedTabId, removedTabId) => {
+ await testTabInfo(addedTabId, "onReplaced (addedTabId)");
+ await testTabInfo(removedTabId, "onReplaced (removedTabId)");
+ });
+
+ // Map window>
+ let seenWindows = new Map();
+ function getWindowById(windowId) {
+ return seenWindows.has(windowId)
+ ? seenWindows.get(windowId)
+ : browser.windows.get(windowId);
+ }
+
+ browser.windows.onCreated.addListener(window => {
+ browser.test.assertFalse(
+ window.incognito,
+ `windows.onCreated monitor extension got expected incognito value`
+ );
+ seenWindows.set(window.id, window);
+ });
+ browser.windows.onRemoved.addListener(async windowId => {
+ let window;
+ try {
+ window = await getWindowById(windowId);
+ } catch (e) {
+ browser.test.fail(
+ `windows.onCreated for id ${windowId} unexpected failure ${e}\n`
+ );
+ return;
+ }
+ browser.test.assertFalse(
+ window.incognito,
+ `windows.onRemoved ${window.id}: monitor extension got expected incognito value`
+ );
+ });
+ browser.windows.onFocusChanged.addListener(async windowId => {
+ if (windowId == browser.windows.WINDOW_ID_NONE) {
+ return;
+ }
+ // onFocusChanged will also fire for blur so check actual window.incognito value.
+ let window;
+ try {
+ window = await getWindowById(windowId);
+ } catch (e) {
+ browser.test.fail(
+ `windows.onFocusChanged for id ${windowId} unexpected failure ${e}\n`
+ );
+ return;
+ }
+ browser.test.assertFalse(
+ window.incognito,
+ `windows.onFocusChanged ${window.id}: monitor extesion got expected incognito value`
+ );
+ seenWindows.set(window.id, window);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ incognitoOverride: "not_allowed",
+ background,
+ });
+ await extension.startup();
+ return extension;
+}
+
+async function getIncognitoWindow(url = "about:privatebrowsing") {
+ // Since events will be limited based on incognito, we need a
+ // spanning extension to get the tab id so we can test access failure.
+
+ function background(expectUrl) {
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.status === "complete" && tab.url === expectUrl) {
+ browser.test.sendMessage("data", { tabId, windowId: tab.windowId });
+ }
+ });
+ }
+
+ let windowWatcher = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background: `(${background})(${JSON.stringify(url)})`,
+ incognitoOverride: "spanning",
+ });
+
+ await windowWatcher.startup();
+ let data = windowWatcher.awaitMessage("data");
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser, url);
+
+ let details = await data;
+ await windowWatcher.unload();
+ return { win, details };
+}
+
+function getScreenAt(left, top, width, height) {
+ const screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
+ Ci.nsIScreenManager
+ );
+ return screenManager.screenForRect(left, top, width, height);
+}
+
+function roundCssPixcel(pixel, screen) {
+ return Math.floor(
+ Math.floor(pixel * screen.defaultCSSScaleFactor) /
+ screen.defaultCSSScaleFactor
+ );
+}
+
+function getCssAvailRect(screen) {
+ const availDeviceLeft = {};
+ const availDeviceTop = {};
+ const availDeviceWidth = {};
+ const availDeviceHeight = {};
+ screen.GetAvailRect(
+ availDeviceLeft,
+ availDeviceTop,
+ availDeviceWidth,
+ availDeviceHeight
+ );
+ const factor = screen.defaultCSSScaleFactor;
+ const left = Math.floor(availDeviceLeft.value / factor);
+ const top = Math.floor(availDeviceTop.value / factor);
+ const width = Math.floor(availDeviceWidth.value / factor);
+ const height = Math.floor(availDeviceHeight.value / factor);
+ return {
+ left,
+ top,
+ width,
+ height,
+ right: left + width,
+ bottom: top + height,
+ };
+}
+
+function isRectContained(actualRect, maxRect) {
+ is(
+ `top=${actualRect.top >= maxRect.top},bottom=${
+ actualRect.bottom <= maxRect.bottom
+ },left=${actualRect.left >= maxRect.left},right=${
+ actualRect.right <= maxRect.right
+ }`,
+ "top=true,bottom=true,left=true,right=true",
+ `Dimension must be inside, top:${actualRect.top}>=${maxRect.top}, bottom:${actualRect.bottom}<=${maxRect.bottom}, left:${actualRect.left}>=${maxRect.left}, right:${actualRect.right}<=${maxRect.right}`
+ );
+}
+
+function getToolboxBackgroundColor() {
+ let toolbox = document.getElementById("navigator-toolbox");
+ // Ignore any potentially ongoing transition.
+ toolbox.style.transitionProperty = "none";
+ let color = window.getComputedStyle(toolbox).backgroundColor;
+ toolbox.style.transitionProperty = "";
+ return color;
+}
diff --git a/browser/components/extensions/test/browser/head_browserAction.js b/browser/components/extensions/test/browser/head_browserAction.js
new file mode 100644
index 0000000000..41fda8fc06
--- /dev/null
+++ b/browser/components/extensions/test/browser/head_browserAction.js
@@ -0,0 +1,368 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported testPopupSize */
+
+// This file is imported into the same scope as head.js.
+
+/* import-globals-from head.js */
+
+// A test helper that retrives an old and new value after a given delay
+// and then check that calls an `isCompleted` callback to check that
+// the value has reached the expected value.
+function waitUntilValue({
+ getValue,
+ isCompleted,
+ message,
+ delay: delayTime,
+ times = 1,
+} = {}) {
+ let i = 0;
+ return BrowserTestUtils.waitForCondition(async () => {
+ const oldVal = await getValue();
+ await delay(delayTime);
+ const newVal = await getValue();
+
+ const done = isCompleted(oldVal, newVal);
+
+ // Reset the counter if the value wasn't the expected one.
+ if (!done) {
+ i = 0;
+ }
+
+ return done && times === ++i;
+ }, message);
+}
+
+async function testPopupSize(
+ standardsMode,
+ browserWin = window,
+ arrowSide = "top"
+) {
+ let docType = standardsMode ? "" : "";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ browser_style: false,
+ },
+ },
+
+ files: {
+ "popup.html": `${docType}
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ if (arrowSide == "top") {
+ // Test the standalone panel for a toolbar button.
+ let browser = await openBrowserActionPanel(extension, browserWin, true);
+
+ let dims = await promiseContentDimensions(browser);
+
+ is(
+ dims.isStandards,
+ standardsMode,
+ "Document has the expected compat mode"
+ );
+
+ let { innerWidth, innerHeight } = dims.window;
+
+ dims = await alterContent(browser, () => {
+ content.document.body.classList.add("bigger");
+ });
+
+ let win = dims.window;
+ Assert.lessOrEqual(
+ Math.abs(win.innerHeight - innerHeight),
+ 1,
+ `Window height should not change (${win.innerHeight} ~= ${innerHeight})`
+ );
+ Assert.greater(
+ win.innerWidth,
+ innerWidth,
+ `Window width should increase (${win.innerWidth} > ${innerWidth})`
+ );
+
+ dims = await alterContent(browser, () => {
+ content.document.body.classList.remove("bigger");
+ });
+
+ win = dims.window;
+
+ // The getContentSize calculation is not always reliable to single-pixel
+ // precision.
+ Assert.lessOrEqual(
+ Math.abs(win.innerHeight - innerHeight),
+ 1,
+ `Window height should return to approximately its original value (${win.innerHeight} ~= ${innerHeight})`
+ );
+ Assert.lessOrEqual(
+ Math.abs(win.innerWidth - innerWidth),
+ 1,
+ `Window width should return to approximately its original value (${win.innerWidth} ~= ${innerWidth})`
+ );
+
+ await closeBrowserAction(extension, browserWin);
+ }
+
+ // Test the PanelUI panel for a menu panel button.
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
+
+ let panel = browserWin.gUnifiedExtensions.panel;
+ panel.setAttribute("animate", "false");
+
+ let shownPromise = Promise.resolve();
+
+ let browser = await openBrowserActionPanel(extension, browserWin);
+
+ // Small changes if this is a fixed width window
+ let isFixedWidth = !widget.disallowSubView;
+
+ // Wait long enough to make sure the initial popup positioning has been completed (
+ // by waiting until the value stays the same for 20 times in a row).
+ await waitUntilValue({
+ getValue: () => panel.getBoundingClientRect().top,
+ isCompleted: (oldVal, newVal) => {
+ return oldVal === newVal;
+ },
+ times: 20,
+ message: "Wait the popup opening to be completed",
+ delay: 500,
+ });
+
+ let origPanelRect = panel.getBoundingClientRect();
+
+ // Check that the panel is still positioned as expected.
+ let checkPanelPosition = () => {
+ is(
+ panel.getAttribute("side"),
+ arrowSide,
+ "Panel arrow is positioned as expected"
+ );
+
+ let panelRect = panel.getBoundingClientRect();
+ if (arrowSide == "top") {
+ is(panelRect.top, origPanelRect.top, "Panel has not moved downwards");
+ Assert.greaterOrEqual(
+ panelRect.bottom,
+ origPanelRect.bottom,
+ `Panel has not shrunk from original size (${panelRect.bottom} >= ${origPanelRect.bottom})`
+ );
+
+ let screenBottom =
+ browserWin.screen.availTop + browserWin.screen.availHeight;
+ let panelBottom = browserWin.mozInnerScreenY + panelRect.bottom;
+ Assert.lessOrEqual(
+ Math.round(panelBottom),
+ screenBottom,
+ `Bottom of popup should be on-screen. (${panelBottom} <= ${screenBottom})`
+ );
+ } else {
+ is(panelRect.bottom, origPanelRect.bottom, "Panel has not moved upwards");
+ Assert.lessOrEqual(
+ panelRect.top,
+ origPanelRect.top,
+ `Panel has not shrunk from original size (${panelRect.top} <= ${origPanelRect.top})`
+ );
+
+ let panelTop = browserWin.mozInnerScreenY + panelRect.top;
+ Assert.greaterOrEqual(
+ panelTop,
+ browserWin.screen.availTop,
+ `Top of popup should be on-screen. (${panelTop} >= ${browserWin.screen.availTop})`
+ );
+ }
+ };
+
+ await awaitBrowserLoaded(browser);
+ await shownPromise;
+
+ // Wait long enough to make sure the initial resize debouncing timer has
+ // expired.
+ await waitUntilValue({
+ getValue: () => promiseContentDimensions(browser),
+ isCompleted: (oldDims, newDims) => {
+ return (
+ oldDims.window.innerWidth === newDims.window.innerWidth &&
+ oldDims.window.innerHeight === newDims.window.innerHeight
+ );
+ },
+ message: "Wait the popup resize to be completed",
+ delay: 500,
+ });
+
+ let dims = await promiseContentDimensions(browser);
+
+ is(dims.isStandards, standardsMode, "Document has the expected compat mode");
+
+ // If the browser's preferred height is smaller than the initial height of the
+ // panel, then it will still take up the full available vertical space. Even
+ // so, we need to check that we've gotten the preferred height calculation
+ // correct, so check that explicitly.
+ let getHeight = () => parseFloat(browser.style.height);
+
+ let { innerWidth, innerHeight } = dims.window;
+ let height = getHeight();
+
+ let setClass = className => {
+ content.document.body.className = className;
+ };
+
+ info(
+ "Increase body children's width. " +
+ "Expect them to wrap, and the frame to grow vertically rather than widen."
+ );
+
+ dims = await alterContent(browser, setClass, "big");
+ let win = dims.window;
+
+ Assert.greater(
+ getHeight(),
+ height,
+ `Browser height should increase (${getHeight()} > ${height})`
+ );
+
+ if (isFixedWidth) {
+ is(win.innerWidth, innerWidth, "Window width should not change");
+ } else {
+ Assert.greaterOrEqual(
+ win.innerWidth,
+ innerWidth,
+ `Window width should increase (${win.innerWidth} >= ${innerWidth})`
+ );
+ }
+ Assert.greaterOrEqual(
+ win.innerHeight,
+ innerHeight,
+ `Window height should increase (${win.innerHeight} >= ${innerHeight})`
+ );
+ Assert.lessOrEqual(
+ win.scrollMaxY,
+ 1,
+ "Document should not be vertically scrollable"
+ );
+
+ checkPanelPosition();
+
+ if (isFixedWidth) {
+ // Test a fixed width window grows in height when elements wrap
+ info(
+ "Increase body children's width and height. " +
+ "Expect them to wrap, and the frame to grow vertically rather than widen."
+ );
+
+ dims = await alterContent(browser, setClass, "bigger");
+ win = dims.window;
+
+ Assert.greater(
+ getHeight(),
+ height,
+ `Browser height should increase (${getHeight()} > ${height})`
+ );
+
+ is(win.innerWidth, innerWidth, "Window width should not change");
+ Assert.greaterOrEqual(
+ win.innerHeight,
+ innerHeight,
+ `Window height should increase (${win.innerHeight} >= ${innerHeight})`
+ );
+ Assert.lessOrEqual(
+ win.scrollMaxY,
+ 1,
+ "Document should not be vertically scrollable"
+ );
+
+ checkPanelPosition();
+ }
+
+ info(
+ "Increase body height beyond the height of the screen. " +
+ "Expect the panel to grow to accommodate, but not larger than the height of the screen."
+ );
+
+ dims = await alterContent(browser, setClass, "huge");
+ win = dims.window;
+
+ Assert.greater(
+ getHeight(),
+ height,
+ `Browser height should increase (${getHeight()} > ${height})`
+ );
+
+ is(win.innerWidth, innerWidth, "Window width should not change");
+ Assert.greater(
+ win.innerHeight,
+ innerHeight,
+ `Window height should increase (${win.innerHeight} > ${innerHeight})`
+ );
+ // Commented out check for the window height here which mysteriously breaks
+ // on infra but not locally. bug 1396843 covers re-enabling this.
+ // ok(win.innerHeight < screen.height, `Window height be less than the screen height (${win.innerHeight} < ${screen.height})`);
+ Assert.greater(
+ win.scrollMaxY,
+ 0,
+ `Document should be vertically scrollable (${win.scrollMaxY} > 0)`
+ );
+
+ checkPanelPosition();
+
+ info("Restore original styling. Expect original dimensions.");
+ dims = await alterContent(browser, setClass, "");
+ win = dims.window;
+
+ is(getHeight(), height, "Browser height should return to its original value");
+
+ is(win.innerWidth, innerWidth, "Window width should not change");
+ is(
+ win.innerHeight,
+ innerHeight,
+ "Window height should return to its original value"
+ );
+ Assert.lessOrEqual(
+ win.scrollMaxY,
+ 1,
+ "Document should not be vertically scrollable"
+ );
+
+ checkPanelPosition();
+
+ await closeBrowserAction(extension, browserWin);
+
+ await extension.unload();
+}
diff --git a/browser/components/extensions/test/browser/head_devtools.js b/browser/components/extensions/test/browser/head_devtools.js
new file mode 100644
index 0000000000..934d4c8a80
--- /dev/null
+++ b/browser/components/extensions/test/browser/head_devtools.js
@@ -0,0 +1,162 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported
+ assertDevToolsExtensionEnabled,
+ closeToolboxForTab,
+ navigateToWithDevToolsOpen
+ openToolboxForTab,
+ registerBlankToolboxPanel,
+ TOOLBOX_BLANK_PANEL_ID,
+*/
+
+ChromeUtils.defineESModuleGetters(this, {
+ loader: "resource://devtools/shared/loader/Loader.sys.mjs",
+ DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
+});
+ChromeUtils.defineLazyGetter(this, "gDevTools", () => {
+ const { gDevTools } = loader.require("devtools/client/framework/devtools");
+ return gDevTools;
+});
+
+const TOOLBOX_BLANK_PANEL_ID = "testBlankPanel";
+
+// Register a blank custom tool so that we don't need to wait the webconsole
+// to be fully loaded/unloaded to prevent intermittent failures (related
+// to a webconsole that is still loading when the test has been completed).
+async function registerBlankToolboxPanel() {
+ const testBlankPanel = {
+ id: TOOLBOX_BLANK_PANEL_ID,
+ url: "about:blank",
+ label: "Blank Tool",
+ isToolSupported() {
+ return true;
+ },
+ build(iframeWindow, toolbox) {
+ return Promise.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ panelDoc: iframeWindow.document,
+ destroy() {},
+ });
+ },
+ };
+
+ registerCleanupFunction(() => {
+ gDevTools.unregisterTool(testBlankPanel.id);
+ });
+
+ gDevTools.registerTool(testBlankPanel);
+}
+
+async function openToolboxForTab(tab, panelId = TOOLBOX_BLANK_PANEL_ID) {
+ if (
+ panelId == TOOLBOX_BLANK_PANEL_ID &&
+ !gDevTools.getToolDefinition(panelId)
+ ) {
+ info(`Registering ${TOOLBOX_BLANK_PANEL_ID} tool to the developer tools`);
+ registerBlankToolboxPanel();
+ }
+
+ const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: panelId });
+ const { url, outerWindowID } = toolbox.target.form;
+ info(
+ `Developer toolbox opened on panel "${panelId}" for target ${JSON.stringify(
+ { url, outerWindowID }
+ )}`
+ );
+ return toolbox;
+}
+
+async function closeToolboxForTab(tab) {
+ await gDevTools.closeToolboxForTab(tab);
+ const tabUrl = tab.linkedBrowser.currentURI.spec;
+ info(`Developer toolbox closed for tab "${tabUrl}"`);
+}
+
+function assertDevToolsExtensionEnabled(uuid, enabled) {
+ for (let toolbox of DevToolsShim.getToolboxes()) {
+ is(
+ enabled,
+ !!toolbox.isWebExtensionEnabled(uuid),
+ `extension is ${enabled ? "enabled" : "disabled"} on toolbox`
+ );
+ }
+}
+
+/**
+ * Navigate the currently selected tab to a new URL and wait for it to load.
+ * Also wait for the toolbox to attach to the new target, if we navigated
+ * to a new process.
+ *
+ * @param {object} tab The tab to redirect.
+ * @param {string} uri The url to be loaded in the current tab.
+ * @param {boolean} isErrorPage You may pass `true` is the URL is an error
+ * page. Otherwise BrowserTestUtils.browserLoaded will wait
+ * for 'load' event, which never fires for error pages.
+ *
+ * @returns {Promise} A promise that resolves when the page has fully loaded.
+ */
+async function navigateToWithDevToolsOpen(tab, uri, isErrorPage = false) {
+ const toolbox = gDevTools.getToolboxForTab(tab);
+ const target = toolbox.target;
+
+ // If we're switching origins, we need to wait for the 'switched-target'
+ // event to make sure everything is ready.
+ // Navigating from/to pages loaded in the parent process, like about:robots,
+ // also spawn new targets.
+ // (If target switching is disabled, the toolbox will reboot)
+ const onTargetSwitched =
+ toolbox.commands.targetCommand.once("switched-target");
+ // Otherwise, if we don't switch target, it is safe to wait for navigate event.
+ const onNavigate = target.once("navigate");
+
+ // If the current top-level target follows the window global lifecycle, a
+ // target switch will occur regardless of process changes.
+ const targetFollowsWindowLifecycle =
+ target.targetForm.followWindowGlobalLifeCycle;
+
+ info(`Load document "${uri}"`);
+ const browser = gBrowser.selectedBrowser;
+ const currentPID = browser.browsingContext.currentWindowGlobal.osPid;
+ const currentBrowsingContextID = browser.browsingContext.id;
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ null,
+ isErrorPage
+ );
+ BrowserTestUtils.startLoadingURIString(browser, uri);
+
+ info(`Waiting for page to be loaded…`);
+ await onBrowserLoaded;
+ info(`→ page loaded`);
+
+ // Compare the PIDs (and not the toolbox's targets) as PIDs are updated also immediately,
+ // while target may be updated slightly later.
+ const switchedToAnotherProcess =
+ currentPID !== browser.browsingContext.currentWindowGlobal.osPid;
+ const switchedToAnotherBrowsingContext =
+ currentBrowsingContextID !== browser.browsingContext.id;
+
+ // If:
+ // - the tab navigated to another process, or,
+ // - the tab navigated to another browsing context, or,
+ // - if the old target follows the window lifecycle
+ // then, expect a target switching.
+ if (
+ switchedToAnotherProcess ||
+ targetFollowsWindowLifecycle ||
+ switchedToAnotherBrowsingContext
+ ) {
+ info(`Waiting for target switch…`);
+ await onTargetSwitched;
+ info(`→ switched-target emitted`);
+ } else {
+ info(`Waiting for target 'navigate' event…`);
+ await onNavigate;
+ info(`→ 'navigate' emitted`);
+ }
+}
diff --git a/browser/components/extensions/test/browser/head_pageAction.js b/browser/components/extensions/test/browser/head_pageAction.js
new file mode 100644
index 0000000000..f80a6d3c98
--- /dev/null
+++ b/browser/components/extensions/test/browser/head_pageAction.js
@@ -0,0 +1,232 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported runTests */
+// This file is imported into the same scope as head.js.
+/* import-globals-from head.js */
+
+{
+ // At the moment extension language negotiation is tied to Firefox language
+ // negotiation result. That means that to test an extension in `es-ES`, we need
+ // to mock `es-ES` being available in Firefox and then request it.
+ //
+ // In the future, we should provide some way for tests to decouple their
+ // language selection from that of Firefox.
+ const avLocales = Services.locale.availableLocales;
+
+ Services.locale.availableLocales = ["en-US", "es-ES"];
+ registerCleanupFunction(() => {
+ Services.locale.availableLocales = avLocales;
+ });
+}
+
+async function runTests(options) {
+ function background(getTests) {
+ let tests;
+
+ // Gets the current details of the page action, and returns a
+ // promise that resolves to an object containing them.
+ async function getDetails(tabId) {
+ return {
+ title: await browser.pageAction.getTitle({ tabId }),
+ popup: await browser.pageAction.getPopup({ tabId }),
+ isShown: await browser.pageAction.isShown({ tabId }),
+ };
+ }
+
+ // Runs the next test in the `tests` array, checks the results,
+ // and passes control back to the outer test scope.
+ function nextTest() {
+ let test = tests.shift();
+
+ test(async expecting => {
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ let { id: tabId, windowId, url } = tab;
+
+ browser.test.log(`Get details: tab={id: ${tabId}, url: ${url}}`);
+
+ // Check that the API returns the expected values, and then
+ // run the next test.
+ let details = await getDetails(tabId);
+ if (expecting) {
+ browser.test.assertEq(
+ expecting.title,
+ details.title,
+ "expected value from getTitle"
+ );
+
+ browser.test.assertEq(
+ expecting.popup,
+ details.popup,
+ "expected value from getPopup"
+ );
+ }
+
+ browser.test.assertEq(
+ !!expecting,
+ details.isShown,
+ "expected value from isShown"
+ );
+
+ // Check that the actual icon has the expected values, then
+ // run the next test.
+ browser.test.sendMessage("nextTest", expecting, windowId, tests.length);
+ });
+ }
+
+ async function runTests() {
+ let tabs = [];
+ let windows = [];
+ tests = getTests(tabs, windows);
+
+ let resultTabs = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+
+ tabs[0] = resultTabs[0].id;
+ windows[0] = resultTabs[0].windowId;
+
+ nextTest();
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "runTests") {
+ runTests();
+ } else if (msg == "runNextTest") {
+ nextTest();
+ } else {
+ browser.test.fail(`Unexpected message: ${msg}`);
+ }
+ });
+
+ runTests();
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: options.manifest,
+
+ files: options.files || {},
+
+ background: `(${background})(${options.getTests})`,
+ });
+
+ let pageActionId;
+ let currentWindow = window;
+ let windows = [];
+
+ async function waitForDetails(details, windowId) {
+ function check() {
+ let { document } = Services.wm.getOuterWindowWithId(windowId);
+ let image = document.getElementById(pageActionId);
+ if (details == null) {
+ return image == null || image.getAttribute("disabled") == "true";
+ }
+ let title = details.title || options.manifest.name;
+ return (
+ !!image &&
+ getListStyleImage(image) == details.icon &&
+ image.getAttribute("tooltiptext") == title &&
+ image.getAttribute("aria-label") == title
+ );
+ // TODO: Popup URL. If this is updated, modify also checkDetails.
+ }
+
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async resolve => {
+ let maxCounter = 10;
+ while (!check() && --maxCounter > 0) {
+ info("checks left: " + maxCounter);
+ await promiseAnimationFrame(currentWindow);
+ }
+ resolve();
+ });
+ }
+
+ function checkDetails(details, windowId) {
+ let { document } = Services.wm.getOuterWindowWithId(windowId);
+ let image = document.getElementById(pageActionId);
+ if (details == null) {
+ ok(
+ image == null || image.getAttribute("disabled") == "true",
+ "image is disabled"
+ );
+ } else {
+ ok(image, "image exists");
+
+ is(getListStyleImage(image), details.icon, "icon URL is correct");
+
+ let title = details.title || options.manifest.name;
+ is(image.getAttribute("tooltiptext"), title, "image title is correct");
+ is(
+ image.getAttribute("aria-label"),
+ title,
+ "image aria-label is correct"
+ );
+ // TODO: Popup URL. If this is updated, modify also waitForDetails.
+ }
+ }
+
+ let testNewWindows = 1;
+
+ let awaitFinish = new Promise(resolve => {
+ extension.onMessage(
+ "nextTest",
+ async (expecting, windowId, testsRemaining) => {
+ if (!pageActionId) {
+ pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+ }
+
+ await waitForDetails(expecting, windowId);
+
+ checkDetails(expecting, windowId);
+
+ if (testsRemaining) {
+ extension.sendMessage("runNextTest");
+ } else if (testNewWindows) {
+ testNewWindows--;
+
+ BrowserTestUtils.openNewBrowserWindow()
+ .then(window => {
+ windows.push(window);
+ currentWindow = window;
+ return focusWindow(window);
+ })
+ .then(() => {
+ extension.sendMessage("runTests");
+ });
+ } else {
+ resolve();
+ }
+ }
+ );
+ });
+
+ let reqLoc = Services.locale.requestedLocales;
+ Services.locale.requestedLocales = ["es-ES"];
+
+ await extension.startup();
+
+ await awaitFinish;
+
+ await extension.unload();
+
+ Services.locale.requestedLocales = reqLoc;
+
+ let node = document.getElementById(pageActionId);
+ is(node, null, "pageAction image removed from document");
+
+ currentWindow = null;
+ for (let win of windows.splice(0)) {
+ node = win.document.getElementById(pageActionId);
+ is(node, null, "pageAction image removed from second document");
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+}
diff --git a/browser/components/extensions/test/browser/head_sessions.js b/browser/components/extensions/test/browser/head_sessions.js
new file mode 100644
index 0000000000..db58c128c6
--- /dev/null
+++ b/browser/components/extensions/test/browser/head_sessions.js
@@ -0,0 +1,64 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported recordInitialTimestamps onlyNewItemsFilter checkRecentlyClosed */
+
+let initialTimestamps = [];
+
+function recordInitialTimestamps(timestamps) {
+ initialTimestamps = timestamps;
+}
+
+function onlyNewItemsFilter(item) {
+ return !initialTimestamps.includes(item.lastModified);
+}
+
+function checkWindow(window) {
+ for (let prop of ["focused", "incognito", "alwaysOnTop"]) {
+ is(window[prop], false, `closed window has the expected value for ${prop}`);
+ }
+ for (let prop of ["state", "type"]) {
+ is(
+ window[prop],
+ "normal",
+ `closed window has the expected value for ${prop}`
+ );
+ }
+}
+
+function checkTab(tab, windowId, incognito) {
+ for (let prop of ["highlighted", "active", "pinned"]) {
+ is(tab[prop], false, `closed tab has the expected value for ${prop}`);
+ }
+ is(tab.windowId, windowId, "closed tab has the expected value for windowId");
+ is(
+ tab.incognito,
+ incognito,
+ "closed tab has the expected value for incognito"
+ );
+}
+
+function checkRecentlyClosed(
+ recentlyClosed,
+ expectedCount,
+ windowId,
+ incognito = false
+) {
+ let sessionIds = new Set();
+ is(
+ recentlyClosed.length,
+ expectedCount,
+ "the expected number of closed tabs/windows was found"
+ );
+ for (let item of recentlyClosed) {
+ if (item.window) {
+ sessionIds.add(item.window.sessionId);
+ checkWindow(item.window);
+ } else if (item.tab) {
+ sessionIds.add(item.tab.sessionId);
+ checkTab(item.tab, windowId, incognito);
+ }
+ }
+ is(sessionIds.size, expectedCount, "each item has a unique sessionId");
+}
diff --git a/browser/components/extensions/test/browser/head_unified_extensions.js b/browser/components/extensions/test/browser/head_unified_extensions.js
new file mode 100644
index 0000000000..941a00a5fb
--- /dev/null
+++ b/browser/components/extensions/test/browser/head_unified_extensions.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* exported clickUnifiedExtensionsItem,
+ closeExtensionsPanel,
+ createExtensions,
+ ensureMaximizedWindow,
+ getMessageBars,
+ getUnifiedExtensionsItem,
+ openExtensionsPanel,
+ openUnifiedExtensionsContextMenu,
+ promiseSetToolbarVisibility
+*/
+
+const getListView = (win = window) => {
+ const { panel } = win.gUnifiedExtensions;
+ ok(panel, "expected panel to be created");
+ return panel.querySelector("#unified-extensions-view");
+};
+
+const openExtensionsPanel = async (win = window) => {
+ const { button } = win.gUnifiedExtensions;
+ ok(button, "expected button");
+
+ const listView = getListView(win);
+ ok(listView, "expected list view");
+
+ const viewShown = BrowserTestUtils.waitForEvent(listView, "ViewShown");
+ button.click();
+ await viewShown;
+};
+
+const closeExtensionsPanel = async (win = window) => {
+ const { button } = win.gUnifiedExtensions;
+ ok(button, "expected button");
+
+ const hidden = BrowserTestUtils.waitForEvent(
+ win.gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ button.click();
+ await hidden;
+};
+
+const getUnifiedExtensionsItem = (extensionId, win = window) => {
+ const view = getListView(win);
+
+ // First try to find a CUI widget, otherwise a custom element when the
+ // extension does not have a browser action.
+ return (
+ view.querySelector(`toolbaritem[data-extensionid="${extensionId}"]`) ||
+ view.querySelector(`unified-extensions-item[extension-id="${extensionId}"]`)
+ );
+};
+
+const openUnifiedExtensionsContextMenu = async (extensionId, win = window) => {
+ const item = getUnifiedExtensionsItem(extensionId, win);
+ ok(item, `expected item for extensionId=${extensionId}`);
+ const button = item.querySelector(".unified-extensions-item-menu-button");
+ ok(button, "expected menu button");
+ // Make sure the button is visible before clicking on it (below) since the
+ // list of extensions can have a scrollbar (when there are many extensions
+ // and/or the window is small-ish).
+ button.scrollIntoView({ block: "center" });
+
+ const menu = win.document.getElementById("unified-extensions-context-menu");
+ ok(menu, "expected menu");
+
+ const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ // Use primary button click to open the context menu.
+ EventUtils.synthesizeMouseAtCenter(button, {}, win);
+ await shown;
+
+ return menu;
+};
+
+const clickUnifiedExtensionsItem = async (
+ win,
+ extensionId,
+ forceEnableButton = false
+) => {
+ // The panel should be closed automatically when we click an extension item.
+ await openExtensionsPanel(win);
+
+ const item = getUnifiedExtensionsItem(extensionId, win);
+ ok(item, `expected item for ${extensionId}`);
+
+ // The action button should be disabled when users aren't supposed to click
+ // on it but it might still be useful to re-enable it for testing purposes.
+ if (forceEnableButton) {
+ let actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ actionButton.disabled = false;
+ ok(!actionButton.disabled, "action button was force-enabled");
+ }
+
+ // Similar to `openUnifiedExtensionsContextMenu()`, we make sure the item is
+ // visible before clicking on it to prevent intermittents.
+ item.scrollIntoView({ block: "center" });
+
+ const popupHidden = BrowserTestUtils.waitForEvent(
+ win.document,
+ "popuphidden",
+ true
+ );
+ EventUtils.synthesizeMouseAtCenter(item, {}, win);
+ await popupHidden;
+};
+
+const createExtensions = (
+ arrayOfManifestData,
+ { useAddonManager = true, incognitoOverride, files } = {}
+) => {
+ return arrayOfManifestData.map(manifestData =>
+ ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "default-extension-name",
+ ...manifestData,
+ },
+ useAddonManager: useAddonManager ? "temporary" : undefined,
+ incognitoOverride,
+ files,
+ })
+ );
+};
+
+/**
+ * Given a window, this test helper resizes it so that the window takes most of
+ * the available screen size (unless the window is already maximized).
+ */
+const ensureMaximizedWindow = async win => {
+ info("ensuring maximized window...");
+
+ // Make sure we wait for window position to have settled
+ // to avoid unexpected failures.
+ let samePositionTimes = 0;
+ let lastScreenTop = win.screen.top;
+ let lastScreenLeft = win.screen.left;
+ win.moveTo(0, 0);
+ await TestUtils.waitForCondition(() => {
+ let isSamePosition =
+ lastScreenTop === win.screen.top && lastScreenLeft === win.screen.left;
+ if (!isSamePosition) {
+ lastScreenTop = win.screen.top;
+ lastScreenLeft = win.screen.left;
+ }
+ samePositionTimes = isSamePosition ? samePositionTimes + 1 : 0;
+ return samePositionTimes === 10;
+ }, "Wait for the chrome window position to settle");
+
+ const widthDiff = Math.max(win.screen.availWidth - win.outerWidth, 0);
+ const heightDiff = Math.max(win.screen.availHeight - win.outerHeight, 0);
+
+ if (widthDiff || heightDiff) {
+ info(
+ `resizing window... widthDiff=${widthDiff} - heightDiff=${heightDiff}`
+ );
+ win.windowUtils.ensureDirtyRootFrame();
+ win.resizeBy(widthDiff, heightDiff);
+ } else {
+ info(`not resizing window!`);
+ }
+
+ // Make sure we wait for window size to have settled.
+ let lastOuterWidth = win.outerWidth;
+ let lastOuterHeight = win.outerHeight;
+ let sameSizeTimes = 0;
+ await TestUtils.waitForCondition(() => {
+ const isSameSize =
+ win.outerWidth === lastOuterWidth && win.outerHeight === lastOuterHeight;
+ if (!isSameSize) {
+ lastOuterWidth = win.outerWidth;
+ lastOuterHeight = win.outerHeight;
+ }
+ sameSizeTimes = isSameSize ? sameSizeTimes + 1 : 0;
+ return sameSizeTimes === 10;
+ }, "Wait for the chrome window size to settle");
+};
+
+const promiseSetToolbarVisibility = (toolbar, visible) => {
+ const visibilityChanged = BrowserTestUtils.waitForMutationCondition(
+ toolbar,
+ { attributeFilter: ["collapsed"] },
+ () => toolbar.collapsed != visible
+ );
+ setToolbarVisibility(toolbar, visible, undefined, false);
+ return visibilityChanged;
+};
+
+const getMessageBars = (win = window) => {
+ const { panel } = win.gUnifiedExtensions;
+ return panel.querySelectorAll(
+ "#unified-extensions-messages-container > moz-message-bar"
+ );
+};
diff --git a/browser/components/extensions/test/browser/head_webNavigation.js b/browser/components/extensions/test/browser/head_webNavigation.js
new file mode 100644
index 0000000000..314ddc9326
--- /dev/null
+++ b/browser/components/extensions/test/browser/head_webNavigation.js
@@ -0,0 +1,49 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported BASE_URL, SOURCE_PAGE, OPENED_PAGE,
+ runCreatedNavigationTargetTest */
+
+const BASE_URL =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser";
+const SOURCE_PAGE = `${BASE_URL}/webNav_createdTargetSource.html`;
+const OPENED_PAGE = `${BASE_URL}/webNav_createdTarget.html`;
+
+async function runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget,
+ expectedWebNavProps,
+}) {
+ await openNavTarget();
+
+ const webNavMsg = await extension.awaitMessage("webNavOnCreated");
+ const createdTabId = await extension.awaitMessage("tabsOnCreated");
+ const completedNavMsg = await extension.awaitMessage("webNavOnCompleted");
+
+ let { sourceTabId, sourceFrameId, url } = expectedWebNavProps;
+
+ is(webNavMsg.tabId, createdTabId, "Got the expected tabId property");
+ is(
+ webNavMsg.sourceTabId,
+ sourceTabId,
+ "Got the expected sourceTabId property"
+ );
+ is(
+ webNavMsg.sourceFrameId,
+ sourceFrameId,
+ "Got the expected sourceFrameId property"
+ );
+ is(webNavMsg.url, url, "Got the expected url property");
+
+ is(
+ completedNavMsg.tabId,
+ createdTabId,
+ "Got the expected webNavigation.onCompleted tabId property"
+ );
+ is(
+ completedNavMsg.url,
+ url,
+ "Got the expected webNavigation.onCompleted url property"
+ );
+}
diff --git a/browser/components/extensions/test/browser/redirect_to.sjs b/browser/components/extensions/test/browser/redirect_to.sjs
new file mode 100644
index 0000000000..a07747efbe
--- /dev/null
+++ b/browser/components/extensions/test/browser/redirect_to.sjs
@@ -0,0 +1,9 @@
+"use strict";
+
+function handleRequest(request, response) {
+ // redirect_to.sjs?ctxmenu-image.png
+ // redirects to : ctxmenu-image.png
+ let redirectUrl = request.queryString;
+ response.setStatusLine(request.httpVersion, "302", "Found");
+ response.setHeader("Location", redirectUrl, false);
+}
diff --git a/browser/components/extensions/test/browser/search-engines/another/manifest.json b/browser/components/extensions/test/browser/search-engines/another/manifest.json
new file mode 100644
index 0000000000..0f78854853
--- /dev/null
+++ b/browser/components/extensions/test/browser/search-engines/another/manifest.json
@@ -0,0 +1,19 @@
+{
+ "name": "another",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "another",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "another@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "another",
+ "search_url": "https://mochi.test:8888/browser/browser/components/search/test/browser/?search={searchTerms}&bar=1",
+ "suggest_url": "https://mochi.test:8888/browser/browser/modules/test/browser/usageTelemetrySearchSuggestions.sjs?{searchTerms}"
+ }
+ }
+}
diff --git a/browser/components/extensions/test/browser/search-engines/basic/manifest.json b/browser/components/extensions/test/browser/search-engines/basic/manifest.json
new file mode 100644
index 0000000000..96b29935cf
--- /dev/null
+++ b/browser/components/extensions/test/browser/search-engines/basic/manifest.json
@@ -0,0 +1,19 @@
+{
+ "name": "basic",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "basic",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "basic@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "basic",
+ "search_url": "https://mochi.test:8888/browser/browser/components/search/test/browser/?search={searchTerms}&foo=1",
+ "suggest_url": "https://mochi.test:8888/browser/browser/modules/test/browser/usageTelemetrySearchSuggestions.sjs?{searchTerms}"
+ }
+ }
+}
diff --git a/browser/components/extensions/test/browser/search-engines/engines.json b/browser/components/extensions/test/browser/search-engines/engines.json
new file mode 100644
index 0000000000..907aaa148f
--- /dev/null
+++ b/browser/components/extensions/test/browser/search-engines/engines.json
@@ -0,0 +1,38 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "basic@search.mozilla.org",
+ "name": "basic",
+ "search_url": "https://mochi.test:8888/browser/browser/components/search/test/browser/?search={searchTerms}&foo=1",
+ "suggest_url": "https://mochi.test:8888/browser/browser/modules/test/browser/usageTelemetrySearchSuggestions.sjs?{searchTerms}"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "yes"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "simple@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true }
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "another@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true }
+ }
+ ]
+ }
+ ]
+}
diff --git a/browser/components/extensions/test/browser/search-engines/simple/manifest.json b/browser/components/extensions/test/browser/search-engines/simple/manifest.json
new file mode 100644
index 0000000000..67d2974753
--- /dev/null
+++ b/browser/components/extensions/test/browser/search-engines/simple/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "Simple Engine",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "Simple engine with a different name from the WebExtension id prefix",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "simple@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Simple Engine",
+ "search_url": "https://example.com",
+ "params": [
+ {
+ "name": "sourceId",
+ "value": "Mozilla-search"
+ },
+ {
+ "name": "search",
+ "value": "{searchTerms}"
+ }
+ ],
+ "suggest_url": "https://example.com?search={searchTerms}"
+ }
+ }
+}
diff --git a/browser/components/extensions/test/browser/searchSuggestionEngine.sjs b/browser/components/extensions/test/browser/searchSuggestionEngine.sjs
new file mode 100644
index 0000000000..a356cbb1db
--- /dev/null
+++ b/browser/components/extensions/test/browser/searchSuggestionEngine.sjs
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function handleRequest(req, resp) {
+ let suffixes = ["foo", "bar"];
+ let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
diff --git a/browser/components/extensions/test/browser/searchSuggestionEngine.xml b/browser/components/extensions/test/browser/searchSuggestionEngine.xml
new file mode 100644
index 0000000000..703d459256
--- /dev/null
+++ b/browser/components/extensions/test/browser/searchSuggestionEngine.xml
@@ -0,0 +1,9 @@
+
+
+
+
+browser_searchSuggestionEngine searchSuggestionEngine.xml
+
+
+
diff --git a/browser/components/extensions/test/browser/silence.ogg b/browser/components/extensions/test/browser/silence.ogg
new file mode 100644
index 0000000000..7bdd68ab27
Binary files /dev/null and b/browser/components/extensions/test/browser/silence.ogg differ
diff --git a/browser/components/extensions/test/browser/wait-a-bit.sjs b/browser/components/extensions/test/browser/wait-a-bit.sjs
new file mode 100644
index 0000000000..e90133d752
--- /dev/null
+++ b/browser/components/extensions/test/browser/wait-a-bit.sjs
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+async function handleRequest(request, response) {
+ response.seizePower();
+
+ await new Promise(r => setTimeout(r, 2000));
+
+ response.write("HTTP/1.1 200 OK\r\n");
+ const body = "wait a bit ok";
+ response.write("Content-Type: text/html\r\n");
+ response.write(`Content-Length: ${body.length}\r\n`);
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
diff --git a/browser/components/extensions/test/browser/webNav_createdTarget.html b/browser/components/extensions/test/browser/webNav_createdTarget.html
new file mode 100644
index 0000000000..e8a985ef28
--- /dev/null
+++ b/browser/components/extensions/test/browser/webNav_createdTarget.html
@@ -0,0 +1,10 @@
+
+
+
+ WebNavigatio onCreatedNavigationTarget target
+
+
+
+ Go back to the source page
+
+
diff --git a/browser/components/extensions/test/browser/webNav_createdTargetSource.html b/browser/components/extensions/test/browser/webNav_createdTargetSource.html
new file mode 100644
index 0000000000..72d4aa56f5
--- /dev/null
+++ b/browser/components/extensions/test/browser/webNav_createdTargetSource.html
@@ -0,0 +1,45 @@
+
+
+
+ WebNavigatio onCreatedNavigationTarget source
+
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html b/browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html
new file mode 100644
index 0000000000..7a9e9ebc4a
--- /dev/null
+++ b/browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html
@@ -0,0 +1,42 @@
+
+
+
+ WebNavigatio onCreatedNavigationTarget source subframe
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/mochitest/.eslintrc.js b/browser/components/extensions/test/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..7802d13962
--- /dev/null
+++ b/browser/components/extensions/test/mochitest/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ env: {
+ browser: true,
+ webextensions: true,
+ },
+};
diff --git a/browser/components/extensions/test/mochitest/mochitest.toml b/browser/components/extensions/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..bc8bd0d40a
--- /dev/null
+++ b/browser/components/extensions/test/mochitest/mochitest.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+skip-if = ["os == 'android'"] # bug 1730213
+support-files = [
+ "../../../../../toolkit/components/extensions/test/mochitest/test_ext_all_apis.js",
+ "../../../../../toolkit/components/extensions/test/mochitest/file_sample.html",
+]
+tags = "webextensions"
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["test_ext_all_apis.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
diff --git a/browser/components/extensions/test/mochitest/test_ext_all_apis.html b/browser/components/extensions/test/mochitest/test_ext_all_apis.html
new file mode 100644
index 0000000000..0433dc5b7e
--- /dev/null
+++ b/browser/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -0,0 +1,83 @@
+
+
+
+ WebExtension test
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/xpcshell/.eslintrc.js b/browser/components/extensions/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..3622fff4f6
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ env: {
+ // The tests in this folder are testing based on WebExtensions, so lets
+ // just define the webextensions environment here.
+ webextensions: true,
+ },
+};
diff --git a/browser/components/extensions/test/xpcshell/data/test/manifest.json b/browser/components/extensions/test/xpcshell/data/test/manifest.json
new file mode 100644
index 0000000000..b14c90e9c4
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/data/test/manifest.json
@@ -0,0 +1,80 @@
+{
+ "name": "MozParamsTest",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test@search.mozilla.org"
+ }
+ },
+ "description": "A test search engine (based on Google search)",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "MozParamsTest",
+ "search_url": "https://example.com/?q={searchTerms}",
+ "params": [
+ {
+ "name": "test-0",
+ "condition": "purpose",
+ "purpose": "contextmenu",
+ "value": "0"
+ },
+ {
+ "name": "test-1",
+ "condition": "purpose",
+ "purpose": "searchbar",
+ "value": "1"
+ },
+ {
+ "name": "test-2",
+ "condition": "purpose",
+ "purpose": "homepage",
+ "value": "2"
+ },
+ {
+ "name": "test-3",
+ "condition": "purpose",
+ "purpose": "keyword",
+ "value": "3"
+ },
+ {
+ "name": "test-4",
+ "condition": "purpose",
+ "purpose": "newtab",
+ "value": "4"
+ },
+ {
+ "name": "simple",
+ "value": "5"
+ },
+ {
+ "name": "term",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "lang",
+ "value": "{language}"
+ },
+ {
+ "name": "locale",
+ "value": "{moz:locale}"
+ },
+ {
+ "name": "prefval",
+ "condition": "pref",
+ "pref": "code"
+ },
+ {
+ "name": "experimenter-1",
+ "condition": "pref",
+ "pref": "nimbus-key-1"
+ },
+ {
+ "name": "experimenter-2",
+ "condition": "pref",
+ "pref": "nimbus-key-2"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/components/extensions/test/xpcshell/data/test2/manifest.json b/browser/components/extensions/test/xpcshell/data/test2/manifest.json
new file mode 100644
index 0000000000..197a993189
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/data/test2/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "MozParamsTest2",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test2@search.mozilla.org"
+ }
+ },
+ "description": "A second test search engine",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "MozParamsTest2",
+ "search_url": "https://example.com/2/?q={searchTerms}",
+ "params": [
+ {
+ "name": "simple2",
+ "value": "5"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/components/extensions/test/xpcshell/head.js b/browser/components/extensions/test/xpcshell/head.js
new file mode 100644
index 0000000000..9ac33637ed
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -0,0 +1,78 @@
+"use strict";
+
+/* exported createHttpServer, promiseConsoleOutput, assertPersistentListeners */
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+// eslint-disable-next-line no-unused-vars
+ChromeUtils.defineESModuleGetters(this, {
+ Extension: "resource://gre/modules/Extension.sys.mjs",
+ ExtensionData: "resource://gre/modules/Extension.sys.mjs",
+ ExtensionTestUtils:
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ HttpServer: "resource://testing-common/httpd.sys.mjs",
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+ Schemas: "resource://gre/modules/Schemas.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+
+ExtensionTestUtils.init(this);
+
+// Persistent Listener test functionality
+const { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
+
+/**
+ * Creates a new HttpServer for testing, and begins listening on the
+ * specified port. Automatically shuts down the server when the test
+ * unit ends.
+ *
+ * @param {integer} [port]
+ * The port to listen on. If omitted, listen on a random
+ * port. The latter is the preferred behavior.
+ *
+ * @returns {HttpServer}
+ */
+function createHttpServer(port = -1) {
+ let server = new HttpServer();
+ server.start(port);
+
+ registerCleanupFunction(() => {
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
+
+ return server;
+}
+
+var promiseConsoleOutput = async function (task) {
+ const DONE = `=== console listener ${Math.random()} done ===`;
+
+ let listener;
+ let messages = [];
+ let awaitListener = new Promise(resolve => {
+ listener = msg => {
+ if (msg == DONE) {
+ resolve();
+ } else {
+ void (msg instanceof Ci.nsIConsoleMessage);
+ messages.push(msg);
+ }
+ };
+ });
+
+ Services.console.registerListener(listener);
+ try {
+ let result = await task();
+
+ Services.console.logStringMessage(DONE);
+ await awaitListener;
+
+ return { messages, result };
+ } finally {
+ Services.console.unregisterListener(listener);
+ }
+};
diff --git a/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js
new file mode 100644
index 0000000000..15d09d1163
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js
@@ -0,0 +1,1725 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "43"
+);
+
+add_task(async function test_bookmarks() {
+ async function background() {
+ let unsortedId, ourId;
+ let initialBookmarkCount = 0;
+ let createdBookmarks = new Set();
+ let createdFolderId;
+ let createdSeparatorId;
+ let collectedEvents = [];
+ const nonExistentId = "000000000000";
+ const bookmarkGuids = {
+ menuGuid: "menu________",
+ toolbarGuid: "toolbar_____",
+ unfiledGuid: "unfiled_____",
+ rootGuid: "root________",
+ };
+
+ function checkOurBookmark(bookmark) {
+ browser.test.assertEq(ourId, bookmark.id, "Bookmark has the expected Id");
+ browser.test.assertTrue(
+ "parentId" in bookmark,
+ "Bookmark has a parentId"
+ );
+ browser.test.assertEq(
+ 0,
+ bookmark.index,
+ "Bookmark has the expected index"
+ ); // We assume there are no other bookmarks.
+ browser.test.assertEq(
+ "http://example.org/",
+ bookmark.url,
+ "Bookmark has the expected url"
+ );
+ browser.test.assertEq(
+ "test bookmark",
+ bookmark.title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertTrue(
+ "dateAdded" in bookmark,
+ "Bookmark has a dateAdded"
+ );
+ browser.test.assertFalse(
+ "dateGroupModified" in bookmark,
+ "Bookmark does not have a dateGroupModified"
+ );
+ browser.test.assertFalse(
+ "unmodifiable" in bookmark,
+ "Bookmark is not unmodifiable"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ bookmark.type,
+ "Bookmark is of type bookmark"
+ );
+ }
+
+ function checkBookmark(expected, bookmark) {
+ browser.test.assertEq(
+ expected.url,
+ bookmark.url,
+ "Bookmark has the expected url"
+ );
+ browser.test.assertEq(
+ expected.title,
+ bookmark.title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ expected.index,
+ bookmark.index,
+ "Bookmark has expected index"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ bookmark.type,
+ "Bookmark is of type bookmark"
+ );
+ if ("parentId" in expected) {
+ browser.test.assertEq(
+ expected.parentId,
+ bookmark.parentId,
+ "Bookmark has the expected parentId"
+ );
+ }
+ }
+
+ function checkOnCreated(
+ id,
+ parentId,
+ index,
+ title,
+ url,
+ dateAdded,
+ type = "bookmark"
+ ) {
+ let createdData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onCreated",
+ createdData.event,
+ "onCreated was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ createdData.id,
+ "onCreated event received the expected id"
+ );
+ let bookmark = createdData.bookmark;
+ browser.test.assertEq(
+ id,
+ bookmark.id,
+ "onCreated event received the expected bookmark id"
+ );
+ browser.test.assertEq(
+ parentId,
+ bookmark.parentId,
+ "onCreated event received the expected bookmark parentId"
+ );
+ browser.test.assertEq(
+ index,
+ bookmark.index,
+ "onCreated event received the expected bookmark index"
+ );
+ browser.test.assertEq(
+ title,
+ bookmark.title,
+ "onCreated event received the expected bookmark title"
+ );
+ browser.test.assertEq(
+ url,
+ bookmark.url,
+ "onCreated event received the expected bookmark url"
+ );
+ browser.test.assertEq(
+ dateAdded,
+ bookmark.dateAdded,
+ "onCreated event received the expected bookmark dateAdded"
+ );
+ browser.test.assertEq(
+ type,
+ bookmark.type,
+ "onCreated event received the expected bookmark type"
+ );
+ }
+
+ function checkOnChanged(id, url, title) {
+ // If both url and title are changed, then url is fired last.
+ let changedData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onChanged",
+ changedData.event,
+ "onChanged was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ changedData.id,
+ "onChanged event received the expected id"
+ );
+ browser.test.assertEq(
+ url,
+ changedData.info.url,
+ "onChanged event received the expected url"
+ );
+ // title is fired first.
+ changedData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onChanged",
+ changedData.event,
+ "onChanged was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ changedData.id,
+ "onChanged event received the expected id"
+ );
+ browser.test.assertEq(
+ title,
+ changedData.info.title,
+ "onChanged event received the expected title"
+ );
+ }
+
+ function checkOnMoved(id, parentId, oldParentId, index, oldIndex) {
+ let movedData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onMoved",
+ movedData.event,
+ "onMoved was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ movedData.id,
+ "onMoved event received the expected id"
+ );
+ let info = movedData.info;
+ browser.test.assertEq(
+ parentId,
+ info.parentId,
+ "onMoved event received the expected parentId"
+ );
+ browser.test.assertEq(
+ oldParentId,
+ info.oldParentId,
+ "onMoved event received the expected oldParentId"
+ );
+ browser.test.assertEq(
+ index,
+ info.index,
+ "onMoved event received the expected index"
+ );
+ browser.test.assertEq(
+ oldIndex,
+ info.oldIndex,
+ "onMoved event received the expected oldIndex"
+ );
+ }
+
+ function checkOnRemoved(id, parentId, index, title, url, type = "folder") {
+ let removedData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onRemoved",
+ removedData.event,
+ "onRemoved was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ removedData.id,
+ "onRemoved event received the expected id"
+ );
+ let info = removedData.info;
+ browser.test.assertEq(
+ parentId,
+ removedData.info.parentId,
+ "onRemoved event received the expected parentId"
+ );
+ browser.test.assertEq(
+ index,
+ removedData.info.index,
+ "onRemoved event received the expected index"
+ );
+ let node = info.node;
+ browser.test.assertEq(
+ id,
+ node.id,
+ "onRemoved event received the expected node id"
+ );
+ browser.test.assertEq(
+ parentId,
+ node.parentId,
+ "onRemoved event received the expected node parentId"
+ );
+ browser.test.assertEq(
+ index,
+ node.index,
+ "onRemoved event received the expected node index"
+ );
+ browser.test.assertEq(
+ url,
+ node.url,
+ "onRemoved event received the expected node url"
+ );
+ browser.test.assertEq(
+ title,
+ node.title,
+ "onRemoved event received the expected node title"
+ );
+ browser.test.assertEq(
+ type,
+ node.type,
+ "onRemoved event received the expected node type"
+ );
+ }
+
+ browser.bookmarks.onChanged.addListener((id, info) => {
+ collectedEvents.push({ event: "onChanged", id, info });
+ });
+
+ browser.bookmarks.onCreated.addListener((id, bookmark) => {
+ collectedEvents.push({ event: "onCreated", id, bookmark });
+ });
+
+ browser.bookmarks.onMoved.addListener((id, info) => {
+ collectedEvents.push({ event: "onMoved", id, info });
+ });
+
+ browser.bookmarks.onRemoved.addListener((id, info) => {
+ collectedEvents.push({ event: "onRemoved", id, info });
+ });
+
+ await browser.test.assertRejects(
+ browser.bookmarks.get(["not-a-bookmark-guid"]),
+ /Invalid value for property 'guid': "not-a-bookmark-guid"/,
+ "Expected error thrown when trying to get a bookmark using an invalid guid"
+ );
+
+ await browser.test
+ .assertRejects(
+ browser.bookmarks.get([nonExistentId]),
+ /Bookmark not found/,
+ "Expected error thrown when trying to get a bookmark using a non-existent Id"
+ )
+ .then(() => {
+ return browser.bookmarks.search({});
+ })
+ .then(results => {
+ initialBookmarkCount = results.length;
+ return browser.bookmarks.create({
+ title: "test bookmark",
+ url: "http://example.org",
+ type: "bookmark",
+ });
+ })
+ .then(result => {
+ ourId = result.id;
+ checkOurBookmark(result);
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected event received"
+ );
+ checkOnCreated(
+ ourId,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "test bookmark",
+ "http://example.org/",
+ result.dateAdded
+ );
+
+ return browser.bookmarks.get(ourId);
+ })
+ .then(results => {
+ browser.test.assertEq(results.length, 1);
+ checkOurBookmark(results[0]);
+
+ unsortedId = results[0].parentId;
+ return browser.bookmarks.get(unsortedId);
+ })
+ .then(results => {
+ let folder = results[0];
+ browser.test.assertEq(1, results.length, "1 bookmark was returned");
+
+ browser.test.assertEq(
+ unsortedId,
+ folder.id,
+ "Folder has the expected id"
+ );
+ browser.test.assertTrue("parentId" in folder, "Folder has a parentId");
+ browser.test.assertTrue("index" in folder, "Folder has an index");
+ browser.test.assertEq(
+ undefined,
+ folder.url,
+ "Folder does not have a url"
+ );
+ browser.test.assertEq(
+ "Other Bookmarks",
+ folder.title,
+ "Folder has the expected title"
+ );
+ browser.test.assertTrue(
+ "dateAdded" in folder,
+ "Folder has a dateAdded"
+ );
+ browser.test.assertTrue(
+ "dateGroupModified" in folder,
+ "Folder has a dateGroupModified"
+ );
+ browser.test.assertFalse(
+ "unmodifiable" in folder,
+ "Folder is not unmodifiable"
+ ); // TODO: Do we want to enable this?
+ browser.test.assertEq(
+ "folder",
+ folder.type,
+ "Folder has a type of folder"
+ );
+
+ return browser.bookmarks.getChildren(unsortedId);
+ })
+ .then(async results => {
+ browser.test.assertEq(1, results.length, "The folder has one child");
+ checkOurBookmark(results[0]);
+
+ await browser.test.assertRejects(
+ browser.bookmarks.update(nonExistentId, { title: "new test title" }),
+ /No bookmarks found for the provided GUID/,
+ "Expected error thrown when trying to update a non-existent bookmark"
+ );
+ return browser.bookmarks.update(ourId, {
+ title: "new test title",
+ url: "http://example.com/",
+ });
+ })
+ .then(async result => {
+ browser.test.assertEq(
+ "new test title",
+ result.title,
+ "Updated bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "http://example.com/",
+ result.url,
+ "Updated bookmark has the expected URL"
+ );
+ browser.test.assertEq(
+ ourId,
+ result.id,
+ "Updated bookmark has the expected id"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ result.type,
+ "Updated bookmark has a type of bookmark"
+ );
+
+ browser.test.assertEq(
+ 2,
+ collectedEvents.length,
+ "2 expected events received"
+ );
+ checkOnChanged(ourId, "http://example.com/", "new test title");
+
+ await browser.test.assertRejects(
+ browser.bookmarks.update(ourId, { url: "this is not a valid url" }),
+ /Invalid bookmark:/,
+ "Expected error thrown when trying update with an invalid url"
+ );
+ return browser.bookmarks.getTree();
+ })
+ .then(results => {
+ browser.test.assertEq(1, results.length, "getTree returns one result");
+ let bookmark = results[0].children.find(
+ bookmarkItem => bookmarkItem.id == unsortedId
+ );
+ browser.test.assertEq(
+ "Other Bookmarks",
+ bookmark.title,
+ "Folder returned from getTree has the expected title"
+ );
+ browser.test.assertEq(
+ "folder",
+ bookmark.type,
+ "Folder returned from getTree has the expected type"
+ );
+
+ return browser.test.assertRejects(
+ browser.bookmarks.create({ parentId: "invalid" }),
+ error =>
+ error.message.includes("Invalid bookmark") &&
+ error.message.includes(`"parentGuid":"invalid"`),
+ "Expected error thrown when trying to create a bookmark with an invalid parentId"
+ );
+ })
+ .then(() => {
+ return browser.bookmarks.remove(ourId);
+ })
+ .then(result => {
+ browser.test.assertEq(
+ undefined,
+ result,
+ "Removing a bookmark returns undefined"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(
+ ourId,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "new test title",
+ "http://example.com/",
+ "bookmark"
+ );
+
+ return browser.test.assertRejects(
+ browser.bookmarks.get(ourId),
+ /Bookmark not found/,
+ "Expected error thrown when trying to get a removed bookmark"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.remove(nonExistentId),
+ /No bookmarks found for the provided GUID/,
+ "Expected error thrown when trying removed a non-existent bookmark"
+ );
+ })
+ .then(() => {
+ // test bookmarks.search
+ return Promise.all([
+ browser.bookmarks.create({
+ title: "Μοζιλλας",
+ url: "http://møzîllä.örg/",
+ }),
+ browser.bookmarks.create({
+ title: "Example",
+ url: "http://example.org/",
+ }),
+ browser.bookmarks.create({ title: "Mozilla Folder", type: "folder" }),
+ browser.bookmarks.create({ title: "EFF", url: "http://eff.org/" }),
+ browser.bookmarks.create({
+ title: "Menu Item",
+ url: "http://menu.org/",
+ parentId: bookmarkGuids.menuGuid,
+ }),
+ browser.bookmarks.create({
+ title: "Toolbar Item",
+ url: "http://toolbar.org/",
+ parentId: bookmarkGuids.toolbarGuid,
+ }),
+ ]);
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 6,
+ collectedEvents.length,
+ "6 expected events received"
+ );
+ checkOnCreated(
+ results[5].id,
+ bookmarkGuids.toolbarGuid,
+ 0,
+ "Toolbar Item",
+ "http://toolbar.org/",
+ results[5].dateAdded
+ );
+ checkOnCreated(
+ results[4].id,
+ bookmarkGuids.menuGuid,
+ 0,
+ "Menu Item",
+ "http://menu.org/",
+ results[4].dateAdded
+ );
+ checkOnCreated(
+ results[3].id,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "EFF",
+ "http://eff.org/",
+ results[3].dateAdded
+ );
+ checkOnCreated(
+ results[2].id,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "Mozilla Folder",
+ undefined,
+ results[2].dateAdded,
+ "folder"
+ );
+ checkOnCreated(
+ results[1].id,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "Example",
+ "http://example.org/",
+ results[1].dateAdded
+ );
+ checkOnCreated(
+ results[0].id,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "Μοζιλλας",
+ "http://xn--mzll-ooa1dud.xn--rg-eka/",
+ results[0].dateAdded
+ );
+
+ for (let result of results) {
+ if (result.title !== "Mozilla Folder") {
+ createdBookmarks.add(result.id);
+ }
+ }
+ let folderResult = results[2];
+ createdFolderId = folderResult.id;
+ return Promise.all([
+ browser.bookmarks.create({
+ title: "Mozilla",
+ url: "http://allizom.org/",
+ parentId: createdFolderId,
+ }),
+ browser.bookmarks.create({
+ parentId: createdFolderId,
+ type: "separator",
+ }),
+ browser.bookmarks.create({
+ title: "Mozilla Corporation",
+ url: "http://allizom.com/",
+ parentId: createdFolderId,
+ }),
+ browser.bookmarks.create({
+ title: "Firefox",
+ url: "http://allizom.org/firefox/",
+ parentId: createdFolderId,
+ }),
+ ])
+ .then(newBookmarks => {
+ browser.test.assertEq(
+ 4,
+ collectedEvents.length,
+ "4 expected events received"
+ );
+ checkOnCreated(
+ newBookmarks[3].id,
+ createdFolderId,
+ 0,
+ "Firefox",
+ "http://allizom.org/firefox/",
+ newBookmarks[3].dateAdded
+ );
+ checkOnCreated(
+ newBookmarks[2].id,
+ createdFolderId,
+ 0,
+ "Mozilla Corporation",
+ "http://allizom.com/",
+ newBookmarks[2].dateAdded
+ );
+ checkOnCreated(
+ newBookmarks[1].id,
+ createdFolderId,
+ 0,
+ "",
+ "data:",
+ newBookmarks[1].dateAdded,
+ "separator"
+ );
+ checkOnCreated(
+ newBookmarks[0].id,
+ createdFolderId,
+ 0,
+ "Mozilla",
+ "http://allizom.org/",
+ newBookmarks[0].dateAdded
+ );
+
+ return browser.bookmarks.create({
+ title: "About Mozilla",
+ url: "http://allizom.org/about/",
+ parentId: createdFolderId,
+ index: 1,
+ });
+ })
+ .then(result => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnCreated(
+ result.id,
+ createdFolderId,
+ 1,
+ "About Mozilla",
+ "http://allizom.org/about/",
+ result.dateAdded
+ );
+
+ // returns all items on empty object
+ return browser.bookmarks.search({});
+ })
+ .then(async bookmarksSearchResults => {
+ browser.test.assertTrue(
+ bookmarksSearchResults.length >= 10,
+ "At least as many bookmarks as added were returned by search({})"
+ );
+
+ await browser.test.assertRejects(
+ browser.bookmarks.remove(createdFolderId),
+ /Cannot remove a non-empty folder/,
+ "Expected error thrown when trying to remove a non-empty folder"
+ );
+ return browser.bookmarks.getSubTree(createdFolderId);
+ });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of nodes returned by getSubTree"
+ );
+ browser.test.assertEq(
+ "Mozilla Folder",
+ results[0].title,
+ "Folder has the expected title"
+ );
+ browser.test.assertEq(
+ bookmarkGuids.unfiledGuid,
+ results[0].parentId,
+ "Folder has the expected parentId"
+ );
+ browser.test.assertEq(
+ "folder",
+ results[0].type,
+ "Folder has the expected type"
+ );
+ let children = results[0].children;
+ browser.test.assertEq(
+ 5,
+ children.length,
+ "Expected number of bookmarks returned by getSubTree"
+ );
+ browser.test.assertEq(
+ "Firefox",
+ children[0].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ children[0].type,
+ "Bookmark has the expected type"
+ );
+ browser.test.assertEq(
+ "About Mozilla",
+ children[1].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ children[1].type,
+ "Bookmark has the expected type"
+ );
+ browser.test.assertEq(
+ 1,
+ children[1].index,
+ "Bookmark has the expected index"
+ );
+ browser.test.assertEq(
+ "Mozilla Corporation",
+ children[2].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "",
+ children[3].title,
+ "Separator has the expected title"
+ );
+ browser.test.assertEq(
+ "data:",
+ children[3].url,
+ "Separator has the expected url"
+ );
+ browser.test.assertEq(
+ "separator",
+ children[3].type,
+ "Separator has the expected type"
+ );
+ browser.test.assertEq(
+ "Mozilla",
+ children[4].title,
+ "Bookmark has the expected title"
+ );
+
+ // throws an error for invalid query objects
+ browser.test.assertThrows(
+ () => browser.bookmarks.search(),
+ /Incorrect argument types for bookmarks.search/,
+ "Expected error thrown when trying to search with no arguments"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.search(null),
+ /Incorrect argument types for bookmarks.search/,
+ "Expected error thrown when trying to search with null as an argument"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.search(() => {}),
+ /Incorrect argument types for bookmarks.search/,
+ "Expected error thrown when trying to search with a function as an argument"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.search({ banana: "banana" }),
+ /an unexpected "banana" property/,
+ "Expected error thrown when trying to search with a banana as an argument"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.search({ url: "spider-man vs. batman" }),
+ /must match the format "url"/,
+ "Expected error thrown when trying to search with a illegally formatted URL"
+ );
+ // queries the full url
+ return browser.bookmarks.search("http://example.org/");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for url search"
+ );
+ checkBookmark(
+ { title: "Example", url: "http://example.org/", index: 2 },
+ results[0]
+ );
+
+ // queries a partial url
+ return browser.bookmarks.search("example.org");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for url search"
+ );
+ checkBookmark(
+ { title: "Example", url: "http://example.org/", index: 2 },
+ results[0]
+ );
+
+ // queries the title
+ return browser.bookmarks.search("EFF");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for title search"
+ );
+ checkBookmark(
+ {
+ title: "EFF",
+ url: "http://eff.org/",
+ index: 0,
+ parentId: bookmarkGuids.unfiledGuid,
+ },
+ results[0]
+ );
+
+ // finds menu items
+ return browser.bookmarks.search("Menu Item");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for menu item search"
+ );
+ checkBookmark(
+ {
+ title: "Menu Item",
+ url: "http://menu.org/",
+ index: 0,
+ parentId: bookmarkGuids.menuGuid,
+ },
+ results[0]
+ );
+
+ // finds toolbar items
+ return browser.bookmarks.search("Toolbar Item");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for toolbar item search"
+ );
+ checkBookmark(
+ {
+ title: "Toolbar Item",
+ url: "http://toolbar.org/",
+ index: 0,
+ parentId: bookmarkGuids.toolbarGuid,
+ },
+ results[0]
+ );
+
+ // finds folders
+ return browser.bookmarks.search("Mozilla Folder");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of folders returned"
+ );
+ browser.test.assertEq(
+ "Mozilla Folder",
+ results[0].title,
+ "Folder has the expected title"
+ );
+ browser.test.assertEq(
+ "folder",
+ results[0].type,
+ "Folder has the expected type"
+ );
+
+ // is case-insensitive
+ return browser.bookmarks.search("corporation");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returnedfor case-insensitive search"
+ );
+ browser.test.assertEq(
+ "Mozilla Corporation",
+ results[0].title,
+ "Bookmark has the expected title"
+ );
+
+ // is case-insensitive for non-ascii
+ return browser.bookmarks.search("ΜοΖΙΛΛΑς");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for non-ascii search"
+ );
+ browser.test.assertEq(
+ "Μοζιλλας",
+ results[0].title,
+ "Bookmark has the expected title"
+ );
+
+ // returns multiple results
+ return browser.bookmarks.search("allizom");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 4,
+ results.length,
+ "Expected number of multiple results returned"
+ );
+ browser.test.assertEq(
+ "Mozilla",
+ results[0].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Mozilla Corporation",
+ results[1].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Firefox",
+ results[2].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "About Mozilla",
+ results[3].title,
+ "Bookmark has the expected title"
+ );
+
+ // accepts a url field
+ return browser.bookmarks.search({ url: "http://allizom.com/" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for url field"
+ );
+ checkBookmark(
+ {
+ title: "Mozilla Corporation",
+ url: "http://allizom.com/",
+ index: 2,
+ },
+ results[0]
+ );
+
+ // normalizes urls
+ return browser.bookmarks.search({ url: "http://allizom.com" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ results.length,
+ 1,
+ "Expected number of results returned for normalized url field"
+ );
+ checkBookmark(
+ {
+ title: "Mozilla Corporation",
+ url: "http://allizom.com/",
+ index: 2,
+ },
+ results[0]
+ );
+
+ // normalizes urls even more
+ return browser.bookmarks.search({ url: "http:allizom.com" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ results.length,
+ 1,
+ "Expected number of results returned for normalized url field"
+ );
+ checkBookmark(
+ {
+ title: "Mozilla Corporation",
+ url: "http://allizom.com/",
+ index: 2,
+ },
+ results[0]
+ );
+
+ // accepts a title field
+ return browser.bookmarks.search({ title: "Mozilla" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ results.length,
+ 1,
+ "Expected number of results returned for title field"
+ );
+ checkBookmark(
+ { title: "Mozilla", url: "http://allizom.org/", index: 4 },
+ results[0]
+ );
+
+ // can combine title and query
+ return browser.bookmarks.search({ title: "Mozilla", query: "allizom" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for title and query fields"
+ );
+ checkBookmark(
+ { title: "Mozilla", url: "http://allizom.org/", index: 4 },
+ results[0]
+ );
+
+ // uses AND conditions
+ return browser.bookmarks.search({ title: "EFF", query: "allizom" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 0,
+ results.length,
+ "Expected number of results returned for non-matching title and query fields"
+ );
+
+ // returns an empty array on item not found
+ return browser.bookmarks.search("microsoft");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 0,
+ results.length,
+ "Expected number of results returned for non-matching search"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.getRecent(""),
+ /Incorrect argument types for bookmarks.getRecent/,
+ "Expected error thrown when calling getRecent with an empty string"
+ );
+ })
+ .then(() => {
+ browser.test.assertThrows(
+ () => browser.bookmarks.getRecent(1.234),
+ /Incorrect argument types for bookmarks.getRecent/,
+ "Expected error thrown when calling getRecent with a decimal number"
+ );
+ })
+ .then(() => {
+ return Promise.all([
+ browser.bookmarks.search("corporation"),
+ browser.bookmarks.getChildren(bookmarkGuids.menuGuid),
+ ]);
+ })
+ .then(results => {
+ let corporationBookmark = results[0][0];
+ let childCount = results[1].length;
+
+ browser.test.assertEq(
+ 2,
+ corporationBookmark.index,
+ "Bookmark has the expected index"
+ );
+
+ return browser.bookmarks
+ .move(corporationBookmark.id, { index: 0 })
+ .then(result => {
+ browser.test.assertEq(
+ 0,
+ result.index,
+ "Bookmark has the expected index"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnMoved(
+ corporationBookmark.id,
+ createdFolderId,
+ createdFolderId,
+ 0,
+ 2
+ );
+
+ return browser.bookmarks.move(corporationBookmark.id, {
+ parentId: bookmarkGuids.menuGuid,
+ });
+ })
+ .then(result => {
+ browser.test.assertEq(
+ bookmarkGuids.menuGuid,
+ result.parentId,
+ "Bookmark has the expected parent"
+ );
+ browser.test.assertEq(
+ childCount,
+ result.index,
+ "Bookmark has the expected index"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnMoved(
+ corporationBookmark.id,
+ bookmarkGuids.menuGuid,
+ createdFolderId,
+ 1,
+ 0
+ );
+
+ return browser.bookmarks.move(corporationBookmark.id, { index: 0 });
+ })
+ .then(result => {
+ browser.test.assertEq(
+ bookmarkGuids.menuGuid,
+ result.parentId,
+ "Bookmark has the expected parent"
+ );
+ browser.test.assertEq(
+ 0,
+ result.index,
+ "Bookmark has the expected index"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnMoved(
+ corporationBookmark.id,
+ bookmarkGuids.menuGuid,
+ bookmarkGuids.menuGuid,
+ 0,
+ 1
+ );
+
+ return browser.bookmarks.move(corporationBookmark.id, {
+ parentId: bookmarkGuids.toolbarGuid,
+ index: 1,
+ });
+ })
+ .then(result => {
+ browser.test.assertEq(
+ bookmarkGuids.toolbarGuid,
+ result.parentId,
+ "Bookmark has the expected parent"
+ );
+ browser.test.assertEq(
+ 1,
+ result.index,
+ "Bookmark has the expected index"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnMoved(
+ corporationBookmark.id,
+ bookmarkGuids.toolbarGuid,
+ bookmarkGuids.menuGuid,
+ 1,
+ 0
+ );
+
+ createdBookmarks.add(corporationBookmark.id);
+ });
+ })
+ .then(() => {
+ return browser.bookmarks.getRecent(4);
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 4,
+ results.length,
+ "Expected number of results returned by getRecent"
+ );
+ let prevDate = results[0].dateAdded;
+ for (let bookmark of results) {
+ browser.test.assertTrue(
+ bookmark.dateAdded <= prevDate,
+ "The recent bookmarks are sorted by dateAdded"
+ );
+ prevDate = bookmark.dateAdded;
+ }
+ let bookmarksByTitle = results.sort((a, b) => {
+ return a.title.localeCompare(b.title);
+ });
+ browser.test.assertEq(
+ "About Mozilla",
+ bookmarksByTitle[0].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Firefox",
+ bookmarksByTitle[1].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Mozilla",
+ bookmarksByTitle[2].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Mozilla Corporation",
+ bookmarksByTitle[3].title,
+ "Bookmark has the expected title"
+ );
+
+ return browser.bookmarks.search({});
+ })
+ .then(results => {
+ let startBookmarkCount = results.length;
+
+ return browser.bookmarks
+ .search({ title: "Mozilla Folder" })
+ .then(result => {
+ return browser.bookmarks.removeTree(result[0].id);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(
+ createdFolderId,
+ bookmarkGuids.unfiledGuid,
+ 1,
+ "Mozilla Folder"
+ );
+
+ return browser.bookmarks.search({}).then(searchResults => {
+ browser.test.assertEq(
+ startBookmarkCount - 5,
+ searchResults.length,
+ "Expected number of results returned after removeTree"
+ );
+ });
+ });
+ })
+ .then(() => {
+ return browser.bookmarks.create({ title: "Empty Folder" });
+ })
+ .then(result => {
+ createdFolderId = result.id;
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnCreated(
+ createdFolderId,
+ bookmarkGuids.unfiledGuid,
+ 3,
+ "Empty Folder",
+ undefined,
+ result.dateAdded,
+ "folder"
+ );
+
+ browser.test.assertEq(
+ "Empty Folder",
+ result.title,
+ "Folder has the expected title"
+ );
+ browser.test.assertEq(
+ "folder",
+ result.type,
+ "Folder has the expected type"
+ );
+
+ return browser.bookmarks.create({
+ parentId: createdFolderId,
+ type: "separator",
+ });
+ })
+ .then(result => {
+ createdSeparatorId = result.id;
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnCreated(
+ createdSeparatorId,
+ createdFolderId,
+ 0,
+ "",
+ "data:",
+ result.dateAdded,
+ "separator"
+ );
+ return browser.bookmarks.remove(createdSeparatorId);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(
+ createdSeparatorId,
+ createdFolderId,
+ 0,
+ "",
+ "data:",
+ "separator"
+ );
+
+ return browser.bookmarks.remove(createdFolderId);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(
+ createdFolderId,
+ bookmarkGuids.unfiledGuid,
+ 3,
+ "Empty Folder"
+ );
+
+ return browser.test.assertRejects(
+ browser.bookmarks.get(createdFolderId),
+ /Bookmark not found/,
+ "Expected error thrown when trying to get a removed folder"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.getChildren(nonExistentId),
+ /root is null/,
+ "Expected error thrown when trying to getChildren for a non-existent folder"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.move(nonExistentId, {}),
+ /No bookmarks found for the provided GUID/,
+ "Expected error thrown when calling move with a non-existent bookmark"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.create({
+ title: "test root folder",
+ parentId: bookmarkGuids.rootGuid,
+ }),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when creating bookmark folder at the root"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.update(bookmarkGuids.rootGuid, {
+ title: "test update title",
+ }),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when updating root"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.remove(bookmarkGuids.rootGuid),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when removing root"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.removeTree(bookmarkGuids.rootGuid),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when removing root tree"
+ );
+ })
+ .then(() => {
+ return browser.bookmarks.create({ title: "Empty Folder" });
+ })
+ .then(async result => {
+ createdFolderId = result.id;
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnCreated(
+ createdFolderId,
+ bookmarkGuids.unfiledGuid,
+ 3,
+ "Empty Folder",
+ undefined,
+ result.dateAdded,
+ "folder"
+ );
+
+ await browser.test.assertRejects(
+ browser.bookmarks.move(createdFolderId, {
+ parentId: bookmarkGuids.rootGuid,
+ }),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when moving bookmark folder to the root"
+ );
+
+ return browser.bookmarks.remove(createdFolderId);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(
+ createdFolderId,
+ bookmarkGuids.unfiledGuid,
+ 3,
+ "Empty Folder",
+ undefined,
+ "folder"
+ );
+
+ return browser.test.assertRejects(
+ browser.bookmarks.get(createdFolderId),
+ "Bookmark not found",
+ "Expected error thrown when trying to get a removed folder"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.move(bookmarkGuids.rootGuid, {
+ parentId: bookmarkGuids.unfiledGuid,
+ }),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when moving root"
+ );
+ })
+ .then(() => {
+ // remove all created bookmarks
+ let promises = Array.from(createdBookmarks, guid =>
+ browser.bookmarks.remove(guid)
+ );
+ return Promise.all(promises);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ createdBookmarks.size,
+ collectedEvents.length,
+ "expected number of events received"
+ );
+
+ return browser.bookmarks.search({});
+ })
+ .then(results => {
+ browser.test.assertEq(
+ initialBookmarkCount,
+ results.length,
+ "All created bookmarks have been removed"
+ );
+
+ return browser.test.notifyPass("bookmarks");
+ })
+ .catch(error => {
+ browser.test.fail(`Error: ${String(error)} :: ${error.stack}`);
+ browser.test.notifyFail("bookmarks");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["bookmarks"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("bookmarks");
+ await extension.unload();
+});
+
+add_task(async function test_get_recent_with_tag_and_query() {
+ function background() {
+ browser.bookmarks.getRecent(100).then(bookmarks => {
+ browser.test.sendMessage("bookmarks", bookmarks);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["bookmarks"],
+ },
+ });
+
+ // Start with an empty bookmarks database.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let createdBookmarks = [];
+ for (let i = 0; i < 3; i++) {
+ let bookmark = {
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: `http://example.com/${i}`,
+ title: `My bookmark ${i}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ };
+ createdBookmarks.unshift(bookmark);
+ await PlacesUtils.bookmarks.insert(bookmark);
+ }
+
+ // Add a tag to the most recent url to prove it doesn't get returned.
+ PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/${i}"), [
+ "Test Tag",
+ ]);
+
+ // Add a query bookmark.
+ let queryURL = `place:parent=${PlacesUtils.bookmarks.menuGuid}&queryType=1`;
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: queryURL,
+ title: "a test query",
+ });
+
+ await extension.startup();
+ let receivedBookmarks = await extension.awaitMessage("bookmarks");
+
+ equal(
+ receivedBookmarks.length,
+ 3,
+ "The expected number of bookmarks was returned."
+ );
+ for (let i = 0; i < 3; i++) {
+ let actual = receivedBookmarks[i];
+ let expected = createdBookmarks[i];
+ equal(actual.url, expected.url, "Bookmark has the expected url.");
+ equal(actual.title, expected.title, "Bookmark has the expected title.");
+ equal(
+ actual.parentId,
+ expected.parentGuid,
+ "Bookmark has the expected parentId."
+ );
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_tree_with_empty_folder() {
+ async function background() {
+ await browser.bookmarks.create({ title: "Empty Folder" });
+ let nonEmptyFolder = await browser.bookmarks.create({
+ title: "Non-Empty Folder",
+ });
+ await browser.bookmarks.create({
+ title: "A bookmark",
+ url: "http://example.com",
+ parentId: nonEmptyFolder.id,
+ });
+
+ let tree = await browser.bookmarks.getSubTree(nonEmptyFolder.parentId);
+ browser.test.assertEq(
+ 0,
+ tree[0].children[0].children.length,
+ "The empty folder returns an empty array for children."
+ );
+ browser.test.assertEq(
+ 1,
+ tree[0].children[1].children.length,
+ "The non-empty folder returns a single item array for children."
+ );
+
+ let children = await browser.bookmarks.getChildren(nonEmptyFolder.parentId);
+ // getChildren should only return immediate children. This is not tested in the
+ // monster test above.
+ for (let child of children) {
+ browser.test.assertEq(
+ undefined,
+ child.children,
+ "Child from getChildren does not contain any children."
+ );
+ }
+
+ browser.test.sendMessage("done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["bookmarks"],
+ },
+ });
+
+ // Start with an empty bookmarks database.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
+
+add_task(
+ {
+ pref_set: [["extensions.eventPages.enabled", true]],
+ },
+ async function test_bookmarks_event_page() {
+ await AddonTestUtils.promiseStartupManager();
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@bookmarks" } },
+ permissions: ["bookmarks"],
+ background: { persistent: false },
+ },
+ background() {
+ browser.bookmarks.onCreated.addListener(() => {
+ browser.test.sendMessage("onCreated");
+ });
+ browser.bookmarks.onRemoved.addListener(() => {
+ browser.test.sendMessage("onRemoved");
+ });
+ browser.bookmarks.onChanged.addListener(() => {});
+ browser.bookmarks.onMoved.addListener(() => {});
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = ["onCreated", "onRemoved", "onChanged", "onMoved"];
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "bookmarks", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "bookmarks", event, {
+ primed: true,
+ });
+ }
+
+ let bookmark = {
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: `http://example.com/12345`,
+ title: `My bookmark 12345`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ };
+ await PlacesUtils.bookmarks.insert(bookmark);
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onCreated");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "bookmarks", event, {
+ primed: false,
+ });
+ }
+
+ await AddonTestUtils.promiseRestartManager();
+ await extension.awaitStartup();
+
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "bookmarks", event, {
+ primed: true,
+ });
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onRemoved");
+
+ await extension.unload();
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js
new file mode 100644
index 0000000000..1257f23600
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js
@@ -0,0 +1,126 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+});
+
+const OLD_NAMES = {
+ [Downloads.PUBLIC]: "old-public",
+ [Downloads.PRIVATE]: "old-private",
+};
+const RECENT_NAMES = {
+ [Downloads.PUBLIC]: "recent-public",
+ [Downloads.PRIVATE]: "recent-private",
+};
+const REFERENCE_DATE = new Date();
+const OLD_DATE = new Date(Number(REFERENCE_DATE) - 10000);
+
+async function downloadExists(list, path) {
+ let listArray = await list.getAll();
+ return listArray.some(i => i.target.path == path);
+}
+
+async function checkDownloads(
+ expectOldExists = true,
+ expectRecentExists = true
+) {
+ for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) {
+ let downloadsList = await Downloads.getList(listType);
+ equal(
+ await downloadExists(downloadsList, OLD_NAMES[listType]),
+ expectOldExists,
+ `Fake old download ${expectOldExists ? "was found" : "was removed"}.`
+ );
+ equal(
+ await downloadExists(downloadsList, RECENT_NAMES[listType]),
+ expectRecentExists,
+ `Fake recent download ${
+ expectRecentExists ? "was found" : "was removed"
+ }.`
+ );
+ }
+}
+
+async function setupDownloads() {
+ let downloadsList = await Downloads.getList(Downloads.ALL);
+ await downloadsList.removeFinished();
+
+ for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) {
+ downloadsList = await Downloads.getList(listType);
+ let download = await Downloads.createDownload({
+ source: {
+ url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1321303",
+ isPrivate: listType == Downloads.PRIVATE,
+ },
+ target: OLD_NAMES[listType],
+ });
+ download.startTime = OLD_DATE;
+ download.canceled = true;
+ await downloadsList.add(download);
+
+ download = await Downloads.createDownload({
+ source: {
+ url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1321303",
+ isPrivate: listType == Downloads.PRIVATE,
+ },
+ target: RECENT_NAMES[listType],
+ });
+ download.startTime = REFERENCE_DATE;
+ download.canceled = true;
+ await downloadsList.add(download);
+ }
+
+ // Confirm everything worked.
+ downloadsList = await Downloads.getList(Downloads.ALL);
+ equal((await downloadsList.getAll()).length, 4, "4 fake downloads added.");
+ checkDownloads();
+}
+
+add_task(async function testDownloads() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, options) => {
+ if (msg == "removeDownloads") {
+ await browser.browsingData.removeDownloads(options);
+ } else {
+ await browser.browsingData.remove(options, { downloads: true });
+ }
+ browser.test.sendMessage("downloadsRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ async function testRemovalMethod(method) {
+ // Clear downloads with no since value.
+ await setupDownloads();
+ extension.sendMessage(method, {});
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(false, false);
+
+ // Clear downloads with recent since value.
+ await setupDownloads();
+ extension.sendMessage(method, { since: REFERENCE_DATE });
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(true, false);
+
+ // Clear downloads with old since value.
+ await setupDownloads();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 100000 });
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(false, false);
+ }
+
+ await extension.startup();
+
+ await testRemovalMethod("removeDownloads");
+ await testRemovalMethod("remove");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js
new file mode 100644
index 0000000000..68a2c2cdc5
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js
@@ -0,0 +1,96 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const REFERENCE_DATE = Date.now();
+const LOGIN_USERNAME = "username";
+const LOGIN_PASSWORD = "password";
+const OLD_HOST = "http://mozilla.org";
+const NEW_HOST = "http://mozilla.com";
+const FXA_HOST = "chrome://FirefoxAccounts";
+
+async function checkLoginExists(origin, shouldExist) {
+ const logins = await Services.logins.searchLoginsAsync({ origin });
+ equal(
+ logins.length,
+ shouldExist ? 1 : 0,
+ `Login for origin ${origin} should ${shouldExist ? "" : "not"} be found.`
+ );
+}
+
+async function addLogin(host, timestamp) {
+ await checkLoginExists(host, false);
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(host, "", null, LOGIN_USERNAME, LOGIN_PASSWORD);
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.timePasswordChanged = timestamp;
+ await Services.logins.addLoginAsync(login);
+ await checkLoginExists(host, true);
+}
+
+async function setupPasswords() {
+ // Remove all logins if any (included FxAccounts one in case one got captured in
+ // a conditioned profile, see Bug 1853617).
+ Services.logins.removeAllLogins();
+ await addLogin(FXA_HOST, REFERENCE_DATE);
+ await addLogin(NEW_HOST, REFERENCE_DATE);
+ await addLogin(OLD_HOST, REFERENCE_DATE - 10000);
+}
+
+add_task(async function testPasswords() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, options) => {
+ if (msg == "removeHistory") {
+ await browser.browsingData.removePasswords(options);
+ } else {
+ await browser.browsingData.remove(options, { passwords: true });
+ }
+ browser.test.sendMessage("passwordsRemoved");
+ });
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ async function testRemovalMethod(method) {
+ // Clear passwords with no since value.
+ await setupPasswords();
+ extension.sendMessage(method, {});
+ await extension.awaitMessage("passwordsRemoved");
+
+ await checkLoginExists(OLD_HOST, false);
+ await checkLoginExists(NEW_HOST, false);
+ await checkLoginExists(FXA_HOST, true);
+
+ // Clear passwords with recent since value.
+ await setupPasswords();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 1000 });
+ await extension.awaitMessage("passwordsRemoved");
+
+ await checkLoginExists(OLD_HOST, true);
+ await checkLoginExists(NEW_HOST, false);
+ await checkLoginExists(FXA_HOST, true);
+
+ // Clear passwords with old since value.
+ await setupPasswords();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 20000 });
+ await extension.awaitMessage("passwordsRemoved");
+
+ await checkLoginExists(OLD_HOST, false);
+ await checkLoginExists(NEW_HOST, false);
+ await checkLoginExists(FXA_HOST, true);
+ }
+
+ await extension.startup();
+
+ await testRemovalMethod("removePasswords");
+ await testRemovalMethod("remove");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js
new file mode 100644
index 0000000000..9d2241895c
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js
@@ -0,0 +1,147 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+ Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
+});
+
+const PREF_DOMAIN = "privacy.cpd.";
+const SETTINGS_LIST = [
+ "cache",
+ "cookies",
+ "history",
+ "formData",
+ "downloads",
+].sort();
+
+add_task(async function testSettingsProperties() {
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.browsingData.settings().then(settings => {
+ browser.test.sendMessage("settings", settings);
+ });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ await extension.startup();
+
+ extension.sendMessage("settings");
+ let settings = await extension.awaitMessage("settings");
+
+ // Verify that we get the keys back we expect.
+ deepEqual(
+ Object.keys(settings.dataToRemove).sort(),
+ SETTINGS_LIST,
+ "dataToRemove contains expected properties."
+ );
+ deepEqual(
+ Object.keys(settings.dataRemovalPermitted).sort(),
+ SETTINGS_LIST,
+ "dataToRemove contains expected properties."
+ );
+
+ let dataTypeSet = settings.dataToRemove;
+ for (let key of Object.keys(dataTypeSet)) {
+ equal(
+ Preferences.get(`${PREF_DOMAIN}${key.toLowerCase()}`),
+ dataTypeSet[key],
+ `${key} property of dataToRemove matches the expected pref.`
+ );
+ }
+
+ dataTypeSet = settings.dataRemovalPermitted;
+ for (let key of Object.keys(dataTypeSet)) {
+ equal(
+ true,
+ dataTypeSet[key],
+ `${key} property of dataRemovalPermitted is true.`
+ );
+ }
+
+ // Explicitly set a pref to both true and false and then check.
+ const SINGLE_OPTION = "cache";
+ const SINGLE_PREF = "privacy.cpd.cache";
+
+ registerCleanupFunction(() => {
+ Preferences.reset(SINGLE_PREF);
+ });
+
+ Preferences.set(SINGLE_PREF, true);
+
+ extension.sendMessage("settings");
+ settings = await extension.awaitMessage("settings");
+ equal(
+ settings.dataToRemove[SINGLE_OPTION],
+ true,
+ "Preference that was set to true returns true."
+ );
+
+ Preferences.set(SINGLE_PREF, false);
+
+ extension.sendMessage("settings");
+ settings = await extension.awaitMessage("settings");
+ equal(
+ settings.dataToRemove[SINGLE_OPTION],
+ false,
+ "Preference that was set to false returns false."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function testSettingsSince() {
+ const TIMESPAN_PREF = "privacy.sanitize.timeSpan";
+ const TEST_DATA = {
+ TIMESPAN_5MIN: Date.now() - 5 * 60 * 1000,
+ TIMESPAN_HOUR: Date.now() - 60 * 60 * 1000,
+ TIMESPAN_2HOURS: Date.now() - 2 * 60 * 60 * 1000,
+ TIMESPAN_EVERYTHING: 0,
+ };
+
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.browsingData.settings().then(settings => {
+ browser.test.sendMessage("settings", settings);
+ });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ await extension.startup();
+
+ registerCleanupFunction(() => {
+ Preferences.reset(TIMESPAN_PREF);
+ });
+
+ for (let timespan in TEST_DATA) {
+ Preferences.set(TIMESPAN_PREF, Sanitizer[timespan]);
+
+ extension.sendMessage("settings");
+ let settings = await extension.awaitMessage("settings");
+
+ // Because it is based on the current timestamp, we cannot know the exact
+ // value to expect for since, so allow a 10s variance.
+ Assert.less(
+ Math.abs(settings.options.since - TEST_DATA[timespan]),
+ 10000,
+ "settings.options contains the expected since value."
+ );
+ }
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js
new file mode 100644
index 0000000000..d61e5b6b5e
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js
@@ -0,0 +1,231 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ HomePage: "resource:///modules/HomePage.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+function promisePrefChanged(expectedValue) {
+ return TestUtils.waitForPrefChange("browser.startup.homepage", value =>
+ value.endsWith(expectedValue)
+ );
+}
+
+const HOMEPAGE_EXTENSION_CONTROLLED =
+ "browser.startup.homepage_override.extensionControlled";
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+async function setupRemoteSettings() {
+ const settings = await RemoteSettings("hijack-blocklists");
+ sinon.stub(settings, "get").returns([
+ {
+ id: "homepage-urls",
+ matches: ["ignore=me"],
+ _status: "synced",
+ },
+ ]);
+}
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ await setupRemoteSettings();
+});
+
+add_task(async function test_overriding_with_ignored_url() {
+ // Manually poke into the ignore list a value to be ignored.
+ HomePage._ignoreList.push("ignore=me");
+ Services.prefs.setBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, false);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "ignore_homepage@example.com",
+ },
+ },
+ chrome_settings_overrides: { homepage: "https://example.com/?ignore=me" },
+ name: "extension",
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ Assert.ok(HomePage.isDefault, "Should still have the default homepage");
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "browser.startup.homepage_override.extensionControlled"
+ ),
+ false,
+ "Should not be extension controlled."
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "ignore",
+ value: "set_blocked_extension",
+ extra: { webExtensionId: "ignore_homepage@example.com" },
+ },
+ ],
+ {
+ category: "homepage",
+ method: "preference",
+ }
+ );
+
+ await extension.unload();
+ HomePage._ignoreList.pop();
+});
+
+add_task(async function test_overriding_cancelled_after_ignore_update() {
+ const oldHomePageIgnoreList = HomePage._ignoreList;
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "ignore_homepage1@example.com",
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: "https://example.com/?ignore1=me",
+ },
+ name: "extension",
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ Assert.ok(!HomePage.isDefault, "Should have overriden the new homepage");
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "browser.startup.homepage_override.extensionControlled"
+ ),
+ true,
+ "Should be extension controlled."
+ );
+
+ let prefChanged = TestUtils.waitForPrefChange(
+ "browser.startup.homepage_override.extensionControlled"
+ );
+
+ await HomePage._handleIgnoreListUpdated({
+ data: {
+ current: [{ id: "homepage-urls", matches: ["ignore1=me"] }],
+ },
+ });
+
+ await prefChanged;
+
+ await TestUtils.waitForCondition(
+ () =>
+ !Services.prefs.getBoolPref(
+ "browser.startup.homepage_override.extensionControlled",
+ false
+ ),
+ "Should not longer be extension controlled"
+ );
+
+ Assert.ok(HomePage.isDefault, "Should have reset the homepage");
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "ignore",
+ value: "saved_reset",
+ },
+ ],
+ {
+ category: "homepage",
+ method: "preference",
+ }
+ );
+
+ await extension.unload();
+ HomePage._ignoreList = oldHomePageIgnoreList;
+});
+
+add_task(async function test_overriding_homepage_locale() {
+ Services.locale.availableLocales = ["en-US", "es-ES"];
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "homepage@example.com",
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: "/__MSG_homepage__",
+ },
+ name: "extension",
+ default_locale: "en",
+ },
+ useAddonManager: "permanent",
+
+ files: {
+ "_locales/en/messages.json": {
+ homepage: {
+ message: "homepage.html",
+ description: "homepage",
+ },
+ },
+
+ "_locales/es_ES/messages.json": {
+ homepage: {
+ message: "default.html",
+ description: "homepage",
+ },
+ },
+ },
+ });
+
+ let prefPromise = promisePrefChanged("homepage.html");
+ await extension.startup();
+ await prefPromise;
+
+ Assert.equal(
+ HomePage.get(),
+ `moz-extension://${extension.uuid}/homepage.html`,
+ "Should have overridden the new homepage"
+ );
+
+ // Set the new locale now, and disable the L10nRegistry reset
+ // when shutting down the addon mananger. This allows us to
+ // restart under a new locale without a lot of fuss.
+ let reqLoc = Services.locale.requestedLocales;
+ Services.locale.requestedLocales = ["es-ES"];
+
+ prefPromise = promisePrefChanged("default.html");
+ await AddonTestUtils.promiseShutdownManager({ clearL10nRegistry: false });
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+ await prefPromise;
+
+ Assert.equal(
+ HomePage.get(),
+ `moz-extension://${extension.uuid}/default.html`,
+ "Should have overridden the new homepage"
+ );
+
+ await extension.unload();
+
+ Services.locale.requestedLocales = reqLoc;
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
new file mode 100644
index 0000000000..aac00a8023
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
@@ -0,0 +1,794 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ HomePage: "resource:///modules/HomePage.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+// Similar to TestUtils.topicObserved, but returns a deferred promise that
+// can be resolved
+function topicObservable(topic, checkFn) {
+ let deferred = Promise.withResolvers();
+ function observer(subject, topic, data) {
+ try {
+ if (checkFn && !checkFn(subject, data)) {
+ return;
+ }
+ deferred.resolve([subject, data]);
+ } catch (ex) {
+ deferred.reject(ex);
+ }
+ }
+ deferred.promise.finally(() => {
+ Services.obs.removeObserver(observer, topic);
+ checkFn = null;
+ });
+ Services.obs.addObserver(observer, topic);
+
+ return deferred;
+}
+
+async function setupRemoteSettings() {
+ const settings = await RemoteSettings("hijack-blocklists");
+ sinon.stub(settings, "get").returns([
+ {
+ id: "homepage-urls",
+ matches: ["ignore=me"],
+ _status: "synced",
+ },
+ ]);
+}
+
+function promisePrefChanged(expectedValue) {
+ return TestUtils.waitForPrefChange("browser.startup.homepage", value =>
+ value.endsWith(expectedValue)
+ );
+}
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ await setupRemoteSettings();
+});
+
+add_task(async function test_overrides_update_removal() {
+ /* This tests the scenario where the manifest key for homepage and/or
+ * search_provider are removed between updates and therefore the
+ * settings are expected to revert. It also tests that an extension
+ * can make a builtin extension the default search without user
+ * interaction. */
+
+ const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
+ const HOMEPAGE_URI = "webext-homepage-1.html";
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI,
+ search_provider: {
+ name: "DuckDuckGo",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let defaultHomepageURL = HomePage.get();
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ Assert.notStrictEqual(
+ defaultEngineName,
+ "DuckDuckGo",
+ "Default engine is not DuckDuckGo."
+ );
+
+ let prefPromise = promisePrefChanged(HOMEPAGE_URI);
+
+ // When an addon is installed that overrides an app-provided engine (builtin)
+ // that is the default, we do not prompt for default.
+ let deferredPrompt = topicObservable(
+ "webextension-defaultsearch-prompt",
+ (subject, message) => {
+ if (subject.wrappedJSObject.id == extension.id) {
+ ok(false, "default override should not prompt");
+ }
+ }
+ );
+
+ await Promise.race([extension.startup(), deferredPrompt.promise]);
+ deferredPrompt.resolve();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI),
+ "Home page url is overridden by the extension."
+ );
+ equal(
+ (await Services.search.getDefault()).name,
+ "DuckDuckGo",
+ "Builtin default engine was set default by extension"
+ );
+
+ extensionInfo.manifest = {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ prefPromise = promisePrefChanged(defaultHomepageURL);
+ await extension.upgrade(extensionInfo);
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ equal(
+ HomePage.get(),
+ defaultHomepageURL,
+ "Home page url reverted to the default after update."
+ );
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine reverted to the default after update."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_overrides_update_adding() {
+ /* This tests the scenario where an addon adds support for
+ * a homepage or search service when upgrading. Neither
+ * should override existing entries for those when added
+ * in an upgrade. Also, a search_provider being added
+ * with is_default should not prompt the user or override
+ * the current default engine. */
+
+ const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
+ const HOMEPAGE_URI = "webext-homepage-1.html";
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let defaultHomepageURL = HomePage.get();
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ Assert.notStrictEqual(
+ defaultEngineName,
+ "DuckDuckGo",
+ "Home page url is not DuckDuckGo."
+ );
+
+ await extension.startup();
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ equal(
+ HomePage.get(),
+ defaultHomepageURL,
+ "Home page url is the default after startup."
+ );
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is the default after startup."
+ );
+
+ extensionInfo.manifest = {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI,
+ search_provider: {
+ name: "DuckDuckGo",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ };
+
+ let prefPromise = promisePrefChanged(HOMEPAGE_URI);
+
+ let deferredUpgradePrompt = topicObservable(
+ "webextension-defaultsearch-prompt",
+ (subject, message) => {
+ if (subject.wrappedJSObject.id == extension.id) {
+ ok(false, "should not prompt on update");
+ }
+ }
+ );
+
+ await Promise.race([
+ extension.upgrade(extensionInfo),
+ deferredUpgradePrompt.promise,
+ ]);
+ deferredUpgradePrompt.resolve();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI),
+ "Home page url is overridden by the extension during upgrade."
+ );
+ // An upgraded extension adding a search engine cannot override
+ // the default engine.
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is still the default after startup."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_overrides_update_homepage_change() {
+ /* This tests the scenario where an addon changes
+ * a homepage url when upgrading. */
+
+ const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
+ const HOMEPAGE_URI = "webext-homepage-1.html";
+ const HOMEPAGE_URI_2 = "webext-homepage-2.html";
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI,
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let prefPromise = promisePrefChanged(HOMEPAGE_URI);
+ await extension.startup();
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI),
+ "Home page url is the extension url after startup."
+ );
+
+ extensionInfo.manifest = {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI_2,
+ },
+ };
+
+ prefPromise = promisePrefChanged(HOMEPAGE_URI_2);
+ await extension.upgrade(extensionInfo);
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI_2),
+ "Home page url is by the extension after upgrade."
+ );
+
+ await extension.unload();
+});
+
+async function withHandlingDefaultSearchPrompt({ extensionId, respond }, cb) {
+ const promptResponseHandled = TestUtils.topicObserved(
+ "webextension-defaultsearch-prompt-response"
+ );
+ const prompted = TestUtils.topicObserved(
+ "webextension-defaultsearch-prompt",
+ (subject, message) => {
+ if (subject.wrappedJSObject.id == extensionId) {
+ return subject.wrappedJSObject.respond(respond);
+ }
+ }
+ );
+
+ await Promise.all([cb(), prompted, promptResponseHandled]);
+}
+
+async function assertUpdateDoNotPrompt(extension, updateExtensionInfo) {
+ let deferredUpgradePrompt = topicObservable(
+ "webextension-defaultsearch-prompt",
+ (subject, message) => {
+ if (subject.wrappedJSObject.id == extension.id) {
+ ok(false, "should not prompt on update");
+ }
+ }
+ );
+
+ await Promise.race([
+ extension.upgrade(updateExtensionInfo),
+ deferredUpgradePrompt.promise,
+ ]);
+ deferredUpgradePrompt.resolve();
+
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+ equal(
+ extension.version,
+ updateExtensionInfo.manifest.version,
+ "The updated addon has the expected version."
+ );
+}
+
+add_task(async function test_default_search_prompts() {
+ /* This tests the scenario where an addon did not gain
+ * default search during install, and later upgrades.
+ * The addon should not gain default in updates.
+ * If the addon is disabled, it should prompt again when
+ * enabled.
+ */
+
+ const EXTENSION_ID = "test_default_update@tests.mozilla.org";
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Example",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ Assert.notStrictEqual(defaultEngineName, "Example", "Search is not Example.");
+
+ // Mock a response from the default search prompt where we
+ // say no to setting this as the default when installing.
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: false },
+ () => extension.startup()
+ );
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is the default after startup."
+ );
+
+ info(
+ "Verify that updating the extension does not prompt and does not take over the default engine"
+ );
+
+ extensionInfo.manifest.version = "2.0";
+ await assertUpdateDoNotPrompt(extension, extensionInfo);
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is still the default after update."
+ );
+
+ info("Verify that disable/enable the extension does prompt the user");
+
+ let addon = await AddonManager.getAddonByID(EXTENSION_ID);
+
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: false },
+ async () => {
+ await addon.disable();
+ await addon.enable();
+ }
+ );
+
+ // we still said no.
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is the default after being disabling/enabling."
+ );
+
+ await extension.unload();
+});
+
+async function test_default_search_on_updating_addons_installed_before_bug1757760({
+ builtinAsInitialDefault,
+}) {
+ /* This tests covers a scenario similar to the previous test but with an extension-settings.json file
+ content like the one that would be available in the profile if the add-on was installed on firefox
+ versions that didn't include the changes from Bug 1757760 (See Bug 1767550).
+ */
+
+ const EXTENSION_ID = `test_old_addon@tests.mozilla.org`;
+ const EXTENSION_ID2 = `test_old_addon2@tests.mozilla.org`;
+
+ const extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.1",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Test SearchEngine",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ };
+
+ const extensionInfo2 = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.2",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID2,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Test SearchEngine2",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ };
+
+ const { ExtensionSettingsStore } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs"
+ );
+
+ async function assertExtensionSettingsStore(
+ extensionInfo,
+ expectedLevelOfControl
+ ) {
+ const { id } = extensionInfo.manifest.browser_specific_settings.gecko;
+ info(`Asserting ExtensionSettingsStore for ${id}`);
+ const item = ExtensionSettingsStore.getSetting(
+ "default_search",
+ "defaultSearch",
+ id
+ );
+ equal(
+ item.value,
+ extensionInfo.manifest.chrome_settings_overrides.search_provider.name,
+ "Got the expected item returned by ExtensionSettingsStore.getSetting"
+ );
+ const control = await ExtensionSettingsStore.getLevelOfControl(
+ id,
+ "default_search",
+ "defaultSearch"
+ );
+ equal(
+ control,
+ expectedLevelOfControl,
+ `Got expected levelOfControl for ${id}`
+ );
+ }
+
+ info("Install test extensions without opt-in to the related search engines");
+
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+ let extension2 = ExtensionTestUtils.loadExtension(extensionInfo2);
+
+ // Mock a response from the default search prompt where we
+ // say no to setting this as the default when installing.
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: false },
+ () => extension.startup()
+ );
+
+ equal(
+ extension.version,
+ "1.1",
+ "first installed addon has the expected version."
+ );
+
+ // Mock a response from the default search prompt where we
+ // say no to setting this as the default when installing.
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID2, respond: false },
+ () => extension2.startup()
+ );
+
+ equal(
+ extension2.version,
+ "1.2",
+ "second installed addon has the expected version."
+ );
+
+ info("Setup preconditions (set the initial default search engine)");
+
+ // Sanity check to be sure the initial engine expected as precondition
+ // for the scenario covered by the current test case.
+ let initialEngine;
+ if (builtinAsInitialDefault) {
+ initialEngine = Services.search.appDefaultEngine;
+ } else {
+ initialEngine = Services.search.getEngineByName(
+ extensionInfo.manifest.chrome_settings_overrides.search_provider.name
+ );
+ }
+ await Services.search.setDefault(
+ initialEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ Assert.equal(
+ defaultEngineName,
+ initialEngine.name,
+ `initial default search engine expected to be ${
+ builtinAsInitialDefault ? "app-provided" : EXTENSION_ID
+ }`
+ );
+ Assert.notEqual(
+ defaultEngineName,
+ extensionInfo2.manifest.chrome_settings_overrides.search_provider.name,
+ "initial default search engine name should not be the same as the second extension search_provider"
+ );
+
+ equal(
+ (await Services.search.getDefault()).name,
+ initialEngine.name,
+ `Default engine should still be set to the ${
+ builtinAsInitialDefault ? "app-provided" : EXTENSION_ID
+ }.`
+ );
+
+ // Mock an update from settings stored as in an older Firefox version where Bug 1757760 was not landed yet.
+ info(
+ "Setup preconditions (inject mock extension-settings.json data and assert on the expected setting and levelOfControl)"
+ );
+
+ let addon = await AddonManager.getAddonByID(EXTENSION_ID);
+ let addon2 = await AddonManager.getAddonByID(EXTENSION_ID2);
+
+ const extensionSettingsData = {
+ version: 2,
+ url_overrides: {},
+ prefs: {},
+ homepageNotification: {},
+ tabHideNotification: {},
+ default_search: {
+ defaultSearch: {
+ initialValue: Services.search.appDefaultEngine.name,
+ precedenceList: [
+ {
+ id: EXTENSION_ID2,
+ // The install dates are used in ExtensionSettingsStore.getLevelOfControl
+ // and to recreate the expected preconditions the last extension installed
+ // should have a installDate timestamp > then the first one.
+ installDate: addon2.installDate.getTime() + 1000,
+ value:
+ extensionInfo2.manifest.chrome_settings_overrides.search_provider
+ .name,
+ // When an addon with a default search engine override is installed in Firefox versions
+ // without the changes landed from Bug 1757760, `enabled` will be set to true in all cases
+ // (Prompt never answered, or when No or Yes is selected by the user).
+ enabled: true,
+ },
+ {
+ id: EXTENSION_ID,
+ installDate: addon.installDate.getTime(),
+ value:
+ extensionInfo.manifest.chrome_settings_overrides.search_provider
+ .name,
+ enabled: true,
+ },
+ ],
+ },
+ },
+ newTabNotification: {},
+ commands: {},
+ };
+
+ const file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("extension-settings.json");
+
+ info(`writing mock settings data into ${file.path}`);
+ await IOUtils.writeJSON(file.path, extensionSettingsData);
+ await ExtensionSettingsStore._reloadFile(false);
+
+ equal(
+ (await Services.search.getDefault()).name,
+ initialEngine.name,
+ "Default engine is still set to the initial one."
+ );
+
+ // The following assertions verify that the migration applied from ExtensionSettingsStore
+ // fixed the inconsistent state and kept the search engine unchanged.
+ //
+ // - With the fixed settings we expect both to be resolved to "controllable_by_this_extension".
+ // - Without the fix applied during the migration the levelOfControl resolved would be:
+ // - for the last installed: "controlled_by_this_extension"
+ // - for the first installed: "controlled_by_other_extensions"
+ await assertExtensionSettingsStore(
+ extensionInfo2,
+ "controlled_by_this_extension"
+ );
+ await assertExtensionSettingsStore(
+ extensionInfo,
+ "controlled_by_other_extensions"
+ );
+
+ info(
+ "Verify that updating the extension does not prompt and does not take over the default engine"
+ );
+
+ extensionInfo2.manifest.version = "2.2";
+ await assertUpdateDoNotPrompt(extension2, extensionInfo2);
+
+ extensionInfo.manifest.version = "2.1";
+ await assertUpdateDoNotPrompt(extension, extensionInfo);
+
+ equal(
+ (await Services.search.getDefault()).name,
+ initialEngine.name,
+ "Default engine is still the same after updating both the test extensions."
+ );
+
+ // After both the extensions have been updated and their inconsistent state
+ // updated internally, both extensions should have levelOfControl "controllable_*".
+ await assertExtensionSettingsStore(
+ extensionInfo2,
+ "controllable_by_this_extension"
+ );
+ await assertExtensionSettingsStore(
+ extensionInfo,
+ // We expect levelOfControl to be controlled_by_this_extension if the test case
+ // is expecting the third party extension to stay set as default.
+ builtinAsInitialDefault
+ ? "controllable_by_this_extension"
+ : "controlled_by_this_extension"
+ );
+
+ info("Verify that disable/enable the extension does prompt the user");
+
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID2, respond: false },
+ async () => {
+ await addon2.disable();
+ await addon2.enable();
+ }
+ );
+
+ // we said no.
+ equal(
+ (await Services.search.getDefault()).name,
+ initialEngine.name,
+ `Default engine should still be the same after disabling/enabling ${EXTENSION_ID2}.`
+ );
+
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: false },
+ async () => {
+ await addon.disable();
+ await addon.enable();
+ }
+ );
+
+ // we said no.
+ equal(
+ (await Services.search.getDefault()).name,
+ Services.search.appDefaultEngine.name,
+ `Default engine should be set to the app default after disabling/enabling ${EXTENSION_ID}.`
+ );
+
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: true },
+ async () => {
+ await addon.disable();
+ await addon.enable();
+ }
+ );
+
+ // we responded yes.
+ equal(
+ (await Services.search.getDefault()).name,
+ extensionInfo.manifest.chrome_settings_overrides.search_provider.name,
+ "Default engine should be set to the one opted-in from the last prompt."
+ );
+
+ await extension.unload();
+ await extension2.unload();
+}
+
+add_task(function test_builtin_default_search_after_updating_old_addons() {
+ return test_default_search_on_updating_addons_installed_before_bug1757760({
+ builtinAsInitialDefault: true,
+ });
+});
+
+add_task(function test_third_party_default_search_after_updating_old_addons() {
+ return test_default_search_on_updating_addons_installed_before_bug1757760({
+ builtinAsInitialDefault: false,
+ });
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js b/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js
new file mode 100644
index 0000000000..030e0b27be
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js
@@ -0,0 +1,56 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionControlledPopup:
+ "resource:///modules/ExtensionControlledPopup.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+
+/*
+ * This function is a unit test for distributions disabling the ExtensionControlledPopup.
+ */
+add_task(async function testDistributionPopup() {
+ let distExtId = "ext-distribution@mochi.test";
+ Services.prefs.setCharPref(
+ `extensions.installedDistroAddon.${distExtId}`,
+ true
+ );
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: distExtId } },
+ name: "Ext Distribution",
+ },
+ });
+
+ let userExtId = "ext-user@mochi.test";
+ let userExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: userExtId } },
+ name: "Ext User Installed",
+ },
+ });
+
+ await extension.startup();
+ await userExtension.startup();
+ await ExtensionSettingsStore.initialize();
+
+ let confirmedType = "extension-controlled-confirmed";
+ equal(
+ new ExtensionControlledPopup({ confirmedType }).userHasConfirmed(distExtId),
+ true,
+ "The popup has been disabled."
+ );
+
+ equal(
+ new ExtensionControlledPopup({ confirmedType }).userHasConfirmed(userExtId),
+ false,
+ "The popup has not been disabled."
+ );
+
+ await extension.unload();
+ await userExtension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_history.js b/browser/components/extensions/test/xpcshell/test_ext_history.js
new file mode 100644
index 0000000000..c0f6c39be7
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_history.js
@@ -0,0 +1,864 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "43"
+);
+
+add_task(async function test_delete() {
+ function background() {
+ let historyClearedCount = 0;
+ let removedUrls = [];
+
+ browser.history.onVisitRemoved.addListener(data => {
+ if (data.allHistory) {
+ historyClearedCount++;
+ browser.test.assertEq(
+ 0,
+ data.urls.length,
+ "onVisitRemoved received an empty urls array"
+ );
+ } else {
+ removedUrls.push(...data.urls);
+ }
+ });
+
+ browser.test.onMessage.addListener((msg, arg) => {
+ if (msg === "delete-url") {
+ browser.history.deleteUrl({ url: arg }).then(result => {
+ browser.test.assertEq(
+ undefined,
+ result,
+ "browser.history.deleteUrl returns nothing"
+ );
+ browser.test.sendMessage("url-deleted");
+ });
+ } else if (msg === "delete-range") {
+ browser.history.deleteRange(arg).then(result => {
+ browser.test.assertEq(
+ undefined,
+ result,
+ "browser.history.deleteRange returns nothing"
+ );
+ browser.test.sendMessage("range-deleted", removedUrls);
+ });
+ } else if (msg === "delete-all") {
+ browser.history.deleteAll().then(result => {
+ browser.test.assertEq(
+ undefined,
+ result,
+ "browser.history.deleteAll returns nothing"
+ );
+ browser.test.sendMessage("history-cleared", [
+ historyClearedCount,
+ removedUrls,
+ ]);
+ });
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ const BASE_URL = "http://mozilla.com/test_history/";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await PlacesUtils.history.clear();
+
+ let historyClearedCount;
+ let visits = [];
+ let visitDate = new Date(1999, 9, 9, 9, 9).getTime();
+
+ function pushVisit(subvisits) {
+ visitDate += 1000;
+ subvisits.push({ date: new Date(visitDate) });
+ }
+
+ // Add 5 visits for one uri and 3 visits for 3 others
+ for (let i = 0; i < 4; ++i) {
+ let visit = {
+ url: `${BASE_URL}${i}`,
+ title: "visit " + i,
+ visits: [],
+ };
+ if (i === 0) {
+ for (let j = 0; j < 5; ++j) {
+ pushVisit(visit.visits);
+ }
+ } else {
+ pushVisit(visit.visits);
+ }
+ visits.push(visit);
+ }
+
+ await PlacesUtils.history.insertMany(visits);
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[0].url),
+ 5,
+ "5 visits for uri found in history database"
+ );
+
+ let testUrl = visits[2].url;
+ ok(
+ await PlacesTestUtils.isPageInDB(testUrl),
+ "expected url found in history database"
+ );
+
+ extension.sendMessage("delete-url", testUrl);
+ await extension.awaitMessage("url-deleted");
+ equal(
+ await PlacesTestUtils.isPageInDB(testUrl),
+ false,
+ "expected url not found in history database"
+ );
+
+ // delete 3 of the 5 visits for url 1
+ let filter = {
+ startTime: visits[0].visits[0].date,
+ endTime: visits[0].visits[2].date,
+ };
+
+ extension.sendMessage("delete-range", filter);
+ let removedUrls = await extension.awaitMessage("range-deleted");
+ ok(
+ !removedUrls.includes(visits[0].url),
+ `${visits[0].url} not received by onVisitRemoved`
+ );
+ ok(
+ await PlacesTestUtils.isPageInDB(visits[0].url),
+ "expected uri found in history database"
+ );
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[0].url),
+ 2,
+ "2 visits for uri found in history database"
+ );
+ ok(
+ await PlacesTestUtils.isPageInDB(visits[1].url),
+ "expected uri found in history database"
+ );
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[1].url),
+ 1,
+ "1 visit for uri found in history database"
+ );
+
+ // delete the rest of the visits for url 1, and the visit for url 2
+ filter.startTime = visits[0].visits[0].date;
+ filter.endTime = visits[1].visits[0].date;
+
+ extension.sendMessage("delete-range", filter);
+ await extension.awaitMessage("range-deleted");
+
+ equal(
+ await PlacesTestUtils.isPageInDB(visits[0].url),
+ false,
+ "expected uri not found in history database"
+ );
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[0].url),
+ 0,
+ "0 visits for uri found in history database"
+ );
+ equal(
+ await PlacesTestUtils.isPageInDB(visits[1].url),
+ false,
+ "expected uri not found in history database"
+ );
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[1].url),
+ 0,
+ "0 visits for uri found in history database"
+ );
+
+ ok(
+ await PlacesTestUtils.isPageInDB(visits[3].url),
+ "expected uri found in history database"
+ );
+
+ extension.sendMessage("delete-all");
+ [historyClearedCount, removedUrls] = await extension.awaitMessage(
+ "history-cleared"
+ );
+ equal(
+ historyClearedCount,
+ 2,
+ "onVisitRemoved called for each clearing of history"
+ );
+ equal(
+ removedUrls.length,
+ 3,
+ "onVisitRemoved called the expected number of times"
+ );
+ for (let i = 1; i < 3; ++i) {
+ let url = visits[i].url;
+ ok(removedUrls.includes(url), `${url} received by onVisitRemoved`);
+ }
+ await extension.unload();
+});
+
+const SINGLE_VISIT_URL = "http://example.com/";
+const DOUBLE_VISIT_URL = "http://example.com/2/";
+const MOZILLA_VISIT_URL = "http://mozilla.com/";
+const REFERENCE_DATE = new Date();
+// pages/visits to add via History.insert
+const PAGE_INFOS = [
+ {
+ url: SINGLE_VISIT_URL,
+ title: `test visit for ${SINGLE_VISIT_URL}`,
+ visits: [{ date: new Date(Number(REFERENCE_DATE) - 1000) }],
+ },
+ {
+ url: DOUBLE_VISIT_URL,
+ title: `test visit for ${DOUBLE_VISIT_URL}`,
+ visits: [
+ { date: REFERENCE_DATE },
+ { date: new Date(Number(REFERENCE_DATE) - 2000) },
+ ],
+ },
+ {
+ url: MOZILLA_VISIT_URL,
+ title: `test visit for ${MOZILLA_VISIT_URL}`,
+ visits: [{ date: new Date(Number(REFERENCE_DATE) - 3000) }],
+ },
+];
+
+add_task(async function test_search() {
+ function background(BGSCRIPT_REFERENCE_DATE) {
+ const futureTime = Date.now() + 24 * 60 * 60 * 1000;
+
+ browser.test.onMessage.addListener(msg => {
+ browser.history
+ .search({ text: "" })
+ .then(results => {
+ browser.test.sendMessage("empty-search", results);
+ return browser.history.search({ text: "mozilla.com" });
+ })
+ .then(results => {
+ browser.test.sendMessage("text-search", results);
+ return browser.history.search({ text: "example.com", maxResults: 1 });
+ })
+ .then(results => {
+ browser.test.sendMessage("max-results-search", results);
+ return browser.history.search({
+ text: "",
+ startTime: BGSCRIPT_REFERENCE_DATE - 2000,
+ endTime: BGSCRIPT_REFERENCE_DATE - 1000,
+ });
+ })
+ .then(results => {
+ browser.test.sendMessage("date-range-search", results);
+ return browser.history.search({ text: "", startTime: futureTime });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 0,
+ results.length,
+ "no results returned for late start time"
+ );
+ return browser.history.search({ text: "", endTime: 0 });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 0,
+ results.length,
+ "no results returned for early end time"
+ );
+ return browser.history.search({
+ text: "",
+ startTime: Date.now(),
+ endTime: 0,
+ });
+ })
+ .then(
+ results => {
+ browser.test.fail(
+ "history.search rejects with startTime that is after the endTime"
+ );
+ },
+ error => {
+ browser.test.assertEq(
+ "The startTime cannot be after the endTime",
+ error.message,
+ "history.search rejects with startTime that is after the endTime"
+ );
+ }
+ )
+ .then(() => {
+ browser.test.notifyPass("search");
+ });
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})(${Number(REFERENCE_DATE)})`,
+ });
+
+ function findResult(url, results) {
+ return results.find(r => r.url === url);
+ }
+
+ function checkResult(results, url, expectedCount) {
+ let result = findResult(url, results);
+ notEqual(result, null, `history.search result was found for ${url}`);
+ equal(
+ result.visitCount,
+ expectedCount,
+ `history.search reports ${expectedCount} visit(s)`
+ );
+ equal(
+ result.title,
+ `test visit for ${url}`,
+ "title for search result is correct"
+ );
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await PlacesUtils.history.clear();
+
+ await PlacesUtils.history.insertMany(PAGE_INFOS);
+
+ extension.sendMessage("check-history");
+
+ let results = await extension.awaitMessage("empty-search");
+ equal(results.length, 3, "history.search with empty text returned 3 results");
+ checkResult(results, SINGLE_VISIT_URL, 1);
+ checkResult(results, DOUBLE_VISIT_URL, 2);
+ checkResult(results, MOZILLA_VISIT_URL, 1);
+
+ results = await extension.awaitMessage("text-search");
+ equal(
+ results.length,
+ 1,
+ "history.search with specific text returned 1 result"
+ );
+ checkResult(results, MOZILLA_VISIT_URL, 1);
+
+ results = await extension.awaitMessage("max-results-search");
+ equal(results.length, 1, "history.search with maxResults returned 1 result");
+ checkResult(results, DOUBLE_VISIT_URL, 2);
+
+ results = await extension.awaitMessage("date-range-search");
+ equal(
+ results.length,
+ 2,
+ "history.search with a date range returned 2 result"
+ );
+ checkResult(results, DOUBLE_VISIT_URL, 2);
+ checkResult(results, SINGLE_VISIT_URL, 1);
+
+ await extension.awaitFinish("search");
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_add_url() {
+ function background() {
+ const TEST_DOMAIN = "http://example.com/";
+
+ browser.test.onMessage.addListener((msg, testData) => {
+ let [details, type] = testData;
+ details.url = details.url || `${TEST_DOMAIN}${type}`;
+ if (msg === "add-url") {
+ details.title = `Title for ${type}`;
+ browser.history
+ .addUrl(details)
+ .then(() => {
+ return browser.history.search({ text: details.url });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "1 result found when searching for added URL"
+ );
+ browser.test.sendMessage("url-added", {
+ details,
+ result: results[0],
+ });
+ });
+ } else if (msg === "expect-failure") {
+ let expectedMsg = testData[2];
+ browser.history.addUrl(details).then(
+ () => {
+ browser.test.fail(`Expected error thrown for ${type}`);
+ },
+ error => {
+ browser.test.assertTrue(
+ error.message.includes(expectedMsg),
+ `"Expected error thrown when trying to add a URL with ${type}`
+ );
+ browser.test.sendMessage("add-failed");
+ }
+ );
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let addTestData = [
+ [{}, "default"],
+ [{ visitTime: new Date() }, "with_date"],
+ [{ visitTime: Date.now() }, "with_ms_number"],
+ [{ visitTime: new Date().toISOString() }, "with_iso_string"],
+ [{ transition: "typed" }, "valid_transition"],
+ ];
+
+ let failTestData = [
+ [
+ { transition: "generated" },
+ "an invalid transition",
+ "|generated| is not a supported transition for history",
+ ],
+ [{ visitTime: Date.now() + 1000000 }, "a future date", "Invalid value"],
+ [{ url: "about.config" }, "an invalid url", "Invalid value"],
+ ];
+
+ async function checkUrl(results) {
+ ok(
+ await PlacesTestUtils.isPageInDB(results.details.url),
+ `${results.details.url} found in history database`
+ );
+ ok(
+ PlacesUtils.isValidGuid(results.result.id),
+ "URL was added with a valid id"
+ );
+ equal(
+ results.result.title,
+ results.details.title,
+ "URL was added with the correct title"
+ );
+ if (results.details.visitTime) {
+ equal(
+ results.result.lastVisitTime,
+ Number(ExtensionCommon.normalizeTime(results.details.visitTime)),
+ "URL was added with the correct date"
+ );
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ await PlacesUtils.history.clear();
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ for (let data of addTestData) {
+ extension.sendMessage("add-url", data);
+ let results = await extension.awaitMessage("url-added");
+ await checkUrl(results);
+ }
+
+ for (let data of failTestData) {
+ extension.sendMessage("expect-failure", data);
+ await extension.awaitMessage("add-failed");
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_get_visits() {
+ async function background() {
+ const TEST_DOMAIN = "http://example.com/";
+ const FIRST_DATE = Date.now();
+ const INITIAL_DETAILS = {
+ url: TEST_DOMAIN,
+ visitTime: FIRST_DATE,
+ transition: "link",
+ };
+
+ let visitIds = new Set();
+
+ async function checkVisit(visit, expected) {
+ visitIds.add(visit.visitId);
+ browser.test.assertEq(
+ expected.visitTime,
+ visit.visitTime,
+ "visit has the correct visitTime"
+ );
+ browser.test.assertEq(
+ expected.transition,
+ visit.transition,
+ "visit has the correct transition"
+ );
+ let results = await browser.history.search({ text: expected.url });
+ // all results will have the same id, so we only need to use the first one
+ browser.test.assertEq(
+ results[0].id,
+ visit.id,
+ "visit has the correct id"
+ );
+ }
+
+ let details = Object.assign({}, INITIAL_DETAILS);
+
+ await browser.history.addUrl(details);
+ let results = await browser.history.getVisits({ url: details.url });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "the expected number of visits were returned"
+ );
+ await checkVisit(results[0], details);
+
+ details.url = `${TEST_DOMAIN}/1/`;
+ await browser.history.addUrl(details);
+
+ results = await browser.history.getVisits({ url: details.url });
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "the expected number of visits were returned"
+ );
+ await checkVisit(results[0], details);
+
+ details.visitTime = FIRST_DATE - 1000;
+ details.transition = "typed";
+ await browser.history.addUrl(details);
+ results = await browser.history.getVisits({ url: details.url });
+
+ browser.test.assertEq(
+ 2,
+ results.length,
+ "the expected number of visits were returned"
+ );
+ await checkVisit(results[0], INITIAL_DETAILS);
+ await checkVisit(results[1], details);
+ browser.test.assertEq(3, visitIds.size, "each visit has a unique visitId");
+ await browser.test.notifyPass("get-visits");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ await PlacesUtils.history.clear();
+ await extension.startup();
+
+ await extension.awaitFinish("get-visits");
+ await extension.unload();
+});
+
+add_task(async function test_transition_types() {
+ const VISIT_URL_PREFIX = "http://example.com/";
+ const TRANSITIONS = [
+ ["link", Ci.nsINavHistoryService.TRANSITION_LINK],
+ ["typed", Ci.nsINavHistoryService.TRANSITION_TYPED],
+ ["auto_bookmark", Ci.nsINavHistoryService.TRANSITION_BOOKMARK],
+ // Only session history contains TRANSITION_EMBED visits,
+ // So global history query cannot find them.
+ // ["auto_subframe", Ci.nsINavHistoryService.TRANSITION_EMBED],
+ // Redirects are not correctly tested here because History
+ // will not make redirect entries hidden.
+ ["link", Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT],
+ ["link", Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY],
+ ["link", Ci.nsINavHistoryService.TRANSITION_DOWNLOAD],
+ ["manual_subframe", Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK],
+ ["reload", Ci.nsINavHistoryService.TRANSITION_RELOAD],
+ ];
+
+ // pages/visits to add via History.insertMany
+ let pageInfos = [];
+ let visitDate = new Date(1999, 9, 9, 9, 9).getTime();
+ for (let [, transitionType] of TRANSITIONS) {
+ pageInfos.push({
+ url: VISIT_URL_PREFIX + transitionType + "/",
+ visits: [
+ { transition: transitionType, date: new Date((visitDate -= 1000)) },
+ ],
+ });
+ }
+
+ function background() {
+ browser.test.onMessage.addListener(async (msg, url) => {
+ switch (msg) {
+ case "search": {
+ let results = await browser.history.search({
+ text: "",
+ startTime: new Date(0),
+ });
+ browser.test.sendMessage("search-result", results);
+ break;
+ }
+ case "get-visits": {
+ let results = await browser.history.getVisits({ url });
+ browser.test.sendMessage("get-visits-result", results);
+ break;
+ }
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background,
+ });
+
+ await PlacesUtils.history.clear();
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await PlacesUtils.history.insertMany(pageInfos);
+
+ extension.sendMessage("search");
+ let results = await extension.awaitMessage("search-result");
+ equal(
+ results.length,
+ pageInfos.length,
+ "search returned expected length of results"
+ );
+ for (let i = 0; i < pageInfos.length; ++i) {
+ equal(results[i].url, pageInfos[i].url, "search returned the expected url");
+
+ extension.sendMessage("get-visits", pageInfos[i].url);
+ let visits = await extension.awaitMessage("get-visits-result");
+ equal(visits.length, 1, "getVisits returned expected length of visits");
+ equal(
+ visits[0].transition,
+ TRANSITIONS[i][0],
+ "getVisits returned the expected transition"
+ );
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_on_visited() {
+ const SINGLE_VISIT_URL = "http://example.com/1/";
+ const DOUBLE_VISIT_URL = "http://example.com/2/";
+ let visitDate = new Date(1999, 9, 9, 9, 9).getTime();
+
+ // pages/visits to add via History.insertMany
+ const PAGE_INFOS = [
+ {
+ url: SINGLE_VISIT_URL,
+ title: `visit to ${SINGLE_VISIT_URL}`,
+ visits: [{ date: new Date(visitDate) }],
+ },
+ {
+ url: DOUBLE_VISIT_URL,
+ title: `visit to ${DOUBLE_VISIT_URL}`,
+ visits: [
+ { date: new Date((visitDate += 1000)) },
+ { date: new Date((visitDate += 1000)) },
+ ],
+ },
+ {
+ url: SINGLE_VISIT_URL,
+ title: "Title Changed",
+ visits: [{ date: new Date(visitDate) }],
+ },
+ ];
+
+ function background() {
+ let onVisitedData = [];
+
+ browser.history.onVisited.addListener(data => {
+ if (data.url.includes("moz-extension")) {
+ return;
+ }
+ onVisitedData.push(data);
+ if (onVisitedData.length == 4) {
+ browser.test.sendMessage("on-visited-data", onVisitedData);
+ }
+ });
+
+ // Verifying onTitleChange Event along with onVisited event
+ browser.history.onTitleChanged.addListener(data => {
+ browser.test.sendMessage("on-title-changed-data", data);
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ await PlacesUtils.history.clear();
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await PlacesUtils.history.insertMany(PAGE_INFOS);
+
+ let onVisitedData = await extension.awaitMessage("on-visited-data");
+
+ function checkOnVisitedData(index, expected) {
+ let onVisited = onVisitedData[index];
+ ok(PlacesUtils.isValidGuid(onVisited.id), "onVisited received a valid id");
+ equal(onVisited.url, expected.url, "onVisited received the expected url");
+ equal(
+ onVisited.title,
+ expected.title,
+ "onVisited received the expected title"
+ );
+ equal(
+ onVisited.lastVisitTime,
+ expected.time,
+ "onVisited received the expected time"
+ );
+ equal(
+ onVisited.visitCount,
+ expected.visitCount,
+ "onVisited received the expected visitCount"
+ );
+ }
+
+ let expected = {
+ url: PAGE_INFOS[0].url,
+ title: PAGE_INFOS[0].title,
+ time: PAGE_INFOS[0].visits[0].date.getTime(),
+ visitCount: 1,
+ };
+ checkOnVisitedData(0, expected);
+
+ expected.url = PAGE_INFOS[1].url;
+ expected.title = PAGE_INFOS[1].title;
+ expected.time = PAGE_INFOS[1].visits[0].date.getTime();
+ checkOnVisitedData(1, expected);
+
+ expected.time = PAGE_INFOS[1].visits[1].date.getTime();
+ expected.visitCount = 2;
+ checkOnVisitedData(2, expected);
+
+ expected.url = PAGE_INFOS[2].url;
+ expected.title = PAGE_INFOS[2].title;
+ expected.time = PAGE_INFOS[2].visits[0].date.getTime();
+ expected.visitCount = 2;
+ checkOnVisitedData(3, expected);
+
+ let onTitleChangedData = await extension.awaitMessage(
+ "on-title-changed-data"
+ );
+ Assert.deepEqual(
+ {
+ id: onVisitedData[3].id,
+ url: SINGLE_VISIT_URL,
+ title: "Title Changed",
+ },
+ onTitleChangedData,
+ "expected event data for onTitleChanged"
+ );
+
+ await extension.unload();
+});
+
+add_task(
+ {
+ pref_set: [["extensions.eventPages.enabled", true]],
+ },
+ async function test_history_event_page() {
+ await AddonTestUtils.promiseStartupManager();
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@history" } },
+ permissions: ["history"],
+ background: { persistent: false },
+ },
+ background() {
+ browser.history.onVisited.addListener(() => {
+ browser.test.sendMessage("onVisited");
+ });
+ browser.history.onVisitRemoved.addListener(() => {
+ browser.test.sendMessage("onVisitRemoved");
+ });
+ browser.history.onTitleChanged.addListener(() => {});
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = ["onVisited", "onVisitRemoved", "onTitleChanged"];
+ await PlacesUtils.history.clear();
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "history", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "history", event, {
+ primed: true,
+ });
+ }
+
+ await PlacesUtils.history.insertMany(PAGE_INFOS);
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onVisited");
+ ok(true, "persistent event woke background");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "history", event, {
+ primed: false,
+ });
+ }
+
+ await AddonTestUtils.promiseRestartManager();
+ await extension.awaitStartup();
+
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "history", event, {
+ primed: true,
+ });
+ }
+
+ await PlacesUtils.history.clear();
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onVisitRemoved");
+
+ await extension.unload();
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
diff --git a/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js
new file mode 100644
index 0000000000..2d2bccc1e2
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js
@@ -0,0 +1,134 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { HomePage } = ChromeUtils.importESModule(
+ "resource:///modules/HomePage.sys.mjs"
+);
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+const { createAppInfo, promiseShutdownManager, promiseStartupManager } =
+ AddonTestUtils;
+
+const EXTENSION_ID = "test_overrides@tests.mozilla.org";
+const HOMEPAGE_EXTENSION_CONTROLLED =
+ "browser.startup.homepage_override.extensionControlled";
+const HOMEPAGE_PRIVATE_ALLOWED =
+ "browser.startup.homepage_override.privateAllowed";
+const HOMEPAGE_URL_PREF = "browser.startup.homepage";
+const HOMEPAGE_URI = "webext-homepage-1.html";
+
+Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true);
+
+AddonTestUtils.init(this);
+AddonTestUtils.usePrivilegedSignatures = false;
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+function promisePrefChange(pref) {
+ return new Promise((resolve, reject) => {
+ Services.prefs.addObserver(pref, function observer() {
+ Services.prefs.removeObserver(pref, observer);
+ resolve(arguments);
+ });
+ });
+}
+
+let defaultHomepageURL;
+
+function verifyPrefSettings(controlled, allowed) {
+ equal(
+ Services.prefs.getBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, false),
+ controlled,
+ "homepage extension controlled"
+ );
+ equal(
+ Services.prefs.getBoolPref(HOMEPAGE_PRIVATE_ALLOWED, false),
+ allowed,
+ "homepage private permission after permission change"
+ );
+
+ if (controlled && allowed) {
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI),
+ "Home page url is overridden by the extension"
+ );
+ } else {
+ equal(HomePage.get(), defaultHomepageURL, "Home page url is default.");
+ }
+}
+
+async function promiseUpdatePrivatePermission(allowed, extension) {
+ info(`update private allowed permission`);
+ await Promise.all([
+ promisePrefChange(HOMEPAGE_PRIVATE_ALLOWED),
+ ExtensionPermissions[allowed ? "add" : "remove"](
+ extension.id,
+ { permissions: ["internal:privateBrowsingAllowed"], origins: [] },
+ extension
+ ),
+ ]);
+
+ verifyPrefSettings(true, allowed);
+}
+
+add_task(async function test_overrides_private() {
+ await promiseStartupManager();
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI,
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ defaultHomepageURL = HomePage.get();
+
+ await extension.startup();
+
+ verifyPrefSettings(true, false);
+
+ equal(HomePage.get(), defaultHomepageURL, "Home page url is default.");
+
+ info("add permission to extension");
+ await promiseUpdatePrivatePermission(true, extension.extension);
+ info("remove permission from extension");
+ await promiseUpdatePrivatePermission(false, extension.extension);
+ // set back to true to test upgrade removing extension control
+ info("add permission back to prepare for upgrade test");
+ await promiseUpdatePrivatePermission(true, extension.extension);
+
+ extensionInfo.manifest = {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ await Promise.all([
+ promisePrefChange(HOMEPAGE_URL_PREF),
+ extension.upgrade(extensionInfo),
+ ]);
+
+ verifyPrefSettings(false, false);
+
+ await extension.unload();
+ await promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest.js b/browser/components/extensions/test/xpcshell/test_ext_manifest.js
new file mode 100644
index 0000000000..b978172ca2
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest.js
@@ -0,0 +1,105 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+
+async function testManifest(manifest, expectedError) {
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ let normalized = await ExtensionTestUtils.normalizeManifest(manifest);
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+
+ if (expectedError) {
+ ok(
+ expectedError.test(normalized.error),
+ `Should have an error for ${JSON.stringify(manifest)}, got ${
+ normalized.error
+ }`
+ );
+ } else {
+ ok(
+ !normalized.error,
+ `Should not have an error ${JSON.stringify(manifest)}, ${
+ normalized.error
+ }`
+ );
+ }
+ return normalized.errors;
+}
+
+const all_actions = [
+ "action",
+ "browser_action",
+ "page_action",
+ "sidebar_action",
+];
+
+add_task(async function test_manifest() {
+ let badpaths = ["", " ", "\t", "http://foo.com/icon.png"];
+ for (let path of badpaths) {
+ for (let action of all_actions) {
+ let manifest_version = action == "action" ? 3 : 2;
+ let manifest = { manifest_version };
+ manifest[action] = { default_icon: path };
+ let error = new RegExp(`Error processing ${action}.default_icon`);
+ await testManifest(manifest, error);
+
+ manifest[action] = { default_icon: { 16: path } };
+ await testManifest(manifest, error);
+ }
+ }
+
+ let paths = [
+ "icon.png",
+ "/icon.png",
+ "./icon.png",
+ "path to an icon.png",
+ " icon.png",
+ ];
+ for (let path of paths) {
+ for (let action of all_actions) {
+ let manifest_version = action == "action" ? 3 : 2;
+ let manifest = { manifest_version };
+ manifest[action] = { default_icon: path };
+ if (action == "sidebar_action") {
+ // Sidebar requires panel.
+ manifest[action].default_panel = "foo.html";
+ }
+ await testManifest(manifest);
+
+ manifest[action] = { default_icon: { 16: path } };
+ if (action == "sidebar_action") {
+ manifest[action].default_panel = "foo.html";
+ }
+ await testManifest(manifest);
+ }
+ }
+});
+
+add_task(async function test_action_version() {
+ let warnings = await testManifest({
+ manifest_version: 3,
+ browser_action: {
+ default_panel: "foo.html",
+ },
+ });
+ Assert.deepEqual(
+ warnings,
+ [`Property "browser_action" is unsupported in Manifest Version 3`],
+ `Manifest v3 with "browser_action" key logs an error.`
+ );
+
+ warnings = await testManifest({
+ manifest_version: 2,
+ action: {
+ default_icon: "",
+ default_panel: "foo.html",
+ },
+ });
+
+ Assert.deepEqual(
+ warnings,
+ [`Property "action" is unsupported in Manifest Version 2`],
+ `Manifest v2 with "action" key first warning is clear.`
+ );
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
new file mode 100644
index 0000000000..8196ab0e24
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
@@ -0,0 +1,52 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_manifest_commands() {
+ const validShortcuts = [
+ "Ctrl+Y",
+ "MacCtrl+Y",
+ "Command+Y",
+ "Alt+Shift+Y",
+ "Ctrl+Alt+Y",
+ "F1",
+ "MediaNextTrack",
+ ];
+ const invalidShortcuts = ["Shift+Y", "Y", "Ctrl+Ctrl+Y", "Ctrl+Command+Y"];
+
+ async function validateShortcut(shortcut, isValid) {
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ commands: {
+ "toggle-feature": {
+ suggested_key: { default: shortcut },
+ description: "Send a 'toggle-feature' event to the extension",
+ },
+ },
+ });
+ if (isValid) {
+ ok(!normalized.error, "There should be no manifest errors.");
+ } else {
+ let expectedError =
+ String.raw`Error processing commands.toggle-feature.suggested_key.default: Error: ` +
+ String.raw`Value "${shortcut}" must consist of ` +
+ String.raw`either a combination of one or two modifiers, including ` +
+ String.raw`a mandatory primary modifier and a key, separated by '+', ` +
+ String.raw`or a media key. For details see: ` +
+ String.raw`https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
+
+ ok(
+ normalized.error.includes(expectedError),
+ `The manifest error ${JSON.stringify(
+ normalized.error
+ )} must contain ${JSON.stringify(expectedError)}`
+ );
+ }
+ }
+
+ for (let shortcut of validShortcuts) {
+ validateShortcut(shortcut, true);
+ }
+ for (let shortcut of invalidShortcuts) {
+ validateShortcut(shortcut, false);
+ }
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js
new file mode 100644
index 0000000000..f81e7d3cb5
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js
@@ -0,0 +1,62 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testKeyword(params) {
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ omnibox: {
+ keyword: params.keyword,
+ },
+ });
+
+ if (params.expectError) {
+ let expectedError =
+ String.raw`omnibox.keyword: String "${params.keyword}" ` +
+ String.raw`must match /^[^?\s:][^\s:]*$/`;
+ ok(
+ normalized.error.includes(expectedError),
+ `The manifest error ${JSON.stringify(normalized.error)} ` +
+ `must contain ${JSON.stringify(expectedError)}`
+ );
+ } else {
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+ }
+}
+
+add_task(async function test_manifest_commands() {
+ // accepted single character keywords
+ await testKeyword({ keyword: "a", expectError: false });
+ await testKeyword({ keyword: "-", expectError: false });
+ await testKeyword({ keyword: "嗨", expectError: false });
+ await testKeyword({ keyword: "*", expectError: false });
+ await testKeyword({ keyword: "/", expectError: false });
+
+ // rejected single character keywords
+ await testKeyword({ keyword: "?", expectError: true });
+ await testKeyword({ keyword: " ", expectError: true });
+ await testKeyword({ keyword: ":", expectError: true });
+
+ // accepted multi-character keywords
+ await testKeyword({ keyword: "aa", expectError: false });
+ await testKeyword({ keyword: "http", expectError: false });
+ await testKeyword({ keyword: "f?a", expectError: false });
+ await testKeyword({ keyword: "fa?", expectError: false });
+ await testKeyword({ keyword: "f/x", expectError: false });
+ await testKeyword({ keyword: "/fx", expectError: false });
+ await testKeyword({ keyword: "fx/", expectError: false });
+
+ // rejected multi-character keywords
+ await testKeyword({ keyword: " a", expectError: true });
+ await testKeyword({ keyword: "a ", expectError: true });
+ await testKeyword({ keyword: " ", expectError: true });
+ await testKeyword({ keyword: " a ", expectError: true });
+ await testKeyword({ keyword: "?fx", expectError: true });
+ await testKeyword({ keyword: "f:x", expectError: true });
+ await testKeyword({ keyword: "fx:", expectError: true });
+ await testKeyword({ keyword: "f x", expectError: true });
+
+ // miscellaneous tests
+ await testKeyword({ keyword: "こんにちは", expectError: false });
+ await testKeyword({ keyword: "http://", expectError: true });
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js
new file mode 100644
index 0000000000..fed7af5d5b
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js
@@ -0,0 +1,85 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals chrome */
+
+Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+
+async function testPermission(options) {
+ function background(bgOptions) {
+ browser.test.sendMessage("typeof-namespace", {
+ browser: typeof browser[bgOptions.namespace],
+ chrome: typeof chrome[bgOptions.namespace],
+ });
+ }
+
+ let extensionDetails = {
+ background: `(${background})(${JSON.stringify(options)})`,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionDetails);
+
+ await extension.startup();
+
+ let types = await extension.awaitMessage("typeof-namespace");
+ equal(
+ types.browser,
+ "undefined",
+ `Type of browser.${options.namespace} without manifest entry`
+ );
+ equal(
+ types.chrome,
+ "undefined",
+ `Type of chrome.${options.namespace} without manifest entry`
+ );
+
+ await extension.unload();
+
+ extensionDetails.manifest = options.manifest;
+ extension = ExtensionTestUtils.loadExtension(extensionDetails);
+
+ await extension.startup();
+
+ types = await extension.awaitMessage("typeof-namespace");
+ equal(
+ types.browser,
+ "object",
+ `Type of browser.${options.namespace} with manifest entry`
+ );
+ equal(
+ types.chrome,
+ "object",
+ `Type of chrome.${options.namespace} with manifest entry`
+ );
+
+ await extension.unload();
+}
+
+add_task(async function test_action() {
+ await testPermission({
+ namespace: "action",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ });
+});
+
+add_task(async function test_browserAction() {
+ await testPermission({
+ namespace: "browserAction",
+ manifest: {
+ browser_action: {},
+ },
+ });
+});
+
+add_task(async function test_pageAction() {
+ await testPermission({
+ namespace: "pageAction",
+ manifest: {
+ page_action: {},
+ },
+ });
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_menu_caller.js b/browser/components/extensions/test/xpcshell/test_ext_menu_caller.js
new file mode 100644
index 0000000000..5aa04bbc78
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_menu_caller.js
@@ -0,0 +1,53 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_create_menu_ext_error() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ async background() {
+ let { fileName } = new Error();
+ browser.menus.create({
+ id: "muted-tab",
+ title: "open link with Menu 1",
+ contexts: ["link"],
+ });
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ id: "muted-tab",
+ title: "open link with Menu 2",
+ contexts: ["link"],
+ },
+ resolve
+ );
+ });
+ browser.test.sendMessage("fileName", fileName);
+ },
+ });
+
+ let fileName;
+ const { messages } = await promiseConsoleOutput(async () => {
+ await extension.startup();
+ fileName = await extension.awaitMessage("fileName");
+ await extension.unload();
+ });
+ let [msg] = messages
+ .filter(m => m.message.includes("Unchecked lastError"))
+ .map(m => m.QueryInterface(Ci.nsIScriptError));
+ equal(msg.sourceName, fileName, "Message source");
+
+ equal(
+ msg.errorMessage,
+ "Unchecked lastError value: Error: ID already exists: muted-tab",
+ "Message content"
+ );
+ equal(msg.lineNumber, 9, "Message line");
+
+ let frame = msg.stack;
+ equal(frame.source, fileName, "Frame source");
+ equal(frame.line, 9, "Frame line");
+ equal(frame.column, 23, "Frame column");
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js b/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js
new file mode 100644
index 0000000000..aa019c6584
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js
@@ -0,0 +1,432 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+Services.prefs.setBoolPref("extensions.eventPages.enabled", true);
+
+function getExtension(id, background, useAddonManager) {
+ return ExtensionTestUtils.loadExtension({
+ useAddonManager,
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ permissions: ["menus"],
+ background: { persistent: false },
+ },
+ background,
+ });
+}
+
+async function expectCached(extension, expect) {
+ let { StartupCache } = ExtensionParent;
+ let cached = await StartupCache.menus.get(extension.id);
+ let createProperties = Array.from(cached.values());
+ equal(cached.size, expect.length, "menus saved in cache");
+ // The menus startupCache is a map and the order is significant
+ // for recreating menus on startup. Ensure that they are in
+ // the expected order. We only verify specific keys here rather
+ // than all menu properties.
+ for (let i in createProperties) {
+ Assert.deepEqual(
+ createProperties[i],
+ expect[i],
+ "expected cached properties exist"
+ );
+ }
+}
+
+function promiseExtensionEvent(wrapper, event) {
+ return new Promise(resolve => {
+ wrapper.extension.once(event, (kind, data) => {
+ resolve(data);
+ });
+ });
+}
+
+add_setup(async () => {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_menu_onInstalled() {
+ async function background() {
+ browser.runtime.onInstalled.addListener(async () => {
+ const parentId = browser.menus.create({
+ contexts: ["all"],
+ title: "parent",
+ id: "test-parent",
+ });
+ browser.menus.create({
+ parentId,
+ title: "click A",
+ id: "test-click-a",
+ });
+ browser.menus.create(
+ {
+ parentId,
+ title: "click B",
+ id: "test-click-b",
+ },
+ () => {
+ browser.test.sendMessage("onInstalled");
+ }
+ );
+ });
+ browser.menus.create(
+ {
+ contexts: ["tab"],
+ title: "top-level",
+ id: "test-top-level",
+ },
+ () => {
+ browser.test.sendMessage("create", browser.runtime.lastError?.message);
+ }
+ );
+
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.log(`onMessage ${msg}`);
+ if (msg == "updatemenu") {
+ await browser.menus.update("test-click-a", { title: "click updated" });
+ } else if (msg == "removemenu") {
+ await browser.menus.remove("test-click-b");
+ } else if (msg == "removeall") {
+ await browser.menus.removeAll();
+ }
+ browser.test.sendMessage("updated");
+ });
+ }
+
+ const extension = getExtension(
+ "test-persist@mochitest",
+ background,
+ "permanent"
+ );
+
+ await extension.startup();
+ let lastError = await extension.awaitMessage("create");
+ Assert.equal(lastError, undefined, "no error creating menu");
+ await extension.awaitMessage("onInstalled");
+ await extension.terminateBackground();
+
+ await expectCached(extension, [
+ {
+ contexts: ["tab"],
+ id: "test-top-level",
+ title: "top-level",
+ },
+ { contexts: ["all"], id: "test-parent", title: "parent" },
+ {
+ id: "test-click-a",
+ parentId: "test-parent",
+ title: "click A",
+ },
+ {
+ id: "test-click-b",
+ parentId: "test-parent",
+ title: "click B",
+ },
+ ]);
+
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-top-level already exists in menus.create.",
+ "correct error creating menu"
+ );
+
+ await AddonTestUtils.promiseRestartManager();
+ await extension.awaitStartup();
+
+ // verify the startupcache
+ await expectCached(extension, [
+ {
+ contexts: ["tab"],
+ id: "test-top-level",
+ title: "top-level",
+ },
+ { contexts: ["all"], id: "test-parent", title: "parent" },
+ {
+ id: "test-click-a",
+ parentId: "test-parent",
+ title: "click A",
+ },
+ {
+ id: "test-click-b",
+ parentId: "test-parent",
+ title: "click B",
+ },
+ ]);
+
+ equal(
+ extension.extension.backgroundState,
+ "stopped",
+ "background is not running"
+ );
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-top-level already exists in menus.create.",
+ "correct error creating menu"
+ );
+
+ extension.sendMessage("updatemenu");
+ await extension.awaitMessage("updated");
+ await extension.terminateBackground();
+
+ // Title change is cached
+ await expectCached(extension, [
+ {
+ contexts: ["tab"],
+ id: "test-top-level",
+ title: "top-level",
+ },
+ { contexts: ["all"], id: "test-parent", title: "parent" },
+ {
+ id: "test-click-a",
+ parentId: "test-parent",
+ title: "click updated",
+ },
+ {
+ id: "test-click-b",
+ parentId: "test-parent",
+ title: "click B",
+ },
+ ]);
+
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-top-level already exists in menus.create.",
+ "correct error creating menu"
+ );
+
+ extension.sendMessage("removemenu");
+ await extension.awaitMessage("updated");
+ await extension.terminateBackground();
+
+ // menu removed
+ await expectCached(extension, [
+ {
+ contexts: ["tab"],
+ id: "test-top-level",
+ title: "top-level",
+ },
+ { contexts: ["all"], id: "test-parent", title: "parent" },
+ {
+ id: "test-click-a",
+ parentId: "test-parent",
+ title: "click updated",
+ },
+ ]);
+
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-top-level already exists in menus.create.",
+ "correct error creating menu"
+ );
+
+ extension.sendMessage("removeall");
+ await extension.awaitMessage("updated");
+ await extension.terminateBackground();
+
+ // menus removed
+ await expectCached(extension, []);
+
+ await extension.unload();
+});
+
+add_task(async function test_menu_nested() {
+ async function background() {
+ browser.test.onMessage.addListener(async (action, properties) => {
+ browser.test.log(`onMessage ${action}`);
+ switch (action) {
+ case "create":
+ await new Promise(resolve => {
+ browser.menus.create(properties, resolve);
+ });
+ break;
+ case "update":
+ {
+ let { id, ...update } = properties;
+ await browser.menus.update(id, update);
+ }
+ break;
+ case "remove":
+ {
+ let { id } = properties;
+ await browser.menus.remove(id);
+ }
+ break;
+ case "removeAll":
+ await browser.menus.removeAll();
+ break;
+ }
+ browser.test.sendMessage("updated");
+ });
+ }
+
+ const extension = getExtension(
+ "test-nesting@mochitest",
+ background,
+ "permanent"
+ );
+ await extension.startup();
+
+ extension.sendMessage("create", {
+ id: "first",
+ contexts: ["all"],
+ title: "first",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "first", title: "first" },
+ ]);
+
+ extension.sendMessage("create", {
+ id: "second",
+ contexts: ["all"],
+ title: "second",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "first", title: "first" },
+ { contexts: ["all"], id: "second", title: "second" },
+ ]);
+
+ extension.sendMessage("create", {
+ id: "third",
+ contexts: ["all"],
+ title: "third",
+ parentId: "first",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "first", title: "first" },
+ { contexts: ["all"], id: "second", title: "second" },
+ {
+ contexts: ["all"],
+ id: "third",
+ parentId: "first",
+ title: "third",
+ },
+ ]);
+
+ extension.sendMessage("create", {
+ id: "fourth",
+ contexts: ["all"],
+ title: "fourth",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "first", title: "first" },
+ { contexts: ["all"], id: "second", title: "second" },
+ {
+ contexts: ["all"],
+ id: "third",
+ parentId: "first",
+ title: "third",
+ },
+ { contexts: ["all"], id: "fourth", title: "fourth" },
+ ]);
+
+ extension.sendMessage("update", {
+ id: "first",
+ parentId: "second",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "second", title: "second" },
+ { contexts: ["all"], id: "fourth", title: "fourth" },
+ {
+ contexts: ["all"],
+ id: "first",
+ title: "first",
+ parentId: "second",
+ },
+ {
+ contexts: ["all"],
+ id: "third",
+ parentId: "first",
+ title: "third",
+ },
+ ]);
+
+ await AddonTestUtils.promiseShutdownManager();
+ // We need to attach an event listener before the
+ // startup event is emitted. Fortunately, we
+ // emit via Management before emitting on extension.
+ let promiseMenus;
+ Management.once("startup", (kind, ext) => {
+ info(`management ${kind} ${ext.id}`);
+ promiseMenus = promiseExtensionEvent(
+ { extension: ext },
+ "webext-menus-created"
+ );
+ });
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+ await extension.wakeupBackground();
+
+ await expectCached(extension, [
+ { contexts: ["all"], id: "second", title: "second" },
+ { contexts: ["all"], id: "fourth", title: "fourth" },
+ {
+ contexts: ["all"],
+ id: "first",
+ title: "first",
+ parentId: "second",
+ },
+ {
+ contexts: ["all"],
+ id: "third",
+ parentId: "first",
+ title: "third",
+ },
+ ]);
+ // validate nesting
+ let menus = await promiseMenus;
+ equal(menus.get("first").parentId, "second", "menuitem parent is correct");
+ equal(
+ menus.get("second").children.length,
+ 1,
+ "menuitem parent has correct number of children"
+ );
+ equal(
+ menus.get("second").root.children.length,
+ 2, // second and forth
+ "menuitem root has correct number of children"
+ );
+
+ extension.sendMessage("remove", {
+ id: "second",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "fourth", title: "fourth" },
+ ]);
+
+ extension.sendMessage("removeAll");
+ await extension.awaitMessage("updated");
+ await expectCached(extension, []);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js b/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js
new file mode 100644
index 0000000000..0a2a9dcd49
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js
@@ -0,0 +1,243 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+});
+
+const { AddonStudies } = ChromeUtils.importESModule(
+ "resource://normandy/lib/AddonStudies.sys.mjs"
+);
+const { NormandyTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/NormandyTestUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { addonStudyFactory } = NormandyTestUtils.factories;
+
+AddonTestUtils.init(this);
+
+// All tests run privileged unless otherwise specified not to.
+function createExtension(backgroundScript, permissions, isPrivileged = true) {
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "test@shield.mozilla.com",
+ },
+ },
+ permissions,
+ },
+ isPrivileged,
+ };
+ return ExtensionTestUtils.loadExtension(extensionData);
+}
+
+async function run(test) {
+ let extension = createExtension(
+ test.backgroundScript,
+ test.permissions || ["normandyAddonStudy"],
+ test.isPrivileged
+ );
+ const promiseValidation = test.validationScript
+ ? test.validationScript(extension)
+ : Promise.resolve();
+
+ await extension.startup();
+
+ await promiseValidation;
+
+ if (test.doneSignal) {
+ await extension.awaitFinish(test.doneSignal);
+ }
+
+ await extension.unload();
+}
+
+add_task(async function setup() {
+ await ExtensionTestUtils.startAddonManager();
+});
+
+add_task(
+ async function test_normandyAddonStudy_without_normandyAddonStudy_permission_privileged() {
+ await run({
+ backgroundScript: () => {
+ browser.test.assertTrue(
+ !browser.normandyAddonStudy,
+ "'normandyAddonStudy' permission is required"
+ );
+ browser.test.notifyPass("normandyAddonStudy_permission");
+ },
+ permissions: [],
+ doneSignal: "normandyAddonStudy_permission",
+ });
+ }
+);
+
+add_task(async function test_normandyAddonStudy_without_privilege() {
+ await run({
+ backgroundScript: () => {
+ browser.test.assertTrue(
+ !browser.normandyAddonStudy,
+ "Extension must be privileged"
+ );
+ browser.test.notifyPass("normandyAddonStudy_permission");
+ },
+ isPrivileged: false,
+ doneSignal: "normandyAddonStudy_permission",
+ });
+});
+
+add_task(async function test_normandyAddonStudy_temporary_without_privilege() {
+ let extension = ExtensionTestUtils.loadExtension({
+ temporarilyInstalled: true,
+ isPrivileged: false,
+ manifest: {
+ permissions: ["normandyAddonStudy"],
+ },
+ });
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ let { messages } = await promiseConsoleOutput(async () => {
+ await Assert.rejects(
+ extension.startup(),
+ /Using the privileged permission/,
+ "Startup failed with privileged permission"
+ );
+ });
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ AddonTestUtils.checkMessages(
+ messages,
+ {
+ expected: [
+ {
+ message:
+ /Using the privileged permission 'normandyAddonStudy' requires a privileged add-on/,
+ },
+ ],
+ },
+ true
+ );
+});
+
+add_task(async function test_getStudy_works() {
+ const study = addonStudyFactory({
+ addonId: "test@shield.mozilla.com",
+ });
+
+ const testWrapper = AddonStudies.withStudies([study]);
+ const test = testWrapper(async () => {
+ await run({
+ backgroundScript: async () => {
+ const result = await browser.normandyAddonStudy.getStudy();
+ browser.test.sendMessage("study", result);
+ },
+ validationScript: async extension => {
+ let studyResult = await extension.awaitMessage("study");
+ deepEqual(
+ studyResult,
+ study,
+ "normandyAddonStudy.getStudy returns the correct study"
+ );
+ },
+ });
+ });
+
+ await test();
+});
+
+add_task(async function test_endStudy_works() {
+ const study = addonStudyFactory({
+ addonId: "test@shield.mozilla.com",
+ });
+
+ const testWrapper = AddonStudies.withStudies([study]);
+ const test = testWrapper(async () => {
+ await run({
+ backgroundScript: async () => {
+ await browser.normandyAddonStudy.endStudy("test");
+ },
+ validationScript: async () => {
+ // Check that `AddonStudies.markAsEnded` was called
+ await TestUtils.topicObserved(
+ "shield-study-ended",
+ (subject, message) => {
+ return message === `${study.recipeId}`;
+ }
+ );
+
+ const addon = await AddonManager.getAddonByID(study.addonId);
+ equal(addon, undefined, "Addon should be uninstalled.");
+ },
+ });
+ });
+
+ await test();
+});
+
+add_task(async function test_getClientMetadata_works() {
+ const study = addonStudyFactory({
+ addonId: "test@shield.mozilla.com",
+ slug: "test-slug",
+ branch: "test-branch",
+ });
+
+ const testWrapper = AddonStudies.withStudies([study]);
+ const test = testWrapper(async () => {
+ await run({
+ backgroundScript: async () => {
+ const metadata = await browser.normandyAddonStudy.getClientMetadata();
+ browser.test.sendMessage("clientMetadata", metadata);
+ },
+ validationScript: async extension => {
+ let clientMetadata = await extension.awaitMessage("clientMetadata");
+
+ Assert.strictEqual(
+ clientMetadata.updateChannel,
+ Services.appinfo.defaultUpdateChannel,
+ "clientMetadata contains correct updateChannel"
+ );
+
+ Assert.strictEqual(
+ clientMetadata.fxVersion,
+ Services.appinfo.version,
+ "clientMetadata contains correct fxVersion"
+ );
+
+ ok("clientID" in clientMetadata, "clientMetadata contains a clientID");
+ },
+ });
+ });
+
+ await test();
+});
+
+add_task(async function test_onUnenroll_works() {
+ const study = addonStudyFactory({
+ addonId: "test@shield.mozilla.com",
+ });
+
+ const testWrapper = AddonStudies.withStudies([study]);
+ const test = testWrapper(async () => {
+ await run({
+ backgroundScript: () => {
+ browser.normandyAddonStudy.onUnenroll.addListener(reason => {
+ browser.test.sendMessage("unenrollReason", reason);
+ });
+ browser.test.sendMessage("bgpageReady");
+ },
+ validationScript: async extension => {
+ await extension.awaitMessage("bgpageReady");
+ await AddonStudies.markAsEnded(study, "test");
+ const unenrollReason = await extension.awaitMessage("unenrollReason");
+ equal(unenrollReason, "test", "Unenroll listener should be called.");
+ },
+ });
+ });
+
+ await test();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js b/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
new file mode 100644
index 0000000000..bd462ec9b6
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
@@ -0,0 +1,81 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Load lazy so we create the app info first.
+ChromeUtils.defineESModuleGetters(this, {
+ PageActions: "resource:///modules/PageActions.sys.mjs",
+});
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { createAppInfo, promiseShutdownManager, promiseStartupManager } =
+ AddonTestUtils;
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "58");
+
+// This is copied and pasted from ext-browser.js and used in ext-pageAction.js.
+// It's used as the PageActions action ID.
+function makeWidgetId(id) {
+ id = id.toLowerCase();
+ // FIXME: This allows for collisions.
+ return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+// Tests that the pinnedToUrlbar property of the PageActions.Action object
+// backing the extension's page action persists across app restarts.
+add_task(async function testAppShutdown() {
+ let extensionData = {
+ useAddonManager: "permanent",
+ manifest: {
+ page_action: {
+ default_title: "test_ext_pageAction_shutdown.js",
+ browser_style: false,
+ },
+ },
+ };
+
+ // Simulate starting up the app.
+ PageActions.init();
+ await promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ // Get the PageAction.Action object. Its pinnedToUrlbar should have been
+ // initialized to true in ext-pageAction.js, when it's created.
+ let actionID = makeWidgetId(extension.id);
+ let action = PageActions.actionForID(actionID);
+ Assert.equal(action.pinnedToUrlbar, true);
+
+ // Simulate restarting the app without first unloading the extension.
+ await promiseShutdownManager();
+ PageActions._reset();
+ await promiseStartupManager();
+ await extension.awaitStartup();
+
+ // Get the action. Its pinnedToUrlbar should remain true.
+ action = PageActions.actionForID(actionID);
+ Assert.equal(action.pinnedToUrlbar, true);
+
+ // Now set its pinnedToUrlbar to false.
+ action.pinnedToUrlbar = false;
+
+ // Simulate restarting the app again without first unloading the extension.
+ await promiseShutdownManager();
+ PageActions._reset();
+ await promiseStartupManager();
+ await extension.awaitStartup();
+
+ action = PageActions.actionForID(actionID);
+ Assert.equal(action.pinnedToUrlbar, true);
+
+ // Now unload the extension and quit the app.
+ await extension.unload();
+ await promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
new file mode 100644
index 0000000000..8c713191cc
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
@@ -0,0 +1,300 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ MockRegistry: "resource://testing-common/MockRegistry.sys.mjs",
+ ctypes: "resource://gre/modules/ctypes.sys.mjs",
+});
+
+do_get_profile();
+
+let tmpDir;
+let baseDir;
+let slug =
+ AppConstants.platform === "linux" ? "pkcs11-modules" : "PKCS11Modules";
+
+add_task(async function setupTest() {
+ tmpDir = await IOUtils.createUniqueDirectory(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "PKCS11"
+ );
+
+ baseDir = PathUtils.join(tmpDir, slug);
+ await IOUtils.makeDirectory(baseDir);
+});
+
+registerCleanupFunction(async () => {
+ await IOUtils.remove(tmpDir, { recursive: true });
+});
+
+const testmodule = PathUtils.join(
+ PathUtils.parent(Services.dirsvc.get("CurWorkD", Ci.nsIFile).path, 5),
+ "security",
+ "manager",
+ "ssl",
+ "tests",
+ "unit",
+ "pkcs11testmodule",
+ ctypes.libraryName("pkcs11testmodule")
+);
+
+// This function was inspired by the native messaging test under
+// toolkit/components/extensions
+
+async function setupManifests(modules) {
+ async function writeManifest(module) {
+ let manifest = {
+ name: module.name,
+ description: module.description,
+ path: module.path,
+ type: "pkcs11",
+ allowed_extensions: [module.id],
+ };
+
+ let manifestPath = PathUtils.join(baseDir, `${module.name}.json`);
+ await IOUtils.writeJSON(manifestPath, manifest);
+
+ return manifestPath;
+ }
+
+ switch (AppConstants.platform) {
+ case "macosx":
+ case "linux":
+ let dirProvider = {
+ getFile(property) {
+ if (
+ property == "XREUserNativeManifests" ||
+ property == "XRESysNativeManifests"
+ ) {
+ return new FileUtils.File(tmpDir);
+ }
+ return null;
+ },
+ };
+
+ Services.dirsvc.registerProvider(dirProvider);
+ registerCleanupFunction(() => {
+ Services.dirsvc.unregisterProvider(dirProvider);
+ });
+
+ for (let module of modules) {
+ await writeManifest(module);
+ }
+ break;
+
+ case "win":
+ const REGKEY = String.raw`Software\Mozilla\PKCS11Modules`;
+
+ let registry = new MockRegistry();
+ registerCleanupFunction(() => {
+ registry.shutdown();
+ });
+
+ for (let module of modules) {
+ let manifestPath = await writeManifest(module);
+ registry.setValue(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGKEY}\\${module.name}`,
+ "",
+ manifestPath
+ );
+ }
+ break;
+
+ default:
+ ok(
+ false,
+ `Loading of PKCS#11 modules is not supported on ${AppConstants.platform}`
+ );
+ }
+}
+
+add_task(async function test_pkcs11() {
+ async function background() {
+ try {
+ const { os } = await browser.runtime.getPlatformInfo();
+ if (os !== "win") {
+ // Expect this call to not throw (explicitly cover regression fixed in Bug 1759162).
+ let isInstalledNonAbsolute = await browser.pkcs11.isModuleInstalled(
+ "testmoduleNonAbsolutePath"
+ );
+ browser.test.assertFalse(
+ isInstalledNonAbsolute,
+ "PKCS#11 module with non absolute path expected to not be installed"
+ );
+ }
+ let isInstalled = await browser.pkcs11.isModuleInstalled("testmodule");
+ browser.test.assertFalse(
+ isInstalled,
+ "PKCS#11 module is not installed before we install it"
+ );
+ await browser.pkcs11.installModule("testmodule", 0);
+ isInstalled = await browser.pkcs11.isModuleInstalled("testmodule");
+ browser.test.assertTrue(
+ isInstalled,
+ "PKCS#11 module is installed after we install it"
+ );
+ let slots = await browser.pkcs11.getModuleSlots("testmodule");
+ browser.test.assertEq(
+ "Test PKCS11 Slot",
+ slots[0].name,
+ "The first slot name matches the expected name"
+ );
+ browser.test.assertEq(
+ "Test PKCS11 Slot 二",
+ slots[1].name,
+ "The second slot name matches the expected name"
+ );
+ browser.test.assertTrue(slots[1].token, "The second slot has a token");
+ browser.test.assertFalse(slots[2].token, "The third slot has no token");
+ browser.test.assertEq(
+ "Test PKCS11 Tokeñ 2 Label",
+ slots[1].token.name,
+ "The token name matches the expected name"
+ );
+ browser.test.assertEq(
+ "Test PKCS11 Manufacturer ID",
+ slots[1].token.manufacturer,
+ "The token manufacturer matches the expected manufacturer"
+ );
+ browser.test.assertEq(
+ "0.0",
+ slots[1].token.HWVersion,
+ "The token hardware version matches the expected version"
+ );
+ browser.test.assertEq(
+ "0.0",
+ slots[1].token.FWVersion,
+ "The token firmware version matches the expected version"
+ );
+ browser.test.assertEq(
+ "",
+ slots[1].token.serial,
+ "The token has no serial number"
+ );
+ browser.test.assertFalse(
+ slots[1].token.isLoggedIn,
+ "The token is not logged in"
+ );
+ await browser.pkcs11.uninstallModule("testmodule");
+ isInstalled = await browser.pkcs11.isModuleInstalled("testmodule");
+ browser.test.assertFalse(
+ isInstalled,
+ "PKCS#11 module is no longer installed after we uninstall it"
+ );
+ await browser.pkcs11.installModule("testmodule");
+ isInstalled = await browser.pkcs11.isModuleInstalled("testmodule");
+ browser.test.assertTrue(
+ isInstalled,
+ "Installing the PKCS#11 module without flags parameter succeeds"
+ );
+ await browser.pkcs11.uninstallModule("testmodule");
+ await browser.test.assertRejects(
+ browser.pkcs11.isModuleInstalled("nonexistingmodule"),
+ /No such PKCS#11 module nonexistingmodule/,
+ "We cannot access modules if no JSON file exists"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.isModuleInstalled("othermodule"),
+ /No such PKCS#11 module othermodule/,
+ "We cannot access modules if we're not listed in the module's manifest file's allowed_extensions key"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.uninstallModule("internalmodule"),
+ /No such PKCS#11 module internalmodule/,
+ "We cannot uninstall the NSS Builtin Roots Module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.installModule("osclientcerts", 0),
+ /No such PKCS#11 module osclientcerts/,
+ "installModule should not work on the built-in osclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.uninstallModule("osclientcerts"),
+ /No such PKCS#11 module osclientcerts/,
+ "uninstallModule should not work on the built-in osclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.isModuleInstalled("osclientcerts"),
+ /No such PKCS#11 module osclientcerts/,
+ "isModuleLoaded should not work on the built-in osclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.getModuleSlots("osclientcerts"),
+ /No such PKCS#11 module osclientcerts/,
+ "getModuleSlots should not work on the built-in osclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.installModule("ipcclientcerts", 0),
+ /No such PKCS#11 module ipcclientcerts/,
+ "installModule should not work on the built-in ipcclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.uninstallModule("ipcclientcerts"),
+ /No such PKCS#11 module ipcclientcerts/,
+ "uninstallModule should not work on the built-in ipcclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.isModuleInstalled("ipcclientcerts"),
+ /No such PKCS#11 module ipcclientcerts/,
+ "isModuleLoaded should not work on the built-in ipcclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.getModuleSlots("ipcclientcerts"),
+ /No such PKCS#11 module ipcclientcerts/,
+ "getModuleSlots should not work on the built-in ipcclientcerts module"
+ );
+ browser.test.notifyPass("pkcs11");
+ } catch (e) {
+ browser.test.fail(`Error: ${String(e)} :: ${e.stack}`);
+ browser.test.notifyFail("pkcs11 failed");
+ }
+ }
+
+ let libDir = FileUtils.getDir("GreBinD", []);
+ await setupManifests([
+ {
+ name: "testmodule",
+ description: "PKCS#11 Test Module",
+ path: testmodule,
+ id: "pkcs11@tests.mozilla.org",
+ },
+ {
+ name: "testmoduleNonAbsolutePath",
+ description: "PKCS#11 Test Module",
+ path: ctypes.libraryName("pkcs11testmodule"),
+ id: "pkcs11@tests.mozilla.org",
+ },
+ {
+ name: "othermodule",
+ description: "PKCS#11 Test Module",
+ path: testmodule,
+ id: "other@tests.mozilla.org",
+ },
+ {
+ name: "internalmodule",
+ description: "Builtin Roots Module",
+ path: PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ ctypes.libraryName("nssckbi")
+ ),
+ id: "pkcs11@tests.mozilla.org",
+ },
+ {
+ name: "osclientcerts",
+ description: "OS Client Cert Module",
+ path: PathUtils.join(libDir.path, ctypes.libraryName("osclientcerts")),
+ id: "pkcs11@tests.mozilla.org",
+ },
+ ]);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["pkcs11"],
+ browser_specific_settings: { gecko: { id: "pkcs11@tests.mozilla.org" } },
+ },
+ background: background,
+ });
+ await extension.startup();
+ await extension.awaitFinish("pkcs11");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js
new file mode 100644
index 0000000000..dd24be3aff
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js
@@ -0,0 +1,263 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+const { SearchUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchUtils.sys.mjs"
+);
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const kSearchEngineURL = "https://example.com/?q={searchTerms}&foo=myparams";
+const kSuggestURL = "https://example.com/fake/suggest/";
+const kSuggestURLParams = "q={searchTerms}&type=list2";
+
+Services.prefs.setBoolPref("browser.search.log", true);
+
+add_task(async function setup() {
+ AddonTestUtils.usePrivilegedSignatures = false;
+ AddonTestUtils.overrideCertDB();
+ await AddonTestUtils.promiseStartupManager();
+ await SearchTestUtils.useTestEngines("data", null, [
+ {
+ webExtension: {
+ id: "test@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+ {
+ webExtension: {
+ id: "test2@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ ],
+ },
+ ]);
+ await Services.search.init();
+ registerCleanupFunction(async () => {
+ await AddonTestUtils.promiseShutdownManager();
+ });
+});
+
+function assertEngineParameters({
+ name,
+ searchURL,
+ suggestionURL,
+ messageSnippet,
+}) {
+ let engine = Services.search.getEngineByName(name);
+ Assert.ok(engine, `Should have found ${name}`);
+
+ Assert.equal(
+ engine.getSubmission("{searchTerms}").uri.spec,
+ encodeURI(searchURL),
+ `Should have ${messageSnippet} the suggestion url.`
+ );
+ Assert.equal(
+ engine.getSubmission("{searchTerms}", URLTYPE_SUGGEST_JSON)?.uri.spec,
+ suggestionURL ? encodeURI(suggestionURL) : suggestionURL,
+ `Should ${messageSnippet} the submission URL.`
+ );
+}
+
+add_task(async function test_extension_changing_to_app_provided_default() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ icons: {
+ 16: "foo.ico",
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ is_default: true,
+ name: "MozParamsTest2",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ suggest_url: kSuggestURL,
+ suggest_url_get_params: kSuggestURLParams,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest2",
+ "Should have switched the default engine."
+ );
+
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: "https://example.com/2/?q={searchTerms}&simple2=5",
+ messageSnippet: "left unchanged",
+ });
+
+ let promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await ext1.unload();
+ await promiseDefaultBrowserChange;
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest",
+ "Should have reverted to the original default engine."
+ );
+});
+
+add_task(async function test_extension_overriding_app_provided_default() {
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY);
+ sinon.stub(settings, "get").returns([
+ {
+ thirdPartyId: "test@thirdparty.example.com",
+ overridesId: "test2@search.mozilla.org",
+ urls: [
+ {
+ search_url: "https://example.com/?q={searchTerms}&foo=myparams",
+ },
+ ],
+ },
+ ]);
+
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "test@thirdparty.example.com",
+ },
+ },
+ icons: {
+ 16: "foo.ico",
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ is_default: true,
+ name: "MozParamsTest2",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ suggest_url: kSuggestURL,
+ suggest_url_get_params: kSuggestURLParams,
+ },
+ },
+ },
+ useAddonManager: "permanent",
+ });
+
+ info("startup");
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest2",
+ "Should have switched the default engine."
+ );
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: kSearchEngineURL,
+ suggestionURL: `${kSuggestURL}?${kSuggestURLParams}`,
+ messageSnippet: "changed",
+ });
+
+ info("disable");
+
+ let promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await ext1.addon.disable();
+ await promiseDefaultBrowserChange;
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest",
+ "Should have reverted to the original default engine."
+ );
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: "https://example.com/2/?q={searchTerms}&simple2=5",
+ messageSnippet: "reverted",
+ });
+
+ info("enable");
+
+ promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await ext1.addon.enable();
+ await promiseDefaultBrowserChange;
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest2",
+ "Should have switched the default engine."
+ );
+
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: kSearchEngineURL,
+ suggestionURL: `${kSuggestURL}?${kSuggestURLParams}`,
+ messageSnippet: "changed",
+ });
+
+ info("unload");
+
+ promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await ext1.unload();
+ await promiseDefaultBrowserChange;
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest",
+ "Should have reverted to the original default engine."
+ );
+
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: "https://example.com/2/?q={searchTerms}&simple2=5",
+ messageSnippet: "reverted",
+ });
+ sinon.restore();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js
new file mode 100644
index 0000000000..10fed4d36b
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js
@@ -0,0 +1,597 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let delay = () => new Promise(resolve => setTimeout(resolve, 0));
+
+const kSearchFormURL = "https://example.com/searchform";
+const kSearchEngineURL = "https://example.com/?search={searchTerms}";
+const kSearchSuggestURL = "https://example.com/?suggest={searchTerms}";
+const kSearchTerm = "foo";
+const kSearchTermIntl = "日";
+const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_extension_adding_engine() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ icons: {
+ 16: "foo.ico",
+ 32: "foo32.ico",
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_form: kSearchFormURL,
+ search_url: kSearchEngineURL,
+ suggest_url: kSearchSuggestURL,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let { baseURI } = ext1.extension;
+ equal(
+ engine.getIconURL(),
+ baseURI.resolve("foo.ico"),
+ "16x16 icon path matches"
+ );
+ equal(
+ engine.getIconURL(16),
+ baseURI.resolve("foo.ico"),
+ "16x16 icon path matches"
+ );
+ // TODO: Bug 1871036 - Differently sized icons are currently incorrectly
+ // handled for add-ons.
+ // equal(
+ // engine.getIconURL(32),
+ // baseURI.resolve("foo32.ico"),
+ // "32x32 icon path matches"
+ // );
+
+ let expectedSuggestURL = kSearchSuggestURL.replace(
+ "{searchTerms}",
+ kSearchTerm
+ );
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ let encodedSubmissionURL = engine.getSubmission(kSearchTermIntl).uri.spec;
+ let testSubmissionURL = kSearchEngineURL.replace(
+ "{searchTerms}",
+ encodeURIComponent(kSearchTermIntl)
+ );
+ equal(
+ encodedSubmissionURL,
+ testSubmissionURL,
+ "Encoded UTF-8 URLs should match"
+ );
+
+ equal(
+ submissionSuggest.uri.spec,
+ expectedSuggestURL,
+ "Suggest URLs should match"
+ );
+
+ equal(engine.searchForm, kSearchFormURL, "Search form URLs should match");
+ await ext1.unload();
+ await delay();
+
+ engine = Services.search.getEngineByName("MozSearch");
+ ok(!engine, "Engine should not exist");
+});
+
+add_task(async function test_extension_adding_engine_with_spaces() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch ",
+ keyword: "MozSearch",
+ search_url: "https://example.com/?q={searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ await ext1.unload();
+ await delay();
+
+ engine = Services.search.getEngineByName("MozSearch");
+ ok(!engine, "Engine should not exist");
+});
+
+add_task(async function test_upgrade_default_position_engine() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "https://example.com/?q={searchTerms}",
+ },
+ },
+ browser_specific_settings: {
+ gecko: {
+ id: "testengine@mozilla.com",
+ },
+ },
+ version: "0.1",
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.moveEngine(engine, 1);
+
+ await ext1.upgrade({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "https://example.com/?q={searchTerms}",
+ },
+ },
+ browser_specific_settings: {
+ gecko: {
+ id: "testengine@mozilla.com",
+ },
+ },
+ version: "0.2",
+ },
+ useAddonManager: "temporary",
+ });
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ engine = Services.search.getEngineByName("MozSearch");
+ equal(
+ Services.search.defaultEngine,
+ engine,
+ "Default engine should still be MozSearch"
+ );
+ equal(
+ (await Services.search.getEngines()).map(e => e.name).indexOf(engine.name),
+ 1,
+ "Engine is in position 1"
+ );
+
+ await ext1.unload();
+ await delay();
+
+ engine = Services.search.getEngineByName("MozSearch");
+ ok(!engine, "Engine should not exist");
+});
+
+add_task(async function test_extension_get_params() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ search_url_get_params: "foo=bar&bar=foo",
+ suggest_url: kSearchSuggestURL,
+ suggest_url_get_params: "foo=bar&bar=foo",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.method, "GET", "Search URLs method is GET");
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission = engine.getSubmission(kSearchTerm);
+ equal(
+ submission.uri.spec,
+ `${expectedURL}&foo=bar&bar=foo`,
+ "Search URLs should match"
+ );
+
+ let expectedSuggestURL = kSearchSuggestURL.replace(
+ "{searchTerms}",
+ kSearchTerm
+ );
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ equal(
+ submissionSuggest.uri.spec,
+ `${expectedSuggestURL}&foo=bar&bar=foo`,
+ "Suggest URLs should match"
+ );
+
+ await ext1.unload();
+});
+
+add_task(async function test_extension_post_params() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ search_url_post_params: "foo=bar&bar=foo",
+ suggest_url: kSearchSuggestURL,
+ suggest_url_post_params: "foo=bar&bar=foo",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.method, "POST", "Search URLs method is POST");
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission = engine.getSubmission(kSearchTerm);
+ equal(submission.uri.spec, expectedURL, "Search URLs should match");
+ // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
+ equal(
+ submission.postData.data.data,
+ "foo=bar&bar=foo",
+ "Search postData should match"
+ );
+
+ let expectedSuggestURL = kSearchSuggestURL.replace(
+ "{searchTerms}",
+ kSearchTerm
+ );
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ equal(
+ submissionSuggest.uri.spec,
+ expectedSuggestURL,
+ "Suggest URLs should match"
+ );
+ equal(
+ submissionSuggest.postData.data.data,
+ "foo=bar&bar=foo",
+ "Suggest postData should match"
+ );
+
+ await ext1.unload();
+});
+
+add_task(async function test_extension_no_query_params() {
+ const ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "https://example.com/{searchTerms}",
+ suggest_url: "https://example.com/suggest/{searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ const encodedSubmissionURL = engine.getSubmission(kSearchTermIntl).uri.spec;
+ const testSubmissionURL =
+ "https://example.com/" + encodeURIComponent(kSearchTermIntl);
+ equal(
+ encodedSubmissionURL,
+ testSubmissionURL,
+ "Encoded UTF-8 URLs should match"
+ );
+
+ const expectedSuggestURL = "https://example.com/suggest/" + kSearchTerm;
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ equal(
+ submissionSuggest.uri.spec,
+ expectedSuggestURL,
+ "Suggest URLs should match"
+ );
+
+ await ext1.unload();
+ await delay();
+
+ engine = Services.search.getEngineByName("MozSearch");
+ ok(!engine, "Engine should not exist");
+});
+
+add_task(async function test_extension_empty_suggestUrl() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ default_locale: "en",
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ search_url_post_params: "foo=bar&bar=foo",
+ suggest_url: "__MSG_suggestUrl__",
+ suggest_url_get_params: "__MSG_suggestUrlGetParams__",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "_locales/en/messages.json": {
+ suggestUrl: {
+ message: "",
+ },
+ suggestUrlGetParams: {
+ message: "",
+ },
+ },
+ },
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.method, "POST", "Search URLs method is POST");
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission = engine.getSubmission(kSearchTerm);
+ equal(submission.uri.spec, expectedURL, "Search URLs should match");
+ // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
+ equal(
+ submission.postData.data.data,
+ "foo=bar&bar=foo",
+ "Search postData should match"
+ );
+
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ ok(!submissionSuggest, "There should be no suggest URL.");
+
+ await ext1.unload();
+});
+
+add_task(async function test_extension_empty_suggestUrl_with_params() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ default_locale: "en",
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ search_url_post_params: "foo=bar&bar=foo",
+ suggest_url: "__MSG_suggestUrl__",
+ suggest_url_get_params: "__MSG_suggestUrlGetParams__",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "_locales/en/messages.json": {
+ suggestUrl: {
+ message: "",
+ },
+ suggestUrlGetParams: {
+ message: "abc",
+ },
+ },
+ },
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.method, "POST", "Search URLs method is POST");
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission = engine.getSubmission(kSearchTerm);
+ equal(submission.uri.spec, expectedURL, "Search URLs should match");
+ // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
+ equal(
+ submission.postData.data.data,
+ "foo=bar&bar=foo",
+ "Search postData should match"
+ );
+
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ ok(!submissionSuggest, "There should be no suggest URL.");
+
+ await ext1.unload();
+});
+
+async function checkBadUrl(searchProviderKey, urlValue) {
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "https://example.com/",
+ [searchProviderKey]: urlValue,
+ },
+ },
+ });
+
+ ok(
+ /Error processing chrome_settings_overrides\.search_provider[^:]*: .* must match/.test(
+ normalized.error
+ ),
+ `Expected error for ${searchProviderKey}:${urlValue} "${normalized.error}"`
+ );
+}
+
+async function checkValidUrl(urlValue) {
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_form: urlValue,
+ search_url: urlValue,
+ suggest_url: urlValue,
+ },
+ },
+ });
+ equal(normalized.error, undefined, `Valid search_provider url: ${urlValue}`);
+}
+
+add_task(async function test_extension_not_allow_http() {
+ await checkBadUrl("search_form", "http://example.com/{searchTerms}");
+ await checkBadUrl("search_url", "http://example.com/{searchTerms}");
+ await checkBadUrl("suggest_url", "http://example.com/{searchTerms}");
+});
+
+add_task(async function test_manifest_disallows_http_localhost_prefix() {
+ await checkBadUrl("search_url", "http://localhost.example.com");
+ await checkBadUrl("search_url", "http://localhost.example.com/");
+ await checkBadUrl("search_url", "http://127.0.0.1.example.com/");
+ await checkBadUrl("search_url", "http://localhost:1234@example.com/");
+});
+
+add_task(async function test_manifest_allow_http_for_localhost() {
+ await checkValidUrl("http://localhost");
+ await checkValidUrl("http://localhost/");
+ await checkValidUrl("http://localhost:/");
+ await checkValidUrl("http://localhost:1/");
+ await checkValidUrl("http://localhost:65535/");
+
+ await checkValidUrl("http://127.0.0.1");
+ await checkValidUrl("http://127.0.0.1:");
+ await checkValidUrl("http://127.0.0.1:/");
+ await checkValidUrl("http://127.0.0.1/");
+ await checkValidUrl("http://127.0.0.1:80/");
+
+ await checkValidUrl("http://[::1]");
+ await checkValidUrl("http://[::1]:");
+ await checkValidUrl("http://[::1]:/");
+ await checkValidUrl("http://[::1]/");
+ await checkValidUrl("http://[::1]:80/");
+});
+
+add_task(async function test_extension_allow_http_for_localhost() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "http://localhost/{searchTerms}",
+ suggest_url: "http://localhost/suggest/{searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ await ext1.unload();
+});
+
+add_task(async function test_search_favicon_mv3() {
+ Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ manifest_version: 3,
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "HTTP Icon in MV3",
+ search_url: "https://example.org/",
+ favicon_url: "https://example.org/icon.png",
+ },
+ },
+ });
+ Assert.ok(
+ normalized.error.endsWith("must be a relative URL"),
+ "Should have an error"
+ );
+ normalized = await ExtensionTestUtils.normalizeManifest({
+ manifest_version: 3,
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "HTTP Icon in MV3",
+ search_url: "https://example.org/",
+ favicon_url: "/icon.png",
+ },
+ },
+ });
+ Assert.ok(!normalized.error, "Should not have an error");
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js
new file mode 100644
index 0000000000..3248c5cefa
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js
@@ -0,0 +1,239 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { NimbusFeatures } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+let { promiseShutdownManager, promiseStartupManager } = AddonTestUtils;
+
+// Note: these lists should be kept in sync with the lists in
+// browser/components/extensions/test/xpcshell/data/test/manifest.json
+// These params are conditional based on how search is initiated.
+const mozParams = [
+ {
+ name: "test-0",
+ condition: "purpose",
+ purpose: "contextmenu",
+ value: "0",
+ },
+ { name: "test-1", condition: "purpose", purpose: "searchbar", value: "1" },
+ { name: "test-2", condition: "purpose", purpose: "homepage", value: "2" },
+ { name: "test-3", condition: "purpose", purpose: "keyword", value: "3" },
+ { name: "test-4", condition: "purpose", purpose: "newtab", value: "4" },
+];
+// These params are always included.
+const params = [
+ { name: "simple", value: "5" },
+ { name: "term", value: "{searchTerms}" },
+ { name: "lang", value: "{language}" },
+ { name: "locale", value: "{moz:locale}" },
+ { name: "prefval", condition: "pref", pref: "code" },
+];
+
+add_task(async function setup() {
+ let readyStub = sinon.stub(NimbusFeatures.search, "ready").resolves();
+ let updateStub = sinon.stub(NimbusFeatures.search, "onUpdate");
+ await promiseStartupManager();
+ await SearchTestUtils.useTestEngines("data", null, [
+ {
+ webExtension: {
+ id: "test@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+ ]);
+ await Services.search.init();
+ registerCleanupFunction(async () => {
+ await promiseShutdownManager();
+ readyStub.restore();
+ updateStub.restore();
+ });
+});
+
+/* This tests setting moz params. */
+add_task(async function test_extension_setting_moz_params() {
+ let defaultBranch = Services.prefs.getDefaultBranch("browser.search.");
+ defaultBranch.setCharPref("param.code", "good");
+
+ let engine = Services.search.getEngineByName("MozParamsTest");
+
+ let extraParams = [];
+ for (let p of params) {
+ if (p.condition == "pref") {
+ extraParams.push(`${p.name}=good`);
+ } else if (p.value == "{searchTerms}") {
+ extraParams.push(`${p.name}=test`);
+ } else if (p.value == "{language}") {
+ extraParams.push(`${p.name}=${Services.locale.requestedLocale || "*"}`);
+ } else if (p.value == "{moz:locale}") {
+ extraParams.push(`${p.name}=${Services.locale.requestedLocale}`);
+ } else {
+ extraParams.push(`${p.name}=${p.value}`);
+ }
+ }
+ let paramStr = extraParams.join("&");
+
+ for (let p of mozParams) {
+ let expectedURL = engine.getSubmission(
+ "test",
+ null,
+ p.condition == "purpose" ? p.purpose : null
+ ).uri.spec;
+ equal(
+ expectedURL,
+ `https://example.com/?q=test&${p.name}=${p.value}&${paramStr}`,
+ "search url is expected"
+ );
+ }
+
+ defaultBranch.setCharPref("param.code", "");
+});
+
+add_task(async function test_nimbus_params() {
+ let sandbox = sinon.createSandbox();
+ let stub = sandbox.stub(NimbusFeatures.search, "getVariable");
+ // These values should match the nimbusParams below and the data/test/manifest.json
+ // search engine configuration
+ stub.withArgs("extraParams").returns([
+ {
+ key: "nimbus-key-1",
+ value: "nimbus-value-1",
+ },
+ {
+ key: "nimbus-key-2",
+ value: "nimbus-value-2",
+ },
+ ]);
+
+ Assert.ok(
+ NimbusFeatures.search.onUpdate.called,
+ "Called to initialize the cache"
+ );
+
+ // Populate the cache with the `getVariable` mock values
+ NimbusFeatures.search.onUpdate.firstCall.args[0]();
+
+ let engine = Services.search.getEngineByName("MozParamsTest");
+
+ // Note: these lists should be kept in sync with the lists in
+ // browser/components/extensions/test/xpcshell/data/test/manifest.json
+ // These params are conditional based on how search is initiated.
+ const nimbusParams = [
+ { name: "experimenter-1", condition: "pref", pref: "nimbus-key-1" },
+ { name: "experimenter-2", condition: "pref", pref: "nimbus-key-2" },
+ ];
+ const experimentCache = {
+ "nimbus-key-1": "nimbus-value-1",
+ "nimbus-key-2": "nimbus-value-2",
+ };
+
+ let extraParams = [];
+ for (let p of params) {
+ if (p.value == "{searchTerms}") {
+ extraParams.push(`${p.name}=test`);
+ } else if (p.value == "{language}") {
+ extraParams.push(`${p.name}=${Services.locale.requestedLocale || "*"}`);
+ } else if (p.value == "{moz:locale}") {
+ extraParams.push(`${p.name}=${Services.locale.requestedLocale}`);
+ } else if (p.condition !== "pref") {
+ // Ignoring pref parameters
+ extraParams.push(`${p.name}=${p.value}`);
+ }
+ }
+ for (let p of nimbusParams) {
+ if (p.condition == "pref") {
+ extraParams.push(`${p.name}=${experimentCache[p.pref]}`);
+ }
+ }
+ let paramStr = extraParams.join("&");
+ for (let p of mozParams) {
+ let expectedURL = engine.getSubmission(
+ "test",
+ null,
+ p.condition == "purpose" ? p.purpose : null
+ ).uri.spec;
+ equal(
+ expectedURL,
+ `https://example.com/?q=test&${p.name}=${p.value}&${paramStr}`,
+ "search url is expected"
+ );
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_extension_setting_moz_params_fail() {
+ // Ensure that the test infra does not automatically make
+ // this privileged.
+ AddonTestUtils.usePrivilegedSignatures = false;
+ Services.prefs.setCharPref(
+ "extensions.installedDistroAddon.test@mochitest",
+ ""
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "test1@mochitest" },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozParamsTest1",
+ search_url: "https://example.com/",
+ params: [
+ {
+ name: "testParam",
+ condition: "purpose",
+ purpose: "contextmenu",
+ value: "0",
+ },
+ { name: "prefval", condition: "pref", pref: "code" },
+ { name: "q", value: "{searchTerms}" },
+ ],
+ },
+ },
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ equal(
+ extension.extension.isPrivileged,
+ false,
+ "extension is not priviledged"
+ );
+ let engine = Services.search.getEngineByName("MozParamsTest1");
+ let expectedURL = engine.getSubmission("test", null, "contextmenu").uri.spec;
+ equal(
+ expectedURL,
+ "https://example.com/?q=test",
+ "engine cannot have conditional or pref params"
+ );
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js
new file mode 100644
index 0000000000..851efd6b2a
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js
@@ -0,0 +1,109 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+// Lazily import ExtensionParent to allow AddonTestUtils.createAppInfo to
+// override Services.appinfo.
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+add_task(async function shutdown_during_search_provider_startup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ is_default: true,
+ name: "dummy name",
+ search_url: "https://example.com/",
+ },
+ },
+ },
+ });
+
+ info("Starting up search extension");
+ await extension.startup();
+ let extStartPromise = AddonTestUtils.waitForSearchProviderStartup(extension, {
+ // Search provider registration is expected to be pending because the search
+ // service has not been initialized yet.
+ expectPending: true,
+ });
+
+ let initialized = false;
+ Services.search.promiseInitialized.then(() => {
+ initialized = true;
+ });
+
+ await extension.addon.disable();
+
+ info("Extension managed to shut down despite the uninitialized search");
+ // Initialize search after extension shutdown to check that it does not cause
+ // any problems, and that the test can continue to test uninstall behavior.
+ Assert.ok(!initialized, "Search service should not have been initialized");
+
+ extension.addon.enable();
+ await extension.awaitStartup();
+
+ // Check that uninstall is blocked until the search registration at startup
+ // has finished. This registration only finished once the search service is
+ // initialized.
+ let uninstallingPromise = new Promise(resolve => {
+ let Management = ExtensionParent.apiManager;
+ Management.on("uninstall", function listener(eventName, { id }) {
+ Management.off("uninstall", listener);
+ Assert.equal(id, extension.id, "Expected extension");
+ resolve();
+ });
+ });
+
+ let extRestartPromise = AddonTestUtils.waitForSearchProviderStartup(
+ extension,
+ {
+ // Search provider registration is expected to be pending again,
+ // because the search service has still not been initialized yet.
+ expectPending: true,
+ }
+ );
+
+ let uninstalledPromise = extension.addon.uninstall();
+ let uninstalled = false;
+ uninstalledPromise.then(() => {
+ uninstalled = true;
+ });
+
+ await uninstallingPromise;
+ Assert.ok(!uninstalled, "Uninstall should not be finished yet");
+ Assert.ok(!initialized, "Search service should still be uninitialized");
+ await Services.search.init();
+ Assert.ok(initialized, "Search service should be initialized");
+
+ // After initializing the search service, the search provider registration
+ // promises should settle eventually.
+
+ // Despite the interrupted startup, the promise should still resolve without
+ // an error.
+ await extStartPromise;
+ // The extension that is still active. The promise should just resolve.
+ await extRestartPromise;
+
+ // After initializing the search service, uninstall should eventually finish.
+ await uninstalledPromise;
+
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js b/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js
new file mode 100644
index 0000000000..2f0d36f6e8
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js
@@ -0,0 +1,193 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+const { AboutNewTab } = ChromeUtils.importESModule(
+ "resource:///modules/AboutNewTab.sys.mjs"
+);
+
+// Lazy load to avoid having Services.appinfo cached first.
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+});
+
+const { HomePage } = ChromeUtils.importESModule(
+ "resource:///modules/HomePage.sys.mjs"
+);
+
+AddonTestUtils.init(this);
+
+// Allow for unsigned addons.
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+add_task(async function test_settings_modules_not_loaded() {
+ await ExtensionParent.apiManager.lazyInit();
+ // Test that no settings modules are loaded.
+ let modules = Array.from(ExtensionParent.apiManager.settingsModules);
+ ok(modules.length, "we have settings modules");
+ for (let name of modules) {
+ ok(
+ !ExtensionParent.apiManager.getModule(name).loaded,
+ `${name} is not loaded`
+ );
+ }
+});
+
+add_task(async function test_settings_validated() {
+ let defaultNewTab = AboutNewTab.newTabURL;
+ equal(defaultNewTab, "about:newtab", "Newtab url is default.");
+ let defaultHomepageURL = HomePage.get();
+ equal(defaultHomepageURL, "about:home", "Home page url is default.");
+
+ let xpi = await AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "test@mochi" } },
+ chrome_url_overrides: {
+ newtab: "/newtab",
+ },
+ chrome_settings_overrides: {
+ homepage: "https://example.com/",
+ },
+ },
+ });
+ let extension = ExtensionTestUtils.expectExtension("test@mochi");
+ let file = await AddonTestUtils.manuallyInstall(xpi);
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+
+ equal(
+ HomePage.get(),
+ "https://example.com/",
+ "Home page url is extension controlled."
+ );
+ ok(
+ AboutNewTab.newTabURL.endsWith("/newtab"),
+ "newTabURL is extension controlled."
+ );
+
+ await AddonTestUtils.promiseShutdownManager();
+ // After shutdown, delete the xpi file.
+ Services.obs.notifyObservers(xpi, "flush-cache-entry");
+ try {
+ file.remove(true);
+ } catch (e) {
+ ok(false, e);
+ }
+ await AddonTestUtils.cleanupTempXPIs();
+
+ // Restart everything, the ExtensionAddonObserver should handle updating state.
+ let prefChanged = TestUtils.waitForPrefChange("browser.startup.homepage");
+ await AddonTestUtils.promiseStartupManager();
+ await prefChanged;
+
+ equal(HomePage.get(), defaultHomepageURL, "Home page url is default.");
+ equal(AboutNewTab.newTabURL, defaultNewTab, "newTabURL is reset to default.");
+ await AddonTestUtils.promiseShutdownManager();
+});
+
+add_task(async function test_settings_validated_safemode() {
+ let defaultNewTab = AboutNewTab.newTabURL;
+ equal(defaultNewTab, "about:newtab", "Newtab url is default.");
+ let defaultHomepageURL = HomePage.get();
+ equal(defaultHomepageURL, "about:home", "Home page url is default.");
+
+ function isDefaultSettings(postfix) {
+ equal(
+ HomePage.get(),
+ defaultHomepageURL,
+ `Home page url is default ${postfix}.`
+ );
+ equal(
+ AboutNewTab.newTabURL,
+ defaultNewTab,
+ `newTabURL is default ${postfix}.`
+ );
+ }
+
+ function isExtensionSettings(postfix) {
+ equal(
+ HomePage.get(),
+ "https://example.com/",
+ `Home page url is extension controlled ${postfix}.`
+ );
+ ok(
+ AboutNewTab.newTabURL.endsWith("/newtab"),
+ `newTabURL is extension controlled ${postfix}.`
+ );
+ }
+
+ async function switchSafeMode(inSafeMode) {
+ await AddonTestUtils.promiseShutdownManager();
+ AddonTestUtils.appInfo.inSafeMode = inSafeMode;
+ await AddonTestUtils.promiseStartupManager();
+ return AddonManager.getAddonByID("test@mochi");
+ }
+
+ let xpi = await AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "test@mochi" } },
+ chrome_url_overrides: {
+ newtab: "/newtab",
+ },
+ chrome_settings_overrides: {
+ homepage: "https://example.com/",
+ },
+ },
+ });
+ let extension = ExtensionTestUtils.expectExtension("test@mochi");
+ await AddonTestUtils.manuallyInstall(xpi);
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+
+ isExtensionSettings("on extension startup");
+
+ // Disable in safemode and verify settings are removed in normal mode.
+ let addon = await switchSafeMode(true);
+ await addon.disable();
+ addon = await switchSafeMode(false);
+ isDefaultSettings("after disabling addon during safemode");
+
+ // Enable in safemode and verify settings are back in normal mode.
+ addon = await switchSafeMode(true);
+ await addon.enable();
+ addon = await switchSafeMode(false);
+ isExtensionSettings("after enabling addon during safemode");
+
+ // Uninstall in safemode and verify settings are removed in normal mode.
+ addon = await switchSafeMode(true);
+ await addon.uninstall();
+ addon = await switchSafeMode(false);
+ isDefaultSettings("after uninstalling addon during safemode");
+
+ await AddonTestUtils.promiseShutdownManager();
+ await AddonTestUtils.cleanupTempXPIs();
+});
+
+// There are more settings modules than used in this test file, they should have been
+// loaded during the test extensions uninstall. Ensure that all settings modules have
+// been loaded.
+add_task(async function test_settings_modules_loaded() {
+ // Test that all settings modules are loaded.
+ let modules = Array.from(ExtensionParent.apiManager.settingsModules);
+ ok(modules.length, "we have settings modules");
+ for (let name of modules) {
+ ok(ExtensionParent.apiManager.getModule(name).loaded, `${name} was loaded`);
+ }
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_topSites.js b/browser/components/extensions/test/xpcshell/test_ext_topSites.js
new file mode 100644
index 0000000000..8064ade1e8
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_topSites.js
@@ -0,0 +1,293 @@
+"use strict";
+
+const { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+);
+const { NewTabUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/NewTabUtils.sys.mjs"
+);
+const { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+const SEARCH_SHORTCUTS_EXPERIMENT_PREF =
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts";
+
+// A small 1x1 test png
+const IMAGE_1x1 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+
+add_task(async function test_topSites() {
+ Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, false);
+ let visits = [];
+ const numVisits = 15; // To make sure we get frecency.
+ let visitDate = new Date(1999, 9, 9, 9, 9).getTime();
+
+ async function setVisit(visit) {
+ for (let j = 0; j < numVisits; ++j) {
+ visitDate -= 1000;
+ visit.visits.push({ date: new Date(visitDate) });
+ }
+ visits.push(visit);
+ await PlacesUtils.history.insert(visit);
+ }
+ // Stick a couple sites into history.
+ for (let i = 0; i < 2; ++i) {
+ await setVisit({
+ url: `http://example${i}.com/`,
+ title: `visit${i}`,
+ visits: [],
+ });
+ await setVisit({
+ url: `http://www.example${i}.com/foobar`,
+ title: `visit${i}-www`,
+ visits: [],
+ });
+ }
+ NewTabUtils.init();
+
+ // Insert a favicon to show that favicons are not returned by default.
+ let faviconData = new Map();
+ faviconData.set("http://example0.com", IMAGE_1x1);
+ await PlacesTestUtils.addFavicons(faviconData);
+
+ // Ensure our links show up in activityStream.
+ let links = await NewTabUtils.activityStreamLinks.getTopSites({
+ onePerDomain: false,
+ topsiteFrecency: 1,
+ });
+
+ equal(
+ links.length,
+ visits.length,
+ "Top sites has been successfully initialized"
+ );
+
+ // Drop the visits.visits for later testing.
+ visits = visits.map(v => {
+ return { url: v.url, title: v.title, favicon: undefined, type: "url" };
+ });
+
+ // Test that results from all providers are returned by default.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["topSites"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async options => {
+ let sites = await browser.topSites.get(options);
+ browser.test.sendMessage("sites", sites);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ function getSites(options) {
+ extension.sendMessage(options);
+ return extension.awaitMessage("sites");
+ }
+
+ Assert.deepEqual(
+ [visits[0], visits[2]],
+ await getSites(),
+ "got topSites default"
+ );
+ Assert.deepEqual(
+ visits,
+ await getSites({ onePerDomain: false }),
+ "got topSites all links"
+ );
+
+ NewTabUtils.activityStreamLinks.blockURL(visits[0]);
+ ok(
+ NewTabUtils.blockedLinks.isBlocked(visits[0]),
+ `link ${visits[0].url} is blocked`
+ );
+
+ Assert.deepEqual(
+ [visits[2], visits[1]],
+ await getSites(),
+ "got topSites with blocked links filtered out"
+ );
+ Assert.deepEqual(
+ [visits[0], visits[2]],
+ await getSites({ includeBlocked: true }),
+ "got topSites with blocked links included"
+ );
+
+ // Test favicon result
+ let topSites = await getSites({ includeBlocked: true, includeFavicon: true });
+ equal(topSites[0].favicon, IMAGE_1x1, "received favicon");
+
+ equal(
+ 1,
+ (await getSites({ limit: 1, includeBlocked: true })).length,
+ "limit 1 topSite"
+ );
+
+ NewTabUtils.uninit();
+ await extension.unload();
+ await PlacesUtils.history.clear();
+ Services.prefs.clearUserPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF);
+});
+
+// Test pinned likns and search shortcuts.
+add_task(async function test_topSites_complete() {
+ Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, true);
+ NewTabUtils.init();
+ let time = new Date();
+ let pinnedIndex = 0;
+ let entries = [
+ {
+ url: `http://pinned1.com/`,
+ title: "pinned1",
+ type: "url",
+ pinned: pinnedIndex++,
+ visitDate: time,
+ },
+ {
+ url: `http://search1.com/`,
+ title: "@search1",
+ type: "search",
+ pinned: pinnedIndex++,
+ visitDate: new Date(--time),
+ },
+ {
+ url: `https://amazon.com`,
+ title: "@amazon",
+ type: "search",
+ visitDate: new Date(--time),
+ },
+ {
+ url: `http://history1.com/`,
+ title: "history1",
+ type: "url",
+ visitDate: new Date(--time),
+ },
+ {
+ url: `http://history2.com/`,
+ title: "history2",
+ type: "url",
+ visitDate: new Date(--time),
+ },
+ {
+ url: `https://blocked1.com/`,
+ title: "blocked1",
+ type: "blocked",
+ visitDate: new Date(--time),
+ },
+ ];
+
+ for (let entry of entries) {
+ // Build up frecency.
+ await PlacesUtils.history.insert({
+ url: entry.url,
+ title: entry.title,
+ visits: new Array(15).fill({
+ date: entry.visitDate,
+ transition: PlacesUtils.history.TRANSITIONS.LINK,
+ }),
+ });
+ // Insert a favicon to show that favicons are not returned by default.
+ await PlacesTestUtils.addFavicons(new Map([[entry.url, IMAGE_1x1]]));
+ if (entry.pinned !== undefined) {
+ let info =
+ entry.type == "search"
+ ? { url: entry.url, label: entry.title, searchTopSite: true }
+ : { url: entry.url, title: entry.title };
+ NewTabUtils.pinnedLinks.pin(info, entry.pinned);
+ }
+ if (entry.type == "blocked") {
+ NewTabUtils.activityStreamLinks.blockURL({ url: entry.url });
+ }
+ }
+
+ // Some transformation is necessary to match output data.
+ let expectedResults = entries
+ .filter(e => e.type != "blocked")
+ .map(e => {
+ e.favicon = undefined;
+ delete e.visitDate;
+ delete e.pinned;
+ return e;
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["topSites"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async options => {
+ let sites = await browser.topSites.get(options);
+ browser.test.sendMessage("sites", sites);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ // Test that results are returned by the API.
+ function getSites(options) {
+ extension.sendMessage(options);
+ return extension.awaitMessage("sites");
+ }
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites({ includePinned: true, includeSearchShortcuts: true }),
+ "got topSites all links"
+ );
+
+ // Test no shortcuts.
+ dump(JSON.stringify(await getSites({ includePinned: true })) + "\n");
+ Assert.ok(
+ !(await getSites({ includePinned: true })).some(
+ link => link.type == "search"
+ ),
+ "should get no shortcuts"
+ );
+
+ // Test favicons.
+ let topSites = await getSites({
+ includePinned: true,
+ includeSearchShortcuts: true,
+ includeFavicon: true,
+ });
+ Assert.ok(
+ topSites.every(f => f.favicon == IMAGE_1x1),
+ "favicon is correct"
+ );
+
+ // Test options.limit.
+ Assert.equal(
+ 1,
+ (
+ await getSites({
+ includePinned: true,
+ includeSearchShortcuts: true,
+ limit: 1,
+ })
+ ).length,
+ "limit to 1 topSite"
+ );
+
+ // Clear history for a pinned entry, then check results.
+ await PlacesUtils.history.remove("http://pinned1.com/");
+ let links = await getSites({ includePinned: true });
+ Assert.ok(
+ links.find(link => link.url == "http://pinned1.com/"),
+ "Check unvisited pinned links are returned."
+ );
+ links = await getSites();
+ Assert.ok(
+ !links.find(link => link.url == "http://pinned1.com/"),
+ "Check unvisited pinned links are not returned."
+ );
+
+ await extension.unload();
+ NewTabUtils.uninit();
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.prefs.clearUserPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF);
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js
new file mode 100644
index 0000000000..9ea6c4eea6
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js
@@ -0,0 +1,340 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+const { AboutNewTab } = ChromeUtils.importESModule(
+ "resource:///modules/AboutNewTab.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const {
+ createAppInfo,
+ promiseRestartManager,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42");
+
+function awaitEvent(eventName) {
+ return new Promise(resolve => {
+ Management.once(eventName, (e, ...args) => resolve(...args));
+ });
+}
+
+const DEFAULT_NEW_TAB_URL = AboutNewTab.newTabURL;
+
+add_task(async function test_multiple_extensions_overriding_newtab_page() {
+ const NEWTAB_URI_2 = "webext-newtab-1.html";
+ const NEWTAB_URI_3 = "webext-newtab-2.html";
+ const EXT_2_ID = "ext2@tests.mozilla.org";
+ const EXT_3_ID = "ext3@tests.mozilla.org";
+
+ const CONTROLLED_BY_THIS = "controlled_by_this_extension";
+ const CONTROLLED_BY_OTHER = "controlled_by_other_extensions";
+ const NOT_CONTROLLABLE = "not_controllable";
+
+ const NEW_TAB_PRIVATE_ALLOWED = "browser.newtab.privateAllowed";
+ const NEW_TAB_EXTENSION_CONTROLLED = "browser.newtab.extensionControlled";
+
+ function background() {
+ browser.test.onMessage.addListener(async msg => {
+ switch (msg) {
+ case "checkNewTabPage":
+ let newTabPage = await browser.browserSettings.newTabPageOverride.get(
+ {}
+ );
+ browser.test.sendMessage("newTabPage", newTabPage);
+ break;
+ case "trySet":
+ let setResult = await browser.browserSettings.newTabPageOverride.set({
+ value: "foo",
+ });
+ browser.test.assertFalse(
+ setResult,
+ "Calling newTabPageOverride.set returns false."
+ );
+ browser.test.sendMessage("newTabPageSet");
+ break;
+ case "tryClear":
+ let clearResult =
+ await browser.browserSettings.newTabPageOverride.clear({});
+ browser.test.assertFalse(
+ clearResult,
+ "Calling newTabPageOverride.clear returns false."
+ );
+ browser.test.sendMessage("newTabPageCleared");
+ break;
+ }
+ });
+ }
+
+ async function checkNewTabPageOverride(
+ ext,
+ expectedValue,
+ expectedLevelOfControl
+ ) {
+ ext.sendMessage("checkNewTabPage");
+ let newTabPage = await ext.awaitMessage("newTabPage");
+
+ ok(
+ newTabPage.value.endsWith(expectedValue),
+ `newTabPageOverride setting returns the expected value ending with: ${expectedValue}.`
+ );
+ equal(
+ newTabPage.levelOfControl,
+ expectedLevelOfControl,
+ `newTabPageOverride setting returns the expected levelOfControl: ${expectedLevelOfControl}.`
+ );
+ }
+
+ function verifyNewTabSettings(ext, expectedLevelOfControl) {
+ if (expectedLevelOfControl !== NOT_CONTROLLABLE) {
+ // Verify the preferences are set as expected.
+ let policy = WebExtensionPolicy.getByID(ext.id);
+ equal(
+ policy && policy.privateBrowsingAllowed,
+ Services.prefs.getBoolPref(NEW_TAB_PRIVATE_ALLOWED),
+ "private browsing flag set correctly"
+ );
+ ok(
+ Services.prefs.getBoolPref(NEW_TAB_EXTENSION_CONTROLLED),
+ `extension controlled flag set correctly`
+ );
+ } else {
+ ok(
+ !Services.prefs.prefHasUserValue(NEW_TAB_PRIVATE_ALLOWED),
+ "controlled flag reset"
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NEW_TAB_EXTENSION_CONTROLLED),
+ "controlled flag reset"
+ );
+ }
+ }
+
+ let extObj = {
+ manifest: {
+ chrome_url_overrides: {},
+ permissions: ["browserSettings"],
+ },
+ useAddonManager: "temporary",
+ background,
+ };
+
+ let ext1 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_url_overrides = { newtab: NEWTAB_URI_2 };
+ extObj.manifest.browser_specific_settings = { gecko: { id: EXT_2_ID } };
+ let ext2 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_url_overrides = { newtab: NEWTAB_URI_3 };
+ extObj.manifest.browser_specific_settings.gecko.id = EXT_3_ID;
+ extObj.incognitoOverride = "spanning";
+ let ext3 = ExtensionTestUtils.loadExtension(extObj);
+
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL is set to the default."
+ );
+
+ await promiseStartupManager();
+
+ await ext1.startup();
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL is still set to the default."
+ );
+
+ await checkNewTabPageOverride(ext1, AboutNewTab.newTabURL, NOT_CONTROLLABLE);
+ verifyNewTabSettings(ext1, NOT_CONTROLLABLE);
+
+ await ext2.startup();
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2),
+ "newTabURL is overridden by the second extension."
+ );
+ await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ // Verify that calling set and clear do nothing.
+ ext2.sendMessage("trySet");
+ await ext2.awaitMessage("newTabPageSet");
+ await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ ext2.sendMessage("tryClear");
+ await ext2.awaitMessage("newTabPageCleared");
+ await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ // Disable the second extension.
+ let addon = await AddonManager.getAddonByID(EXT_2_ID);
+ let disabledPromise = awaitEvent("shutdown");
+ await addon.disable();
+ await disabledPromise;
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL url is reset to the default after second extension is disabled."
+ );
+ await checkNewTabPageOverride(ext1, AboutNewTab.newTabURL, NOT_CONTROLLABLE);
+ verifyNewTabSettings(ext1, NOT_CONTROLLABLE);
+
+ // Re-enable the second extension.
+ let enabledPromise = awaitEvent("ready");
+ await addon.enable();
+ await enabledPromise;
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2),
+ "newTabURL is overridden by the second extension."
+ );
+ await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ await ext1.unload();
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2),
+ "newTabURL is still overridden by the second extension."
+ );
+ await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ await ext3.startup();
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3),
+ "newTabURL is overridden by the third extension."
+ );
+ await checkNewTabPageOverride(ext2, NEWTAB_URI_3, CONTROLLED_BY_OTHER);
+ verifyNewTabSettings(ext3, CONTROLLED_BY_THIS);
+
+ // Disable the second extension.
+ disabledPromise = awaitEvent("shutdown");
+ await addon.disable();
+ await disabledPromise;
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3),
+ "newTabURL is still overridden by the third extension."
+ );
+ await checkNewTabPageOverride(ext3, NEWTAB_URI_3, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext3, CONTROLLED_BY_THIS);
+
+ // Re-enable the second extension.
+ enabledPromise = awaitEvent("ready");
+ await addon.enable();
+ await enabledPromise;
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3),
+ "newTabURL is still overridden by the third extension."
+ );
+ await checkNewTabPageOverride(ext3, NEWTAB_URI_3, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext3, CONTROLLED_BY_THIS);
+
+ await ext3.unload();
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2),
+ "newTabURL reverts to being overridden by the second extension."
+ );
+ await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ await ext2.unload();
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL url is reset to the default."
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NEW_TAB_PRIVATE_ALLOWED),
+ "controlled flag reset"
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NEW_TAB_EXTENSION_CONTROLLED),
+ "controlled flag reset"
+ );
+
+ await promiseShutdownManager();
+});
+
+// Tests that we handle the upgrade/downgrade process correctly
+// when an extension is installed temporarily on top of a permanently
+// installed one.
+add_task(async function test_temporary_installation() {
+ const ID = "newtab@tests.mozilla.org";
+ const PAGE1 = "page1.html";
+ const PAGE2 = "page2.html";
+
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL is set to the default."
+ );
+
+ await promiseStartupManager();
+
+ let permanent = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: ID },
+ },
+ chrome_url_overrides: {
+ newtab: PAGE1,
+ },
+ },
+ useAddonManager: "permanent",
+ });
+
+ await permanent.startup();
+ ok(
+ AboutNewTab.newTabURL.endsWith(PAGE1),
+ "newTabURL is overridden by permanent extension."
+ );
+
+ let temporary = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: ID },
+ },
+ chrome_url_overrides: {
+ newtab: PAGE2,
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await temporary.startup();
+ ok(
+ AboutNewTab.newTabURL.endsWith(PAGE2),
+ "newTabURL is overridden by temporary extension."
+ );
+
+ await promiseRestartManager();
+ await permanent.awaitStartup();
+
+ ok(
+ AboutNewTab.newTabURL.endsWith(PAGE1),
+ "newTabURL is back to the value set by permanent extension."
+ );
+
+ await permanent.unload();
+
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL is set back to the default."
+ );
+ await promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js
new file mode 100644
index 0000000000..17ee81e5ef
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js
@@ -0,0 +1,127 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AboutNewTab } = ChromeUtils.importESModule(
+ "resource:///modules/AboutNewTab.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const {
+ createAppInfo,
+ createTempWebExtensionFile,
+ promiseCompleteAllInstalls,
+ promiseFindAddonUpdates,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+// Allow for unsigned addons.
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42");
+
+add_task(async function test_url_overrides_newtab_update() {
+ const EXTENSION_ID = "test_url_overrides_update@tests.mozilla.org";
+ const NEWTAB_URI = "webext-newtab-1.html";
+ const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+
+ const testServer = createHttpServer();
+ const port = testServer.identity.primaryPort;
+
+ // The test extension uses an insecure update url.
+ Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+ testServer.registerPathHandler("/test_update.json", (request, response) => {
+ response.write(`{
+ "addons": {
+ "${EXTENSION_ID}": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:${port}/addons/test_url_overrides-2.0.xpi"
+ }
+ ]
+ }
+ }
+ }`);
+ });
+
+ let webExtensionFile = createTempWebExtensionFile({
+ manifest: {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ },
+ });
+
+ testServer.registerFile(
+ "/addons/test_url_overrides-2.0.xpi",
+ webExtensionFile
+ );
+
+ await promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ update_url: `http://localhost:${port}/test_update.json`,
+ },
+ },
+ chrome_url_overrides: { newtab: NEWTAB_URI },
+ },
+ });
+
+ let defaultNewTabURL = AboutNewTab.newTabURL;
+ equal(
+ AboutNewTab.newTabURL,
+ defaultNewTabURL,
+ `Default newtab url is ${defaultNewTabURL}.`
+ );
+
+ await extension.startup();
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI),
+ "Newtab url is overridden by the extension."
+ );
+
+ let update = await promiseFindAddonUpdates(extension.addon);
+ let install = update.updateAvailable;
+
+ await promiseCompleteAllInstalls([install]);
+
+ await extension.awaitStartup();
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ equal(
+ AboutNewTab.newTabURL,
+ defaultNewTabURL,
+ "Newtab url reverted to the default after update."
+ );
+
+ await extension.unload();
+
+ await promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/xpcshell.toml b/browser/components/extensions/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..e98f696264
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/xpcshell.toml
@@ -0,0 +1,69 @@
+[DEFAULT]
+skip-if = ["os == 'android'"] # bug 1730213
+head = "head.js"
+firefox-appdir = "browser"
+tags = "webextensions condprof"
+dupe-manifest = ""
+
+["test_ext_bookmarks.js"]
+skip-if = ["condprof"] # Bug 1769184 - by design for now
+
+["test_ext_browsingData_downloads.js"]
+
+["test_ext_browsingData_passwords.js"]
+skip-if = ["tsan"] # Times out, bug 1612707
+
+["test_ext_browsingData_settings.js"]
+
+["test_ext_chrome_settings_overrides_home.js"]
+
+["test_ext_chrome_settings_overrides_update.js"]
+
+["test_ext_distribution_popup.js"]
+
+["test_ext_history.js"]
+
+["test_ext_homepage_overrides_private.js"]
+
+["test_ext_manifest.js"]
+
+["test_ext_manifest_commands.js"]
+run-sequentially = "very high failure rate in parallel"
+
+["test_ext_manifest_omnibox.js"]
+
+["test_ext_manifest_permissions.js"]
+
+["test_ext_menu_caller.js"]
+
+["test_ext_menu_startup.js"]
+
+["test_ext_normandyAddonStudy.js"]
+
+["test_ext_pageAction_shutdown.js"]
+
+["test_ext_pkcs11_management.js"]
+
+["test_ext_settings_overrides_defaults.js"]
+skip-if = ["condprof"] # Bug 1776135 - by design, modifies search settings at start of test
+support-files = [
+ "data/test/manifest.json",
+ "data/test2/manifest.json",
+]
+
+["test_ext_settings_overrides_search.js"]
+
+["test_ext_settings_overrides_search_mozParam.js"]
+skip-if = ["condprof"] # Bug 1776652
+support-files = ["data/test/manifest.json"]
+
+["test_ext_settings_overrides_shutdown.js"]
+
+["test_ext_settings_validate.js"]
+
+["test_ext_topSites.js"]
+skip-if = ["condprof"] # Bug 1769184 - by design for now
+
+["test_ext_url_overrides_newtab.js"]
+
+["test_ext_url_overrides_newtab_update.js"]
--
cgit v1.2.3