From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- .../extensions/test/browser/.eslintrc.js | 11 + .../test/browser/browser-serviceworker.ini | 9 + .../components/extensions/test/browser/browser.ini | 54 + .../browser_ext_background_serviceworker.js | 285 +++ ...r_ext_background_serviceworker_pref_disabled.js | 125 ++ .../test/browser/browser_ext_downloads_filters.js | 138 ++ .../test/browser/browser_ext_downloads_referrer.js | 91 + ...rowser_ext_eventpage_disableResetIdleForTest.js | 83 + .../browser_ext_extension_page_tab_navigated.js | 226 ++ .../test/browser/browser_ext_management_themes.js | 177 ++ .../test/browser/browser_ext_test_mock.js | 47 + ..._ext_themes_additional_backgrounds_alignment.js | 119 + .../browser_ext_themes_alpha_accentcolor.js | 39 + .../test/browser/browser_ext_themes_arrowpanels.js | 88 + .../browser_ext_themes_autocomplete_popup.js | 173 ++ .../browser/browser_ext_themes_chromeparity.js | 213 ++ .../browser_ext_themes_dynamic_getCurrent.js | 203 ++ .../browser_ext_themes_dynamic_onUpdated.js | 154 ++ .../browser/browser_ext_themes_dynamic_updates.js | 217 ++ .../test/browser/browser_ext_themes_experiment.js | 450 ++++ .../test/browser/browser_ext_themes_findbar.js | 229 ++ .../browser_ext_themes_getCurrent_differentExt.js | 149 ++ .../test/browser/browser_ext_themes_highlight.js | 63 + .../test/browser/browser_ext_themes_incognito.js | 77 + .../test/browser/browser_ext_themes_lwtsupport.js | 69 + .../browser_ext_themes_multiple_backgrounds.js | 289 +++ .../test/browser/browser_ext_themes_ntp_colors.js | 206 ++ .../browser_ext_themes_ntp_colors_perwindow.js | 240 ++ .../test/browser/browser_ext_themes_pbm.js | 439 ++++ .../test/browser/browser_ext_themes_persistence.js | 64 + .../test/browser/browser_ext_themes_reset.js | 112 + .../browser/browser_ext_themes_sanitization.js | 187 ++ .../test/browser/browser_ext_themes_separators.js | 76 + .../test/browser/browser_ext_themes_sidebars.js | 275 +++ .../browser/browser_ext_themes_static_onUpdated.js | 126 ++ .../test/browser/browser_ext_themes_tab_line.js | 39 + .../test/browser/browser_ext_themes_tab_loading.js | 51 + .../browser/browser_ext_themes_tab_selected.js | 54 + .../test/browser/browser_ext_themes_tab_text.js | 70 + .../browser/browser_ext_themes_theme_transition.js | 48 + .../browser/browser_ext_themes_toolbar_fields.js | 183 ++ .../browser_ext_themes_toolbar_fields_focus.js | 107 + .../browser_ext_themes_toolbarbutton_colors.js | 63 + .../browser_ext_themes_toolbarbutton_icons.js | 109 + .../test/browser/browser_ext_themes_toolbars.js | 105 + .../test/browser/browser_ext_themes_warnings.js | 144 ++ .../browser/browser_ext_thumbnails_bg_extension.js | 94 + .../browser/browser_ext_webNavigation_eventpage.js | 72 + ...browser_ext_webRequest_redirect_mozextension.js | 48 + .../browser/browser_ext_windows_popup_title.js | 133 ++ .../extensions/test/browser/data/test-download.txt | 1 + .../test/browser/data/test_downloads_referrer.html | 10 + toolkit/components/extensions/test/browser/head.js | 126 ++ .../extensions/test/browser/head_serviceworker.js | 123 ++ .../data/extension-with-bg-sw/manifest.json | 11 + .../marionette/data/extension-with-bg-sw/sw.js | 3 + .../extensions/test/marionette/manifest.ini | 2 + .../test/marionette/service_worker_testutils.py | 48 + ...nsion_serviceworkers_purged_on_pref_disabled.py | 56 + ...orary_extension_serviceworkers_not_persisted.py | 54 + .../extensions/test/mochitest/.eslintrc.js | 12 + .../extensions/test/mochitest/chrome.ini | 38 + .../test/mochitest/chrome_cleanup_script.js | 65 + .../extensions/test/mochitest/chrome_head.js | 1 + .../test/mochitest/file_WebNavigation_page1.html | 12 + .../test/mochitest/file_WebNavigation_page2.html | 7 + .../test/mochitest/file_WebNavigation_page3.html | 9 + .../test/mochitest/file_WebRequest_page3.html | 10 + .../test/mochitest/file_contains_iframe.html | 13 + .../test/mochitest/file_contains_img.html | 12 + .../mochitest/file_contentscript_activeTab.html | 11 + .../mochitest/file_contentscript_activeTab2.html | 10 + .../test/mochitest/file_contentscript_iframe.html | 10 + .../extensions/test/mochitest/file_green.html | 3 + .../extensions/test/mochitest/file_green_blue.html | 16 + .../extensions/test/mochitest/file_image_bad.png | Bin 0 -> 5401 bytes .../extensions/test/mochitest/file_image_good.png | Bin 0 -> 580 bytes .../extensions/test/mochitest/file_image_great.png | Bin 0 -> 580 bytes .../test/mochitest/file_image_redirect.png | Bin 0 -> 5401 bytes .../extensions/test/mochitest/file_indexedDB.html | 28 + .../extensions/test/mochitest/file_mixed.html | 13 + .../test/mochitest/file_redirect_cors_bypass.html | 30 + .../test/mochitest/file_redirect_data_uri.html | 9 + .../test/mochitest/file_remote_frame.html | 20 + .../extensions/test/mochitest/file_sample.html | 13 + .../extensions/test/mochitest/file_sample.txt | 1 + .../test/mochitest/file_sample.txt^headers^ | 1 + .../extensions/test/mochitest/file_script_bad.js | 3 + .../extensions/test/mochitest/file_script_good.js | 12 + .../test/mochitest/file_script_redirect.js | 3 + .../extensions/test/mochitest/file_script_xhr.js | 9 + .../test/mochitest/file_serviceWorker.html | 16 + .../mochitest/file_simple_sandboxed_frame.html | 23 + .../mochitest/file_simple_sandboxed_subframe.html | 10 + .../extensions/test/mochitest/file_simple_xhr.html | 19 + .../test/mochitest/file_simple_xhr_frame.html | 19 + .../test/mochitest/file_simple_xhr_frame2.html | 23 + .../test/mochitest/file_slowed_document.sjs | 49 + .../test/mochitest/file_streamfilter.txt | 1 + .../extensions/test/mochitest/file_style_bad.css | 3 + .../extensions/test/mochitest/file_style_good.css | 3 + .../test/mochitest/file_style_redirect.css | 3 + .../test/mochitest/file_tabs_permission_page1.html | 10 + .../test/mochitest/file_tabs_permission_page2.html | 11 + .../test/mochitest/file_third_party.html | 21 + .../test/mochitest/file_to_drawWindow.html | 9 + .../file_webNavigation_clientRedirect.html | 9 + ...e_webNavigation_clientRedirect_httpHeaders.html | 8 + ...gation_clientRedirect_httpHeaders.html^headers^ | 1 + .../file_webNavigation_frameClientRedirect.html | 12 + .../file_webNavigation_frameRedirect.html | 12 + .../file_webNavigation_manualSubframe.html | 12 + .../file_webNavigation_manualSubframe_page1.html | 8 + .../file_webNavigation_manualSubframe_page2.html | 7 + .../test/mochitest/file_with_about_blank.html | 10 + .../test/mochitest/file_with_images.html | 10 + .../mochitest/file_with_subframes_and_embed.html | 21 + .../test/mochitest/file_with_xorigin_frame.html | 6 + .../components/extensions/test/mochitest/head.js | 124 ++ .../extensions/test/mochitest/head_cookies.js | 287 +++ .../test/mochitest/head_notifications.js | 167 ++ .../test/mochitest/head_unlimitedStorage.js | 45 + .../extensions/test/mochitest/head_webrequest.js | 482 ++++ .../components/extensions/test/mochitest/hsts.sjs | 10 + .../extensions/test/mochitest/mochitest-common.ini | 239 ++ .../extensions/test/mochitest/mochitest-remote.ini | 8 + .../test/mochitest/mochitest-serviceworker.ini | 24 + .../extensions/test/mochitest/mochitest.ini | 13 + .../extensions/test/mochitest/mochitest_console.js | 54 + .../extensions/test/mochitest/oauth.html | 26 + .../extensions/test/mochitest/redirect_auto.sjs | 24 + .../extensions/test/mochitest/redirection.sjs | 6 + .../extensions/test/mochitest/return_headers.sjs | 19 + .../extensions/test/mochitest/serviceWorker.js | 0 .../extensions/test/mochitest/slow_response.sjs | 60 + .../test/mochitest/test_check_startupcache.html | 61 + .../test_chrome_ext_contentscript_data_uri.html | 104 + .../test_chrome_ext_contentscript_telemetry.html | 68 + ...ext_contentscript_unrecognizedprop_warning.html | 80 + .../mochitest/test_chrome_ext_downloads_open.html | 114 + .../test_chrome_ext_downloads_saveAs.html | 257 +++ .../test_chrome_ext_downloads_uniquify.html | 116 + .../mochitest/test_chrome_ext_permissions.html | 172 ++ .../test_chrome_ext_svg_context_fill.html | 204 ++ .../test_chrome_ext_trackingprotection.html | 98 + ...est_chrome_ext_webnavigation_resolved_urls.html | 81 + ...st_chrome_ext_webrequest_background_events.html | 94 + ...est_chrome_ext_webrequest_host_permissions.html | 89 + .../test_chrome_ext_webrequest_mozextension.html | 193 ++ .../test_chrome_native_messaging_paths.html | 55 + .../extensions/test/mochitest/test_ext_action.html | 51 + .../test/mochitest/test_ext_activityLog.html | 390 ++++ .../extensions/test/mochitest/test_ext_all_apis.js | 245 +++ .../test/mochitest/test_ext_async_clipboard.html | 401 ++++ .../test/mochitest/test_ext_background_canvas.html | 42 + .../test/mochitest/test_ext_background_page.html | 84 + .../mochitest/test_ext_background_page_dpi.html | 46 + .../test_ext_browserAction_openPopup.html | 183 ++ ...t_browserAction_openPopup_incognito_window.html | 151 ++ .../test_ext_browserAction_openPopup_windowId.html | 162 ++ ...t_ext_browserAction_openPopup_without_pref.html | 58 + .../mochitest/test_ext_browsingData_indexedDB.html | 159 ++ .../test_ext_browsingData_localStorage.html | 323 +++ .../test_ext_browsingData_pluginData.html | 69 + .../test_ext_browsingData_serviceWorkers.html | 141 ++ .../mochitest/test_ext_browsingData_settings.html | 65 + .../test_ext_canvas_resistFingerprinting.html | 64 + .../test/mochitest/test_ext_clipboard.html | 210 ++ .../test/mochitest/test_ext_clipboard_image.html | 262 +++ .../test_ext_contentscript_about_blank.html | 116 + .../test_ext_contentscript_activeTab.html | 711 ++++++ .../mochitest/test_ext_contentscript_cache.html | 117 + .../mochitest/test_ext_contentscript_canvas.html | 134 ++ .../test_ext_contentscript_devtools_metadata.html | 77 + .../test_ext_contentscript_fission_frame.html | 109 + .../test_ext_contentscript_getFrameId.html | 189 ++ .../test_ext_contentscript_incognito.html | 101 + .../test_ext_contentscript_permission.html | 59 + .../test/mochitest/test_ext_cookies.html | 367 ++++ .../mochitest/test_ext_cookies_containers.html | 98 + .../test/mochitest/test_ext_cookies_expiry.html | 72 + .../mochitest/test_ext_cookies_first_party.html | 316 +++ .../test/mochitest/test_ext_cookies_incognito.html | 107 + .../test_ext_cookies_permissions_bad.html | 115 + .../test_ext_cookies_permissions_good.html | 89 + .../test/mochitest/test_ext_dnr_tabIds.html | 137 ++ .../test/mochitest/test_ext_dnr_upgradeScheme.html | 132 ++ .../mochitest/test_ext_downloads_download.html | 90 + ...test_ext_embeddedimg_iframe_frameAncestors.html | 94 + .../mochitest/test_ext_exclude_include_globs.html | 91 + .../test_ext_extension_iframe_messaging.html | 124 ++ .../mochitest/test_ext_external_messaging.html | 110 + .../test/mochitest/test_ext_generate.html | 48 + .../test/mochitest/test_ext_geolocation.html | 86 + .../test/mochitest/test_ext_identity.html | 390 ++++ .../extensions/test/mochitest/test_ext_idle.html | 68 + .../test_ext_inIncognitoContext_window.html | 49 + .../test/mochitest/test_ext_listener_proxies.html | 62 + .../mochitest/test_ext_new_tab_processType.html | 168 ++ .../test/mochitest/test_ext_notifications.html | 340 +++ .../mochitest/test_ext_optional_permissions.html | 98 + .../test/mochitest/test_ext_protocolHandlers.html | 580 +++++ .../test/mochitest/test_ext_redirect_jar.html | 92 + .../test_ext_request_urlClassification.html | 130 ++ .../test/mochitest/test_ext_runtime_connect.html | 83 + .../test/mochitest/test_ext_runtime_connect2.html | 102 + .../mochitest/test_ext_runtime_connect_iframe.html | 136 ++ .../mochitest/test_ext_runtime_connect_twoway.html | 126 ++ .../mochitest/test_ext_runtime_disconnect.html | 77 + .../test/mochitest/test_ext_script_filenames.html | 62 + .../test_ext_scripting_contentScripts.html | 1532 +++++++++++++ .../test_ext_scripting_executeScript.html | 1479 +++++++++++++ ...test_ext_scripting_executeScript_activeTab.html | 144 ++ ..._scripting_executeScript_injectImmediately.html | 215 ++ .../mochitest/test_ext_scripting_insertCSS.html | 395 ++++ .../mochitest/test_ext_scripting_permissions.html | 149 ++ .../mochitest/test_ext_scripting_removeCSS.html | 135 ++ .../test_ext_sendmessage_doublereply.html | 100 + .../mochitest/test_ext_sendmessage_frameId.html | 45 + .../test_ext_sendmessage_no_receiver.html | 115 + .../test/mochitest/test_ext_sendmessage_reply.html | 78 + .../mochitest/test_ext_sendmessage_reply2.html | 202 ++ .../test/mochitest/test_ext_storage_cleanup.html | 235 ++ .../test_ext_storage_manager_capabilities.html | 130 ++ .../mochitest/test_ext_storage_smoke_test.html | 108 + .../mochitest/test_ext_streamfilter_multiple.html | 91 + .../test_ext_streamfilter_processswitch.html | 76 + .../mochitest/test_ext_subframes_privileges.html | 340 +++ .../test/mochitest/test_ext_tabs_captureTab.html | 324 +++ .../test_ext_tabs_create_cookieStoreId.html | 210 ++ .../test_ext_tabs_executeScript_good.html | 162 ++ .../test/mochitest/test_ext_tabs_permissions.html | 752 +++++++ .../test/mochitest/test_ext_tabs_query_popup.html | 102 + .../test/mochitest/test_ext_tabs_sendMessage.html | 152 ++ .../extensions/test/mochitest/test_ext_test.html | 341 +++ .../test/mochitest/test_ext_unlimitedStorage.html | 138 ++ .../test_ext_web_accessible_incognito.html | 170 ++ .../test_ext_web_accessible_resources.html | 567 +++++ .../test/mochitest/test_ext_webnavigation.html | 610 ++++++ .../mochitest/test_ext_webnavigation_filters.html | 313 +++ .../test_ext_webnavigation_incognito.html | 105 + .../test_ext_webrequest_and_proxy_filter.html | 131 ++ .../test/mochitest/test_ext_webrequest_auth.html | 181 ++ .../test_ext_webrequest_background_events.html | 120 + .../test/mochitest/test_ext_webrequest_basic.html | 447 ++++ .../test/mochitest/test_ext_webrequest_errors.html | 59 + .../test/mochitest/test_ext_webrequest_filter.html | 228 ++ .../mochitest/test_ext_webrequest_frameId.html | 215 ++ .../test/mochitest/test_ext_webrequest_hsts.html | 223 ++ .../test_ext_webrequest_redirect_bypass_cors.html | 75 + .../test_ext_webrequest_redirect_data_uri.html | 83 + .../mochitest/test_ext_webrequest_upgrade.html | 139 ++ .../test/mochitest/test_ext_webrequest_upload.html | 265 +++ .../mochitest/test_ext_window_postMessage.html | 104 + .../test/mochitest/test_startup_canary.html | 76 + .../mochitest/test_verify_non_remote_mode.html | 32 + .../test/mochitest/test_verify_remote_mode.html | 22 + .../test/mochitest/test_verify_sw_mode.html | 24 + .../test/mochitest/webrequest_chromeworker.js | 9 + .../extensions/test/mochitest/webrequest_test.jsm | 20 + .../extensions/test/mochitest/webrequest_worker.js | 3 + .../extensions/test/xpcshell/.eslintrc.js | 13 + .../test/xpcshell/data/TestWorkerWatcherChild.jsm | 68 + .../test/xpcshell/data/TestWorkerWatcherParent.jsm | 24 + .../extensions/test/xpcshell/data/dummy_page.html | 7 + .../test/xpcshell/data/empty_file_download.txt | 0 .../test/xpcshell/data/file download.txt | 1 + .../test/xpcshell/data/file_WebRequest_page2.html | 25 + .../data/file_WebRequest_permission_original.html | 19 + .../data/file_WebRequest_permission_original.js | 2 + .../file_WebRequest_permission_redirected.html | 19 + .../data/file_WebRequest_permission_redirected.js | 2 + .../xpcshell/data/file_content_script_errors.html | 7 + .../extensions/test/xpcshell/data/file_csp.html | 14 + .../test/xpcshell/data/file_csp.html^headers^ | 1 + .../data/file_do_load_script_subresource.html | 9 + .../test/xpcshell/data/file_document_open.html | 21 + .../test/xpcshell/data/file_document_write.html | 36 + .../test/xpcshell/data/file_download.html | 12 + .../test/xpcshell/data/file_download.txt | 1 + .../extensions/test/xpcshell/data/file_iframe.html | 9 + .../test/xpcshell/data/file_image_bad.png | Bin 0 -> 5401 bytes .../test/xpcshell/data/file_image_good.png | Bin 0 -> 580 bytes .../test/xpcshell/data/file_image_redirect.png | Bin 0 -> 5401 bytes .../test/xpcshell/data/file_page_xhr.html | 34 + .../test/xpcshell/data/file_permission_xhr.html | 61 + .../xpcshell/data/file_privilege_escalation.html | 13 + .../extensions/test/xpcshell/data/file_sample.html | 12 + .../data/file_sample_registered_styles.html | 13 + .../extensions/test/xpcshell/data/file_script.html | 14 + .../test/xpcshell/data/file_script_bad.js | 12 + .../test/xpcshell/data/file_script_good.js | 12 + .../test/xpcshell/data/file_script_redirect.js | 3 + .../test/xpcshell/data/file_script_xhr.js | 9 + .../test/xpcshell/data/file_shadowdom.html | 13 + .../test/xpcshell/data/file_style_bad.css | 3 + .../test/xpcshell/data/file_style_good.css | 3 + .../test/xpcshell/data/file_style_redirect.css | 3 + .../test/xpcshell/data/file_stylesheet_cache.css | 1 + .../test/xpcshell/data/file_stylesheet_cache.html | 3 + .../xpcshell/data/file_stylesheet_cache_2.html | 19 + .../test/xpcshell/data/file_toplevel.html | 12 + .../test/xpcshell/data/file_with_iframe.html | 11 + .../xpcshell/data/file_with_xorigin_frame.html | 10 + .../extensions/test/xpcshell/data/lorem.html.gz | Bin 0 -> 392 bytes .../extensions/test/xpcshell/data/pixel_green.gif | Bin 0 -> 35 bytes .../extensions/test/xpcshell/data/pixel_red.gif | Bin 0 -> 35 bytes .../components/extensions/test/xpcshell/head.js | 353 +++ .../extensions/test/xpcshell/head_dnr.js | 178 ++ .../extensions/test/xpcshell/head_e10s.js | 8 + .../extensions/test/xpcshell/head_legacy_ep.js | 13 + .../test/xpcshell/head_native_messaging.js | 152 ++ .../extensions/test/xpcshell/head_remote.js | 7 + .../extensions/test/xpcshell/head_schemas.js | 127 ++ .../test/xpcshell/head_service_worker.js | 158 ++ .../extensions/test/xpcshell/head_storage.js | 1330 ++++++++++++ .../extensions/test/xpcshell/head_sync.js | 66 + .../extensions/test/xpcshell/head_telemetry.js | 172 ++ .../extensions/test/xpcshell/native_messaging.ini | 19 + .../test/xpcshell/test_ExtensionShortcutKeyMap.js | 142 ++ .../test_ExtensionStorageSync_migration_kinto.js | 86 + .../extensions/test/xpcshell/test_MatchPattern.js | 602 +++++ .../test/xpcshell/test_StorageSyncService.js | 274 +++ .../xpcshell/test_WebExtensionContentScript.js | 321 +++ .../test/xpcshell/test_WebExtensionPolicy.js | 620 ++++++ .../test/xpcshell/test_change_remote_mode.js | 20 + .../test/xpcshell/test_csp_custom_policies.js | 303 +++ .../extensions/test/xpcshell/test_csp_validator.js | 322 +++ .../test/xpcshell/test_ext_MessageManagerProxy.js | 80 + .../test/xpcshell/test_ext_activityLog.js | 77 + .../test_ext_adoption_with_private_field_xrays.js | 160 ++ .../test/xpcshell/test_ext_adoption_with_xrays.js | 129 ++ .../extensions/test/xpcshell/test_ext_alarms.js | 346 +++ .../test/xpcshell/test_ext_alarms_does_not_fire.js | 34 + .../test/xpcshell/test_ext_alarms_periodic.js | 50 + .../test/xpcshell/test_ext_alarms_replaces.js | 56 + .../test/xpcshell/test_ext_api_permissions.js | 75 + .../test_ext_asyncAPICall_isHandlingUserInput.js | 149 ++ .../xpcshell/test_ext_background_api_injection.js | 35 + .../xpcshell/test_ext_background_early_shutdown.js | 190 ++ .../test_ext_background_generated_load_events.js | 23 + .../test_ext_background_generated_reload.js | 24 + .../xpcshell/test_ext_background_global_history.js | 24 + .../test_ext_background_private_browsing.js | 44 + .../test_ext_background_runtime_connect_params.js | 88 + .../xpcshell/test_ext_background_service_worker.js | 323 +++ .../xpcshell/test_ext_background_sub_windows.js | 46 + .../test/xpcshell/test_ext_background_teardown.js | 98 + .../test/xpcshell/test_ext_background_telemetry.js | 99 + .../test_ext_background_window_properties.js | 41 + .../test/xpcshell/test_ext_brokenlinks.js | 54 + .../test/xpcshell/test_ext_browserSettings.js | 536 +++++ .../xpcshell/test_ext_browserSettings_homepage.js | 36 + .../test/xpcshell/test_ext_browsingData.js | 48 + .../test_ext_browsingData_cookies_cache.js | 456 ++++ .../test_ext_browsingData_cookies_cookieStoreId.js | 192 ++ .../extensions/test/xpcshell/test_ext_cache_api.js | 303 +++ .../test/xpcshell/test_ext_captivePortal.js | 202 ++ .../test/xpcshell/test_ext_captivePortal_url.js | 53 + .../xpcshell/test_ext_clear_cached_resources.js | 417 ++++ .../xpcshell/test_ext_contentScripts_register.js | 809 +++++++ .../xpcshell/test_ext_content_security_policy.js | 362 ++++ .../test/xpcshell/test_ext_contentscript.js | 270 +++ .../test_ext_contentscript_about_blank_start.js | 78 + .../test_ext_contentscript_api_injection.js | 65 + .../test_ext_contentscript_async_loading.js | 79 + .../test_ext_contentscript_canvas_tainting.js | 128 ++ .../xpcshell/test_ext_contentscript_context.js | 359 +++ .../test_ext_contentscript_context_isolation.js | 168 ++ .../test_ext_contentscript_create_iframe.js | 177 ++ .../test/xpcshell/test_ext_contentscript_csp.js | 433 ++++ .../test/xpcshell/test_ext_contentscript_css.js | 48 + .../test_ext_contentscript_dynamic_registration.js | 206 ++ .../test/xpcshell/test_ext_contentscript_errors.js | 127 ++ .../test_ext_contentscript_exporthelpers.js | 98 + .../xpcshell/test_ext_contentscript_importmap.js | 124 ++ .../test_ext_contentscript_in_background.js | 43 + .../xpcshell/test_ext_contentscript_json_api.js | 102 + .../test_ext_contentscript_module_import.js | 277 +++ .../test_ext_contentscript_perf_observers.js | 71 + .../test_ext_contentscript_permissions_change.js | 104 + .../test_ext_contentscript_permissions_fetch.js | 87 + .../test_ext_contentscript_restrictSchemes.js | 149 ++ .../test_ext_contentscript_scriptCreated.js | 61 + .../xpcshell/test_ext_contentscript_teardown.js | 101 + .../test_ext_contentscript_triggeringPrincipal.js | 1383 ++++++++++++ ...ntscript_unregister_during_loadContentScript.js | 91 + .../test_ext_contentscript_xml_prettyprint.js | 75 + .../test_ext_contentscript_xorigin_frame.js | 62 + .../test/xpcshell/test_ext_contentscript_xrays.js | 59 + .../extensions/test/xpcshell/test_ext_contexts.js | 201 ++ .../test/xpcshell/test_ext_contexts_gc.js | 277 +++ .../xpcshell/test_ext_contextual_identities.js | 591 +++++ .../test/xpcshell/test_ext_cookieBehaviors.js | 567 +++++ .../test/xpcshell/test_ext_cookies_errors.js | 168 ++ .../test/xpcshell/test_ext_cookies_firstParty.js | 334 +++ .../test/xpcshell/test_ext_cookies_onChanged.js | 142 ++ .../test/xpcshell/test_ext_cookies_partitionKey.js | 898 ++++++++ .../test/xpcshell/test_ext_cookies_samesite.js | 114 + .../test/xpcshell/test_ext_cors_mozextension.js | 220 ++ .../test/xpcshell/test_ext_csp_frame_ancestors.js | 221 ++ .../test/xpcshell/test_ext_csp_upgrade_requests.js | 74 + .../test/xpcshell/test_ext_debugging_utils.js | 316 +++ .../test/xpcshell/test_ext_dnr_allowAllRequests.js | 96 + .../extensions/test/xpcshell/test_ext_dnr_api.js | 256 +++ .../test/xpcshell/test_ext_dnr_dynamic_rules.js | 870 ++++++++ .../test/xpcshell/test_ext_dnr_modifyHeaders.js | 1073 +++++++++ .../test/xpcshell/test_ext_dnr_private_browsing.js | 130 ++ .../xpcshell/test_ext_dnr_redirect_transform.js | 725 +++++++ .../test/xpcshell/test_ext_dnr_session_rules.js | 985 +++++++++ .../test/xpcshell/test_ext_dnr_static_rules.js | 1322 +++++++++++ .../xpcshell/test_ext_dnr_system_restrictions.js | 66 + .../test/xpcshell/test_ext_dnr_tabIds.js | 247 +++ .../test/xpcshell/test_ext_dnr_testMatchOutcome.js | 1085 +++++++++ .../test/xpcshell/test_ext_dnr_urlFilter.js | 1101 ++++++++++ .../test/xpcshell/test_ext_dnr_webrequest.js | 296 +++ .../xpcshell/test_ext_dnr_without_webrequest.js | 739 +++++++ .../extensions/test/xpcshell/test_ext_dns.js | 176 ++ .../extensions/test/xpcshell/test_ext_downloads.js | 38 + .../xpcshell/test_ext_downloads_cookieStoreId.js | 469 ++++ .../test/xpcshell/test_ext_downloads_cookies.js | 219 ++ .../test/xpcshell/test_ext_downloads_download.js | 680 ++++++ .../test/xpcshell/test_ext_downloads_eventpage.js | 162 ++ .../test/xpcshell/test_ext_downloads_misc.js | 1073 +++++++++ .../xpcshell/test_ext_downloads_partitionKey.js | 199 ++ .../test/xpcshell/test_ext_downloads_private.js | 306 +++ .../test/xpcshell/test_ext_downloads_search.js | 682 ++++++ .../test/xpcshell/test_ext_downloads_urlencoded.js | 257 +++ .../test/xpcshell/test_ext_error_location.js | 48 + .../test/xpcshell/test_ext_eventpage_idle.js | 575 +++++ .../test/xpcshell/test_ext_eventpage_settings.js | 166 ++ .../test/xpcshell/test_ext_eventpage_warning.js | 98 + .../test/xpcshell/test_ext_experiments.js | 377 ++++ .../extensions/test/xpcshell/test_ext_extension.js | 74 + .../test_ext_extensionPreferencesManager.js | 885 ++++++++ .../xpcshell/test_ext_extensionSettingsStore.js | 1089 ++++++++++ .../test_ext_extension_content_telemetry.js | 146 ++ .../xpcshell/test_ext_extension_page_navigated.js | 339 +++ .../xpcshell/test_ext_extension_startup_failure.js | 46 + .../test_ext_extension_startup_telemetry.js | 88 + .../test/xpcshell/test_ext_file_access.js | 193 ++ .../xpcshell/test_ext_geckoProfiler_control.js | 208 ++ .../test/xpcshell/test_ext_geckoProfiler_schema.js | 68 + .../extensions/test/xpcshell/test_ext_geturl.js | 64 + .../extensions/test/xpcshell/test_ext_i18n.js | 571 +++++ .../extensions/test/xpcshell/test_ext_i18n_css.js | 197 ++ .../extensions/test/xpcshell/test_ext_idle.js | 361 +++ .../extensions/test/xpcshell/test_ext_incognito.js | 127 ++ .../test/xpcshell/test_ext_indexedDB_principal.js | 101 + .../extensions/test/xpcshell/test_ext_ipcBlob.js | 150 ++ .../test/xpcshell/test_ext_json_parser.js | 108 + .../extensions/test/xpcshell/test_ext_l10n.js | 166 ++ .../test/xpcshell/test_ext_localStorage.js | 50 + .../test/xpcshell/test_ext_management.js | 339 +++ .../xpcshell/test_ext_management_uninstall_self.js | 146 ++ .../extensions/test/xpcshell/test_ext_manifest.js | 280 +++ .../test_ext_manifest_content_security_policy.js | 114 + .../test/xpcshell/test_ext_manifest_incognito.js | 45 + .../test_ext_manifest_minimum_chrome_version.js | 12 + .../test_ext_manifest_minimum_opera_version.js | 12 + .../test/xpcshell/test_ext_manifest_themes.js | 35 + .../test/xpcshell/test_ext_messaging_startup.js | 280 +++ .../test/xpcshell/test_ext_native_messaging.js | 1051 +++++++++ .../xpcshell/test_ext_native_messaging_perf.js | 130 ++ .../test_ext_native_messaging_unresponsive.js | 85 + .../test/xpcshell/test_ext_networkStatus.js | 208 ++ .../xpcshell/test_ext_notifications_incognito.js | 105 + .../xpcshell/test_ext_notifications_unsupported.js | 41 + .../xpcshell/test_ext_onmessage_removelistener.js | 30 + .../test/xpcshell/test_ext_performance_counters.js | 86 + .../test/xpcshell/test_ext_permission_warnings.js | 855 ++++++++ .../test/xpcshell/test_ext_permission_xhr.js | 240 ++ .../test/xpcshell/test_ext_permissions.js | 1003 +++++++++ .../test/xpcshell/test_ext_permissions_api.js | 465 ++++ .../test/xpcshell/test_ext_permissions_migrate.js | 252 +++ .../xpcshell/test_ext_permissions_uninstall.js | 157 ++ .../test/xpcshell/test_ext_persistent_events.js | 1268 +++++++++++ .../extensions/test/xpcshell/test_ext_privacy.js | 984 +++++++++ .../test/xpcshell/test_ext_privacy_disable.js | 195 ++ .../test_ext_privacy_nonPersistentCookies.js | 53 + .../test/xpcshell/test_ext_privacy_update.js | 165 ++ .../test_ext_proxy_authorization_via_proxyinfo.js | 116 + .../test/xpcshell/test_ext_proxy_config.js | 614 ++++++ .../xpcshell/test_ext_proxy_containerIsolation.js | 59 + .../test/xpcshell/test_ext_proxy_onauthrequired.js | 302 +++ .../test/xpcshell/test_ext_proxy_settings.js | 107 + .../test/xpcshell/test_ext_proxy_socks.js | 660 ++++++ .../test/xpcshell/test_ext_proxy_speculative.js | 52 + .../test/xpcshell/test_ext_proxy_startup.js | 147 ++ .../extensions/test/xpcshell/test_ext_redirects.js | 660 ++++++ .../test_ext_runtime_connect_no_receiver.js | 26 + .../xpcshell/test_ext_runtime_getBackgroundPage.js | 172 ++ .../xpcshell/test_ext_runtime_getBrowserInfo.js | 26 + .../xpcshell/test_ext_runtime_getPlatformInfo.js | 36 + .../test/xpcshell/test_ext_runtime_id.js | 46 + .../xpcshell/test_ext_runtime_messaging_self.js | 84 + .../test_ext_runtime_onInstalled_and_onStartup.js | 599 +++++ .../test/xpcshell/test_ext_runtime_ports.js | 69 + .../test/xpcshell/test_ext_runtime_ports_gc.js | 170 ++ .../test/xpcshell/test_ext_runtime_sendMessage.js | 462 ++++ .../xpcshell/test_ext_runtime_sendMessage_args.js | 118 + .../test_ext_runtime_sendMessage_errors.js | 66 + .../test_ext_runtime_sendMessage_multiple.js | 67 + .../test_ext_runtime_sendMessage_no_receiver.js | 93 + .../test/xpcshell/test_ext_same_site_cookies.js | 131 ++ .../test/xpcshell/test_ext_same_site_redirects.js | 233 ++ .../test/xpcshell/test_ext_sandbox_var.js | 42 + .../test/xpcshell/test_ext_sandboxed_resource.js | 55 + .../extensions/test/xpcshell/test_ext_schema.js | 79 + .../extensions/test/xpcshell/test_ext_schemas.js | 2118 ++++++++++++++++++ .../xpcshell/test_ext_schemas_allowed_contexts.js | 158 ++ .../test/xpcshell/test_ext_schemas_async.js | 350 +++ .../test/xpcshell/test_ext_schemas_interactive.js | 173 ++ .../test_ext_schemas_manifest_permissions.js | 171 ++ .../test/xpcshell/test_ext_schemas_privileged.js | 160 ++ .../test/xpcshell/test_ext_schemas_revoke.js | 505 +++++ .../test/xpcshell/test_ext_schemas_roots.js | 240 ++ .../test/xpcshell/test_ext_schemas_versioned.js | 714 ++++++ .../test/xpcshell/test_ext_script_filenames.js | 366 ++++ .../xpcshell/test_ext_scripting_contentScripts.js | 412 ++++ .../test_ext_scripting_contentScripts_css.js | 330 +++ .../test_ext_scripting_contentScripts_file.js | 77 + .../test/xpcshell/test_ext_scripting_mv2.js | 23 + .../test_ext_scripting_persistAcrossSessions.js | 759 +++++++ .../xpcshell/test_ext_scripting_startupCache.js | 165 ++ .../test_ext_scripting_updateContentScripts.js | 114 + .../extensions/test/xpcshell/test_ext_secfetch.js | 352 +++ .../extensions/test/xpcshell/test_ext_shadowdom.js | 59 + .../test/xpcshell/test_ext_shared_array_buffer.js | 104 + .../test/xpcshell/test_ext_shared_workers.js | 40 + .../test/xpcshell/test_ext_shutdown_cleanup.js | 41 + .../extensions/test/xpcshell/test_ext_simple.js | 190 ++ .../test/xpcshell/test_ext_startupData.js | 55 + .../test/xpcshell/test_ext_startup_cache.js | 178 ++ .../xpcshell/test_ext_startup_cache_telemetry.js | 167 ++ .../test/xpcshell/test_ext_startup_perf.js | 73 + .../xpcshell/test_ext_startup_request_handler.js | 64 + .../xpcshell/test_ext_storage_content_local.js | 39 + .../test/xpcshell/test_ext_storage_content_sync.js | 31 + .../test_ext_storage_content_sync_kinto.js | 31 + .../test_ext_storage_idb_data_migration.js | 787 +++++++ .../test/xpcshell/test_ext_storage_local.js | 79 + .../test/xpcshell/test_ext_storage_managed.js | 216 ++ .../xpcshell/test_ext_storage_managed_policy.js | 55 + .../test_ext_storage_quota_exceeded_errors.js | 82 + .../test/xpcshell/test_ext_storage_sanitizer.js | 109 + .../test/xpcshell/test_ext_storage_sync.js | 35 + .../test/xpcshell/test_ext_storage_sync_kinto.js | 2292 ++++++++++++++++++++ .../xpcshell/test_ext_storage_sync_kinto_crypto.js | 118 + .../test/xpcshell/test_ext_storage_tab.js | 245 +++ .../test/xpcshell/test_ext_storage_telemetry.js | 364 ++++ .../test/xpcshell/test_ext_tab_teardown.js | 97 + .../extensions/test/xpcshell/test_ext_telemetry.js | 917 ++++++++ .../extensions/test/xpcshell/test_ext_test_mock.js | 55 + .../test/xpcshell/test_ext_test_wrapper.js | 62 + .../test/xpcshell/test_ext_theme_experiments.js | 109 + .../test/xpcshell/test_ext_trustworthy_origin.js | 20 + .../test/xpcshell/test_ext_unknown_permissions.js | 60 + .../test/xpcshell/test_ext_unlimitedStorage.js | 211 ++ .../test/xpcshell/test_ext_unload_frame.js | 230 ++ .../test/xpcshell/test_ext_userScripts.js | 730 +++++++ .../test/xpcshell/test_ext_userScripts_exports.js | 1108 ++++++++++ .../test/xpcshell/test_ext_userScripts_register.js | 142 ++ .../extensions/test/xpcshell/test_ext_wasm.js | 135 ++ .../test/xpcshell/test_ext_webRequest_auth.js | 425 ++++ .../test/xpcshell/test_ext_webRequest_cached.js | 311 +++ .../test_ext_webRequest_cancelWithReason.js | 69 + .../test_ext_webRequest_containerIsolation.js | 59 + .../test/xpcshell/test_ext_webRequest_download.js | 43 + .../test_ext_webRequest_eventPage_StreamFilter.js | 351 +++ .../test_ext_webRequest_filterResponseData.js | 611 ++++++ .../xpcshell/test_ext_webRequest_filterTypes.js | 85 + .../xpcshell/test_ext_webRequest_filter_urls.js | 35 + .../test_ext_webRequest_from_extension_page.js | 57 + .../test/xpcshell/test_ext_webRequest_host.js | 99 + .../test/xpcshell/test_ext_webRequest_incognito.js | 88 + .../test/xpcshell/test_ext_webRequest_mergecsp.js | 545 +++++ .../xpcshell/test_ext_webRequest_permission.js | 154 ++ .../test_ext_webRequest_redirectProperty.js | 65 + .../test_ext_webRequest_redirect_StreamFilter.js | 129 ++ .../test_ext_webRequest_redirect_mozextension.js | 47 + .../xpcshell/test_ext_webRequest_requestSize.js | 57 + .../xpcshell/test_ext_webRequest_responseBody.js | 765 +++++++ .../test_ext_webRequest_restrictedHeaders.js | 252 +++ .../xpcshell/test_ext_webRequest_set_cookie.js | 308 +++ .../test/xpcshell/test_ext_webRequest_startup.js | 756 +++++++ .../test_ext_webRequest_startup_StreamFilter.js | 79 + .../xpcshell/test_ext_webRequest_style_cache.js | 49 + .../test/xpcshell/test_ext_webRequest_suspend.js | 290 +++ .../test_ext_webRequest_urlclassification.js | 43 + .../xpcshell/test_ext_webRequest_userContextId.js | 41 + .../xpcshell/test_ext_webRequest_viewsource.js | 95 + .../test_ext_webRequest_viewsource_StreamFilter.js | 144 ++ .../test/xpcshell/test_ext_webRequest_webSocket.js | 55 + .../extensions/test/xpcshell/test_ext_webSocket.js | 162 ++ .../xpcshell/test_ext_web_accessible_resources.js | 147 ++ .../test_ext_web_accessible_resources_matches.js | 468 ++++ .../test/xpcshell/test_ext_xhr_capabilities.js | 72 + .../extensions/test/xpcshell/test_ext_xhr_cors.js | 223 ++ .../test_extension_permissions_migration.js | 99 + .../test/xpcshell/test_load_all_api_modules.js | 169 ++ .../test/xpcshell/test_locale_converter.js | 146 ++ .../extensions/test/xpcshell/test_locale_data.js | 221 ++ .../test/xpcshell/test_native_manifests.js | 444 ++++ .../test/xpcshell/test_proxy_failover.js | 323 +++ .../test/xpcshell/test_proxy_incognito.js | 95 + .../test/xpcshell/test_proxy_info_results.js | 469 ++++ .../test/xpcshell/test_proxy_listener.js | 298 +++ .../test/xpcshell/test_proxy_userContextId.js | 43 + .../test/xpcshell/test_site_permissions.js | 387 ++++ .../test/xpcshell/test_webRequest_ancestors.js | 79 + .../test/xpcshell/test_webRequest_cookies.js | 102 + .../test/xpcshell/test_webRequest_filtering.js | 182 ++ .../test/xpcshell/webidl-api/.eslintrc.js | 9 + .../test/xpcshell/webidl-api/head_webidl_api.js | 313 +++ .../xpcshell/webidl-api/test_ext_webidl_api.js | 486 +++++ .../test_ext_webidl_api_event_callback.js | 575 +++++ .../test_ext_webidl_api_request_handler.js | 443 ++++ .../test_ext_webidl_api_schema_errors.js | 102 + .../test_ext_webidl_api_schema_formatters.js | 99 + .../webidl-api/test_ext_webidl_runtime_port.js | 220 ++ .../test/xpcshell/webidl-api/xpcshell.ini | 32 + .../test/xpcshell/xpcshell-common-e10s.ini | 21 + .../extensions/test/xpcshell/xpcshell-common.ini | 424 ++++ .../extensions/test/xpcshell/xpcshell-content.ini | 70 + .../extensions/test/xpcshell/xpcshell-e10s.ini | 30 + .../test/xpcshell/xpcshell-legacy-ep.ini | 21 + .../extensions/test/xpcshell/xpcshell-remote.ini | 42 + .../test/xpcshell/xpcshell-serviceworker.ini | 31 + .../extensions/test/xpcshell/xpcshell.ini | 99 + 630 files changed, 116542 insertions(+) create mode 100644 toolkit/components/extensions/test/browser/.eslintrc.js create mode 100644 toolkit/components/extensions/test/browser/browser-serviceworker.ini create mode 100644 toolkit/components/extensions/test/browser/browser.ini create mode 100644 toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_management_themes.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_test_mock.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_reset.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_separators.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js create mode 100644 toolkit/components/extensions/test/browser/data/test-download.txt create mode 100644 toolkit/components/extensions/test/browser/data/test_downloads_referrer.html create mode 100644 toolkit/components/extensions/test/browser/head.js create mode 100644 toolkit/components/extensions/test/browser/head_serviceworker.js create mode 100644 toolkit/components/extensions/test/marionette/data/extension-with-bg-sw/manifest.json create mode 100644 toolkit/components/extensions/test/marionette/data/extension-with-bg-sw/sw.js create mode 100644 toolkit/components/extensions/test/marionette/manifest.ini create mode 100644 toolkit/components/extensions/test/marionette/service_worker_testutils.py create mode 100644 toolkit/components/extensions/test/marionette/test_extension_serviceworkers_purged_on_pref_disabled.py create mode 100644 toolkit/components/extensions/test/marionette/test_temporary_extension_serviceworkers_not_persisted.py create mode 100644 toolkit/components/extensions/test/mochitest/.eslintrc.js create mode 100644 toolkit/components/extensions/test/mochitest/chrome.ini create mode 100644 toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js create mode 100644 toolkit/components/extensions/test/mochitest/chrome_head.js create mode 100644 toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html create mode 100644 toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html create mode 100644 toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html create mode 100644 toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html create mode 100644 toolkit/components/extensions/test/mochitest/file_contains_iframe.html create mode 100644 toolkit/components/extensions/test/mochitest/file_contains_img.html create mode 100644 toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html create mode 100644 toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html create mode 100644 toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html create mode 100644 toolkit/components/extensions/test/mochitest/file_green.html create mode 100644 toolkit/components/extensions/test/mochitest/file_green_blue.html create mode 100644 toolkit/components/extensions/test/mochitest/file_image_bad.png create mode 100644 toolkit/components/extensions/test/mochitest/file_image_good.png create mode 100644 toolkit/components/extensions/test/mochitest/file_image_great.png create mode 100644 toolkit/components/extensions/test/mochitest/file_image_redirect.png create mode 100644 toolkit/components/extensions/test/mochitest/file_indexedDB.html create mode 100644 toolkit/components/extensions/test/mochitest/file_mixed.html create mode 100644 toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html create mode 100644 toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html create mode 100644 toolkit/components/extensions/test/mochitest/file_remote_frame.html create mode 100644 toolkit/components/extensions/test/mochitest/file_sample.html create mode 100644 toolkit/components/extensions/test/mochitest/file_sample.txt create mode 100644 toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ create mode 100644 toolkit/components/extensions/test/mochitest/file_script_bad.js create mode 100644 toolkit/components/extensions/test/mochitest/file_script_good.js create mode 100644 toolkit/components/extensions/test/mochitest/file_script_redirect.js create mode 100644 toolkit/components/extensions/test/mochitest/file_script_xhr.js create mode 100644 toolkit/components/extensions/test/mochitest/file_serviceWorker.html create mode 100644 toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html create mode 100644 toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html create mode 100644 toolkit/components/extensions/test/mochitest/file_simple_xhr.html create mode 100644 toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html create mode 100644 toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html create mode 100644 toolkit/components/extensions/test/mochitest/file_slowed_document.sjs create mode 100644 toolkit/components/extensions/test/mochitest/file_streamfilter.txt create mode 100644 toolkit/components/extensions/test/mochitest/file_style_bad.css create mode 100644 toolkit/components/extensions/test/mochitest/file_style_good.css create mode 100644 toolkit/components/extensions/test/mochitest/file_style_redirect.css create mode 100644 toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html create mode 100644 toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html create mode 100644 toolkit/components/extensions/test/mochitest/file_third_party.html create mode 100644 toolkit/components/extensions/test/mochitest/file_to_drawWindow.html create mode 100644 toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html create mode 100644 toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html create mode 100644 toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ create mode 100644 toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html create mode 100644 toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html create mode 100644 toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html create mode 100644 toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html create mode 100644 toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html create mode 100644 toolkit/components/extensions/test/mochitest/file_with_about_blank.html create mode 100644 toolkit/components/extensions/test/mochitest/file_with_images.html create mode 100644 toolkit/components/extensions/test/mochitest/file_with_subframes_and_embed.html create mode 100644 toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html create mode 100644 toolkit/components/extensions/test/mochitest/head.js create mode 100644 toolkit/components/extensions/test/mochitest/head_cookies.js create mode 100644 toolkit/components/extensions/test/mochitest/head_notifications.js create mode 100644 toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js create mode 100644 toolkit/components/extensions/test/mochitest/head_webrequest.js create mode 100644 toolkit/components/extensions/test/mochitest/hsts.sjs create mode 100644 toolkit/components/extensions/test/mochitest/mochitest-common.ini create mode 100644 toolkit/components/extensions/test/mochitest/mochitest-remote.ini create mode 100644 toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini create mode 100644 toolkit/components/extensions/test/mochitest/mochitest.ini create mode 100644 toolkit/components/extensions/test/mochitest/mochitest_console.js create mode 100644 toolkit/components/extensions/test/mochitest/oauth.html create mode 100644 toolkit/components/extensions/test/mochitest/redirect_auto.sjs create mode 100644 toolkit/components/extensions/test/mochitest/redirection.sjs create mode 100644 toolkit/components/extensions/test/mochitest/return_headers.sjs create mode 100644 toolkit/components/extensions/test/mochitest/serviceWorker.js create mode 100644 toolkit/components/extensions/test/mochitest/slow_response.sjs create mode 100644 toolkit/components/extensions/test/mochitest/test_check_startupcache.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_svg_context_fill.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html create mode 100644 toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_action.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_activityLog.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_all_apis.js create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_background_page.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_background_page_dpi.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_incognito_window.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_windowId.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_without_pref.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_clipboard.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_getFrameId.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_cookies.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_dnr_tabIds.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_extension_iframe_messaging.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_generate.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_geolocation.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_identity.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_idle.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_notifications.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_optional_permissions.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_script_filenames.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_injectImmediately.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_scripting_insertCSS.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_scripting_permissions.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_scripting_removeCSS.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_tabs_create_cookieStoreId.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_tabs_executeScript_good.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_test.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html create mode 100644 toolkit/components/extensions/test/mochitest/test_startup_canary.html create mode 100644 toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html create mode 100644 toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html create mode 100644 toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html create mode 100644 toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js create mode 100644 toolkit/components/extensions/test/mochitest/webrequest_test.jsm create mode 100644 toolkit/components/extensions/test/mochitest/webrequest_worker.js create mode 100644 toolkit/components/extensions/test/xpcshell/.eslintrc.js create mode 100644 toolkit/components/extensions/test/xpcshell/data/TestWorkerWatcherChild.jsm create mode 100644 toolkit/components/extensions/test/xpcshell/data/TestWorkerWatcherParent.jsm create mode 100644 toolkit/components/extensions/test/xpcshell/data/dummy_page.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/empty_file_download.txt create mode 100644 toolkit/components/extensions/test/xpcshell/data/file download.txt create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_WebRequest_page2.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_original.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_original.js create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_redirected.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_redirected.js create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_content_script_errors.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_csp.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_csp.html^headers^ create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_do_load_script_subresource.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_document_open.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_document_write.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_download.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_download.txt create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_iframe.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_image_bad.png create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_image_good.png create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_image_redirect.png create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_page_xhr.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_permission_xhr.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_privilege_escalation.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_sample.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_sample_registered_styles.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_script.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_script_bad.js create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_script_good.js create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_script_redirect.js create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_script_xhr.js create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_shadowdom.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_style_bad.css create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_style_good.css create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_style_redirect.css create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache.css create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache_2.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_toplevel.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_with_iframe.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_with_xorigin_frame.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/lorem.html.gz create mode 100644 toolkit/components/extensions/test/xpcshell/data/pixel_green.gif create mode 100644 toolkit/components/extensions/test/xpcshell/data/pixel_red.gif create mode 100644 toolkit/components/extensions/test/xpcshell/head.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_dnr.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_e10s.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_legacy_ep.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_native_messaging.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_remote.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_schemas.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_service_worker.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_storage.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_sync.js create mode 100644 toolkit/components/extensions/test/xpcshell/head_telemetry.js create mode 100644 toolkit/components/extensions/test/xpcshell/native_messaging.ini create mode 100644 toolkit/components/extensions/test/xpcshell/test_ExtensionShortcutKeyMap.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ExtensionStorageSync_migration_kinto.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_MatchPattern.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_StorageSyncService.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_change_remote_mode.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_csp_validator.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_MessageManagerProxy.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_activityLog.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_private_field_xrays.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_xrays.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_alarms.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_asyncAPICall_isHandlingUserInput.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_api_injection.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_service_worker.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_teardown.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_telemetry.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_brokenlinks.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_browserSettings_homepage.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_browsingData.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cache.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cookieStoreId.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_cache_api.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_captivePortal.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_captivePortal_url.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_clear_cached_resources.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentScripts_register.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_content_security_policy.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_about_blank_start.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_api_injection.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_async_loading.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_canvas_tainting.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_context.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_context_isolation.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_create_iframe.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_css.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_exporthelpers.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_importmap.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_in_background.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_json_api.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_module_import.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_perf_observers.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_fetch.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_restrictSchemes.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_scriptCreated.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_teardown.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_unregister_during_loadContentScript.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xml_prettyprint.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xorigin_frame.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xrays.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contexts.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_cookieBehaviors.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_cookies_errors.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_cookies_firstParty.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_cookies_onChanged.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_cookies_partitionKey.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_cookies_samesite.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_cors_mozextension.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_csp_frame_ancestors.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_csp_upgrade_requests.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_debugging_utils.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_allowAllRequests.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_api.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_modifyHeaders.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_private_browsing.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_redirect_transform.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_session_rules.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_system_restrictions.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_tabIds.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_testMatchOutcome.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_urlFilter.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_webrequest.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_without_webrequest.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dns.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_cookieStoreId.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_cookies.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_eventpage.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_partitionKey.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_private.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_downloads_urlencoded.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_error_location.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_eventpage_idle.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_eventpage_settings.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_eventpage_warning.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_experiments.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_extension.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_extension_content_telemetry.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_extension_page_navigated.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_extension_startup_failure.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_extension_startup_telemetry.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_file_access.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_geckoProfiler_control.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_geckoProfiler_schema.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_geturl.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_i18n.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_i18n_css.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_idle.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_incognito.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_indexedDB_principal.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_ipcBlob.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_l10n.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_management.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_manifest.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_opera_version.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_manifest_themes.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_messaging_startup.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_networkStatus.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_notifications_incognito.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_notifications_unsupported.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_performance_counters.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_permission_xhr.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_permissions.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_permissions_api.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_permissions_migrate.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_permissions_uninstall.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_persistent_events.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_privacy.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_privacy_disable.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_privacy_nonPersistentCookies.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_privacy_update.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_proxy_authorization_via_proxyinfo.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_proxy_config.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_proxy_containerIsolation.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_proxy_settings.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_proxy_speculative.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_proxy_startup.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_redirects.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBackgroundPage.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_id.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_messaging_self.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_ports.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_ports_gc.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_args.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_multiple.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_same_site_cookies.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_same_site_redirects.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_sandbox_var.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_sandboxed_resource.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schema.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas_interactive.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas_privileged.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas_revoke.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas_roots.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_schemas_versioned.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_script_filenames.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_scripting_mv2.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_scripting_persistAcrossSessions.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_scripting_startupCache.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_secfetch.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_shadowdom.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_shared_array_buffer.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_shared_workers.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_shutdown_cleanup.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_simple.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_startupData.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_startup_cache.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_startup_cache_telemetry.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_startup_perf.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_startup_request_handler.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_content_local.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_content_sync.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_content_sync_kinto.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_local.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_managed.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_managed_policy.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_quota_exceeded_errors.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_sanitizer.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_sync_kinto.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_sync_kinto_crypto.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_tab.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_tab_teardown.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_telemetry.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_test_mock.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_test_wrapper.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_theme_experiments.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_trustworthy_origin.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_unknown_permissions.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_unlimitedStorage.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_unload_frame.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_userScripts_exports.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_userScripts_register.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_wasm.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_auth.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_cached.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_cancelWithReason.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_containerIsolation.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_download.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_eventPage_StreamFilter.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_filterResponseData.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_filterTypes.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_filter_urls.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_from_extension_page.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_host.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_incognito.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_mergecsp.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_permission.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_redirectProperty.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_redirect_StreamFilter.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_redirect_mozextension.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_requestSize.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_responseBody.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_restrictedHeaders.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_set_cookie.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_startup.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_startup_StreamFilter.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_style_cache.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_suspend.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_urlclassification.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_userContextId.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_viewsource.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_viewsource_StreamFilter.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_webSocket.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webSocket.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_web_accessible_resources.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_web_accessible_resources_matches.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_xhr_capabilities.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_xhr_cors.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_extension_permissions_migration.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_load_all_api_modules.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_locale_converter.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_locale_data.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_native_manifests.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_proxy_failover.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_proxy_incognito.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_proxy_info_results.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_proxy_listener.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_proxy_userContextId.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_site_permissions.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_webRequest_ancestors.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_webRequest_cookies.js create mode 100644 toolkit/components/extensions/test/xpcshell/test_webRequest_filtering.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/.eslintrc.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_errors.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_formatters.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_runtime_port.js create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/xpcshell.ini create mode 100644 toolkit/components/extensions/test/xpcshell/xpcshell-common-e10s.ini create mode 100644 toolkit/components/extensions/test/xpcshell/xpcshell-common.ini create mode 100644 toolkit/components/extensions/test/xpcshell/xpcshell-content.ini create mode 100644 toolkit/components/extensions/test/xpcshell/xpcshell-e10s.ini create mode 100644 toolkit/components/extensions/test/xpcshell/xpcshell-legacy-ep.ini create mode 100644 toolkit/components/extensions/test/xpcshell/xpcshell-remote.ini create mode 100644 toolkit/components/extensions/test/xpcshell/xpcshell-serviceworker.ini create mode 100644 toolkit/components/extensions/test/xpcshell/xpcshell.ini (limited to 'toolkit/components/extensions/test') diff --git a/toolkit/components/extensions/test/browser/.eslintrc.js b/toolkit/components/extensions/test/browser/.eslintrc.js new file mode 100644 index 0000000000..ef228570e3 --- /dev/null +++ b/toolkit/components/extensions/test/browser/.eslintrc.js @@ -0,0 +1,11 @@ +"use strict"; + +module.exports = { + env: { + webextensions: true, + }, + + rules: { + "no-shadow": "off", + }, +}; diff --git a/toolkit/components/extensions/test/browser/browser-serviceworker.ini b/toolkit/components/extensions/test/browser/browser-serviceworker.ini new file mode 100644 index 0000000000..58e9082f7b --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser-serviceworker.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + head_serviceworker.js + data/** + +prefs = + extensions.backgroundServiceWorker.enabled=true + +[browser_ext_background_serviceworker.js] diff --git a/toolkit/components/extensions/test/browser/browser.ini b/toolkit/components/extensions/test/browser/browser.ini new file mode 100644 index 0000000000..392ba90e01 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser.ini @@ -0,0 +1,54 @@ +[DEFAULT] +support-files = + head.js + data/** + +[browser_ext_background_serviceworker_pref_disabled.js] +[browser_ext_downloads_filters.js] +[browser_ext_downloads_referrer.js] +https_first_disabled = true +[browser_ext_eventpage_disableResetIdleForTest.js] +[browser_ext_extension_page_tab_navigated.js] +[browser_ext_management_themes.js] +skip-if = verify +[browser_ext_test_mock.js] +[browser_ext_themes_additional_backgrounds_alignment.js] +[browser_ext_themes_alpha_accentcolor.js] +[browser_ext_themes_arrowpanels.js] +[browser_ext_themes_autocomplete_popup.js] +[browser_ext_themes_chromeparity.js] +[browser_ext_themes_dynamic_getCurrent.js] +[browser_ext_themes_dynamic_onUpdated.js] +[browser_ext_themes_dynamic_updates.js] +[browser_ext_themes_experiment.js] +[browser_ext_themes_findbar.js] +[browser_ext_themes_getCurrent_differentExt.js] +[browser_ext_themes_highlight.js] +[browser_ext_themes_incognito.js] +[browser_ext_themes_lwtsupport.js] +[browser_ext_themes_multiple_backgrounds.js] +[browser_ext_themes_ntp_colors.js] +[browser_ext_themes_ntp_colors_perwindow.js] +[browser_ext_themes_pbm.js] +[browser_ext_themes_persistence.js] +[browser_ext_themes_reset.js] +[browser_ext_themes_sanitization.js] +[browser_ext_themes_separators.js] +[browser_ext_themes_sidebars.js] +[browser_ext_themes_static_onUpdated.js] +[browser_ext_themes_tab_line.js] +[browser_ext_themes_tab_loading.js] +[browser_ext_themes_tab_selected.js] +[browser_ext_themes_tab_text.js] +[browser_ext_themes_toolbar_fields_focus.js] +[browser_ext_themes_toolbar_fields.js] +[browser_ext_themes_toolbarbutton_colors.js] +[browser_ext_themes_toolbarbutton_icons.js] +[browser_ext_themes_toolbars.js] +[browser_ext_themes_theme_transition.js] +[browser_ext_themes_warnings.js] +[browser_ext_thumbnails_bg_extension.js] +support-files = !/toolkit/components/thumbnails/test/head.js +[browser_ext_webNavigation_eventpage.js] +[browser_ext_webRequest_redirect_mozextension.js] +[browser_ext_windows_popup_title.js] diff --git a/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js b/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js new file mode 100644 index 0000000000..4f32cd2c8e --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js @@ -0,0 +1,285 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* globals getBackgroundServiceWorkerRegistration, waitForServiceWorkerTerminated */ + +Services.scriptloader.loadSubScript( + new URL("head_serviceworker.js", gTestPath).href, + this +); + +add_task(assert_background_serviceworker_pref_enabled); + +add_task(async function test_serviceWorker_register_guarded_by_pref() { + // Test with backgroundServiceWorkeEnable set to true and the + // extensions.serviceWorkerRegist.allowed pref set to false. + // NOTE: the scenario with backgroundServiceWorkeEnable set to false + // is part of "browser_ext_background_serviceworker_pref_disabled.js". + + await SpecialPowers.pushPrefEnv({ + set: [["extensions.serviceWorkerRegister.allowed", false]], + }); + + let extensionData = { + files: { + "page.html": "", + "page.js": async function() { + browser.test.assertEq( + undefined, + navigator.serviceWorker, + "navigator.serviceWorker should be undefined" + ); + browser.test.sendMessage("test-serviceWorker-register-disallowed"); + }, + "sw.js": "", + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + // Verify that an extension page can't register a moz-extension url + // as a service worker. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `moz-extension://${extension.uuid}/page.html`, + }, + async () => { + await extension.awaitMessage("test-serviceWorker-register-disallowed"); + } + ); + + await extension.unload(); + + await SpecialPowers.popPrefEnv(); + + // Test again with the pref set to true. + + await SpecialPowers.pushPrefEnv({ + set: [["extensions.serviceWorkerRegister.allowed", true]], + }); + + extension = ExtensionTestUtils.loadExtension({ + files: { + ...extensionData.files, + "page.js": async function() { + try { + await navigator.serviceWorker.register("sw.js"); + } catch (err) { + browser.test.fail( + `Unexpected error on registering a service worker: ${err}` + ); + throw err; + } finally { + browser.test.sendMessage("test-serviceworker-register-allowed"); + } + }, + }, + }); + await extension.startup(); + + // Verify that an extension page can register a moz-extension url + // as a service worker if enabled by the related pref. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `moz-extension://${extension.uuid}/page.html`, + }, + async () => { + await extension.awaitMessage("test-serviceworker-register-allowed"); + } + ); + + await extension.unload(); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_cache_api_allowed() { + // Verify that Cache API support for moz-extension url availability is + // conditioned only by the extensions.backgroundServiceWorker.enabled pref. + // NOTE: the scenario with backgroundServiceWorkeEnable set to false + // is part of "browser_ext_background_serviceworker_pref_disabled.js". + const extension = ExtensionTestUtils.loadExtension({ + async background() { + try { + let cache = await window.caches.open("test-cache-api"); + browser.test.assertTrue( + await window.caches.has("test-cache-api"), + "CacheStorage.has should resolve to true" + ); + + // Test that adding and requesting cached moz-extension urls + // works as well. + let url = browser.runtime.getURL("file.txt"); + await cache.add(url); + const content = await cache.match(url).then(res => res.text()); + browser.test.assertEq( + "file content", + content, + "Got the expected content from the cached moz-extension url" + ); + + // Test that deleting the cache storage works as expected. + browser.test.assertTrue( + await window.caches.delete("test-cache-api"), + "Cache deleted successfully" + ); + browser.test.assertTrue( + !(await window.caches.has("test-cache-api")), + "CacheStorage.has should resolve to false" + ); + } catch (err) { + browser.test.fail(`Unexpected error on using Cache API: ${err}`); + throw err; + } finally { + browser.test.sendMessage("test-cache-api-allowed"); + } + }, + files: { + "file.txt": "file content", + }, + }); + + await extension.startup(); + await extension.awaitMessage("test-cache-api-allowed"); + await extension.unload(); +}); + +function createTestSWScript({ postMessageReply }) { + return ` + self.onmessage = msg => { + dump("Background ServiceWorker - onmessage handler\\n"); + msg.ports[0].postMessage("${postMessageReply}"); + dump("Background ServiceWorker - postMessage\\n"); + }; + dump("Background ServiceWorker - executed\\n"); + `; +} + +async function testServiceWorker({ extension, expectMessageReply }) { + // Verify that the WebExtensions framework has successfully registered the + // background service worker declared in the extension manifest. + const swRegInfo = getBackgroundServiceWorkerRegistration(extension); + + // Activate the background service worker by exchanging a message + // with it. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `moz-extension://${extension.uuid}/page.html`, + }, + async browser => { + let msgFromV1 = await SpecialPowers.spawn( + browser, + [swRegInfo.scriptURL], + async url => { + const { active } = await content.navigator.serviceWorker.ready; + const { port1, port2 } = new content.MessageChannel(); + + return new Promise(resolve => { + port1.onmessage = msg => resolve(msg.data); + active.postMessage("test", [port2]); + }); + } + ); + + Assert.deepEqual( + msgFromV1, + expectMessageReply, + "Got the expected reply from the extension service worker" + ); + } + ); +} + +function loadTestExtension({ version }) { + const postMessageReply = `reply:sw-v${version}`; + + return ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + version, + background: { + service_worker: "sw.js", + }, + browser_specific_settings: { gecko: { id: "test-bg-sw@mochi.test" } }, + }, + files: { + "page.html": "", + "sw.js": createTestSWScript({ postMessageReply }), + }, + }); +} + +async function assertWorkerIsRunningInExtensionProcess(extension) { + // Activate the background service worker by exchanging a message + // with it. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `moz-extension://${extension.uuid}/page.html`, + }, + async browser => { + const workerScriptURL = `moz-extension://${extension.uuid}/sw.js`; + const workerDebuggerURLs = await SpecialPowers.spawn( + browser, + [workerScriptURL], + async url => { + await content.navigator.serviceWorker.ready; + const wdm = Cc[ + "@mozilla.org/dom/workers/workerdebuggermanager;1" + ].getService(Ci.nsIWorkerDebuggerManager); + + return Array.from(wdm.getWorkerDebuggerEnumerator()) + .map(wd => { + return wd.url; + }) + .filter(swURL => swURL == url); + } + ); + + Assert.deepEqual( + workerDebuggerURLs, + [workerScriptURL], + "The worker should be running in the extension child process" + ); + } + ); +} + +add_task(async function test_background_serviceworker_with_no_ext_apis() { + const extensionV1 = loadTestExtension({ version: "1" }); + await extensionV1.startup(); + + const swRegInfo = getBackgroundServiceWorkerRegistration(extensionV1); + const { uuid } = extensionV1; + + await assertWorkerIsRunningInExtensionProcess(extensionV1); + await testServiceWorker({ + extension: extensionV1, + expectMessageReply: "reply:sw-v1", + }); + + // Load a new version of the same addon and verify that the + // expected worker script is being executed. + const extensionV2 = loadTestExtension({ version: "2" }); + await extensionV2.startup(); + is(extensionV2.uuid, uuid, "The extension uuid did not change as expected"); + + await testServiceWorker({ + extension: extensionV2, + expectMessageReply: "reply:sw-v2", + }); + + await Promise.all([ + extensionV2.unload(), + // test extension v1 wrapper has to be unloaded explicitly, otherwise + // will be detected as a failure by the test harness. + extensionV1.unload(), + ]); + await waitForServiceWorkerTerminated(swRegInfo); + await waitForServiceWorkerRegistrationsRemoved(extensionV2); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js b/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js new file mode 100644 index 0000000000..e472a6d064 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js @@ -0,0 +1,125 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function assert_background_serviceworker_pref_disabled() { + is( + WebExtensionPolicy.backgroundServiceWorkerEnabled, + false, + "Expect extensions.backgroundServiceWorker.enabled to be false" + ); +}); + +add_task(async function test_background_serviceworker_disallowed() { + const id = "test-disallowed-worker@test"; + + const extensionData = { + manifest: { + background: { + service_worker: "sw.js", + }, + applicantions: { gecko: { id } }, + useAddonManager: "temporary", + }, + }; + + SimpleTest.waitForExplicitFinish(); + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [ + { + message: /Reading manifest: Error processing background: background.service_worker is currently disabled/, + }, + ]); + }); + + const extension = ExtensionTestUtils.loadExtension(extensionData); + await Assert.rejects( + extension.startup(), + /startup failed/, + "Startup failed with background.service_worker while disabled by pref" + ); + + SimpleTest.endMonitorConsole(); + await waitForConsole; +}); + +add_task(async function test_serviceWorker_register_disallowed() { + // Verify that setting extensions.serviceWorkerRegist.allowed pref to false + // doesn't allow serviceWorker.register if backgroundServiceWorkeEnable is + // set to false + + await SpecialPowers.pushPrefEnv({ + set: [["extensions.serviceWorkerRegister.allowed", true]], + }); + + let extensionData = { + files: { + "page.html": "", + "page.js": async function() { + try { + await navigator.serviceWorker.register("sw.js"); + browser.test.fail( + `An extension page should not be able to register a serviceworker successfully` + ); + } catch (err) { + browser.test.assertEq( + String(err), + "SecurityError: The operation is insecure.", + "Got the expected error on registering a service worker from a script" + ); + } + browser.test.sendMessage("test-serviceWorker-register-disallowed"); + }, + "sw.js": "", + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + // Verify that an extension page can't register a moz-extension url + // as a service worker. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `moz-extension://${extension.uuid}/page.html`, + }, + async () => { + await extension.awaitMessage("test-serviceWorker-register-disallowed"); + } + ); + + await extension.unload(); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_cache_api_disallowed() { + // Verify that Cache API support for moz-extension url availability is also + // conditioned by the extensions.backgroundServiceWorker.enabled pref. + const extension = ExtensionTestUtils.loadExtension({ + async background() { + try { + const cache = await window.caches.open("test-cache-api"); + let url = browser.runtime.getURL("file.txt"); + await browser.test.assertRejects( + cache.add(url), + new RegExp(`Cache.add: Request URL ${url} must be either`), + "Got the expected rejections on calling cache.add with a moz-extension:// url" + ); + } catch (err) { + browser.test.fail(`Unexpected error on using Cache API: ${err}`); + throw err; + } finally { + browser.test.sendMessage("test-cache-api-disallowed"); + } + }, + files: { + "file.txt": "file content", + }, + }); + + await extension.startup(); + await extension.awaitMessage("test-cache-api-disallowed"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js b/toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js new file mode 100644 index 0000000000..f8672597cd --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_downloads_filters.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"; + +async function testAppliedFilters(ext, expectedFilter, expectedFilterCount) { + let tempDir = FileUtils.getDir( + "TmpD", + [`testDownloadDir-${Math.random()}`], + true + ); + + let filterCount = 0; + + let MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window); + MockFilePicker.displayDirectory = tempDir; + MockFilePicker.returnValue = MockFilePicker.returnCancel; + MockFilePicker.appendFiltersCallback = function(fp, val) { + const hexstr = "0x" + ("000" + val.toString(16)).substr(-3); + filterCount++; + if (filterCount < expectedFilterCount) { + is(val, expectedFilter, "Got expected filter: " + hexstr); + } else if (filterCount == expectedFilterCount) { + is(val, MockFilePicker.filterAll, "Got all files filter: " + hexstr); + } else { + is(val, null, "Got unexpected filter: " + hexstr); + } + }; + MockFilePicker.showCallback = function(fp) { + const filename = fp.defaultString; + info("MockFilePicker - save as: " + filename); + }; + + let manifest = { + description: ext, + permissions: ["downloads"], + }; + + const extension = ExtensionTestUtils.loadExtension({ + manifest: manifest, + + background: async function() { + let ext = chrome.runtime.getManifest().description; + await browser.test.assertRejects( + browser.downloads.download({ + url: "http://any-origin/any-path/any-resource", + filename: "any-file" + ext, + saveAs: true, + }), + "Download canceled by the user", + "expected request to be canceled" + ); + browser.test.sendMessage("canceled"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("canceled"); + await extension.unload(); + + is( + filterCount, + expectedFilterCount, + "Got correct number of filters: " + filterCount + ); + + MockFilePicker.cleanup(); + + tempDir.remove(true); +} + +// Missing extension +add_task(async function testDownload_missing_All() { + await testAppliedFilters("", null, 1); +}); + +// Unrecognized extension +add_task(async function testDownload_unrecognized_All() { + await testAppliedFilters(".xxx", null, 1); +}); + +// Recognized extensions +add_task(async function testDownload_html_HTML() { + await testAppliedFilters(".html", Ci.nsIFilePicker.filterHTML, 2); +}); + +add_task(async function testDownload_xhtml_HTML() { + await testAppliedFilters(".xhtml", Ci.nsIFilePicker.filterHTML, 2); +}); + +add_task(async function testDownload_txt_Text() { + await testAppliedFilters(".txt", Ci.nsIFilePicker.filterText, 2); +}); + +add_task(async function testDownload_text_Text() { + await testAppliedFilters(".text", Ci.nsIFilePicker.filterText, 2); +}); + +add_task(async function testDownload_jpe_Images() { + await testAppliedFilters(".jpe", Ci.nsIFilePicker.filterImages, 2); +}); + +add_task(async function testDownload_tif_Images() { + await testAppliedFilters(".tif", Ci.nsIFilePicker.filterImages, 2); +}); + +add_task(async function testDownload_webp_Images() { + await testAppliedFilters(".webp", Ci.nsIFilePicker.filterImages, 2); +}); + +add_task(async function testDownload_xml_XML() { + await testAppliedFilters(".xml", Ci.nsIFilePicker.filterXML, 2); +}); + +add_task(async function testDownload_aac_Audio() { + await testAppliedFilters(".aac", Ci.nsIFilePicker.filterAudio, 2); +}); + +add_task(async function testDownload_mp3_Audio() { + await testAppliedFilters(".mp3", Ci.nsIFilePicker.filterAudio, 2); +}); + +add_task(async function testDownload_wma_Audio() { + await testAppliedFilters(".wma", Ci.nsIFilePicker.filterAudio, 2); +}); + +add_task(async function testDownload_avi_Video() { + await testAppliedFilters(".avi", Ci.nsIFilePicker.filterVideo, 2); +}); + +add_task(async function testDownload_mp4_Video() { + await testAppliedFilters(".mp4", Ci.nsIFilePicker.filterVideo, 2); +}); + +add_task(async function testDownload_xvid_Video() { + await testAppliedFilters(".xvid", Ci.nsIFilePicker.filterVideo, 2); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js b/toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js new file mode 100644 index 0000000000..0e88a15782 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js @@ -0,0 +1,91 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +const URL_PATH = "browser/toolkit/components/extensions/test/browser/data"; +const TEST_URL = `http://example.com/${URL_PATH}/test_downloads_referrer.html`; +const DOWNLOAD_URL = `http://example.com/${URL_PATH}/test-download.txt`; + +async function triggerSaveAs({ selector }) { + const contextMenu = window.document.getElementById("contentAreaContextMenu"); + const popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + await BrowserTestUtils.synthesizeMouseAtCenter( + selector, + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + await popupshown; + let saveLinkCommand = window.document.getElementById("context-savelink"); + contextMenu.activateItem(saveLinkCommand); +} + +add_setup(() => { + const tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + tempDir.append("test-download-dir"); + if (!tempDir.exists()) { + tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + + let MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window); + registerCleanupFunction(function() { + MockFilePicker.cleanup(); + + if (tempDir.exists()) { + tempDir.remove(true); + } + }); + + MockFilePicker.displayDirectory = tempDir; + MockFilePicker.showCallback = function(fp) { + info("MockFilePicker: shown"); + const filename = fp.defaultString; + info("MockFilePicker: save as " + filename); + const destFile = tempDir.clone(); + destFile.append(filename); + MockFilePicker.setFiles([destFile]); + info("MockFilePicker: showCallback done"); + }; +}); + +add_task(async function test_download_item_referrer_info() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["downloads"], + }, + async background() { + browser.downloads.onCreated.addListener(async downloadInfo => { + browser.test.sendMessage("download-on-created", downloadInfo); + }); + browser.downloads.onChanged.addListener(async downloadInfo => { + // Wait download to be completed. + if (downloadInfo.state?.current !== "complete") { + return; + } + browser.test.sendMessage("download-completed"); + }); + + // Call an API method implemented in the parent process to make sure + // registering the downloas.onCreated event listener has been completed. + await browser.runtime.getBrowserInfo(); + + browser.test.sendMessage("bg-page:ready"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("bg-page:ready"); + + await BrowserTestUtils.withNewTab({ gBrowser, url: TEST_URL }, async () => { + await triggerSaveAs({ selector: "a.test-link" }); + const downloadInfo = await extension.awaitMessage("download-on-created"); + is(downloadInfo.url, DOWNLOAD_URL, "Got the expected download url"); + is(downloadInfo.referrer, TEST_URL, "Got the expected referrer"); + }); + + // Wait for the download to have been completed and removed. + await extension.awaitMessage("download-completed"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js b/toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js new file mode 100644 index 0000000000..0bab6fd9bf --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js @@ -0,0 +1,83 @@ +"use strict"; + +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); + +const { AppUiTestDelegate } = ChromeUtils.import( + "resource://testing-common/AppUiTestDelegate.jsm" +); + +// Ignore error "Actor 'Conduits' destroyed before query 'RunListener' was resolved" +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Actor 'Conduits' destroyed before query 'RunListener'/ +); + +async function run_test_disableResetIdleForTest(options) { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + action: {}, + }, + background() { + browser.action.onClicked.addListener(async () => { + browser.test.notifyPass("action-clicked"); + // Deliberately keep this listener active to simulate a still active listener + // callback, while calling extension.terminateBackground(). + await new Promise(() => {}); + }); + + browser.test.sendMessage("background-ready"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + // After startup, the listener should be persistent but not primed. + assertPersistentListeners(extension, "browserAction", "onClicked", { + primed: false, + }); + + // Terminating the background should prime the persistent listener. + await extension.terminateBackground(); + assertPersistentListeners(extension, "browserAction", "onClicked", { + primed: true, + }); + + // Wake up the background, and verify the listener is no longer primed. + await AppUiTestDelegate.clickBrowserAction(window, extension.id); + await extension.awaitFinish("action-clicked"); + await AppUiTestDelegate.closeBrowserAction(window, extension.id); + await extension.awaitMessage("background-ready"); + assertPersistentListeners(extension, "browserAction", "onClicked", { + primed: false, + }); + + // Terminate the background again, while the onClicked listener is still + // being executed. + // With options.disableResetIdleForTest = true, the termination should NOT + // be skipped and the listener should become primed again. + // With options.disableResetIdleForTest = false or unset, the termination + // should be skipped and the listener should not become primed. + await extension.terminateBackground(options); + assertPersistentListeners(extension, "browserAction", "onClicked", { + primed: !!options?.disableResetIdleForTest, + }); + + await extension.unload(); +} + +// Verify default behaviour when terminating a background while a +// listener is still running: The background should not be terminated +// and the listener should not become primed. Not specifyiny a value +// for disableResetIdleForTest defauls to disableResetIdleForTest:false. +add_task(async function test_disableResetIdleForTest_default() { + await run_test_disableResetIdleForTest(); +}); + +// Verify that disableResetIdleForTest:true is honoured and terminating +// a background while a listener is still running is enforced: The +// background should be terminated and the listener should become primed. +add_task(async function test_disableResetIdleForTest_true() { + await run_test_disableResetIdleForTest({ disableResetIdleForTest: true }); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js b/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js new file mode 100644 index 0000000000..4bb5d21dbd --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); + +// The test tasks in this test file tends to trigger an intermittent +// exception raised from JSActor::AfterDestroy, because of a race between +// when the WebExtensions API event is being emitted from the parent process +// and the navigation triggered on the test extension pages. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Actor 'Conduits' destroyed before query 'RunListener' was resolved/ +); + +AddonTestUtils.initMochitest(this); + +const server = AddonTestUtils.createHttpServer({ + hosts: ["example.com", "anotherwebpage.org"], +}); + +server.registerPathHandler("/", (request, response) => { + response.write(` + + + + test webpage + + + `); +}); + +function createTestExtPage({ script }) { + return ` + + + + + + + `; +} + +function createTestExtPageScript(name) { + return `(${function(pageName) { + browser.webRequest.onBeforeRequest.addListener( + details => { + browser.test.log( + `Extension page "${pageName}" got a webRequest event: ${details.url}` + ); + browser.test.sendMessage(`event-received:${pageName}`); + }, + { types: ["main_frame"], urls: ["http://example.com/*"] } + ); + /* eslint-disable mozilla/balanced-listeners */ + window.addEventListener("pageshow", () => { + browser.test.log(`Extension page "${pageName}" got a pageshow event`); + browser.test.sendMessage(`pageshow:${pageName}`); + }); + window.addEventListener("pagehide", () => { + browser.test.log(`Extension page "${pageName}" got a pagehide event`); + browser.test.sendMessage(`pagehide:${pageName}`); + }); + /* eslint-enable mozilla/balanced-listeners */ + }})("${name}");`; +} + +// Triggers a WebRequest listener registered by the test extensions by +// opening a tab on the given web page URL and then closing it after +// it did load. +async function triggerWebRequestListener(webPageURL, pause) { + let webPageTab = await BrowserTestUtils.openNewForegroundTab( + { + gBrowser, + url: webPageURL, + }, + true /* waitForLoad */, + true /* waitForStop */ + ); + BrowserTestUtils.removeTab(webPageTab); +} + +// The following tests tasks are testing the expected behaviors related to same-process and cross-process +// navigations for an extension page, similarly to test_ext_extension_page_navigated.js, but unlike its +// xpcshell counterpart this tests are only testing that after navigating back to an extension page +// previously stored in the BFCache the WebExtensions events subscribed are being received as expected. + +add_task(async function test_extension_page_sameprocess_navigation() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "http://example.com/*"], + }, + files: { + "extpage1.html": createTestExtPage({ script: "extpage1.js" }), + "extpage1.js": createTestExtPageScript("extpage1"), + "extpage2.html": createTestExtPage({ script: "extpage2.js" }), + "extpage2.js": createTestExtPageScript("extpage2"), + }, + }); + + await extension.startup(); + + const policy = WebExtensionPolicy.getByID(extension.id); + + const extPageURL1 = policy.extension.baseURI.resolve("extpage1.html"); + const extPageURL2 = policy.extension.baseURI.resolve("extpage2.html"); + + info("Opening extension page in a new tab"); + const extPageTab = await BrowserTestUtils.addTab(gBrowser, extPageURL1); + let browser = gBrowser.getBrowserForTab(extPageTab); + info("Wait for the extension page to be loaded"); + await extension.awaitMessage("pageshow:extpage1"); + + await triggerWebRequestListener("http://example.com"); + await extension.awaitMessage("event-received:extpage1"); + ok(true, "extpage1 got a webRequest event as expected"); + + info("Load a second extension page in the same tab"); + BrowserTestUtils.loadURI(browser, extPageURL2); + + info("Wait extpage1 to receive a pagehide event"); + await extension.awaitMessage("pagehide:extpage1"); + info("Wait extpage2 to receive a pageshow event"); + await extension.awaitMessage("pageshow:extpage2"); + + info( + "Trigger a web request event and expect extpage2 to be the only one receiving it" + ); + await triggerWebRequestListener("http://example.com"); + await extension.awaitMessage("event-received:extpage2"); + ok(true, "extpage2 got a webRequest event as expected"); + + info( + "Navigating back to extpage1 and expect extpage2 to be the only one receiving the webRequest event" + ); + + browser.goBack(); + info("Wait for extpage1 to receive a pageshow event"); + await extension.awaitMessage("pageshow:extpage1"); + info("Wait for extpage2 to receive a pagehide event"); + await extension.awaitMessage("pagehide:extpage2"); + + // We only expect extpage1 to be able to receive API events. + await triggerWebRequestListener("http://example.com"); + await extension.awaitMessage("event-received:extpage1"); + ok(true, "extpage1 got a webRequest event as expected"); + + BrowserTestUtils.removeTab(extPageTab); + await extension.awaitMessage("pagehide:extpage1"); + + await extension.unload(); +}); + +add_task(async function test_extension_page_context_navigated_to_web_page() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "http://example.com/*"], + }, + files: { + "extpage.html": createTestExtPage({ script: "extpage.js" }), + "extpage.js": createTestExtPageScript("extpage"), + }, + }); + + await extension.startup(); + + const policy = WebExtensionPolicy.getByID(extension.id); + + const extPageURL = policy.extension.baseURI.resolve("extpage.html"); + // NOTE: this test will navigate the extension page to a webpage url that + // isn't matching the match pattern the test extension is going to use + // in its webRequest event listener, otherwise the extension page being + // navigated will be intermittently able to receive an event before it + // is navigated to the webpage url (and moved into the BFCache or destroyed) + // and trigger an intermittent failure of this test. + const webPageURL = "http://anotherwebpage.org/"; + const triggerWebRequestURL = "http://example.com/"; + + info("Opening extension page in a new tab"); + const extPageTab1 = await BrowserTestUtils.addTab(gBrowser, extPageURL); + let browserForTab1 = gBrowser.getBrowserForTab(extPageTab1); + info("Wait for the extension page to be loaded"); + await extension.awaitMessage("pageshow:extpage"); + + info("Navigate the tab from the extension page to a web page"); + let promiseLoaded = BrowserTestUtils.browserLoaded( + browserForTab1, + false, + webPageURL + ); + BrowserTestUtils.loadURI(browserForTab1, webPageURL); + info("Wait the tab to have loaded the new webpage url"); + await promiseLoaded; + info("Wait the extension page to receive a pagehide event"); + await extension.awaitMessage("pagehide:extpage"); + + // Trigger a webRequest listener, the extension page is expected to + // not be active, if that isn't the case a test message will be queued + // and will trigger an explicit test failure. + await triggerWebRequestListener(triggerWebRequestURL); + + info("Navigate back to the extension page"); + browserForTab1.goBack(); + info("Wait for extension page to receive a pageshow event"); + await extension.awaitMessage("pageshow:extpage"); + + await triggerWebRequestListener(triggerWebRequestURL); + await extension.awaitMessage("event-received:extpage"); + ok( + true, + "extpage got a webRequest event as expected after being restored from BFCache" + ); + + info("Cleanup and exit test"); + BrowserTestUtils.removeTab(extPageTab1); + + info("Wait the extension page to receive a pagehide event"); + await extension.awaitMessage("pagehide:extpage"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_management_themes.js b/toolkit/components/extensions/test/browser/browser_ext_management_themes.js new file mode 100644 index 0000000000..d3cfa536b8 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_management_themes.js @@ -0,0 +1,177 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +const { BuiltInThemes } = ChromeUtils.importESModule( + "resource:///modules/BuiltInThemes.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); + +add_task(async function test_management_themes() { + await BuiltInThemes.ensureBuiltInThemes(); + + const TEST_ID = "test_management_themes@tests.mozilla.com"; + + let theme = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Simple theme test", + version: "1.0", + description: "test theme", + theme: { + images: { + theme_frame: "image1.png", + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + useAddonManager: "temporary", + }); + + async function background(TEST_ID) { + browser.management.onInstalled.addListener(info => { + if (info.name == TEST_ID) { + return; + } + browser.test.log(`${info.name} was installed`); + browser.test.assertEq(info.type, "theme", "addon is theme"); + browser.test.sendMessage("onInstalled", info.name); + }); + browser.management.onDisabled.addListener(info => { + browser.test.log(`${info.name} was disabled`); + browser.test.assertEq(info.type, "theme", "addon is theme"); + browser.test.sendMessage("onDisabled", info.name); + }); + browser.management.onEnabled.addListener(info => { + browser.test.log(`${info.name} was enabled`); + browser.test.assertEq(info.type, "theme", "addon is theme"); + browser.test.sendMessage("onEnabled", info.name); + }); + browser.management.onUninstalled.addListener(info => { + browser.test.log(`${info.name} was uninstalled`); + browser.test.assertEq(info.type, "theme", "addon is theme"); + browser.test.sendMessage("onUninstalled", info.name); + }); + + async function getAddon(type) { + let addons = await browser.management.getAll(); + let themes = addons.filter(addon => addon.type === "theme"); + const STANDARD_BUILTIN_THEME_IDS = [ + "default-theme@mozilla.org", + "firefox-compact-light@mozilla.org", + "firefox-compact-dark@mozilla.org", + "firefox-alpenglow@mozilla.org", + ]; + // Check that management.getAll returns the built-in themes and our test + // extension. + for (let id of [...STANDARD_BUILTIN_THEME_IDS, TEST_ID]) { + let builtInExtension = addons.find(addon => { + return addon.id === id; + }); + browser.test.assertTrue( + !!builtInExtension, + `The extension with id ${id} was returned by getAll.` + ); + } + let found; + for (let addon of themes) { + browser.test.assertEq(addon.type, "theme", "addon is theme"); + if (type == "theme" && addon.id.includes("temporary-addon")) { + found = addon; + } else if (type == "enabled" && addon.enabled) { + found = addon; + } + } + return found; + } + + browser.test.onMessage.addListener(async msg => { + let theme = await getAddon("theme"); + browser.test.assertEq( + theme.description, + "test theme", + "description is correct" + ); + browser.test.assertTrue(theme.enabled, "theme is enabled"); + await browser.management.setEnabled(theme.id, false); + + theme = await getAddon("theme"); + + browser.test.assertTrue(!theme.enabled, "theme is disabled"); + let addon = getAddon("enabled"); + browser.test.assertTrue(addon, "another theme was enabled"); + + await browser.management.setEnabled(theme.id, true); + theme = await getAddon("theme"); + addon = await getAddon("enabled"); + browser.test.assertEq(theme.id, addon.id, "theme is enabled"); + + browser.test.sendMessage("done"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: TEST_ID, + }, + }, + name: TEST_ID, + permissions: ["management"], + }, + background: `(${background})("${TEST_ID}")`, + useAddonManager: "temporary", + }); + await extension.startup(); + + await theme.startup(); + is( + await extension.awaitMessage("onInstalled"), + "Simple theme test", + "webextension theme installed" + ); + is( + await extension.awaitMessage("onDisabled"), + "System theme — auto", + "default disabled" + ); + + extension.sendMessage("test"); + is( + await extension.awaitMessage("onEnabled"), + "System theme — auto", + "default enabled" + ); + is( + await extension.awaitMessage("onDisabled"), + "Simple theme test", + "addon disabled" + ); + is( + await extension.awaitMessage("onEnabled"), + "Simple theme test", + "addon enabled" + ); + is( + await extension.awaitMessage("onDisabled"), + "System theme — auto", + "default disabled" + ); + await extension.awaitMessage("done"); + + await Promise.all([theme.unload(), extension.awaitMessage("onUninstalled")]); + + is( + await extension.awaitMessage("onEnabled"), + "System theme — auto", + "default enabled" + ); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_test_mock.js b/toolkit/components/extensions/test/browser/browser_ext_test_mock.js new file mode 100644 index 0000000000..de496b7631 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_test_mock.js @@ -0,0 +1,47 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// This test verifies that the extension mocks behave consistently, regardless +// of test type (xpcshell vs browser test). +// See also toolkit/components/extensions/test/xpcshell/test_ext_test_mock.js + +// Check the state of the extension object. This should be consistent between +// browser tests and xpcshell tests. +async function checkExtensionStartupAndUnload(ext) { + await ext.startup(); + Assert.ok(ext.id, "Extension ID should be available"); + Assert.ok(ext.uuid, "Extension UUID should be available"); + await ext.unload(); + // Once set nothing clears the UUID. + Assert.ok(ext.uuid, "Extension UUID exists after unload"); +} + +add_task(async function test_MockExtension() { + // When "useAddonManager" is set, a MockExtension is created in the main + // process, which does not necessarily behave identically to an Extension. + let ext = ExtensionTestUtils.loadExtension({ + // xpcshell/test_ext_test_mock.js tests "temporary", so here we use + // "permanent" to have even more test coverage. + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: "@permanent-mock-extension" } }, + }, + }); + + Assert.ok(!ext.id, "Extension ID is initially unavailable"); + Assert.ok(!ext.uuid, "Extension UUID is initially unavailable"); + await checkExtensionStartupAndUnload(ext); + Assert.ok(ext.id, "Extension ID exists after unload"); +}); + +add_task(async function test_generated_Extension() { + let ext = ExtensionTestUtils.loadExtension({ + manifest: {}, + }); + + Assert.ok(!ext.id, "Extension ID is initially unavailable"); + Assert.ok(!ext.uuid, "Extension UUID is initially unavailable"); + await checkExtensionStartupAndUnload(ext); + Assert.ok(ext.id, "Extension ID exists after unload"); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js b/toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js new file mode 100644 index 0000000000..cc814b4ae8 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js @@ -0,0 +1,119 @@ +"use strict"; + +// Case 1 - When there is a theme_frame image and additional_backgrounds_alignment is not specified. +// So background-position should default to "right top" +add_task(async function test_default_additional_backgrounds_alignment() { + const RIGHT_TOP = "100% 0%"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + additional_backgrounds: ["image1.png", "image1.png"], + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let toolbox = document.querySelector("#navigator-toolbox"); + let toolboxCS = window.getComputedStyle(toolbox); + if (backgroundColorSetOnRoot()) { + let docEl = document.documentElement; + let rootCS = window.getComputedStyle(docEl); + + Assert.equal( + rootCS.getPropertyValue("background-position"), + `${RIGHT_TOP}, ${RIGHT_TOP}`, + "root contains theme_frame and lwt-background-alignment properties" + ); + Assert.equal( + toolboxCS.getPropertyValue("background-position"), + RIGHT_TOP, + toolbox.id + " contains lwt-background-alignment properties" + ); + } else { + /** + * We expect duplicate background-position values because we apply `right top` + * once for theme_frame, and again as the default value of + * --lwt-background-alignment. + */ + Assert.equal( + toolboxCS.getPropertyValue("background-position"), + `${RIGHT_TOP}, ${RIGHT_TOP}`, + toolbox.id + + " contains theme_frame and default lwt-background-alignment properties" + ); + } + + await extension.unload(); +}); + +// Case 2 - When there is a theme_frame image and additional_backgrounds_alignment is specified. +add_task(async function test_additional_backgrounds_alignment() { + const LEFT_BOTTOM = "0% 100%"; + const CENTER_CENTER = "50% 50%"; + const RIGHT_TOP = "100% 0%"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + additional_backgrounds: ["image1.png", "image1.png", "image1.png"], + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + }, + properties: { + additional_backgrounds_alignment: [ + "left bottom", + "center center", + "right top", + ], + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let toolbox = document.querySelector("#navigator-toolbox"); + let toolboxCS = window.getComputedStyle(toolbox); + if (backgroundColorSetOnRoot()) { + let docEl = document.documentElement; + let rootCS = window.getComputedStyle(docEl); + Assert.equal( + rootCS.getPropertyValue("background-position"), + `${RIGHT_TOP}, ${LEFT_BOTTOM}, ${CENTER_CENTER}, ${RIGHT_TOP}`, + "root contains theme_frame and additional_backgrounds alignment properties" + ); + Assert.equal( + toolboxCS.getPropertyValue("background-position"), + LEFT_BOTTOM + ", " + CENTER_CENTER + ", " + RIGHT_TOP, + toolbox.id + " contains additional_backgrounds alignment properties" + ); + } else { + Assert.equal( + toolboxCS.getPropertyValue("background-position"), + RIGHT_TOP + ", " + LEFT_BOTTOM + ", " + CENTER_CENTER + ", " + RIGHT_TOP, + toolbox.id + + " contains theme_frame and additional_backgrounds alignment properties" + ); + } + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js b/toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js new file mode 100644 index 0000000000..2799d37551 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js @@ -0,0 +1,39 @@ +"use strict"; + +add_task(async function test_alpha_frame_color() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: "rgba(230, 128, 0, 0.1)", + tab_background_text: TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let computedStyle; + if (backgroundColorSetOnRoot()) { + let docEl = window.document.documentElement; + computedStyle = window.getComputedStyle(docEl); + } else { + let toolbox = document.querySelector("#navigator-toolbox"); + computedStyle = window.getComputedStyle(toolbox); + } + + Assert.equal( + computedStyle.backgroundColor, + "rgb(230, 128, 0)", + "Window background color should be opaque" + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js b/toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js new file mode 100644 index 0000000000..5f9bf188fc --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js @@ -0,0 +1,88 @@ +"use strict"; + +function openIdentityPopup() { + let promise = BrowserTestUtils.waitForEvent( + window, + "popupshown", + true, + event => event.target == gIdentityHandler._identityPopup + ); + gIdentityHandler._identityIconBox.click(); + return promise; +} + +function closeIdentityPopup() { + let promise = BrowserTestUtils.waitForEvent( + gIdentityHandler._identityPopup, + "popuphidden" + ); + gIdentityHandler._identityPopup.hidePopup(); + return promise; +} + +// This test checks applied WebExtension themes that attempt to change +// popup properties + +add_task(async function test_popup_styling(browser, accDoc) { + const POPUP_BACKGROUND_COLOR = "#FF0000"; + const POPUP_TEXT_COLOR = "#008000"; + const POPUP_BORDER_COLOR = "#0000FF"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + popup: POPUP_BACKGROUND_COLOR, + popup_text: POPUP_TEXT_COLOR, + popup_border: POPUP_BORDER_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "https://example.com" }, + async function(browser) { + await extension.startup(); + + // Open the information arrow panel + await openIdentityPopup(); + + let arrowContent = gIdentityHandler._identityPopup.panelContent; + let arrowContentComputedStyle = window.getComputedStyle(arrowContent); + // Ensure popup background color was set properly + Assert.equal( + arrowContentComputedStyle.getPropertyValue("background-color"), + `rgb(${hexToRGB(POPUP_BACKGROUND_COLOR).join(", ")})`, + "Popup background color should have been themed" + ); + + // Ensure popup text color was set properly + Assert.equal( + arrowContentComputedStyle.getPropertyValue("color"), + `rgb(${hexToRGB(POPUP_TEXT_COLOR).join(", ")})`, + "Popup text color should have been themed" + ); + + Assert.equal( + arrowContentComputedStyle.getPropertyValue("--panel-description-color"), + `rgba(${hexToRGB(POPUP_TEXT_COLOR).join(", ")}, 0.7)`, + "Popup text description color should have been themed" + ); + + // Ensure popup border color was set properly + testBorderColor(arrowContent, POPUP_BORDER_COLOR); + + await closeIdentityPopup(); + await extension.unload(); + } + ); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js b/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js new file mode 100644 index 0000000000..115a22cdd7 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js @@ -0,0 +1,173 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// popup properties are applied correctly to the autocomplete bar. +const POPUP_COLOR_DARK = "#00A400"; +const POPUP_COLOR_BRIGHT = "#85A4FF"; +const POPUP_TEXT_COLOR_DARK = "#000000"; +const POPUP_TEXT_COLOR_BRIGHT = "#ffffff"; +const POPUP_SELECTED_COLOR = "#9400ff"; +const POPUP_SELECTED_TEXT_COLOR = "#09b9a6"; + +const POPUP_URL_COLOR_DARK = "#0061e0"; +const POPUP_ACTION_COLOR_DARK = "#5b5b66"; +const POPUP_URL_COLOR_BRIGHT = "#00ddff"; +const POPUP_ACTION_COLOR_BRIGHT = "#bfbfc9"; + +const SEARCH_TERM = "urlbar-reflows-" + Date.now(); + +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +add_setup(async function() { + await PlacesUtils.history.clear(); + const NUM_VISITS = 10; + let visits = []; + + for (let i = 0; i < NUM_VISITS; ++i) { + visits.push({ + uri: `http://example.com/urlbar-reflows-${i}`, + title: `Reflow test for URL bar entry #${i} - ${SEARCH_TERM}`, + }); + } + + await PlacesTestUtils.addVisits(visits); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_popup_url() { + // Load a manifest with popup_text being dark (bright background). Test for + // dark text properties. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_field_focus: POPUP_COLOR_BRIGHT, + toolbar_field_text_focus: POPUP_TEXT_COLOR_DARK, + popup_highlight: POPUP_SELECTED_COLOR, + popup_highlight_text: POPUP_SELECTED_TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + await BrowserTestUtils.removeTab(tab); + }); + + let visits = []; + + for (let i = 0; i < maxResults; i++) { + visits.push({ uri: makeURI("http://example.com/autocomplete/?" + i) }); + } + + await PlacesTestUtils.addVisits(visits); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "example.com/autocomplete", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, maxResults - 1); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + maxResults, + "Should get maxResults=" + maxResults + " results" + ); + + // Set the selected attribute to true to test the highlight popup properties + UrlbarTestUtils.setSelectedRowIndex(window, 1); + let actionResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let urlResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + let resultCS = window.getComputedStyle(urlResult.element.row._content); + + Assert.equal( + resultCS.backgroundColor, + `rgb(${hexToRGB(POPUP_SELECTED_COLOR).join(", ")})`, + `Popup highlight background color should be set to ${POPUP_SELECTED_COLOR}` + ); + + Assert.equal( + resultCS.color, + `rgb(${hexToRGB(POPUP_SELECTED_TEXT_COLOR).join(", ")})`, + `Popup highlight color should be set to ${POPUP_SELECTED_TEXT_COLOR}` + ); + + // Now set the index to somewhere not on the first two, so that we can test both + // url and action text colors. + UrlbarTestUtils.setSelectedRowIndex(window, 2); + + Assert.equal( + window.getComputedStyle(urlResult.element.url).color, + `rgb(${hexToRGB(POPUP_URL_COLOR_DARK).join(", ")})`, + `Urlbar popup url color should be set to ${POPUP_URL_COLOR_DARK}` + ); + + Assert.equal( + window.getComputedStyle(actionResult.element.action).color, + `rgb(${hexToRGB(POPUP_ACTION_COLOR_DARK).join(", ")})`, + `Urlbar popup action color should be set to ${POPUP_ACTION_COLOR_DARK}` + ); + + await extension.unload(); + + // Load a manifest with popup_text being bright (dark background). Test for + // bright text properties. + extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_field_focus: POPUP_COLOR_DARK, + toolbar_field_text_focus: POPUP_TEXT_COLOR_BRIGHT, + popup_highlight: POPUP_SELECTED_COLOR, + popup_highlight_text: POPUP_SELECTED_TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + Assert.equal( + window.getComputedStyle(urlResult.element.url).color, + `rgb(${hexToRGB(POPUP_URL_COLOR_BRIGHT).join(", ")})`, + `Urlbar popup url color should be set to ${POPUP_URL_COLOR_BRIGHT}` + ); + + Assert.equal( + window.getComputedStyle(actionResult.element.action).color, + `rgb(${hexToRGB(POPUP_ACTION_COLOR_BRIGHT).join(", ")})`, + `Urlbar popup action color should be set to ${POPUP_ACTION_COLOR_BRIGHT}` + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js b/toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js new file mode 100644 index 0000000000..e88c78fa93 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js @@ -0,0 +1,213 @@ +"use strict"; + +add_task(async function test_support_theme_frame() { + const FRAME_COLOR = [71, 105, 91]; + const TAB_TEXT_COLOR = [0, 0, 0]; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "face.png", + }, + colors: { + frame: FRAME_COLOR, + tab_background_text: TAB_TEXT_COLOR, + }, + }, + }, + files: { + "face.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA), + }, + }); + + await extension.startup(); + + let docEl = window.document.documentElement; + Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set"); + + Assert.ok( + docEl.hasAttribute("lwtheme-image"), + "LWT image attribute should be set" + ); + + Assert.equal( + docEl.getAttribute("lwtheme-brighttext"), + null, + "LWT text color attribute should not be set" + ); + + let toolbox = document.querySelector("#navigator-toolbox"); + let toolboxCS = window.getComputedStyle(toolbox); + + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + Assert.ok( + rootCS.backgroundImage.includes("face.png"), + `The backgroundImage should use face.png. Actual value is: ${toolboxCS.backgroundImage}` + ); + Assert.equal( + rootCS.backgroundColor, + "rgb(" + FRAME_COLOR.join(", ") + ")", + "Expected correct background color" + ); + } else { + Assert.ok( + toolboxCS.backgroundImage.includes("face.png"), + `The backgroundImage should use face.png. Actual value is: ${toolboxCS.backgroundImage}` + ); + Assert.equal( + toolboxCS.backgroundColor, + "rgb(" + FRAME_COLOR.join(", ") + ")", + "Expected correct background color" + ); + } + Assert.equal( + toolboxCS.color, + "rgb(" + TAB_TEXT_COLOR.join(", ") + ")", + "Expected correct text color" + ); + + await extension.unload(); + + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); + + Assert.ok( + !docEl.hasAttribute("lwtheme-image"), + "LWT image attribute should not be set" + ); + + Assert.ok( + !docEl.hasAttribute("lwtheme-brighttext"), + "LWT text color attribute should not be set" + ); +}); + +add_task(async function test_support_theme_frame_inactive() { + const FRAME_COLOR = [71, 105, 91]; + const FRAME_COLOR_INACTIVE = [255, 0, 0]; + const TAB_TEXT_COLOR = [207, 221, 192]; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: FRAME_COLOR, + frame_inactive: FRAME_COLOR_INACTIVE, + tab_background_text: TAB_TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let docEl = window.document.documentElement; + let toolbox = document.querySelector("#navigator-toolbox"); + let toolboxCS = window.getComputedStyle(toolbox); + + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + Assert.equal( + rootCS.backgroundColor, + "rgb(" + FRAME_COLOR.join(", ") + ")", + "Window background is set to the colors.frame property" + ); + } else { + Assert.equal( + toolboxCS.backgroundColor, + "rgb(" + FRAME_COLOR.join(", ") + ")", + "Window background is set to the colors.frame property" + ); + } + + // Now we'll open a new window to see if the inactive browser accent color changed + let window2 = await BrowserTestUtils.openNewBrowserWindow(); + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + Assert.equal( + rootCS.backgroundColor, + "rgb(" + FRAME_COLOR_INACTIVE.join(", ") + ")", + `Inactive window root background color should be ${FRAME_COLOR_INACTIVE}` + ); + } else { + Assert.equal( + toolboxCS.backgroundColor, + "rgb(" + FRAME_COLOR_INACTIVE.join(", ") + ")", + `Inactive window background color should be ${FRAME_COLOR_INACTIVE}` + ); + } + + await BrowserTestUtils.closeWindow(window2); + await extension.unload(); + + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); +}); + +add_task(async function test_lack_of_theme_frame_inactive() { + const FRAME_COLOR = [71, 105, 91]; + const TAB_TEXT_COLOR = [207, 221, 192]; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: FRAME_COLOR, + tab_background_text: TAB_TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let docEl = window.document.documentElement; + let toolbox = document.querySelector("#navigator-toolbox"); + let toolboxCS = window.getComputedStyle(toolbox); + + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + Assert.equal( + rootCS.backgroundColor, + "rgb(" + FRAME_COLOR.join(", ") + ")", + "Window background is set to the colors.frame property" + ); + } else { + Assert.equal( + toolboxCS.backgroundColor, + "rgb(" + FRAME_COLOR.join(", ") + ")", + "Window background is set to the colors.frame property" + ); + } + + // Now we'll open a new window to make sure the inactive browser accent color stayed the same + let window2 = await BrowserTestUtils.openNewBrowserWindow(); + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + Assert.equal( + rootCS.backgroundColor, + "rgb(" + FRAME_COLOR.join(", ") + ")", + "Inactive window background should not change if colors.frame_inactive isn't set" + ); + } else { + Assert.equal( + toolboxCS.backgroundColor, + "rgb(" + FRAME_COLOR.join(", ") + ")", + "Inactive window background should not change if colors.frame_inactive isn't set" + ); + } + + await BrowserTestUtils.closeWindow(window2); + await extension.unload(); + + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js new file mode 100644 index 0000000000..4a379edfbf --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js @@ -0,0 +1,203 @@ +"use strict"; + +// This test checks whether browser.theme.getCurrent() works correctly in different +// configurations and with different parameter. + +// PNG image data for a simple red dot. +const BACKGROUND_1 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; +// PNG image data for the Mozilla dino head. +const BACKGROUND_2 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; + +add_task(async function test_get_current() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + const ACCENT_COLOR_1 = "#a14040"; + const TEXT_COLOR_1 = "#fac96e"; + + const ACCENT_COLOR_2 = "#03fe03"; + const TEXT_COLOR_2 = "#0ef325"; + + const theme1 = { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR_1, + tab_background_text: TEXT_COLOR_1, + }, + }; + + const theme2 = { + images: { + theme_frame: "image2.png", + }, + colors: { + frame: ACCENT_COLOR_2, + tab_background_text: TEXT_COLOR_2, + }, + }; + + function ensureWindowFocused(winId) { + browser.test.log("Waiting for focused window to be " + winId); + // eslint-disable-next-line no-async-promise-executor + return new Promise(async resolve => { + let listener = windowId => { + if (windowId === winId) { + browser.windows.onFocusChanged.removeListener(listener); + resolve(); + } + }; + // We first add a listener and then check whether the window is + // focused using .get(), because the .get() Promise resolving + // could race with the listener running, in which case we'd + // never be notified. + browser.windows.onFocusChanged.addListener(listener); + let { focused } = await browser.windows.get(winId); + if (focused) { + browser.windows.onFocusChanged.removeListener(listener); + resolve(); + } + }); + } + + function testTheme1(returnedTheme) { + browser.test.assertTrue( + returnedTheme.images.theme_frame.includes("image1.png"), + "Theme 1 theme_frame image should be applied" + ); + browser.test.assertEq( + ACCENT_COLOR_1, + returnedTheme.colors.frame, + "Theme 1 frame color should be applied" + ); + browser.test.assertEq( + TEXT_COLOR_1, + returnedTheme.colors.tab_background_text, + "Theme 1 tab_background_text color should be applied" + ); + } + + function testTheme2(returnedTheme) { + browser.test.assertTrue( + returnedTheme.images.theme_frame.includes("image2.png"), + "Theme 2 theme_frame image should be applied" + ); + browser.test.assertEq( + ACCENT_COLOR_2, + returnedTheme.colors.frame, + "Theme 2 frame color should be applied" + ); + browser.test.assertEq( + TEXT_COLOR_2, + returnedTheme.colors.tab_background_text, + "Theme 2 tab_background_text color should be applied" + ); + } + + function testEmptyTheme(returnedTheme) { + browser.test.assertEq( + JSON.stringify({ colors: null, images: null, properties: null }), + JSON.stringify(returnedTheme), + JSON.stringify(returnedTheme, null, 2) + ); + } + + browser.test.log("Testing getCurrent() with initial unthemed window"); + const firstWin = await browser.windows.getCurrent(); + testEmptyTheme(await browser.theme.getCurrent()); + testEmptyTheme(await browser.theme.getCurrent(firstWin.id)); + + browser.test.log("Testing getCurrent() with after theme.update()"); + await browser.theme.update(theme1); + testTheme1(await browser.theme.getCurrent()); + testTheme1(await browser.theme.getCurrent(firstWin.id)); + + browser.test.log( + "Testing getCurrent() with after theme.update(windowId)" + ); + const secondWin = await browser.windows.create(); + await ensureWindowFocused(secondWin.id); + await browser.theme.update(secondWin.id, theme2); + testTheme2(await browser.theme.getCurrent()); + testTheme1(await browser.theme.getCurrent(firstWin.id)); + testTheme2(await browser.theme.getCurrent(secondWin.id)); + + browser.test.log("Testing getCurrent() after window focus change"); + let focusChanged = ensureWindowFocused(firstWin.id); + await browser.windows.update(firstWin.id, { focused: true }); + await focusChanged; + testTheme1(await browser.theme.getCurrent()); + testTheme1(await browser.theme.getCurrent(firstWin.id)); + testTheme2(await browser.theme.getCurrent(secondWin.id)); + + browser.test.log( + "Testing getCurrent() after another window focus change" + ); + focusChanged = ensureWindowFocused(secondWin.id); + await browser.windows.update(secondWin.id, { focused: true }); + await focusChanged; + testTheme2(await browser.theme.getCurrent()); + testTheme1(await browser.theme.getCurrent(firstWin.id)); + testTheme2(await browser.theme.getCurrent(secondWin.id)); + + browser.test.log("Testing getCurrent() after theme.reset(windowId)"); + await browser.theme.reset(firstWin.id); + testTheme2(await browser.theme.getCurrent()); + testTheme1(await browser.theme.getCurrent(firstWin.id)); + testTheme2(await browser.theme.getCurrent(secondWin.id)); + + browser.test.log( + "Testing getCurrent() after reset and window focus change" + ); + focusChanged = ensureWindowFocused(firstWin.id); + await browser.windows.update(firstWin.id, { focused: true }); + await focusChanged; + testTheme1(await browser.theme.getCurrent()); + testTheme1(await browser.theme.getCurrent(firstWin.id)); + testTheme2(await browser.theme.getCurrent(secondWin.id)); + + browser.test.log("Testing getCurrent() after theme.update(windowId)"); + await browser.theme.update(firstWin.id, theme1); + testTheme1(await browser.theme.getCurrent()); + testTheme1(await browser.theme.getCurrent(firstWin.id)); + testTheme2(await browser.theme.getCurrent(secondWin.id)); + + browser.test.log("Testing getCurrent() after theme.reset()"); + await browser.theme.reset(); + testEmptyTheme(await browser.theme.getCurrent()); + testEmptyTheme(await browser.theme.getCurrent(firstWin.id)); + testEmptyTheme(await browser.theme.getCurrent(secondWin.id)); + + browser.test.log("Testing getCurrent() after closing a window"); + await browser.windows.remove(secondWin.id); + testEmptyTheme(await browser.theme.getCurrent()); + testEmptyTheme(await browser.theme.getCurrent(firstWin.id)); + + browser.test.log("Testing update calls with invalid window ID"); + await browser.test.assertRejects( + browser.theme.reset(secondWin.id), + /Invalid window/, + "Invalid window should throw" + ); + await browser.test.assertRejects( + browser.theme.update(secondWin.id, theme2), + /Invalid window/, + "Invalid window should throw" + ); + browser.test.notifyPass("get_current"); + }, + manifest: { + permissions: ["theme"], + }, + files: { + "image1.png": BACKGROUND_1, + "image2.png": BACKGROUND_2, + }, + }); + + await extension.startup(); + await extension.awaitFinish("get_current"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js new file mode 100644 index 0000000000..34c7162810 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js @@ -0,0 +1,154 @@ +"use strict"; + +// This test checks whether browser.theme.onUpdated works correctly with different +// types of dynamic theme updates. + +// PNG image data for a simple red dot. +const BACKGROUND_1 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; +// PNG image data for the Mozilla dino head. +const BACKGROUND_2 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; + +add_task(async function test_on_updated() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + const ACCENT_COLOR_1 = "#a14040"; + const TEXT_COLOR_1 = "#fac96e"; + + const ACCENT_COLOR_2 = "#03fe03"; + const TEXT_COLOR_2 = "#0ef325"; + + const theme1 = { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR_1, + tab_background_text: TEXT_COLOR_1, + }, + }; + + const theme2 = { + images: { + theme_frame: "image2.png", + }, + colors: { + frame: ACCENT_COLOR_2, + tab_background_text: TEXT_COLOR_2, + }, + }; + + function testTheme1(returnedTheme) { + browser.test.assertTrue( + returnedTheme.images.theme_frame.includes("image1.png"), + "Theme 1 theme_frame image should be applied" + ); + browser.test.assertEq( + ACCENT_COLOR_1, + returnedTheme.colors.frame, + "Theme 1 frame color should be applied" + ); + browser.test.assertEq( + TEXT_COLOR_1, + returnedTheme.colors.tab_background_text, + "Theme 1 tab_background_text color should be applied" + ); + } + + function testTheme2(returnedTheme) { + browser.test.assertTrue( + returnedTheme.images.theme_frame.includes("image2.png"), + "Theme 2 theme_frame image should be applied" + ); + browser.test.assertEq( + ACCENT_COLOR_2, + returnedTheme.colors.frame, + "Theme 2 frame color should be applied" + ); + browser.test.assertEq( + TEXT_COLOR_2, + returnedTheme.colors.tab_background_text, + "Theme 2 tab_background_text color should be applied" + ); + } + + const firstWin = await browser.windows.getCurrent(); + const secondWin = await browser.windows.create(); + + const onceThemeUpdated = () => + new Promise(resolve => { + const listener = updateInfo => { + browser.theme.onUpdated.removeListener(listener); + resolve(updateInfo); + }; + browser.theme.onUpdated.addListener(listener); + }); + + browser.test.log("Testing update with no windowId parameter"); + let updateInfo1 = onceThemeUpdated(); + await browser.theme.update(theme1); + updateInfo1 = await updateInfo1; + testTheme1(updateInfo1.theme); + browser.test.assertTrue( + !updateInfo1.windowId, + "No window id on first update" + ); + + browser.test.log("Testing update with windowId parameter"); + let updateInfo2 = onceThemeUpdated(); + await browser.theme.update(secondWin.id, theme2); + updateInfo2 = await updateInfo2; + testTheme2(updateInfo2.theme); + browser.test.assertEq( + secondWin.id, + updateInfo2.windowId, + "window id on second update" + ); + + browser.test.log("Testing reset with windowId parameter"); + let updateInfo3 = onceThemeUpdated(); + await browser.theme.reset(firstWin.id); + updateInfo3 = await updateInfo3; + browser.test.assertEq( + 0, + Object.keys(updateInfo3.theme).length, + "Empty theme given on reset" + ); + browser.test.assertEq( + firstWin.id, + updateInfo3.windowId, + "window id on third update" + ); + + browser.test.log("Testing reset with no windowId parameter"); + let updateInfo4 = onceThemeUpdated(); + await browser.theme.reset(); + updateInfo4 = await updateInfo4; + browser.test.assertEq( + 0, + Object.keys(updateInfo4.theme).length, + "Empty theme given on reset" + ); + browser.test.assertTrue( + !updateInfo4.windowId, + "no window id on fourth update" + ); + + browser.test.log("Cleaning up test"); + await browser.windows.remove(secondWin.id); + browser.test.notifyPass("onUpdated"); + }, + manifest: { + permissions: ["theme"], + }, + files: { + "image1.png": BACKGROUND_1, + "image2.png": BACKGROUND_2, + }, + }); + + await extension.startup(); + await extension.awaitFinish("onUpdated"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js new file mode 100644 index 0000000000..c6af66b5e5 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js @@ -0,0 +1,217 @@ +"use strict"; + +// PNG image data for a simple red dot. +const BACKGROUND_1 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; +const ACCENT_COLOR_1 = "#a14040"; +const TEXT_COLOR_1 = "#fac96e"; + +// PNG image data for the Mozilla dino head. +const BACKGROUND_2 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; +const ACCENT_COLOR_2 = "#03fe03"; +const TEXT_COLOR_2 = "#0ef325"; + +function hexToRGB(hex) { + hex = parseInt(hex.indexOf("#") > -1 ? hex.substring(1) : hex, 16); + return ( + "rgb(" + [hex >> 16, (hex & 0x00ff00) >> 8, hex & 0x0000ff].join(", ") + ")" + ); +} + +function validateTheme(backgroundImage, accentColor, textColor, isLWT) { + let docEl = window.document.documentElement; + let rootCS = window.getComputedStyle(docEl); + + let toolbox = document.querySelector("#navigator-toolbox"); + let toolboxCS = window.getComputedStyle(toolbox); + + if (isLWT) { + Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set"); + Assert.equal( + docEl.getAttribute("lwtheme-brighttext"), + "true", + "LWT text color attribute should be set" + ); + } + + if (accentColor.startsWith("#")) { + accentColor = hexToRGB(accentColor); + } + if (textColor.startsWith("#")) { + textColor = hexToRGB(textColor); + } + if (backgroundColorSetOnRoot()) { + Assert.ok( + rootCS.backgroundImage.includes(backgroundImage), + "Expected correct background image" + ); + Assert.equal( + rootCS.backgroundColor, + accentColor, + "Expected correct accent color" + ); + } else { + Assert.ok( + toolboxCS.backgroundImage.includes(backgroundImage), + "Expected correct background image" + ); + Assert.equal( + toolboxCS.backgroundColor, + accentColor, + "Expected correct accent color" + ); + } + + Assert.equal(rootCS.color, textColor, "Expected correct text color"); +} + +add_task(async function test_dynamic_theme_updates() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["theme"], + }, + files: { + "image1.png": BACKGROUND_1, + "image2.png": BACKGROUND_2, + }, + background() { + browser.test.onMessage.addListener((msg, details) => { + if (msg === "update-theme") { + browser.theme.update(details).then(() => { + browser.test.sendMessage("theme-updated"); + }); + } else { + browser.theme.reset().then(() => { + browser.test.sendMessage("theme-reset"); + }); + } + }); + }, + }); + + let rootCS = window.getComputedStyle(window.document.documentElement); + let toolboxCS = window.getComputedStyle( + window.document.documentElement.querySelector("#navigator-toolbox") + ); + await extension.startup(); + + extension.sendMessage("update-theme", { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR_1, + tab_background_text: TEXT_COLOR_1, + }, + }); + + await extension.awaitMessage("theme-updated"); + + validateTheme("image1.png", ACCENT_COLOR_1, TEXT_COLOR_1, true); + + // Check with the LWT aliases (to update on Firefox 69, because the + // LWT aliases are going to be removed). + extension.sendMessage("update-theme", { + images: { + theme_frame: "image2.png", + }, + colors: { + frame: ACCENT_COLOR_2, + tab_background_text: TEXT_COLOR_2, + }, + }); + + await extension.awaitMessage("theme-updated"); + + validateTheme("image2.png", ACCENT_COLOR_2, TEXT_COLOR_2, true); + + extension.sendMessage("reset-theme"); + + await extension.awaitMessage("theme-reset"); + + let { color } = rootCS; + let { backgroundImage, backgroundColor } = toolboxCS; + if (backgroundColorSetOnRoot()) { + backgroundImage = rootCS.backgroundImage; + backgroundColor = rootCS.backgroundColor; + } + validateTheme(backgroundImage, backgroundColor, color, false); + + await extension.unload(); + + let docEl = window.document.documentElement; + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); +}); + +add_task(async function test_dynamic_theme_updates_with_data_url() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["theme"], + }, + background() { + browser.test.onMessage.addListener((msg, details) => { + if (msg === "update-theme") { + browser.theme.update(details).then(() => { + browser.test.sendMessage("theme-updated"); + }); + } else { + browser.theme.reset().then(() => { + browser.test.sendMessage("theme-reset"); + }); + } + }); + }, + }); + + let rootCS = window.getComputedStyle(window.document.documentElement); + let toolboxCS = window.getComputedStyle( + window.document.documentElement.querySelector("#navigator-toolbox") + ); + await extension.startup(); + + extension.sendMessage("update-theme", { + images: { + theme_frame: BACKGROUND_1, + }, + colors: { + frame: ACCENT_COLOR_1, + tab_background_text: TEXT_COLOR_1, + }, + }); + + await extension.awaitMessage("theme-updated"); + + validateTheme(BACKGROUND_1, ACCENT_COLOR_1, TEXT_COLOR_1, true); + + extension.sendMessage("update-theme", { + images: { + theme_frame: BACKGROUND_2, + }, + colors: { + frame: ACCENT_COLOR_2, + tab_background_text: TEXT_COLOR_2, + }, + }); + + await extension.awaitMessage("theme-updated"); + + validateTheme(BACKGROUND_2, ACCENT_COLOR_2, TEXT_COLOR_2, true); + + extension.sendMessage("reset-theme"); + + await extension.awaitMessage("theme-reset"); + + let { color } = rootCS; + let { backgroundImage, backgroundColor } = toolboxCS; + if (backgroundColorSetOnRoot()) { + backgroundImage = rootCS.backgroundImage; + backgroundColor = rootCS.backgroundColor; + } + validateTheme(backgroundImage, backgroundColor, color, false); + + await extension.unload(); + + let docEl = window.document.documentElement; + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js b/toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js new file mode 100644 index 0000000000..e07f4a50d2 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js @@ -0,0 +1,450 @@ +"use strict"; + +const { AddonSettings } = ChromeUtils.import( + "resource://gre/modules/addons/AddonSettings.jsm" +); + +// This test checks whether the theme experiments work +add_task(async function test_experiment_static_theme() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + theme: { + colors: { + some_color_property: "#ff00ff", + }, + images: { + some_image_property: "background.jpg", + }, + properties: { + some_random_property: "no-repeat", + }, + }, + theme_experiment: { + colors: { + some_color_property: "--some-color-property", + }, + images: { + some_image_property: "--some-image-property", + }, + properties: { + some_random_property: "--some-random-property", + }, + }, + }, + }); + + const root = window.document.documentElement; + + is( + root.style.getPropertyValue("--some-color-property"), + "", + "Color property should be unset" + ); + is( + root.style.getPropertyValue("--some-image-property"), + "", + "Image property should be unset" + ); + is( + root.style.getPropertyValue("--some-random-property"), + "", + "Generic Property should be unset." + ); + + await extension.startup(); + + const testExperimentApplied = rootEl => { + if (AddonSettings.EXPERIMENTS_ENABLED) { + is( + rootEl.style.getPropertyValue("--some-color-property"), + hexToCSS("#ff00ff"), + "Color property should be parsed and set." + ); + ok( + rootEl.style + .getPropertyValue("--some-image-property") + .startsWith("url("), + "Image property should be parsed." + ); + ok( + rootEl.style + .getPropertyValue("--some-image-property") + .endsWith("background.jpg)"), + "Image property should be set." + ); + is( + rootEl.style.getPropertyValue("--some-random-property"), + "no-repeat", + "Generic Property should be set." + ); + } else { + is( + rootEl.style.getPropertyValue("--some-color-property"), + "", + "Color property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-image-property"), + "", + "Image property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-random-property"), + "", + "Generic Property should be unset." + ); + } + }; + + info("Testing that current window updated with the experiment applied"); + testExperimentApplied(root); + + info("Testing that new window initialized with the experiment applied"); + const newWindow = await BrowserTestUtils.openNewBrowserWindow(); + const newWindowRoot = newWindow.document.documentElement; + testExperimentApplied(newWindowRoot); + + await extension.unload(); + + info("Testing that both windows unapplied the experiment"); + for (const rootEl of [root, newWindowRoot]) { + is( + rootEl.style.getPropertyValue("--some-color-property"), + "", + "Color property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-image-property"), + "", + "Image property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-random-property"), + "", + "Generic Property should be unset." + ); + } + await BrowserTestUtils.closeWindow(newWindow); +}); + +add_task(async function test_experiment_dynamic_theme() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + permissions: ["theme"], + theme_experiment: { + colors: { + some_color_property: "--some-color-property", + }, + images: { + some_image_property: "--some-image-property", + }, + properties: { + some_random_property: "--some-random-property", + }, + }, + }, + background() { + const theme = { + colors: { + some_color_property: "#ff00ff", + }, + images: { + some_image_property: "background.jpg", + }, + properties: { + some_random_property: "no-repeat", + }, + }; + browser.test.onMessage.addListener(msg => { + if (msg === "update-theme") { + browser.theme.update(theme).then(() => { + browser.test.sendMessage("theme-updated"); + }); + } else { + browser.theme.reset().then(() => { + browser.test.sendMessage("theme-reset"); + }); + } + }); + }, + }); + + await extension.startup(); + + const root = window.document.documentElement; + + is( + root.style.getPropertyValue("--some-color-property"), + "", + "Color property should be unset" + ); + is( + root.style.getPropertyValue("--some-image-property"), + "", + "Image property should be unset" + ); + is( + root.style.getPropertyValue("--some-random-property"), + "", + "Generic Property should be unset." + ); + + extension.sendMessage("update-theme"); + await extension.awaitMessage("theme-updated"); + + const testExperimentApplied = rootEl => { + if (AddonSettings.EXPERIMENTS_ENABLED) { + is( + rootEl.style.getPropertyValue("--some-color-property"), + hexToCSS("#ff00ff"), + "Color property should be parsed and set." + ); + ok( + rootEl.style + .getPropertyValue("--some-image-property") + .startsWith("url("), + "Image property should be parsed." + ); + ok( + rootEl.style + .getPropertyValue("--some-image-property") + .endsWith("background.jpg)"), + "Image property should be set." + ); + is( + rootEl.style.getPropertyValue("--some-random-property"), + "no-repeat", + "Generic Property should be set." + ); + } else { + is( + rootEl.style.getPropertyValue("--some-color-property"), + "", + "Color property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-image-property"), + "", + "Image property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-random-property"), + "", + "Generic Property should be unset." + ); + } + }; + testExperimentApplied(root); + + const newWindow = await BrowserTestUtils.openNewBrowserWindow(); + const newWindowRoot = newWindow.document.documentElement; + + testExperimentApplied(newWindowRoot); + + extension.sendMessage("reset-theme"); + await extension.awaitMessage("theme-reset"); + + for (const rootEl of [root, newWindowRoot]) { + is( + rootEl.style.getPropertyValue("--some-color-property"), + "", + "Color property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-image-property"), + "", + "Image property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-random-property"), + "", + "Generic Property should be unset." + ); + } + + extension.sendMessage("update-theme"); + await extension.awaitMessage("theme-updated"); + + testExperimentApplied(root); + testExperimentApplied(newWindowRoot); + + await extension.unload(); + + for (const rootEl of [root, newWindowRoot]) { + is( + rootEl.style.getPropertyValue("--some-color-property"), + "", + "Color property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-image-property"), + "", + "Image property should be unset" + ); + is( + rootEl.style.getPropertyValue("--some-random-property"), + "", + "Generic Property should be unset." + ); + } + + await BrowserTestUtils.closeWindow(newWindow); +}); + +add_task(async function test_experiment_stylesheet() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + theme: { + colors: { + menu_button_background: "#ff00ff", + }, + }, + theme_experiment: { + stylesheet: "experiment.css", + colors: { + menu_button_background: "--menu-button-background", + }, + }, + }, + files: { + "experiment.css": `#PanelUI-menu-button { + background-color: var(--menu-button-background); + fill: white; + }`, + }, + }); + + const root = window.document.documentElement; + const menuButton = document.getElementById("PanelUI-menu-button"); + const computedStyle = window.getComputedStyle(menuButton); + const expectedColor = hexToCSS("#ff00ff"); + const expectedFill = hexToCSS("#ffffff"); + + is( + root.style.getPropertyValue("--menu-button-background"), + "", + "Variable should be unset" + ); + isnot( + computedStyle.backgroundColor, + expectedColor, + "Menu button should not have custom background" + ); + isnot( + computedStyle.fill, + expectedFill, + "Menu button should not have stylesheet fill" + ); + + await extension.startup(); + + if (AddonSettings.EXPERIMENTS_ENABLED) { + // Wait for stylesheet load. + await BrowserTestUtils.waitForCondition( + () => computedStyle.fill === expectedFill + ); + + is( + root.style.getPropertyValue("--menu-button-background"), + expectedColor, + "Variable should be parsed and set." + ); + is( + computedStyle.backgroundColor, + expectedColor, + "Menu button should be have correct background" + ); + is( + computedStyle.fill, + expectedFill, + "Menu button should be have correct fill" + ); + } else { + is( + root.style.getPropertyValue("--menu-button-background"), + "", + "Variable should be unset" + ); + isnot( + computedStyle.backgroundColor, + expectedColor, + "Menu button should not have custom background" + ); + isnot( + computedStyle.fill, + expectedFill, + "Menu button should not have stylesheet fill" + ); + } + + await extension.unload(); + + is( + root.style.getPropertyValue("--menu-button-background"), + "", + "Variable should be unset" + ); + isnot( + computedStyle.backgroundColor, + expectedColor, + "Menu button should not have custom background" + ); + isnot( + computedStyle.fill, + expectedFill, + "Menu button should not have stylesheet fill" + ); +}); + +// This test checks whether the theme experiments are allowed for non privileged +// theme installed non-temporarily if AddonSettings.EXPERIMENTS_ENABLED is true. +add_task(async function test_experiment_installed_non_temporarily() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.experiments.enabled", true]], + }); + + if (!AddonSettings.EXPERIMENTS_ENABLED) { + info( + "Skipping test case on build where AddonSettings.EXPERIMENTS_ENABLED is false" + ); + return; + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + some_color_property: "#ff00ff", + }, + }, + theme_experiment: { + colors: { + some_color_property: "--some-color-property", + }, + }, + }, + }); + + const root = window.document.documentElement; + + is( + root.style.getPropertyValue("--some-color-property"), + "", + "Color property should be unset" + ); + + await extension.startup(); + + is( + root.style.getPropertyValue("--some-color-property"), + hexToCSS("#ff00ff"), + "Color property should be parsed and set." + ); + + await extension.unload(); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js b/toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js new file mode 100644 index 0000000000..ad8d7ab666 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js @@ -0,0 +1,229 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// the toolbar and toolbar_field properties also theme the findbar. + +function assertHasNoBorders(element) { + let cs = window.getComputedStyle(element); + Assert.equal(cs.borderTopWidth, "0px", "should have no top border"); + Assert.equal(cs.borderRightWidth, "0px", "should have no right border"); + Assert.equal(cs.borderBottomWidth, "0px", "should have no bottom border"); + Assert.equal(cs.borderLeftWidth, "0px", "should have no left border"); +} + +add_task(async function test_support_toolbar_properties_on_findbar() { + const TOOLBAR_COLOR = "#ff00ff"; + const TOOLBAR_TEXT_COLOR = "#9400ff"; + const ACCENT_COLOR_INACTIVE = "#ffff00"; + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + frame_inactive: ACCENT_COLOR_INACTIVE, + tab_background_text: TEXT_COLOR, + toolbar: TOOLBAR_COLOR, + bookmark_text: TOOLBAR_TEXT_COLOR, + }, + }, + }, + }); + + await extension.startup(); + await gBrowser.getFindBar(); + + let findbar_button = gFindBar.getElement("highlight"); + + info("Checking findbar background is set as toolbar color"); + Assert.equal( + window.getComputedStyle(gFindBar).backgroundColor, + hexToCSS(ACCENT_COLOR), + "Findbar background color should be the same as toolbar background color." + ); + + info("Checking findbar and checkbox text color use toolbar text color"); + const rgbColor = hexToCSS(TOOLBAR_TEXT_COLOR); + Assert.equal( + window.getComputedStyle(gFindBar).color, + rgbColor, + "Findbar text color should be the same as toolbar text color." + ); + Assert.equal( + window.getComputedStyle(findbar_button).color, + rgbColor, + "Findbar checkbox text color should be toolbar text color." + ); + + // Open a new window to check frame_inactive + let window2 = await BrowserTestUtils.openNewBrowserWindow(); + Assert.equal( + window.getComputedStyle(gFindBar).backgroundColor, + hexToCSS(ACCENT_COLOR_INACTIVE), + "Findbar background changed in inactive window." + ); + await BrowserTestUtils.closeWindow(window2); + + await extension.unload(); +}); + +add_task(async function test_support_toolbar_field_properties_on_findbar() { + let findbar_prev_button = gFindBar.getElement("find-previous"); + let findbar_next_button = gFindBar.getElement("find-next"); + + assertHasNoBorders(findbar_prev_button); + assertHasNoBorders(findbar_next_button); + + const TOOLBAR_FIELD_COLOR = "#ff00ff"; + const TOOLBAR_FIELD_TEXT_COLOR = "#9400ff"; + const TOOLBAR_FIELD_BORDER_COLOR = "#ffffff"; + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_field: TOOLBAR_FIELD_COLOR, + toolbar_field_text: TOOLBAR_FIELD_TEXT_COLOR, + toolbar_field_border: TOOLBAR_FIELD_BORDER_COLOR, + }, + }, + }, + }); + + await extension.startup(); + await gBrowser.getFindBar(); + + let findbar_textbox = gFindBar.getElement("findbar-textbox"); + + info( + "Checking findbar textbox background is set as toolbar field background color" + ); + Assert.equal( + window.getComputedStyle(findbar_textbox).backgroundColor, + hexToCSS(TOOLBAR_FIELD_COLOR), + "Findbar textbox background color should be the same as toolbar field color." + ); + + info("Checking findbar textbox color is set as toolbar field text color"); + Assert.equal( + window.getComputedStyle(findbar_textbox).color, + hexToCSS(TOOLBAR_FIELD_TEXT_COLOR), + "Findbar textbox text color should be the same as toolbar field text color." + ); + testBorderColor(findbar_textbox, TOOLBAR_FIELD_BORDER_COLOR); + + assertHasNoBorders(findbar_prev_button); + assertHasNoBorders(findbar_next_button); + + await extension.unload(); +}); + +// Test that theme properties are *not* applied with a theme_frame (see bug 1506913) +add_task(async function test_toolbar_properties_on_findbar_with_theme_frame() { + const TOOLBAR_COLOR = "#ff00ff"; + const TOOLBAR_TEXT_COLOR = "#9400ff"; + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar: TOOLBAR_COLOR, + bookmark_text: TOOLBAR_TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + await gBrowser.getFindBar(); + + let findbar_button = gFindBar.getElement("highlight"); + + info("Checking findbar background is *not* set as toolbar color"); + Assert.notEqual( + window.getComputedStyle(gFindBar).backgroundColor, + hexToCSS(ACCENT_COLOR), + "Findbar background color should not be set by theme." + ); + + info( + "Checking findbar and button text color is *not* set as toolbar text color" + ); + Assert.notEqual( + window.getComputedStyle(gFindBar).color, + hexToCSS(TOOLBAR_TEXT_COLOR), + "Findbar text color should not be set by theme." + ); + Assert.notEqual( + window.getComputedStyle(findbar_button).color, + hexToCSS(TOOLBAR_TEXT_COLOR), + "Findbar button text color should not be set by theme." + ); + + await extension.unload(); +}); + +add_task( + async function test_toolbar_field_properties_on_findbar_with_theme_frame() { + const TOOLBAR_FIELD_COLOR = "#ff00ff"; + const TOOLBAR_FIELD_TEXT_COLOR = "#9400ff"; + const TOOLBAR_FIELD_BORDER_COLOR = "#ffffff"; + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_field: TOOLBAR_FIELD_COLOR, + toolbar_field_text: TOOLBAR_FIELD_TEXT_COLOR, + toolbar_field_border: TOOLBAR_FIELD_BORDER_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + await gBrowser.getFindBar(); + + let findbar_textbox = gFindBar.getElement("findbar-textbox"); + + Assert.notEqual( + window.getComputedStyle(findbar_textbox).backgroundColor, + hexToCSS(TOOLBAR_FIELD_COLOR), + "Findbar textbox background color should not be set by theme." + ); + + Assert.notEqual( + window.getComputedStyle(findbar_textbox).color, + hexToCSS(TOOLBAR_FIELD_TEXT_COLOR), + "Findbar textbox text color should not be set by theme." + ); + + await extension.unload(); + } +); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js b/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js new file mode 100644 index 0000000000..80a5983ea6 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js @@ -0,0 +1,149 @@ +"use strict"; + +// This test checks whether browser.theme.getCurrent() works correctly when theme +// does not originate from extension querying the theme. +const THEME = { + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, +}; + +add_task(async function test_getcurrent() { + const theme = ExtensionTestUtils.loadExtension(THEME); + const extension = ExtensionTestUtils.loadExtension({ + background() { + browser.theme.onUpdated.addListener(() => { + browser.theme.getCurrent().then(theme => { + browser.test.sendMessage("theme-updated", theme); + if (!theme?.images) { + return; + } + + // Try to access the theme_frame image + fetch(theme.images.theme_frame) + .then(() => { + browser.test.sendMessage("theme-image", { success: true }); + }) + .catch(e => { + browser.test.sendMessage("theme-image", { + success: false, + error: e.message, + }); + }); + }); + }); + }, + }); + + await extension.startup(); + + info("Testing getCurrent after static theme startup"); + let updatedPromise = extension.awaitMessage("theme-updated"); + let imageLoaded = extension.awaitMessage("theme-image"); + await theme.startup(); + let receivedTheme = await updatedPromise; + Assert.ok( + receivedTheme.images.theme_frame.includes("image1.png"), + "getCurrent returns correct theme_frame image" + ); + Assert.equal( + receivedTheme.colors.frame, + ACCENT_COLOR, + "getCurrent returns correct frame color" + ); + Assert.equal( + receivedTheme.colors.tab_background_text, + TEXT_COLOR, + "getCurrent returns correct tab_background_text color" + ); + Assert.deepEqual(await imageLoaded, { success: true }, "theme image loaded"); + + info("Testing getCurrent after static theme unload"); + updatedPromise = extension.awaitMessage("theme-updated"); + await theme.unload(); + receivedTheme = await updatedPromise; + Assert.equal( + JSON.stringify({ colors: null, images: null, properties: null }), + JSON.stringify(receivedTheme), + "getCurrent returns empty theme" + ); + + await extension.unload(); +}); + +add_task(async function test_getcurrent_privateBrowsing() { + const theme = ExtensionTestUtils.loadExtension(THEME); + + const extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + manifest: { + sidebar_action: { + default_panel: "sidebar.html", + }, + }, + files: { + "sidebar.html": ` + + + Test Extension Sidebar + + + + `, + "sidebar.js": function() { + browser.theme.getCurrent().then(theme => { + if (!theme?.images) { + browser.test.fail( + `Missing expected images from theme.getCurrent result` + ); + return; + } + + // Try to access the theme_frame image + fetch(theme.images.theme_frame) + .then(() => { + browser.test.sendMessage("theme-image", { success: true }); + }) + .catch(e => { + browser.test.sendMessage("theme-image", { + success: false, + error: e.message, + }); + }); + }); + }, + }, + }); + + await extension.startup(); + await theme.startup(); + + const privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + const { ExtensionCommon } = ChromeUtils.import( + "resource://gre/modules/ExtensionCommon.jsm" + ); + const { makeWidgetId } = ExtensionCommon; + privateWin.SidebarUI.show(`${makeWidgetId(extension.id)}-sidebar-action`); + + let imageLoaded = extension.awaitMessage("theme-image"); + Assert.deepEqual(await imageLoaded, { success: true }, "theme image loaded"); + + await extension.unload(); + await theme.unload(); + + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js b/toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js new file mode 100644 index 0000000000..1325890e62 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js @@ -0,0 +1,63 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// the color of the font and background in a selection are applied properly. +const { CustomizableUITestUtils } = ChromeUtils.import( + "resource://testing-common/CustomizableUITestUtils.jsm" +); +let gCUITestUtils = new CustomizableUITestUtils(window); +add_setup(async function() { + await gCUITestUtils.addSearchBar(); + await gFindBarPromise; + registerCleanupFunction(() => { + gFindBar.close(); + gCUITestUtils.removeSearchBar(); + }); +}); + +add_task(async function test_support_selection() { + const HIGHLIGHT_TEXT_COLOR = "#9400FF"; + const HIGHLIGHT_COLOR = "#F89919"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + toolbar_field_highlight: HIGHLIGHT_COLOR, + toolbar_field_highlight_text: HIGHLIGHT_TEXT_COLOR, + }, + }, + }, + }); + await extension.startup(); + + let fields = [ + gURLBar.inputField, + document.querySelector("#searchbar .searchbar-textbox"), + document.querySelector(".findbar-textbox"), + ].filter(field => { + let bounds = field.getBoundingClientRect(); + return bounds.width > 0 && bounds.height > 0; + }); + + Assert.equal(fields.length, 3, "Should be testing three elements"); + + info( + `Checking background colors and colors for ${fields.length} toolbar input fields.` + ); + for (let field of fields) { + info(`Testing ${field.id || field.className}`); + field.focus(); + Assert.equal( + window.getComputedStyle(field, "::selection").backgroundColor, + hexToCSS(HIGHLIGHT_COLOR), + "Input selection background should be set." + ); + Assert.equal( + window.getComputedStyle(field, "::selection").color, + hexToCSS(HIGHLIGHT_TEXT_COLOR), + "Input selection color should be set." + ); + } + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js b/toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js new file mode 100644 index 0000000000..d9beb0f9a8 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js @@ -0,0 +1,77 @@ +"use strict"; + +add_task(async function test_theme_incognito_not_allowed() { + let windowExtension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + async background() { + const theme = { + colors: { + frame: "black", + tab_background_text: "black", + }, + }; + let window = await browser.windows.create({ incognito: true }); + browser.test.onMessage.addListener(async message => { + if (message == "update") { + browser.theme.update(window.id, theme); + return; + } + await browser.windows.remove(window.id); + browser.test.sendMessage("done"); + }); + browser.test.sendMessage("ready", window.id); + }, + manifest: { + permissions: ["theme"], + }, + }); + await windowExtension.startup(); + let wId = await windowExtension.awaitMessage("ready"); + + async function background(windowId) { + const theme = { + colors: { + frame: "black", + tab_background_text: "black", + }, + }; + + browser.theme.onUpdated.addListener(info => { + browser.test.log("got theme onChanged"); + browser.test.fail("theme"); + }); + await browser.test.assertRejects( + browser.theme.getCurrent(windowId), + /Invalid window ID/, + "API should reject getting window theme" + ); + await browser.test.assertRejects( + browser.theme.update(windowId, theme), + /Invalid window ID/, + "API should reject updating theme" + ); + await browser.test.assertRejects( + browser.theme.reset(windowId), + /Invalid window ID/, + "API should reject reseting theme on window" + ); + + browser.test.sendMessage("start"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${background})(${wId})`, + manifest: { + permissions: ["theme"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("start"); + windowExtension.sendMessage("update"); + + windowExtension.sendMessage("close"); + await windowExtension.awaitMessage("done"); + await windowExtension.unload(); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js b/toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js new file mode 100644 index 0000000000..4293e5eb19 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js @@ -0,0 +1,69 @@ +"use strict"; + +const DEFAULT_THEME_BG_COLOR = "rgb(255, 255, 255)"; +const DEFAULT_THEME_TEXT_COLOR = "rgb(0, 0, 0)"; + +add_task(async function test_deprecated_LWT_properties_ignored() { + // This test uses deprecated theme properties, so warnings are expected. + ExtensionTestUtils.failOnSchemaWarnings(false); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + headerURL: "image1.png", + }, + colors: { + accentcolor: ACCENT_COLOR, + textcolor: TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let docEl = window.document.documentElement; + let docStyle = window.getComputedStyle(docEl); + let navigatorStyle = window.getComputedStyle( + docEl.querySelector("#navigator-toolbox") + ); + + Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set"); + Assert.ok( + !docEl.hasAttribute("lwtheme-image"), + "LWT image attribute should not be set on deprecated headerURL alias" + ); + Assert.ok( + !docEl.getAttribute("lwtheme-brighttext"), + "LWT text color attribute should not be set on deprecated textcolor alias" + ); + + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + Assert.equal( + rootCS.backgroundColor, + DEFAULT_THEME_BG_COLOR, + "Expected default theme background color" + ); + } else { + Assert.equal( + navigatorStyle.backgroundColor, + DEFAULT_THEME_BG_COLOR, + "Expected default theme background color" + ); + } + + Assert.equal( + docStyle.color, + DEFAULT_THEME_TEXT_COLOR, + "Expected default theme text color" + ); + + await extension.unload(); + + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js b/toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js new file mode 100644 index 0000000000..03ad0316c3 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js @@ -0,0 +1,289 @@ +"use strict"; + +add_task(async function test_support_backgrounds_position() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "face1.png", + additional_backgrounds: ["face2.png", "face2.png", "face2.png"], + }, + colors: { + frame: `rgb(${FRAME_COLOR.join(",")})`, + tab_background_text: `rgb(${TAB_BACKGROUND_TEXT_COLOR.join(",")})`, + }, + properties: { + additional_backgrounds_alignment: [ + "left top", + "center top", + "right bottom", + ], + }, + }, + }, + files: { + "face1.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA), + "face2.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA), + }, + }); + + await extension.startup(); + + let docEl = window.document.documentElement; + let toolbox = document.querySelector("#navigator-toolbox"); + + Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set"); + Assert.equal( + docEl.getAttribute("lwtheme-brighttext"), + "true", + "LWT text color attribute should be set" + ); + + let toolboxCS = window.getComputedStyle(toolbox); + let toolboxBgImage = toolboxCS.backgroundImage.split(",")[0].trim(); + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + let rootBgImage = rootCS.backgroundImage.split(",")[0].trim(); + Assert.ok( + rootBgImage.includes("face1.png"), + `The backgroundImage should use face1.png. Actual value is: ${rootBgImage}` + ); + Assert.equal( + toolboxCS.backgroundImage, + Array(3) + .fill(toolboxBgImage) + .join(", "), + "The backgroundImage should use face2.png three times." + ); + Assert.equal( + toolboxCS.backgroundPosition, + "0% 0%, 50% 0%, 100% 100%", + "The backgroundPosition should use the three values provided." + ); + } else { + Assert.equal( + toolboxCS.backgroundImage, + [1, 2, 2, 2] + .map(num => toolboxBgImage.replace(/face[\d]*/, `face${num}`)) + .join(", "), + "The backgroundImage should use face1.png once and face2.png three times." + ); + Assert.equal( + toolboxCS.backgroundPosition, + "100% 0%, 0% 0%, 50% 0%, 100% 100%", + "The backgroundPosition should use the three values provided, preceded by the default for theme_frame." + ); + /** + * We expect duplicate background-repeat values because we apply `no-repeat` + * once for theme_frame, and again as the default value of + * --lwt-background-tiling. + */ + Assert.equal( + toolboxCS.backgroundRepeat, + "no-repeat, no-repeat", + "The backgroundPosition should use the default value." + ); + } + + await extension.unload(); + + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); + toolboxCS = window.getComputedStyle(toolbox); + + // Styles should've reverted to their initial values. + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + Assert.equal(rootCS.backgroundImage, "none"); + Assert.equal(rootCS.backgroundPosition, "0% 0%"); + Assert.equal(rootCS.backgroundRepeat, "repeat"); + } + Assert.equal(toolboxCS.backgroundImage, "none"); + Assert.equal(toolboxCS.backgroundPosition, "0% 0%"); + Assert.equal(toolboxCS.backgroundRepeat, "repeat"); +}); + +add_task(async function test_support_backgrounds_repeat() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "face0.png", + additional_backgrounds: ["face1.png", "face2.png", "face3.png"], + }, + colors: { + frame: FRAME_COLOR, + tab_background_text: TAB_BACKGROUND_TEXT_COLOR, + }, + properties: { + additional_backgrounds_tiling: ["repeat-x", "repeat-y", "repeat"], + }, + }, + }, + files: { + "face0.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA), + "face1.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA), + "face2.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA), + "face3.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA), + }, + }); + + await extension.startup(); + + let docEl = window.document.documentElement; + let toolbox = document.querySelector("#navigator-toolbox"); + + Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set"); + Assert.equal( + docEl.getAttribute("lwtheme-brighttext"), + "true", + "LWT text color attribute should be set" + ); + + let toolboxCS = window.getComputedStyle(toolbox); + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + let rootImage = rootCS.backgroundImage.split(",")[0].trim(); + Assert.ok( + rootImage.includes("face0.png"), + `The backgroundImage should use face.png. Actual value is: ${rootImage}` + ); + Assert.equal( + [1, 2, 3] + .map(num => rootImage.replace(/face[\d]*/, `face${num}`)) + .join(", "), + toolboxCS.backgroundImage, + "The backgroundImage should use face.png three times." + ); + Assert.equal( + rootCS.backgroundPosition, + "100% 0%, 100% 0%", + "The backgroundPosition should use the default value for root." + ); + Assert.equal( + toolboxCS.backgroundPosition, + "100% 0%", + "The backgroundPosition should use the default value for navigator-toolbox." + ); + Assert.equal( + rootCS.backgroundRepeat, + "no-repeat, repeat-x, repeat-y, repeat", + "The backgroundRepeat should use the default values for root." + ); + Assert.equal( + toolboxCS.backgroundRepeat, + "repeat-x, repeat-y, repeat", + "The backgroundRepeat should use the three values provided for navigator-toolbox." + ); + } else { + let toolboxImage = toolboxCS.backgroundImage.split(",")[0].trim(); + Assert.equal( + [0, 1, 2, 3] + .map(num => toolboxImage.replace(/face[\d]*/, `face${num}`)) + .join(", "), + toolboxCS.backgroundImage, + "The backgroundImage should use face.png four times." + ); + /** + * We expect duplicate background-position values because we apply `right top` + * once for theme_frame, and again as the default value of + * --lwt-background-alignment. + */ + Assert.equal( + toolboxCS.backgroundPosition, + "100% 0%, 100% 0%", + "The backgroundPosition should use the default value for navigator-toolbox." + ); + Assert.equal( + toolboxCS.backgroundRepeat, + "no-repeat, repeat-x, repeat-y, repeat", + "The backgroundRepeat should use the three values provided for --lwt-background-tiling, preceeded by the default for theme_frame." + ); + } + + await extension.unload(); + + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); +}); + +add_task(async function test_additional_images_check() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "face.png", + }, + colors: { + frame: FRAME_COLOR, + tab_background_text: TAB_BACKGROUND_TEXT_COLOR, + }, + properties: { + additional_backgrounds_tiling: ["repeat-x", "repeat-y", "repeat"], + }, + }, + }, + files: { + "face.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA), + }, + }); + + await extension.startup(); + + let docEl = window.document.documentElement; + let toolbox = document.querySelector("#navigator-toolbox"); + + Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set"); + Assert.equal( + docEl.getAttribute("lwtheme-brighttext"), + "true", + "LWT text color attribute should be set" + ); + + let toolboxCS = window.getComputedStyle(toolbox); + if (backgroundColorSetOnRoot()) { + let rootCS = window.getComputedStyle(docEl); + let bgImage = rootCS.backgroundImage.split(",")[0].trim(); + Assert.ok( + bgImage.includes("face.png"), + `The backgroundImage should use face.png. Actual value is: ${bgImage}` + ); + Assert.equal( + "none", + toolboxCS.backgroundImage, + "The backgroundImage should not use face.png." + ); + Assert.equal( + rootCS.backgroundPosition, + "100% 0%, 100% 0%", + "The backgroundPosition should use the default value." + ); + Assert.equal( + rootCS.backgroundRepeat, + "no-repeat, no-repeat", + "The backgroundPosition should use only one (default) value for the header and the default additional images." + ); + } else { + let bgImage = toolboxCS.backgroundImage.split(",")[0].trim(); + Assert.ok( + bgImage.includes("face.png"), + `The backgroundImage should use face.png. Actual value is: ${bgImage}` + ); + Assert.ok( + bgImage.includes("face.png"), + `The backgroundImage should use face.png. Actual value is: ${bgImage}` + ); + Assert.equal( + toolboxCS.backgroundPosition, + "100% 0%, 100% 0%", + "The backgroundPosition should use the default value." + ); + Assert.equal( + toolboxCS.backgroundRepeat, + "no-repeat, no-repeat", + "The backgroundRepeat should use the default value." + ); + } + + await extension.unload(); + + Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set"); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js new file mode 100644 index 0000000000..34c4d211cb --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js @@ -0,0 +1,206 @@ +"use strict"; +// This test checks whether the new tab page color properties work. + +function waitForAboutNewTabReady(browser, url) { + // Stop-gap fix for https://bugzilla.mozilla.org/show_bug.cgi?id=1697196#c24 + return SpecialPowers.spawn(browser, [url], async url => { + let doc = content.document; + await ContentTaskUtils.waitForCondition( + () => doc.querySelector(".outer-wrapper"), + `Waiting for page wrapper to be initialized at ${url}` + ); + }); +} + +/** + * Test whether the selected browser has the new tab page theme applied + * + * @param {object} theme that is applied + * @param {boolean} isBrightText whether the brighttext attribute should be set + */ +async function test_ntp_theme(theme, isBrightText) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme, + }, + }); + + let browser = gBrowser.selectedBrowser; + + let { + originalBackground, + originalCardBackground, + originalColor, + } = await SpecialPowers.spawn(browser, [], function() { + let doc = content.document; + ok( + !doc.body.hasAttribute("lwt-newtab"), + "New tab page should not have lwt-newtab attribute" + ); + ok( + !doc.body.hasAttribute("lwt-newtab-brighttext"), + `New tab page should not have lwt-newtab-brighttext attribute` + ); + + return { + originalBackground: content.getComputedStyle(doc.body).backgroundColor, + originalCardBackground: content.getComputedStyle( + doc.querySelector(".top-site-outer .tile") + ).backgroundColor, + originalColor: content.getComputedStyle( + doc.querySelector(".outer-wrapper") + ).color, + // We check the value of --newtab-link-primary-color directly because the + // elements on which it is applied are hard to test. It is most visible in + // the "learn more" link in the Pocket section. We cannot show the Pocket + // section since it hits the network, and the usual workarounds to change + // its backend only work in browser/. This variable is also used in + // the Edit Top Site modal, but showing/hiding that is very verbose and + // would make this test almost unreadable. + originalLinks: content + .getComputedStyle(doc.querySelector("body")) + .getPropertyValue("--newtab-link-primary-color"), + }; + }); + + await extension.startup(); + + Services.ppmm.sharedData.flush(); + + await SpecialPowers.spawn( + browser, + [ + { + isBrightText, + background: hexToCSS(theme.colors.ntp_background), + card_background: hexToCSS(theme.colors.ntp_card_background), + color: hexToCSS(theme.colors.ntp_text), + }, + ], + async function({ isBrightText, background, card_background, color }) { + let doc = content.document; + ok( + doc.body.hasAttribute("lwt-newtab"), + "New tab page should have lwt-newtab attribute" + ); + is( + doc.body.hasAttribute("lwt-newtab-brighttext"), + isBrightText, + `New tab page should${ + !isBrightText ? " not" : "" + } have lwt-newtab-brighttext attribute` + ); + + is( + content.getComputedStyle(doc.body).backgroundColor, + background, + "New tab page background should be set." + ); + is( + content.getComputedStyle(doc.querySelector(".top-site-outer .tile")) + .backgroundColor, + card_background, + "New tab page card background should be set." + ); + is( + content.getComputedStyle(doc.querySelector(".outer-wrapper")).color, + color, + "New tab page text color should be set." + ); + } + ); + + await extension.unload(); + + Services.ppmm.sharedData.flush(); + + await SpecialPowers.spawn( + browser, + [ + { + originalBackground, + originalCardBackground, + originalColor, + }, + ], + function({ originalBackground, originalCardBackground, originalColor }) { + let doc = content.document; + ok( + !doc.body.hasAttribute("lwt-newtab"), + "New tab page should not have lwt-newtab attribute" + ); + ok( + !doc.body.hasAttribute("lwt-newtab-brighttext"), + `New tab page should not have lwt-newtab-brighttext attribute` + ); + + is( + content.getComputedStyle(doc.body).backgroundColor, + originalBackground, + "New tab page background should be reset." + ); + is( + content.getComputedStyle(doc.querySelector(".top-site-outer .tile")) + .backgroundColor, + originalCardBackground, + "New tab page card background should be reset." + ); + is( + content.getComputedStyle(doc.querySelector(".outer-wrapper")).color, + originalColor, + "New tab page text color should be reset." + ); + } + ); +} + +add_task(async function test_support_ntp_colors() { + await SpecialPowers.pushPrefEnv({ + set: [ + // BrowserTestUtils.withNewTab waits for about:newtab to load + // so we disable preloading before running the test. + ["browser.newtab.preload", false], + // Force prefers-color-scheme to "light", as otherwise it might be + // derived from the theme, but we hard-code the light styles on this + // test. + ["layout.css.prefers-color-scheme.content-override", 1], + // Override the system color scheme to light so this test passes on + // machines with dark system color scheme. + ["ui.systemUsesDarkTheme", 0], + ], + }); + NewTabPagePreloading.removePreloadedBrowser(window); + for (let url of ["about:newtab", "about:home"]) { + info("Opening url: " + url); + await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => { + await waitForAboutNewTabReady(browser, url); + await test_ntp_theme( + { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + ntp_background: "#add8e6", + ntp_card_background: "#ffffff", + ntp_text: "#00008b", + }, + }, + false, + url + ); + + await test_ntp_theme( + { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + ntp_background: "#00008b", + ntp_card_background: "#000000", + ntp_text: "#add8e6", + }, + }, + true, + url + ); + }); + } +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js new file mode 100644 index 0000000000..14090550c5 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js @@ -0,0 +1,240 @@ +"use strict"; + +// This test checks whether the new tab page color properties work per-window. + +function waitForAboutNewTabReady(browser, url) { + // Stop-gap fix for https://bugzilla.mozilla.org/show_bug.cgi?id=1697196#c24 + return SpecialPowers.spawn(browser, [url], async url => { + let doc = content.document; + await ContentTaskUtils.waitForCondition( + () => doc.querySelector(".outer-wrapper"), + `Waiting for page wrapper to be initialized at ${url}` + ); + }); +} + +/** + * Test whether a given browser has the new tab page theme applied + * + * @param {object} browser to test against + * @param {object} theme that is applied + * @param {boolean} isBrightText whether the brighttext attribute should be set + * @returns {Promise} The task as a promise + */ +function test_ntp_theme(browser, theme, isBrightText) { + Services.ppmm.sharedData.flush(); + return SpecialPowers.spawn( + browser, + [ + { + isBrightText, + background: hexToCSS(theme.colors.ntp_background), + card_background: hexToCSS(theme.colors.ntp_card_background), + color: hexToCSS(theme.colors.ntp_text), + }, + ], + function({ isBrightText, background, card_background, color }) { + let doc = content.document; + ok( + doc.body.hasAttribute("lwt-newtab"), + "New tab page should have lwt-newtab attribute" + ); + is( + doc.body.hasAttribute("lwt-newtab-brighttext"), + isBrightText, + `New tab page should${ + !isBrightText ? " not" : "" + } have lwt-newtab-brighttext attribute` + ); + + is( + content.getComputedStyle(doc.body).backgroundColor, + background, + "New tab page background should be set." + ); + is( + content.getComputedStyle(doc.querySelector(".top-site-outer .tile")) + .backgroundColor, + card_background, + "New tab page card background should be set." + ); + is( + content.getComputedStyle(doc.querySelector(".outer-wrapper")).color, + color, + "New tab page text color should be set." + ); + } + ); +} + +/** + * Test whether a given browser has the default theme applied + * + * @param {object} browser to test against + * @param {string} url being tested + * @returns {Promise} The task as a promise + */ +function test_ntp_default_theme(browser, url) { + Services.ppmm.sharedData.flush(); + return SpecialPowers.spawn( + browser, + [ + { + background: hexToCSS("#F9F9FB"), + color: hexToCSS("#15141A"), + }, + ], + function({ background, color }) { + let doc = content.document; + ok( + !doc.body.hasAttribute("lwt-newtab"), + "New tab page should not have lwt-newtab attribute" + ); + ok( + !doc.body.hasAttribute("lwt-newtab-brighttext"), + `New tab page should not have lwt-newtab-brighttext attribute` + ); + + is( + content.getComputedStyle(doc.body).backgroundColor, + background, + "New tab page background should be reset." + ); + is( + content.getComputedStyle(doc.querySelector(".outer-wrapper")).color, + color, + "New tab page text color should be reset." + ); + } + ); +} + +add_task(async function test_per_window_ntp_theme() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["theme"], + }, + async background() { + function promiseWindowChecked() { + return new Promise(resolve => { + let listener = msg => { + if (msg == "checked-window") { + browser.test.onMessage.removeListener(listener); + resolve(); + } + }; + browser.test.onMessage.addListener(listener); + }); + } + + function removeWindow(winId) { + return new Promise(resolve => { + let listener = removedWinId => { + if (removedWinId == winId) { + browser.windows.onRemoved.removeListener(listener); + resolve(); + } + }; + browser.windows.onRemoved.addListener(listener); + browser.windows.remove(winId); + }); + } + + async function checkWindow(theme, isBrightText, winId) { + let windowChecked = promiseWindowChecked(); + browser.test.sendMessage("check-window", { + theme, + isBrightText, + winId, + }); + await windowChecked; + } + + const darkTextTheme = { + colors: { + frame: "#add8e6", + tab_background_text: "#000", + ntp_background: "#add8e6", + ntp_card_background: "#ff0000", + ntp_text: "#000", + }, + }; + + const brightTextTheme = { + colors: { + frame: "#00008b", + tab_background_text: "#add8e6", + ntp_background: "#00008b", + ntp_card_background: "#00ff00", + ntp_text: "#add8e6", + }, + }; + + let { id: winId } = await browser.windows.getCurrent(); + // We are opening about:blank instead of the default homepage, + // because using the default homepage results in intermittent + // test failures on debug builds due to browser window leaks. + // A side effect of testing on about:blank is that + // test_ntp_default_theme cannot test properties used only on + // about:newtab, like ntp_card_background. + let { id: secondWinId } = await browser.windows.create({ + url: "about:blank", + }); + + browser.test.log("Test that single window update works"); + await browser.theme.update(winId, darkTextTheme); + await checkWindow(darkTextTheme, false, winId); + await checkWindow(null, false, secondWinId); + + browser.test.log("Test that applying different themes on both windows"); + await browser.theme.update(secondWinId, brightTextTheme); + await checkWindow(darkTextTheme, false, winId); + await checkWindow(brightTextTheme, true, secondWinId); + + browser.test.log("Test resetting the theme on one window"); + await browser.theme.reset(winId); + await checkWindow(null, false, winId); + await checkWindow(brightTextTheme, true, secondWinId); + + await removeWindow(secondWinId); + await checkWindow(null, false, winId); + browser.test.notifyPass("perwindow-ntp-theme"); + }, + }); + + extension.onMessage( + "check-window", + async ({ theme, isBrightText, winId }) => { + let win = Services.wm.getOuterWindowWithId(winId); + win.NewTabPagePreloading.removePreloadedBrowser(win); + // These pages were initially chosen because LightweightThemeChild.jsm + // treats them specially. + for (let url of ["about:newtab", "about:home"]) { + info("Opening url: " + url); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url }, + async browser => { + await waitForAboutNewTabReady(browser, url); + if (theme) { + await test_ntp_theme(browser, theme, isBrightText); + } else { + await test_ntp_default_theme(browser, url); + } + } + ); + } + extension.sendMessage("checked-window"); + } + ); + + // BrowserTestUtils.withNewTab waits for about:newtab to load + // so we disable preloading before running the test. + await SpecialPowers.setBoolPref("browser.newtab.preload", false); + registerCleanupFunction(() => { + SpecialPowers.clearUserPref("browser.newtab.preload"); + }); + + await extension.startup(); + await extension.awaitFinish("perwindow-ntp-theme"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js b/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js new file mode 100644 index 0000000000..62eb4e68b1 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.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"; + +/** + * Tests that we apply dark theme variants to PBM windows where applicable. + */ + +const { BuiltInThemes } = ChromeUtils.importESModule( + "resource:///modules/BuiltInThemes.sys.mjs" +); +const { PromptTestUtils } = ChromeUtils.import( + "resource://testing-common/PromptTestUtils.jsm" +); + +const IS_LINUX = AppConstants.platform == "linux"; + +const LIGHT_THEME_ID = "firefox-compact-light@mozilla.org"; +const DARK_THEME_ID = "firefox-compact-dark@mozilla.org"; + +// This tests opens many chrome windows which is slow on debug builds. +requestLongerTimeout(2); + +/** + * Test a window's theme color scheme. + * + * @param {*} options - Test options. + * @param {Window} options.win - Window object to test. + * @param {boolean} options.colorScheme - Whether expected chrome color scheme + * is dark (true) or light (false). + * @param {boolean} options.expectLWTAttributes - Whether the window should + * have the LWT attributes set matching the color scheme. + * @param {boolean} options.expectDefaultDarkAttribute - Whether the window + * should have the "lwt-default-theme-in-dark-mode" attribute. + */ +async function testWindowColorScheme({ + win, + expectDark, + expectLWTAttributes, + expectDefaultDarkAttribute, +}) { + let docEl = win.document.documentElement; + + is( + docEl.hasAttribute("lwt-default-theme-in-dark-mode"), + expectDefaultDarkAttribute, + `Window should${ + expectDefaultDarkAttribute ? "" : " not" + } have lwt-default-theme-in-dark-mode attribute.` + ); + + if (expectLWTAttributes) { + ok(docEl.hasAttribute("lwtheme"), "Window should have LWT attribute."); + is( + docEl.getAttribute("lwtheme-brighttext"), + expectDark ? "true" : null, + "LWT text color attribute should be set." + ); + } else { + ok(!docEl.hasAttribute("lwtheme"), "Window should not have LWT attribute."); + ok( + !docEl.hasAttribute("lwtheme-brighttext"), + "LWT text color attribute should not be set." + ); + } +} + +/** + * Match the prefers-color-scheme media query and return the results. + * + * @param {object} options + * @param {Window} options.win - If chrome=true, window to test, otherwise + * parent window of the content window to test. + * @param {boolean} options.chrome - If true the media queries will be matched + * against the supplied chrome window. Otherwise they will be matched against + * the content window. + * @returns {Promise<{light: boolean, dark: boolean}>} - Resolves with an + * object of the media query results. + */ +function getPrefersColorSchemeInfo({ win, chrome = false }) { + let fn = async windowObj => { + // If called in the parent, we use the supplied win object. Otherwise use + // the content window global. + let win = windowObj || content; + + // LookAndFeel updates are async. + await new Promise(resolve => { + win.requestAnimationFrame(() => win.requestAnimationFrame(resolve)); + }); + return { + light: win.matchMedia("(prefers-color-scheme: light)").matches, + dark: win.matchMedia("(prefers-color-scheme: dark)").matches, + }; + }; + + if (chrome) { + return fn(win); + } + + return SpecialPowers.spawn(win.gBrowser.selectedBrowser, [], fn); +} + +add_setup(async function() { + // Set system theme to light to ensure consistency across test machines. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.theme.dark-private-windows", true], + ["ui.systemUsesDarkTheme", 0], + ], + }); + // Ensure the built-in themes are initialized. + await BuiltInThemes.ensureBuiltInThemes(); + + // The previous test, browser_ext_themes_ntp_colors.js has side effects. + // Switch to a theme, then switch back to the default theme to reach a + // consistent themeData state. Without this, themeData in + // LightWeightConsumer#_update does not contain darkTheme data and PBM windows + // don't get themed correctly. + let lightTheme = await AddonManager.getAddonByID(LIGHT_THEME_ID); + await lightTheme.enable(); + await lightTheme.disable(); +}); + +// For the default theme with light color scheme, private browsing windows +// should be themed dark. +// The PBM window's content should not be themed dark. +add_task(async function test_default_theme_light() { + info("Normal browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: window, + expectDark: false, + expectLWTAttributes: false, + expectDefaultDarkAttribute: false, + }); + + let windowB = await BrowserTestUtils.openNewBrowserWindow(); + + info("Additional normal browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: windowB, + expectDark: false, + expectLWTAttributes: false, + expectDefaultDarkAttribute: false, + }); + + let pbmWindowA = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindowA, + expectDark: true, + expectLWTAttributes: true, + expectDefaultDarkAttribute: true, + }); + + let prefersColorScheme = await getPrefersColorSchemeInfo({ win: pbmWindowA }); + ok( + prefersColorScheme.light && !prefersColorScheme.dark, + "Content of dark themed PBM window should still be themed light" + ); + + let pbmWindowB = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + info("Additional private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindowB, + expectDark: true, + expectLWTAttributes: true, + expectDefaultDarkAttribute: true, + }); + + await BrowserTestUtils.closeWindow(windowB); + await BrowserTestUtils.closeWindow(pbmWindowA); + await BrowserTestUtils.closeWindow(pbmWindowB); +}); + +// For the default theme with dark color scheme, normal and private browsing +// windows should be themed dark. +add_task(async function test_default_theme_dark() { + // Set the system theme to dark. The default theme will follow this color + // scheme. + await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 1]] }); + + info("Normal browsing window should be in dark mode."); + await testWindowColorScheme({ + win: window, + expectDark: true, + expectLWTAttributes: !IS_LINUX, + expectDefaultDarkAttribute: !IS_LINUX, + }); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: true, + expectLWTAttributes: !IS_LINUX, + expectDefaultDarkAttribute: !IS_LINUX, + }); + + await BrowserTestUtils.closeWindow(pbmWindow); + + await SpecialPowers.popPrefEnv(); +}); + +// For the light theme both normal and private browsing windows should have a +// bright color scheme applied. +add_task(async function test_light_theme_builtin() { + let lightTheme = await AddonManager.getAddonByID(LIGHT_THEME_ID); + await lightTheme.enable(); + + info("Normal browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: window, + expectDark: false, + expectLWTAttributes: true, + expectDefaultDarkAttribute: false, + }); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + info("Private browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: false, + expectLWTAttributes: true, + expectDefaultDarkAttribute: false, + }); + + await BrowserTestUtils.closeWindow(pbmWindow); + await lightTheme.disable(); +}); + +// For the dark theme both normal and private browsing should have a dark color +// scheme applied. +add_task(async function test_dark_theme_builtin() { + let darkTheme = await AddonManager.getAddonByID(DARK_THEME_ID); + await darkTheme.enable(); + + info("Normal browsing window should be in dark mode."); + await testWindowColorScheme({ + win: window, + expectDark: true, + expectLWTAttributes: true, + expectDefaultDarkAttribute: false, + }); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: true, + expectLWTAttributes: true, + expectDefaultDarkAttribute: false, + }); + + await BrowserTestUtils.closeWindow(pbmWindow); + await darkTheme.disable(); +}); + +// When switching between default, light and dark theme the private browsing +// window color scheme should update accordingly. +add_task(async function test_theme_switch_updates_existing_pbm_win() { + let windowB = await BrowserTestUtils.openNewBrowserWindow(); + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + info("Normal browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: window, + expectDark: false, + expectLWTAttributes: false, + expectDefaultDarkAttribute: false, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: true, + expectLWTAttributes: true, + expectDefaultDarkAttribute: true, + }); + + info("Enabling light theme."); + let lightTheme = await AddonManager.getAddonByID(LIGHT_THEME_ID); + await lightTheme.enable(); + + info("Normal browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: window, + expectDark: false, + expectLWTAttributes: true, + expectDefaultDarkAttribute: false, + }); + + info("Private browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: false, + expectLWTAttributes: true, + expectDefaultDarkAttribute: false, + }); + + await lightTheme.disable(); + + info("Enabling dark theme."); + let darkTheme = await AddonManager.getAddonByID(DARK_THEME_ID); + await darkTheme.enable(); + + info("Normal browsing window should be in dark mode."); + await testWindowColorScheme({ + win: window, + expectDark: true, + expectLWTAttributes: true, + expectDefaultDarkAttribute: false, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: true, + expectLWTAttributes: true, + expectDefaultDarkAttribute: false, + }); + + await darkTheme.disable(); + + await BrowserTestUtils.closeWindow(windowB); + await BrowserTestUtils.closeWindow(pbmWindow); +}); + +// pageInfo windows should inherit the PBM window dark theme. +add_task(async function test_pbm_dark_page_info() { + for (let isPBM of [false, true]) { + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: isPBM, + }); + let windowTypeStr = isPBM ? "private" : "normal"; + + info(`Opening pageInfo from ${windowTypeStr} browsing.`); + + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "https://example.com" }, + async () => { + let pageInfo = win.BrowserPageInfo(null, "securityTab"); + await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); + + let prefersColorScheme = await getPrefersColorSchemeInfo({ + win: pageInfo, + chrome: true, + }); + if (isPBM) { + ok( + !prefersColorScheme.light && prefersColorScheme.dark, + "pageInfo from private window should be themed dark." + ); + } else { + ok( + prefersColorScheme.light && !prefersColorScheme.dark, + "pageInfo from normal window should be themed light." + ); + } + + pageInfo.close(); + } + ); + + await BrowserTestUtils.closeWindow(win); + } +}); + +// Prompts should inherit the PBM window dark theme. +add_task(async function test_pbm_dark_prompts() { + const { MODAL_TYPE_TAB, MODAL_TYPE_CONTENT } = Services.prompt; + + for (let isPBM of [false, true]) { + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: isPBM, + }); + + // TODO: Once Bug 1751953 has been fixed, we can also test MODAL_TYPE_WINDOW + // here. + for (let modalType of [MODAL_TYPE_TAB, MODAL_TYPE_CONTENT]) { + let windowTypeStr = isPBM ? "private" : "normal"; + let modalTypeStr = modalType == MODAL_TYPE_TAB ? "tab" : "content"; + + info(`Opening ${modalTypeStr} prompt from ${windowTypeStr} browsing.`); + + let openPromise = PromptTestUtils.waitForPrompt( + win.gBrowser.selectedBrowser, + { + modalType, + promptType: "alert", + } + ); + let promptPromise = Services.prompt.asyncAlert( + win.gBrowser.selectedBrowser.browsingContext, + modalType, + "Hello", + "Hello, world!" + ); + + let dialog = await openPromise; + + let prefersColorScheme = await getPrefersColorSchemeInfo({ + win: dialog.ui.prompt, + chrome: true, + }); + + if (isPBM) { + ok( + !prefersColorScheme.light && prefersColorScheme.dark, + "Prompt from private window should be themed dark." + ); + } else { + ok( + prefersColorScheme.light && !prefersColorScheme.dark, + "Prompt from normal window should be themed light." + ); + } + + await PromptTestUtils.handlePrompt(dialog); + await promptPromise; + } + + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js b/toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js new file mode 100644 index 0000000000..526c3a0883 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js @@ -0,0 +1,64 @@ +"use strict"; + +// This test checks whether applied WebExtension themes are persisted and applied +// on newly opened windows. + +add_task(async function test_multiple_windows() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let docEl = window.document.documentElement; + let toolbox = document.querySelector("#navigator-toolbox"); + let computedStyle = window.getComputedStyle( + backgroundColorSetOnRoot() ? docEl : toolbox + ); + + Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set"); + Assert.equal( + docEl.getAttribute("lwtheme-brighttext"), + "true", + "LWT text color attribute should be set" + ); + Assert.ok( + computedStyle.backgroundImage.includes("image1.png"), + "Expected background image" + ); + + // Now we'll open a new window to see if the theme is also applied there. + let window2 = await BrowserTestUtils.openNewBrowserWindow(); + docEl = window2.document.documentElement; + toolbox = window2.document.querySelector("#navigator-toolbox"); + computedStyle = window.getComputedStyle( + backgroundColorSetOnRoot() ? docEl : toolbox + ); + + Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set"); + Assert.equal( + docEl.getAttribute("lwtheme-brighttext"), + "true", + "LWT text color attribute should be set" + ); + Assert.ok( + computedStyle.backgroundImage.includes("image1.png"), + "Expected background image" + ); + + await BrowserTestUtils.closeWindow(window2); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_reset.js b/toolkit/components/extensions/test/browser/browser_ext_themes_reset.js new file mode 100644 index 0000000000..d8b3b14073 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_reset.js @@ -0,0 +1,112 @@ +"use strict"; + +add_task(async function theme_reset_global_static_theme() { + let global_theme_extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: "#123456", + tab_background_text: "#fedcba", + }, + }, + }, + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["theme"], + }, + async background() { + await browser.theme.reset(); + let theme_after_reset = await browser.theme.getCurrent(); + + browser.test.assertEq( + "#123456", + theme_after_reset.colors.frame, + "Theme from other extension should not be cleared upon reset()" + ); + + let theme = { + colors: { + frame: "#CF723F", + }, + }; + + await browser.theme.update(theme); + await browser.theme.reset(); + let final_reset_theme = await browser.theme.getCurrent(); + + browser.test.assertEq( + JSON.stringify({ colors: null, images: null, properties: null }), + JSON.stringify(final_reset_theme), + "Should reset when extension had replaced the global theme" + ); + browser.test.sendMessage("done"); + }, + }); + await global_theme_extension.startup(); + await extension.startup(); + await extension.awaitMessage("done"); + + await global_theme_extension.unload(); + await extension.unload(); +}); + +add_task(async function theme_reset_by_windowId() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["theme"], + }, + async background() { + let theme = { + colors: { + frame: "#CF723F", + }, + }; + + let { id: winId } = await browser.windows.getCurrent(); + await browser.theme.update(winId, theme); + let update_theme = await browser.theme.getCurrent(winId); + + browser.test.onMessage.addListener(async () => { + let current_theme = await browser.theme.getCurrent(winId); + browser.test.assertEq( + update_theme.colors.frame, + current_theme.colors.frame, + "Should not be reset by a reset(windowId) call from another extension" + ); + + browser.test.sendMessage("done"); + }); + + browser.test.sendMessage("ready", winId); + }, + }); + + let anotherExtension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["theme"], + }, + background() { + browser.test.onMessage.addListener(async winId => { + await browser.theme.reset(winId); + browser.test.sendMessage("done"); + }); + }, + }); + + await extension.startup(); + let winId = await extension.awaitMessage("ready"); + + await anotherExtension.startup(); + + // theme.reset should be ignored if the theme was set by another extension. + anotherExtension.sendMessage(winId); + await anotherExtension.awaitMessage("done"); + + extension.sendMessage(); + await extension.awaitMessage("done"); + + await anotherExtension.unload(); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js b/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js new file mode 100644 index 0000000000..2e7359ce29 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js @@ -0,0 +1,187 @@ +"use strict"; + +// This test checks color sanitization in various situations + +add_task(async function test_sanitization_invalid() { + // This test checks that invalid values are sanitized + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + bookmark_text: "ntimsfavoriteblue", + }, + }, + }, + }); + + await extension.startup(); + + let navbar = document.querySelector("#nav-bar"); + Assert.equal( + window.getComputedStyle(navbar).color, + "rgb(0, 0, 0)", + "All invalid values should always compute to black." + ); + + await extension.unload(); +}); + +add_task(async function test_sanitization_css_variables() { + // This test checks that CSS variables are sanitized + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + bookmark_text: "var(--arrowpanel-dimmed)", + }, + }, + }, + }); + + await extension.startup(); + + let navbar = document.querySelector("#nav-bar"); + Assert.equal( + window.getComputedStyle(navbar).color, + "rgb(0, 0, 0)", + "All CSS variables should always compute to black." + ); + + await extension.unload(); +}); + +add_task(async function test_sanitization_important() { + // This test checks that the sanitizer cannot be fooled with !important + let stylesheetAttr = `href="data:text/css,*{color:red!important}" type="text/css"`; + let stylesheet = document.createProcessingInstruction( + "xml-stylesheet", + stylesheetAttr + ); + let load = BrowserTestUtils.waitForEvent(stylesheet, "load"); + document.insertBefore(stylesheet, document.documentElement); + await load; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + bookmark_text: "green", + }, + }, + }, + }); + + await extension.startup(); + + let navbar = document.querySelector("#nav-bar"); + Assert.equal( + window.getComputedStyle(navbar).color, + "rgb(255, 0, 0)", + "Sheet applies" + ); + + stylesheet.remove(); + + Assert.equal( + window.getComputedStyle(navbar).color, + "rgb(0, 128, 0)", + "Shouldn't be able to fool the color sanitizer with !important" + ); + + await extension.unload(); +}); + +add_task(async function test_sanitization_transparent() { + // This test checks whether transparent values are applied properly + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_top_separator: "transparent", + }, + }, + }, + }); + + await extension.startup(); + + let navbar = document.querySelector("#nav-bar"); + Assert.ok( + window.getComputedStyle(navbar).boxShadow.includes("rgba(0, 0, 0, 0)"), + "Top separator should be transparent" + ); + + await extension.unload(); +}); + +add_task(async function test_sanitization_transparent_frame_color() { + // This test checks whether transparent frame color falls back to white. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: "transparent", + tab_background_text: TEXT_COLOR, + }, + }, + }, + }); + + await extension.startup(); + + let toolbox = document.querySelector("#navigator-toolbox"); + let toolboxCS = window.getComputedStyle(toolbox); + + if (backgroundColorSetOnRoot()) { + let docEl = document.documentElement; + let rootCS = window.getComputedStyle(docEl); + Assert.equal( + rootCS.backgroundColor, + "rgb(255, 255, 255)", + "Accent color should be white" + ); + } else { + Assert.equal( + toolboxCS.backgroundColor, + "rgb(255, 255, 255)", + "Accent color should be white" + ); + } + + await extension.unload(); +}); + +add_task( + async function test_sanitization_transparent_tab_background_text_color() { + // This test checks whether transparent textcolor falls back to black. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: "transparent", + }, + }, + }, + }); + + await extension.startup(); + + let docEl = document.documentElement; + Assert.equal( + window.getComputedStyle(docEl).color, + "rgb(0, 0, 0)", + "Text color should be black" + ); + + await extension.unload(); + } +); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js b/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js new file mode 100644 index 0000000000..4da4927ccf --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js @@ -0,0 +1,76 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// the separator colors are applied properly. + +add_task(async function test_support_separator_properties() { + const SEPARATOR_TOP_COLOR = "#ff00ff"; + const SEPARATOR_VERTICAL_COLOR = "#f0000f"; + const SEPARATOR_FIELD_COLOR = "#9400ff"; + const SEPARATOR_BOTTOM_COLOR = "#3366cc"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_top_separator: SEPARATOR_TOP_COLOR, + toolbar_vertical_separator: SEPARATOR_VERTICAL_COLOR, + // This property is deprecated, but left in to check it doesn't + // unexpectedly break the theme installation. + toolbar_field_separator: SEPARATOR_FIELD_COLOR, + toolbar_bottom_separator: SEPARATOR_BOTTOM_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + // Test the deprecated color property. + let deprecatedMessagePromise = new Promise(resolve => { + Services.console.registerListener(function listener(msg) { + if (msg.message.includes("toolbar_field_separator")) { + resolve(); + Services.console.unregisterListener(listener); + } + }); + }); + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + info("Wait for property deprecation message"); + await deprecatedMessagePromise; + + let navbar = document.querySelector("#nav-bar"); + Assert.ok( + window + .getComputedStyle(navbar) + .boxShadow.includes(`rgb(${hexToRGB(SEPARATOR_TOP_COLOR).join(", ")})`), + "Top separator color properly set" + ); + + let panelUIButton = document.querySelector("#PanelUI-button"); + // Bug 1712334: This should test bookmark item toolbar separators instead + Assert.equal( + window + .getComputedStyle(panelUIButton) + .getPropertyValue("border-image-source"), + "none", + "No vertical separator on app menu" + ); + + let toolbox = document.querySelector("#navigator-toolbox"); + Assert.equal( + window.getComputedStyle(toolbox).borderBottomColor, + `rgb(${hexToRGB(SEPARATOR_BOTTOM_COLOR).join(", ")})`, + "Bottom separator color properly set" + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js new file mode 100644 index 0000000000..35a7955f9f --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js @@ -0,0 +1,275 @@ +"use strict"; + +// This test checks whether the sidebar color properties work. + +/** + * Test whether the selected browser has the sidebar theme applied + * + * @param {object} theme that is applied + * @param {boolean} isBrightText whether the brighttext attribute should be set + */ +async function test_sidebar_theme(theme, isBrightText) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme, + }, + }); + + const sidebarBox = document.getElementById("sidebar-box"); + const content = SidebarUI.browser.contentWindow; + const root = content.document.documentElement; + + ok( + !sidebarBox.hasAttribute("lwt-sidebar"), + "Sidebar box should not have lwt-sidebar attribute" + ); + ok( + !root.hasAttribute("lwt-sidebar"), + "Sidebar should not have lwt-sidebar attribute" + ); + ok( + !root.hasAttribute("lwt-sidebar-brighttext"), + "Sidebar should not have lwt-sidebar-brighttext attribute" + ); + ok( + !root.hasAttribute("lwt-sidebar-highlight"), + "Sidebar should not have lwt-sidebar-highlight attribute" + ); + + const rootCS = content.getComputedStyle(root); + const originalBackground = rootCS.backgroundColor; + const originalColor = rootCS.color; + + // ::-moz-tree-row(selected, focus) computed style can't be accessed, so we create a fake one. + const highlightCS = { + get backgroundColor() { + // Standardize to rgb like other computed style. + let color = rootCS.getPropertyValue( + "--lwt-sidebar-highlight-background-color" + ); + let [r, g, b] = color + .replace("rgba(", "") + .split(",") + .map(channel => parseInt(channel, 10)); + return `rgb(${r}, ${g}, ${b})`; + }, + + get color() { + let color = rootCS.getPropertyValue("--lwt-sidebar-highlight-text-color"); + let [r, g, b] = color + .replace("rgba(", "") + .split(",") + .map(channel => parseInt(channel, 10)); + return `rgb(${r}, ${g}, ${b})`; + }, + }; + const originalHighlightBackground = highlightCS.backgroundColor; + const originalHighlightColor = highlightCS.color; + + await extension.startup(); + + Services.ppmm.sharedData.flush(); + + const actualBackground = hexToCSS(theme.colors.sidebar) || originalBackground; + const actualColor = hexToCSS(theme.colors.sidebar_text) || originalColor; + const actualHighlightBackground = + hexToCSS(theme.colors.sidebar_highlight) || originalHighlightBackground; + const actualHighlightColor = + hexToCSS(theme.colors.sidebar_highlight_text) || originalHighlightColor; + const isCustomHighlight = !!theme.colors.sidebar_highlight_text; + const isCustomSidebar = !!theme.colors.sidebar_text; + + is( + sidebarBox.hasAttribute("lwt-sidebar"), + isCustomSidebar, + `Sidebar box should${ + !isCustomSidebar ? " not" : "" + } have lwt-sidebar attribute` + ); + is( + root.hasAttribute("lwt-sidebar"), + isCustomSidebar, + `Sidebar should${!isCustomSidebar ? " not" : ""} have lwt-sidebar attribute` + ); + is( + root.hasAttribute("lwt-sidebar-brighttext"), + isBrightText, + `Sidebar should${ + !isBrightText ? " not" : "" + } have lwt-sidebar-brighttext attribute` + ); + is( + root.hasAttribute("lwt-sidebar-highlight"), + isCustomHighlight, + `Sidebar should${ + !isCustomHighlight ? " not" : "" + } have lwt-sidebar-highlight attribute` + ); + + if (isCustomSidebar) { + const sidebarBoxCS = window.getComputedStyle(sidebarBox); + is( + sidebarBoxCS.backgroundColor, + actualBackground, + "Sidebar box background should be set." + ); + is( + sidebarBoxCS.color, + actualColor, + "Sidebar box text color should be set." + ); + } + + is( + rootCS.backgroundColor, + actualBackground, + "Sidebar background should be set." + ); + is(rootCS.color, actualColor, "Sidebar text color should be set."); + + is( + highlightCS.backgroundColor, + actualHighlightBackground, + "Sidebar highlight background color should be set." + ); + is( + highlightCS.color, + actualHighlightColor, + "Sidebar highlight text color should be set." + ); + + await extension.unload(); + + Services.ppmm.sharedData.flush(); + + ok( + !sidebarBox.hasAttribute("lwt-sidebar"), + "Sidebar box should not have lwt-sidebar attribute" + ); + ok( + !root.hasAttribute("lwt-sidebar"), + "Sidebar should not have lwt-sidebar attribute" + ); + ok( + !root.hasAttribute("lwt-sidebar-brighttext"), + "Sidebar should not have lwt-sidebar-brighttext attribute" + ); + ok( + !root.hasAttribute("lwt-sidebar-highlight"), + "Sidebar should not have lwt-sidebar-highlight attribute" + ); + + is( + rootCS.backgroundColor, + originalBackground, + "Sidebar background should be reset." + ); + is(rootCS.color, originalColor, "Sidebar text color should be reset."); + is( + highlightCS.backgroundColor, + originalHighlightBackground, + "Sidebar highlight background color should be reset." + ); + is( + highlightCS.color, + originalHighlightColor, + "Sidebar highlight text color should be reset." + ); +} + +add_task(async function test_support_sidebar_colors() { + for (let command of ["viewBookmarksSidebar", "viewHistorySidebar"]) { + info("Executing command: " + command); + + await SidebarUI.show(command); + + await test_sidebar_theme( + { + colors: { + sidebar: "#fafad2", // lightgoldenrodyellow + sidebar_text: "#2f4f4f", // darkslategrey + }, + }, + false + ); + + await test_sidebar_theme( + { + colors: { + sidebar: "#8b4513", // saddlebrown + sidebar_text: "#ffa07a", // lightsalmon + }, + }, + true + ); + + await test_sidebar_theme( + { + colors: { + sidebar: "#fffafa", // snow + sidebar_text: "#663399", // rebeccapurple + sidebar_highlight: "#7cfc00", // lawngreen + sidebar_highlight_text: "#ffefd5", // papayawhip + }, + }, + false + ); + + await test_sidebar_theme( + { + colors: { + sidebar_highlight: "#a0522d", // sienna + sidebar_highlight_text: "#fff5ee", // seashell + }, + }, + false + ); + } +}); + +add_task(async function test_support_sidebar_border_color() { + const LIGHT_SALMON = "#ffa07a"; + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + sidebar_border: LIGHT_SALMON, + }, + }, + }, + }); + + await extension.startup(); + + const sidebarHeader = document.getElementById("sidebar-header"); + const sidebarHeaderCS = window.getComputedStyle(sidebarHeader); + + is( + sidebarHeaderCS.borderBottomColor, + hexToCSS(LIGHT_SALMON), + "Sidebar header border should be colored properly" + ); + + if (AppConstants.platform !== "linux") { + const sidebarSplitter = document.getElementById("sidebar-splitter"); + const sidebarSplitterCS = window.getComputedStyle(sidebarSplitter); + + is( + sidebarSplitterCS.borderInlineEndColor, + hexToCSS(LIGHT_SALMON), + "Sidebar splitter should be colored properly" + ); + + SidebarUI.reversePosition(); + + is( + sidebarSplitterCS.borderInlineStartColor, + hexToCSS(LIGHT_SALMON), + "Sidebar splitter should be colored properly after switching sides" + ); + + SidebarUI.reversePosition(); + } + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js b/toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js new file mode 100644 index 0000000000..4a6d9a92f6 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js @@ -0,0 +1,126 @@ +"use strict"; + +// This test checks whether browser.theme.onUpdated works +// when a static theme is applied + +add_task(async function test_on_updated() { + const theme = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + const extension = ExtensionTestUtils.loadExtension({ + background() { + browser.theme.onUpdated.addListener(updateInfo => { + browser.test.sendMessage("theme-updated", updateInfo); + }); + }, + }); + + await extension.startup(); + + info("Testing update event on static theme startup"); + let updatedPromise = extension.awaitMessage("theme-updated"); + await theme.startup(); + const { theme: receivedTheme, windowId } = await updatedPromise; + Assert.ok(!windowId, "No window id in static theme update event"); + Assert.ok( + receivedTheme.images.theme_frame.includes("image1.png"), + "Theme theme_frame image should be applied" + ); + Assert.equal( + receivedTheme.colors.frame, + ACCENT_COLOR, + "Theme frame color should be applied" + ); + Assert.equal( + receivedTheme.colors.tab_background_text, + TEXT_COLOR, + "Theme tab_background_text color should be applied" + ); + + info("Testing update event on static theme unload"); + updatedPromise = extension.awaitMessage("theme-updated"); + await theme.unload(); + const updateInfo = await updatedPromise; + Assert.ok(!windowId, "No window id in static theme update event on unload"); + Assert.equal( + Object.keys(updateInfo.theme), + 0, + "unloading theme sends empty theme in update event" + ); + + await extension.unload(); +}); + +add_task(async function test_on_updated_eventpage() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.eventPages.enabled", true]], + }); + const theme = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: "watcher@mochitest" } }, + background: { persistent: false }, + }, + background() { + browser.theme.onUpdated.addListener(updateInfo => { + browser.test.sendMessage("theme-updated", updateInfo); + }); + }, + }); + + await extension.startup(); + assertPersistentListeners(extension, "theme", "onUpdated", { + primed: false, + }); + + await extension.terminateBackground(); + assertPersistentListeners(extension, "theme", "onUpdated", { + primed: true, + }); + + info("Testing update event on static theme startup"); + let updatedPromise = extension.awaitMessage("theme-updated"); + await theme.startup(); + const { theme: receivedTheme, windowId } = await updatedPromise; + Assert.ok(!windowId, "No window id in static theme update event"); + Assert.ok( + receivedTheme.images.theme_frame.includes("image1.png"), + "Theme theme_frame image should be applied" + ); + await theme.unload(); + await extension.awaitMessage("theme-updated"); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js new file mode 100644 index 0000000000..e4bd8cb99b --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js @@ -0,0 +1,39 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// the color of the tab line are applied properly. + +add_task(async function test_support_tab_line() { + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + const TAB_LINE_COLOR = "#ff0000"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: "#000", + tab_line: TAB_LINE_COLOR, + }, + }, + }, + }); + + await extension.startup(); + + info("Checking selected tab line color"); + let selectedTab = newWin.document.querySelector(".tabbrowser-tab[selected]"); + let tab = selectedTab.querySelector(".tab-background"); + let element = tab; + let property = "outline-color"; + let computedValue = newWin.getComputedStyle(element)[property]; + let expectedColor = `rgb(${hexToRGB(TAB_LINE_COLOR).join(", ")})`; + + Assert.ok( + computedValue.includes(expectedColor), + `Tab line should be displayed in the box shadow of the tab: ${computedValue}` + ); + + await extension.unload(); + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js new file mode 100644 index 0000000000..1e402dbcc6 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js @@ -0,0 +1,51 @@ +"use strict"; + +add_task(async function test_support_tab_loading_filling() { + const TAB_LOADING_COLOR = "#FF0000"; + + // Make sure we use the animating loading icon + await SpecialPowers.pushPrefEnv({ + set: [["ui.prefersReducedMotion", 0]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: "#000", + toolbar: "#124455", + tab_background_text: "#9400ff", + tab_loading: TAB_LOADING_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + info("Checking selected tab loading indicator colors"); + + let selectedTab = document.querySelector( + ".tabbrowser-tab[visuallyselected=true]" + ); + + selectedTab.setAttribute("busy", "true"); + selectedTab.setAttribute("progress", "true"); + + let throbber = selectedTab.throbber; + Assert.equal( + window.getComputedStyle(throbber, "::before").fill, + `rgb(${hexToRGB(TAB_LOADING_COLOR).join(", ")})`, + "Throbber is filled with theme color" + ); + + selectedTab.removeAttribute("busy"); + selectedTab.removeAttribute("progress"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js new file mode 100644 index 0000000000..21f3c6d38b --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js @@ -0,0 +1,54 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// the background color of selected tab are applied correctly. + +add_task(async function test_tab_background_color_property() { + const TAB_BACKGROUND_COLOR = "#9400ff"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + tab_selected: TAB_BACKGROUND_COLOR, + }, + }, + }, + }); + + await extension.startup(); + + info("Checking selected tab color"); + + let openTab = document.querySelector( + ".tabbrowser-tab[visuallyselected=true]" + ); + let openTabBackground = openTab.querySelector(".tab-background"); + + let selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + let selectedTabBackground = selectedTab.querySelector(".tab-background"); + + let openTabGradient = window + .getComputedStyle(openTabBackground) + .getPropertyValue("background-image"); + let selectedTabGradient = window + .getComputedStyle(selectedTabBackground) + .getPropertyValue("background-image"); + + let rgbRegex = /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)/g; + let selectedTabColors = selectedTabGradient.match(rgbRegex); + + Assert.equal( + selectedTabColors[0], + "rgb(" + hexToRGB(TAB_BACKGROUND_COLOR).join(", ") + ")", + "Selected tab background color should be set." + ); + Assert.equal(openTabGradient, "none"); + + gBrowser.removeTab(selectedTab); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js new file mode 100644 index 0000000000..d819f3a5f1 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js @@ -0,0 +1,70 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// the text color of the selected tab are applied properly. + +add_task(async function test_support_tab_text_property_css_color() { + const TAB_TEXT_COLOR = "#9400ff"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + tab_text: TAB_TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + info("Checking selected tab colors"); + let selectedTab = document.querySelector(".tabbrowser-tab[selected]"); + Assert.equal( + window.getComputedStyle(selectedTab).color, + "rgb(" + hexToRGB(TAB_TEXT_COLOR).join(", ") + ")", + "Selected tab text color should be set." + ); + + await extension.unload(); +}); + +add_task(async function test_support_tab_text_chrome_array() { + const TAB_TEXT_COLOR = [148, 0, 255]; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: FRAME_COLOR, + tab_background_text: TAB_BACKGROUND_TEXT_COLOR, + tab_text: TAB_TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + info("Checking selected tab colors"); + let selectedTab = document.querySelector(".tabbrowser-tab[selected]"); + Assert.equal( + window.getComputedStyle(selectedTab).color, + "rgb(" + TAB_TEXT_COLOR.join(", ") + ")", + "Selected tab text color should be set." + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js b/toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js new file mode 100644 index 0000000000..39934200ac --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js @@ -0,0 +1,48 @@ +"use strict"; + +// This test checks whether the applied theme transition effects are applied +// correctly. + +add_task(async function test_theme_transition_effects() { + const TOOLBAR = "#f27489"; + const TEXT_COLOR = "#000000"; + const TRANSITION_PROPERTY = "background-color"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + tab_background_text: TEXT_COLOR, + toolbar: TOOLBAR, + bookmark_text: TEXT_COLOR, + }, + }, + }, + }); + + await extension.startup(); + + // check transition effect for toolbars + let navbar = document.querySelector("#nav-bar"); + let navbarCS = window.getComputedStyle(navbar); + + Assert.ok( + navbarCS + .getPropertyValue("transition-property") + .includes(TRANSITION_PROPERTY), + "Transition property set for #nav-bar" + ); + + let bookmarksBar = document.querySelector("#PersonalToolbar"); + setToolbarVisibility(bookmarksBar, true, false, true); + let bookmarksBarCS = window.getComputedStyle(bookmarksBar); + + Assert.ok( + bookmarksBarCS + .getPropertyValue("transition-property") + .includes(TRANSITION_PROPERTY), + "Transition property set for #PersonalToolbar" + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js new file mode 100644 index 0000000000..74e0674245 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js @@ -0,0 +1,183 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// the background color and the color of the navbar text fields are applied properly. + +const { CustomizableUITestUtils } = ChromeUtils.import( + "resource://testing-common/CustomizableUITestUtils.jsm" +); +let gCUITestUtils = new CustomizableUITestUtils(window); + +add_setup(async function() { + await gCUITestUtils.addSearchBar(); + registerCleanupFunction(() => { + gCUITestUtils.removeSearchBar(); + }); +}); + +add_task(async function test_support_toolbar_field_properties() { + const TOOLBAR_FIELD_BACKGROUND = "#ff00ff"; + const TOOLBAR_FIELD_COLOR = "#00ff00"; + const TOOLBAR_FIELD_BORDER = "#aaaaff"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_field: TOOLBAR_FIELD_BACKGROUND, + toolbar_field_text: TOOLBAR_FIELD_COLOR, + toolbar_field_border: TOOLBAR_FIELD_BORDER, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let root = document.documentElement; + // Remove the `remotecontrol` attribute since it interferes with the urlbar styling. + root.removeAttribute("remotecontrol"); + registerCleanupFunction(() => { + root.setAttribute("remotecontrol", "true"); + }); + + let fields = [ + document.querySelector("#urlbar-background"), + BrowserSearch.searchBar, + ].filter(field => { + let bounds = field.getBoundingClientRect(); + return bounds.width > 0 && bounds.height > 0; + }); + + Assert.equal(fields.length, 2, "Should be testing two elements"); + + info( + `Checking toolbar background colors and colors for ${fields.length} toolbar fields.` + ); + for (let field of fields) { + info(`Testing ${field.id || field.className}`); + Assert.equal( + window.getComputedStyle(field).backgroundColor, + hexToCSS(TOOLBAR_FIELD_BACKGROUND), + "Field background should be set." + ); + Assert.equal( + window.getComputedStyle(field).color, + hexToCSS(TOOLBAR_FIELD_COLOR), + "Field color should be set." + ); + testBorderColor(field, TOOLBAR_FIELD_BORDER); + } + + await extension.unload(); +}); + +add_task(async function test_support_toolbar_field_brighttext() { + let root = document.documentElement; + // Remove the `remotecontrol` attribute since it interferes with the urlbar styling. + root.removeAttribute("remotecontrol"); + registerCleanupFunction(() => { + root.setAttribute("remotecontrol", "true"); + }); + let urlbar = gURLBar.textbox; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_field: "#fff", + toolbar_field_text: "#000", + }, + }, + }, + }); + + await extension.startup(); + + Assert.equal( + window.getComputedStyle(urlbar).color, + hexToCSS("#000000"), + "Color has been set" + ); + Assert.ok( + !root.hasAttribute("lwt-toolbar-field-brighttext"), + "Brighttext attribute should not be set" + ); + + await extension.unload(); + + extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_field: "#000", + toolbar_field_text: "#fff", + }, + }, + }, + }); + + await extension.startup(); + + Assert.equal( + window.getComputedStyle(urlbar).color, + hexToCSS("#ffffff"), + "Color has been set" + ); + Assert.ok( + root.hasAttribute("lwt-toolbar-field-brighttext"), + "Brighttext attribute should be set" + ); + + await extension.unload(); +}); + +// Verifies that we apply the lwt-toolbar-field-brighttext attribute when +// toolbar fields are dark text on a dark background. +add_task(async function test_support_toolbar_field_brighttext_dark_on_dark() { + let root = document.documentElement; + // Remove the `remotecontrol` attribute since it interferes with the urlbar styling. + root.removeAttribute("remotecontrol"); + registerCleanupFunction(() => { + root.setAttribute("remotecontrol", "true"); + }); + let urlbar = gURLBar.textbox; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar_field: "#000", + toolbar_field_text: "#111111", + }, + }, + }, + }); + + await extension.startup(); + + Assert.equal( + window.getComputedStyle(urlbar).color, + hexToCSS("#111111"), + "Color has been set" + ); + Assert.ok( + root.hasAttribute("lwt-toolbar-field-brighttext"), + "Brighttext attribute should be set" + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js new file mode 100644 index 0000000000..b03dc043aa --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js @@ -0,0 +1,107 @@ +"use strict"; + +add_setup(async function() { + // Remove the `remotecontrol` attribute since it interferes with the urlbar styling. + document.documentElement.removeAttribute("remotecontrol"); + registerCleanupFunction(() => { + document.documentElement.setAttribute("remotecontrol", "true"); + }); +}); + +add_task(async function test_toolbar_field_focus() { + const TOOLBAR_FIELD_BACKGROUND = "#FF00FF"; + const TOOLBAR_FIELD_COLOR = "#00FF00"; + const TOOLBAR_FOCUS_BACKGROUND = "#FF0000"; + const TOOLBAR_FOCUS_TEXT = "#9400FF"; + const TOOLBAR_FOCUS_BORDER = "#FFFFFF"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: "#FF0000", + tab_background_color: "#ffffff", + toolbar_field: TOOLBAR_FIELD_BACKGROUND, + toolbar_field_text: TOOLBAR_FIELD_COLOR, + toolbar_field_focus: TOOLBAR_FOCUS_BACKGROUND, + toolbar_field_text_focus: TOOLBAR_FOCUS_TEXT, + toolbar_field_border_focus: TOOLBAR_FOCUS_BORDER, + }, + }, + }, + }); + + await extension.startup(); + + info("Checking toolbar field's focus color"); + + let urlBar = document.querySelector("#urlbar-background"); + gURLBar.textbox.setAttribute("focused", "true"); + let style = window.getComputedStyle(urlBar); + + Assert.equal( + style.backgroundColor, + `rgb(${hexToRGB(TOOLBAR_FOCUS_BACKGROUND).join(", ")})`, + "Background Color is changed" + ); + Assert.equal( + style.color, + `rgb(${hexToRGB(TOOLBAR_FOCUS_TEXT).join(", ")})`, + "Text Color is changed" + ); + Assert.equal( + style.outlineColor, + `rgb(${hexToRGB(TOOLBAR_FOCUS_BORDER).join(", ")})`, + "Focus ring color" + ); + + gURLBar.textbox.removeAttribute("focused"); + + Assert.equal( + style.backgroundColor, + `rgb(${hexToRGB(TOOLBAR_FIELD_BACKGROUND).join(", ")})`, + "Background Color is set back to initial" + ); + Assert.equal( + style.color, + `rgb(${hexToRGB(TOOLBAR_FIELD_COLOR).join(", ")})`, + "Text Color is set back to initial" + ); + await extension.unload(); +}); + +add_task(async function test_toolbar_field_focus_low_alpha() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: "#FF0000", + tab_background_color: "#ffffff", + toolbar_field: "#FF00FF", + toolbar_field_text: "#00FF00", + toolbar_field_focus: "rgba(0, 0, 255, 0.4)", + toolbar_field_text_focus: "red", + toolbar_field_border_focus: "#FFFFFF", + }, + }, + }, + }); + + await extension.startup(); + gURLBar.textbox.setAttribute("focused", "true"); + + let urlBar = document.querySelector("#urlbar-background"); + Assert.equal( + window.getComputedStyle(urlBar).backgroundColor, + `rgba(0, 0, 255, 0.9)`, + "Background color has minimum opacity enforced" + ); + Assert.equal( + window.getComputedStyle(urlBar).color, + `rgb(255, 255, 255)`, + "Text color has been overridden to match background" + ); + + gURLBar.textbox.removeAttribute("focused"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js new file mode 100644 index 0000000000..37c082b36f --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js @@ -0,0 +1,63 @@ +"use strict"; + +/* globals InspectorUtils */ + +// This test checks whether applied WebExtension themes that attempt to change +// the button background color properties are applied correctly. + +add_task(async function setup_home_button() { + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("home-button") + ); +}); + +add_task(async function test_button_background_properties() { + const BUTTON_BACKGROUND_ACTIVE = "#FFFFFF"; + const BUTTON_BACKGROUND_HOVER = "#59CBE8"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + button_background_active: BUTTON_BACKGROUND_ACTIVE, + button_background_hover: BUTTON_BACKGROUND_HOVER, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let toolbarButton = document.querySelector("#home-button"); + let toolbarButtonIcon = toolbarButton.icon; + let toolbarButtonIconCS = window.getComputedStyle(toolbarButtonIcon); + + InspectorUtils.addPseudoClassLock(toolbarButton, ":hover"); + + Assert.equal( + toolbarButtonIconCS.getPropertyValue("background-color"), + `rgb(${hexToRGB(BUTTON_BACKGROUND_HOVER).join(", ")})`, + "Toolbar button hover background is set." + ); + + InspectorUtils.addPseudoClassLock(toolbarButton, ":active"); + + Assert.equal( + toolbarButtonIconCS.getPropertyValue("background-color"), + `rgb(${hexToRGB(BUTTON_BACKGROUND_ACTIVE).join(", ")})`, + "Toolbar button active background is set!" + ); + + InspectorUtils.clearPseudoClassLocks(toolbarButton); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js new file mode 100644 index 0000000000..2802c6ac33 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js @@ -0,0 +1,109 @@ +"use strict"; + +// This test checks applied WebExtension themes that attempt to change +// icon color properties + +add_task(async function setup_home_button() { + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("home-button") + ); +}); + +add_task(async function test_icons_properties() { + const ICONS_COLOR = "#001b47"; + const ICONS_ATTENTION_COLOR = "#44ba77"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + icons: ICONS_COLOR, + icons_attention: ICONS_ATTENTION_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let toolbarbutton = document.querySelector("#home-button"); + Assert.equal( + window.getComputedStyle(toolbarbutton).getPropertyValue("fill"), + `rgb(${hexToRGB(ICONS_COLOR).join(", ")})`, + "Buttons fill color set!" + ); + + let starButton = document.querySelector("#star-button"); + starButton.setAttribute("starred", "true"); + + let starComputedStyle = window.getComputedStyle(starButton); + Assert.equal( + starComputedStyle.getPropertyValue( + "--lwt-toolbarbutton-icon-fill-attention" + ), + `rgb(${hexToRGB(ICONS_ATTENTION_COLOR).join(", ")})`, + "Variable is properly set" + ); + Assert.equal( + starComputedStyle.getPropertyValue("fill"), + `rgb(${hexToRGB(ICONS_ATTENTION_COLOR).join(", ")})`, + "Starred icon fill is properly set" + ); + + starButton.removeAttribute("starred"); + + await extension.unload(); +}); + +add_task(async function test_no_icons_properties() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + images: { + theme_frame: "image1.png", + }, + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + }, + }, + }, + files: { + "image1.png": BACKGROUND, + }, + }); + + await extension.startup(); + + let toolbarbutton = document.querySelector("#home-button"); + let toolbarbuttonCS = window.getComputedStyle(toolbarbutton); + let currentColor = toolbarbuttonCS.getPropertyValue("color"); + Assert.equal( + window.getComputedStyle(toolbarbutton).getPropertyValue("fill"), + currentColor, + "Button fill color should be currentColor when no icon color specified." + ); + + let starButton = document.querySelector("#star-button"); + starButton.setAttribute("starred", "true"); + let starComputedStyle = window.getComputedStyle(starButton); + Assert.equal( + starComputedStyle.getPropertyValue( + "--lwt-toolbarbutton-icon-fill-attention" + ), + "", + "Icon attention fill should not be set when the value is not specified in the manifest." + ); + starButton.removeAttribute("starred"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js new file mode 100644 index 0000000000..ee31d80888 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js @@ -0,0 +1,105 @@ +"use strict"; + +// This test checks whether applied WebExtension themes that attempt to change +// the background color of toolbars are applied properly. + +add_task(async function test_support_toolbar_property() { + const TOOLBAR_COLOR = "#ff00ff"; + const TOOLBAR_TEXT_COLOR = "#9400ff"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar: TOOLBAR_COLOR, + toolbar_text: TOOLBAR_TEXT_COLOR, + }, + }, + }, + }); + + let toolbox = document.querySelector("#navigator-toolbox"); + let toolbars = [ + ...toolbox.querySelectorAll("toolbar:not(#TabsToolbar)"), + ].filter(toolbar => { + let bounds = toolbar.getBoundingClientRect(); + return bounds.width > 0 && bounds.height > 0; + }); + + let transitionPromise = waitForTransition(toolbars[0], "background-color"); + await extension.startup(); + await transitionPromise; + + info(`Checking toolbar colors for ${toolbars.length} toolbars.`); + for (let toolbar of toolbars) { + info(`Testing ${toolbar.id}`); + Assert.equal( + window.getComputedStyle(toolbar).backgroundColor, + hexToCSS(TOOLBAR_COLOR), + "Toolbar background color should be set." + ); + Assert.equal( + window.getComputedStyle(toolbar).color, + hexToCSS(TOOLBAR_TEXT_COLOR), + "Toolbar text color should be set." + ); + } + + info("Checking selected tab colors"); + let selectedTab = document.querySelector(".tabbrowser-tab[selected]"); + Assert.equal( + window.getComputedStyle(selectedTab).color, + hexToCSS(TOOLBAR_TEXT_COLOR), + "Selected tab text color should be set." + ); + + await extension.unload(); +}); + +add_task(async function test_bookmark_text_property() { + const TOOLBAR_COLOR = [255, 0, 255]; + const TOOLBAR_TEXT_COLOR = [48, 0, 255]; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + toolbar: TOOLBAR_COLOR, + bookmark_text: TOOLBAR_TEXT_COLOR, + }, + }, + }, + }); + + await extension.startup(); + + let toolbox = document.querySelector("#navigator-toolbox"); + let toolbars = [ + ...toolbox.querySelectorAll("toolbar:not(#TabsToolbar)"), + ].filter(toolbar => { + let bounds = toolbar.getBoundingClientRect(); + return bounds.width > 0 && bounds.height > 0; + }); + + info(`Checking toolbar colors for ${toolbars.length} toolbars.`); + for (let toolbar of toolbars) { + info(`Testing ${toolbar.id}`); + Assert.equal( + window.getComputedStyle(toolbar).color, + rgbToCSS(TOOLBAR_TEXT_COLOR), + "bookmark_text should be an alias for toolbar_text" + ); + } + + info("Checking selected tab colors"); + let selectedTab = document.querySelector(".tabbrowser-tab[selected]"); + Assert.equal( + window.getComputedStyle(selectedTab).color, + rgbToCSS(TOOLBAR_TEXT_COLOR), + "Selected tab text color should be set." + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js b/toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js new file mode 100644 index 0000000000..37ea9b8bbe --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js @@ -0,0 +1,144 @@ +"use strict"; + +const { AddonSettings } = ChromeUtils.import( + "resource://gre/modules/addons/AddonSettings.jsm" +); + +// This test checks that theme warnings are properly emitted. + +function waitForConsole(task, message) { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async resolve => { + SimpleTest.monitorConsole(resolve, [ + { + message: new RegExp(message), + }, + ]); + await task(); + SimpleTest.endMonitorConsole(); + }); +} + +add_setup(async function() { + SimpleTest.waitForExplicitFinish(); +}); + +add_task(async function test_static_theme() { + for (const property of ["colors", "images", "properties"]) { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + [property]: { + such_property: "much_wow", + }, + }, + }, + }); + await waitForConsole( + extension.startup, + `Unrecognized theme property found: ${property}.such_property` + ); + await extension.unload(); + } +}); + +add_task(async function test_dynamic_theme() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["theme"], + }, + background() { + browser.test.onMessage.addListener((msg, details) => { + if (msg === "update-theme") { + browser.theme.update(details).then(() => { + browser.test.sendMessage("theme-updated"); + }); + } else { + browser.theme.reset().then(() => { + browser.test.sendMessage("theme-reset"); + }); + } + }); + }, + }); + + await extension.startup(); + + for (const property of ["colors", "images", "properties"]) { + extension.sendMessage("update-theme", { + [property]: { + such_property: "much_wow", + }, + }); + await waitForConsole( + () => extension.awaitMessage("theme-updated"), + `Unrecognized theme property found: ${property}.such_property` + ); + } + + await extension.unload(); +}); + +add_task(async function test_experiments_enabled() { + info("Testing that experiments are handled correctly on nightly and deved"); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + theme: { + properties: { + such_property: "much_wow", + unknown_property: "very_unknown", + }, + }, + theme_experiment: { + properties: { + such_property: "--such-property", + }, + }, + }, + }); + if (!AddonSettings.EXPERIMENTS_ENABLED) { + await waitForConsole( + extension.startup, + "This extension is not allowed to run theme experiments" + ); + } else { + await waitForConsole( + extension.startup, + "Unrecognized theme property found: properties.unknown_property" + ); + } + await extension.unload(); +}); + +add_task(async function test_experiments_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.experiments.enabled", false]], + }); + + info( + "Testing that experiments are handled correctly when experiements pref is disabled" + ); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + properties: { + such_property: "much_wow", + }, + }, + theme_experiment: { + properties: { + such_property: "--such-property", + }, + }, + }, + }); + await waitForConsole( + extension.startup, + "This extension is not allowed to run theme experiments" + ); + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js b/toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js new file mode 100644 index 0000000000..96a2216067 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js @@ -0,0 +1,94 @@ +"use strict"; + +/* import-globals-from ../../../thumbnails/test/head.js */ +loadTestSubscript("../../../thumbnails/test/head.js"); + +// The service that creates thumbnails of webpages in the background loads a +// web page in the background (with several features disabled). Extensions +// should be able to observe requests, but not run content scripts. +add_task(async function test_thumbnails_background_visibility_to_extensions() { + const iframeUrl = "http://example.com/?iframe"; + const testPageUrl = bgTestPageURL({ iframe: iframeUrl }); + // ^ testPageUrl is http://mochi.test:8888/.../thumbnails_background.sjs?... + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + // ":8888" omitted due to bug 1362809. + matches: [ + "http://mochi.test/*/thumbnails_background.sjs*", + "http://example.com/?iframe*", + ], + js: ["contentscript.js"], + run_at: "document_start", + all_frames: true, + }, + ], + permissions: [ + "webRequest", + "webRequestBlocking", + "http://example.com/*", + "http://mochi.test/*", + ], + }, + files: { + "contentscript.js": () => { + // Content scripts are not expected to be run in the page of the + // thumbnail service, so this should never execute. + new Image().src = "http://example.com/?unexpected-content-script"; + browser.test.fail("Content script ran in thumbs, unexpectedly."); + }, + }, + background() { + let requests = []; + browser.webRequest.onBeforeRequest.addListener( + ({ url, tabId, frameId, type }) => { + browser.test.assertEq(-1, tabId, "Thumb page is not a tab"); + // We want to know if frameId is 0 or non-negative (or possibly -1). + if (type === "sub_frame") { + browser.test.assertTrue(frameId > 0, `frame ${frameId} for ${url}`); + } else { + browser.test.assertEq(0, frameId, `frameId for ${type} ${url}`); + } + requests.push({ type, url }); + }, + { + types: ["main_frame", "sub_frame", "image"], + urls: ["*://*/*"], + }, + ["blocking"] + ); + browser.test.onMessage.addListener(msg => { + browser.test.assertEq("get-results", msg, "expected message"); + browser.test.sendMessage("webRequest-results", requests); + }); + }, + }); + + await extension.startup(); + + ok(!thumbnailExists(testPageUrl), "Thumbnail should not be cached yet."); + + await bgCapture(testPageUrl); + ok(thumbnailExists(testPageUrl), "Thumbnail should be cached after capture"); + removeThumbnail(testPageUrl); + + extension.sendMessage("get-results"); + Assert.deepEqual( + await extension.awaitMessage("webRequest-results"), + [ + { + type: "main_frame", + url: testPageUrl, + }, + { + type: "sub_frame", + url: iframeUrl, + }, + ], + "Expected requests via webRequest" + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js b/toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js new file mode 100644 index 0000000000..d898cb96a4 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js @@ -0,0 +1,72 @@ +"use strict"; + +add_task(async function webnav_test_eventpage() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.eventPages.enabled", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webNavigation", "*://mochi.test/*"], + background: { persistent: false }, + }, + background() { + const EVENTS = [ + "onTabReplaced", + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", + "onHistoryStateUpdated", + ]; + + for (let event of EVENTS) { + browser.webNavigation[event].addListener(() => {}); + } + browser.test.sendMessage("ready"); + }, + }); + + // onTabReplaced is never persisted, it is an empty event handler. + const EVENTS = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", + "onHistoryStateUpdated", + ]; + + await extension.startup(); + await extension.awaitMessage("ready"); + for (let event of EVENTS) { + assertPersistentListeners(extension, "webNavigation", event, { + primed: false, + }); + } + + await extension.terminateBackground(); + for (let event of EVENTS) { + assertPersistentListeners(extension, "webNavigation", event, { + primed: true, + }); + } + + // wake up the background, we don't really care which event does it, + // we're just verifying the state after. + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + await extension.awaitMessage("ready"); + for (let event of EVENTS) { + assertPersistentListeners(extension, "webNavigation", event, { + primed: false, + }); + } + + await BrowserTestUtils.closeWindow(newWin); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js b/toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js new file mode 100644 index 0000000000..674a10a5ef --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js @@ -0,0 +1,48 @@ +"use strict"; + +// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1573456 +add_task(async function test_mozextension_page_loaded_in_extension_process() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "https://example.com/*", + ], + web_accessible_resources: ["test.html"], + }, + files: { + "test.html": '', + "test.js": () => { + browser.test.assertTrue( + browser.webRequest, + "webRequest API should be available" + ); + + browser.test.sendMessage("test_done"); + }, + }, + background: () => { + browser.webRequest.onBeforeRequest.addListener( + () => { + return { + redirectUrl: browser.runtime.getURL("test.html"), + }; + }, + { urls: ["*://*/redir"] }, + ["blocking"] + ); + }, + }); + await extension.startup(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/redir" + ); + + await extension.awaitMessage("test_done"); + + await extension.unload(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js b/toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js new file mode 100644 index 0000000000..666d4f324f --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js @@ -0,0 +1,133 @@ +"use strict"; + +// Check that extension popup windows contain the name of the extension +// as well as the title of the loaded document, but not the URL. +add_task(async function test_popup_title() { + const name = "custom_title_number_9_please"; + const docTitle = "popup-test-title"; + + const extensionWithImplicitHostPermission = ExtensionTestUtils.loadExtension({ + manifest: { + name, + }, + async background() { + let popup; + + // Called after the popup loads + browser.runtime.onMessage.addListener(async ({ docTitle }) => { + const name = browser.runtime.getManifest().name; + const { id } = await popup; + const { title } = await browser.windows.get(id); + + browser.test.assertTrue( + title.includes(name), + "popup title must include extension name" + ); + browser.test.assertTrue( + title.includes(docTitle), + "popup title must include extension document title" + ); + browser.test.assertFalse( + title.includes("moz-extension:"), + "popup title must not include extension URL" + ); + + // share window data with other extensions + browser.test.sendMessage("windowData", { + id: id, + fullTitle: title, + }); + + browser.test.onMessage.addListener(async message => { + if (message === "cleanup") { + await browser.windows.remove(id); + browser.test.sendMessage("finishedCleanup"); + } + }); + + browser.test.sendMessage("done"); + }); + + popup = browser.windows.create({ + url: "/index.html", + type: "popup", + }); + }, + files: { + "index.html": ` + + ${docTitle}, + + `, + "index.js": `addEventListener( + "load", + () => browser.runtime.sendMessage({docTitle: document.title}) + );`, + }, + }); + + const extensionWithoutPermissions = ExtensionTestUtils.loadExtension({ + async background() { + const { id } = await new Promise(resolve => { + browser.test.onMessage.addListener(message => { + resolve(message); + }); + }); + + const { title } = await browser.windows.get(id); + + browser.test.assertEq( + title, + undefined, + "popup window must not include title" + ); + + browser.test.sendMessage("done"); + }, + }); + + const extensionWithTabsPermission = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + async background() { + const { id, fullTitle } = await new Promise(resolve => { + browser.test.onMessage.addListener(message => { + resolve(message); + }); + }); + + const { title } = await browser.windows.get(id); + + browser.test.assertEq( + title, + fullTitle, + "popup title equals expected title" + ); + + browser.test.sendMessage("done"); + }, + }); + + await extensionWithoutPermissions.startup(); + await extensionWithTabsPermission.startup(); + await extensionWithImplicitHostPermission.startup(); + + const windowData = await extensionWithImplicitHostPermission.awaitMessage( + "windowData" + ); + + extensionWithoutPermissions.sendMessage(windowData); + extensionWithTabsPermission.sendMessage(windowData); + + await extensionWithoutPermissions.awaitMessage("done"); + await extensionWithTabsPermission.awaitMessage("done"); + await extensionWithImplicitHostPermission.awaitMessage("done"); + + extensionWithImplicitHostPermission.sendMessage("cleanup"); + await extensionWithImplicitHostPermission.awaitMessage("finishedCleanup"); + + await extensionWithoutPermissions.unload(); + await extensionWithTabsPermission.unload(); + await extensionWithImplicitHostPermission.unload(); +}); diff --git a/toolkit/components/extensions/test/browser/data/test-download.txt b/toolkit/components/extensions/test/browser/data/test-download.txt new file mode 100644 index 0000000000..f416e0e291 --- /dev/null +++ b/toolkit/components/extensions/test/browser/data/test-download.txt @@ -0,0 +1 @@ +test download content diff --git a/toolkit/components/extensions/test/browser/data/test_downloads_referrer.html b/toolkit/components/extensions/test/browser/data/test_downloads_referrer.html new file mode 100644 index 0000000000..85410abfcd --- /dev/null +++ b/toolkit/components/extensions/test/browser/data/test_downloads_referrer.html @@ -0,0 +1,10 @@ + + + + + Test downloads referrer + + + test link + + diff --git a/toolkit/components/extensions/test/browser/head.js b/toolkit/components/extensions/test/browser/head.js new file mode 100644 index 0000000000..9e84413e82 --- /dev/null +++ b/toolkit/components/extensions/test/browser/head.js @@ -0,0 +1,126 @@ +/* exported ACCENT_COLOR, BACKGROUND, ENCODED_IMAGE_DATA, FRAME_COLOR, TAB_TEXT_COLOR, + TEXT_COLOR, TAB_BACKGROUND_TEXT_COLOR, imageBufferFromDataURI, hexToCSS, hexToRGB, testBorderColor, + waitForTransition, loadTestSubscript, backgroundColorSetOnRoot, assertPersistentListeners */ + +"use strict"; + +const { ClientEnvironmentBase } = ChromeUtils.import( + "resource://gre/modules/components-utils/ClientEnvironment.jsm" +); + +const BACKGROUND = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0" + + "DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; +const ENCODED_IMAGE_DATA = + "iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0h" + + "STQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAdhwAAHYcBj+XxZQAAB5dJREFUSMd" + + "91vmTlEcZB/Bvd7/vO+/ce83O3gfLDUsC4VgIghBUEo2GM9GCFTaQBEISA1qIEVNQ4aggJDGIgAGTlFUKKcqKQpVHaQyny7FrCMiywp4ze+/Mzs67M/P" + + "O+3a3v5jdWo32H/B86vv0U083weecV3+0C8lkEh6PhzS3tuLkieMSAKo3fW9Mb1eoUtM0jemerukLllzrbGlKheovUpeqkmt113hPfx/27tyFF7+/bbg" + + "e+U9g20s7kEwmMXXGNLrp2fWi4V5z/tFjJ3fWX726INbfU2xx0yelkJAKdJf3Xl5+2QcPTpv2U0JZR+u92+xvly5ygKDm20/hlX17/jvB6VNnIKXEOyd" + + "O0iFh4PLVy0XV1U83Vk54QI7JK+bl+UE5vjRfTCzJ5eWBTFEayBLjisvljKmzwmtWrVkEAPNmVrEZkyfh+fU1n59k//7X4Fbz8MK2DRSAWLNq/Yc36y9" + + "+3UVMsyAYVPMy/MTvdBKvriJhphDq6xa9vf0i1GMwPVhM5s9bsLw/EvtN2kywwnw/nzBuLDZs2z4auXGjHuvWbmBQdT5v7qytn165fLCyyGtXTR6j5GV" + + "kIsvlBCwTVNgQhMKCRDQ2iIbmJv7BpU+Ykl02UFOzdt6gkbzTEQ5Rl2KL3W8eGUE+/ssFXK+rJQ8vWigLgjk5z9ZsvpOniJzVi+ZKTUhCuATTKCjhoLA" + + "hhQAsjrSZBJcm7rZ22O+ev6mMmTLj55eu1T+jU8GOH/kJf2TZCiifIQsXfwEbN2yktxoaeYbf93DKSORMnTOZE0aZaVlQGYVKJCgjEJSCcgLB0xDERjI" + + "NFBUEaXmuB20t95eEutr0xrufpo4eepMAkMPIxx+dx9at25EWQNXsh77q0Bzwen0ShEF32HCrCpjksAWHFAKqokFhgEJt2DKJeFoQv8eDuz3duaseXZY" + + "dixthaQ+NRlRCcKO+FgCweP68wswMF/yZWcTkNpLJFAZEGi6XC07NCUIIoqaNSLQfFALCEpCSEL/bK/wuw+12sKlDQzKs6k5yZt+rI+2aNKUSNdUbSSQ" + + "Wh2mJP46rGPeYrjtkY0M7jFgciUQCiqqgrCAfBTle3G9rR1NHN3SnDq9Lg49QlBQEcbfbQCKZlhQEDkXBih27RpDOrmacfP8YB4CfHT7uNXrCMFM2FdD" + + "BVQ5TE/A5HbDSJoSpQXAbXm8A4b5+gKrwulU4KKEBnwuzHpiQu+n1jQoQsM+9cYQMT9fvf/FLBYTaDqdzbfgft95PKzbPyQqwnlAXGkJtGIgNYnJpMfw" + + "OghLG0GJE0ZdiaOnsQ16OD6XZLkiRROdAgud5sxk8ridsy/pQU1VlOIkZN6QtAGnx0FA0AtXvIA4C5OX4kOWbiLRhQBDApTmgJuLwEonMgBvjgpmgjIE" + + "hhX7DAIVKNeqE05/dJbgEgRy5eOJ1ieXr1gJA7ZNLTrVVlAZLyopLJAUlHsrAMrwwrRQ4t6E5VHgSBExjcGpO0JQNizCE05a41dhOi+cXXVm144e1AHD" + + "1vXfFMOLy+KSHEDoEJLZ8s+ZWKpUusWwpFKiMUQ4jbiAaj8Hp9oExBsMCUpEIfD6JLKZjKJVGV3RIZGdm0qxA5qmz+/cgMhBVuuMRewRRGF7fe4BYHMg" + + "N5LxdV3vhy1EjrrjA5GAyTuKpFHricfS0dSDNCQRPoSyQgSSPI+UBEtwShiWUQEHw5mMvbz4JRcXvDr3B3dBG1sq5X53GlMcX4JWVTyvRQcOumDD2vfK" + + "cjOqiQDZPGBF2ryUEnjRhJlP4d6/BiQ1TABPKiyQhgtzvjPCJlQ/OGRwauqESSUPX68U3Vi4fGeH83Hwc3bYHBWUV0m0k4HB6z7aGu6sznDos00R3exg" + + "l5ZMwc+FMaJoKKxHFnbo6DMYiELBlqLOXDBq8dsvuPTfKALpwdbX42iMLsHjLd0Zv4RNvvY1wZxdZunyVDGZm6D/47sv12RqbmOPVhG5LGnAH4S8sgu7" + + "1oK/pn2BWAoYw0dDbaTd19iqlZROejwzEjqgMSuXUifak8jF49JnNI0kAoGrBfET7+uXOrS+y5ta21JzZsw7faW45XJaXxSvyAtTpkOi483fwtAWP1wt" + + "vrhvd/VFx+26zojr9Les2PnfaTNu4cuGvvKe9BVv3/RgARiNTpk/Hod17MWikxcqzzfhK/+1jL2xc+YQAX1ISDHLV7WTpQQaLcASzPEiB41ZrmEeHkrT" + + "Q49uz/aXn+iilLKXq/MmlS0e/jFcuX4SmaQAAKSXlnIvVy1aQ6EBMFgRyCznDpfGFwdKqirF2tu5SdIeGrkiP+KS5yb7dHtIKsnI++kP9rS8RQvjmxxe" + + "jePxD2HHwwP9FdCllurGhUbx14CAbiMc4Y2qVJqwLbo0qfpdLSilILB4Xg0mT6h7vnSWzZn9RoaynobWF3K6rk1NmzMWZ83/+37+V4a1cVg5JACYF45b" + + "FGVVWOFS2V1HUCjOdBqW0Q9fYb7N9/tcSptnldjpott8rFEXBO+f+NKrWMHL9Wu1nSUAIAaUUa59aAyE43E4X3bD8W6K5K6x1h1snRaMDJDuQf7+vrzf" + + "eG+mgfrcLHh3C79bx6wttGEqERiH/AjPohWMouv2ZAAAAAElFTkSuQmCC"; +const ACCENT_COLOR = "#a14040"; +const TEXT_COLOR = "#fac96e"; +// For testing aliases of the colors above: +const FRAME_COLOR = [71, 105, 91]; +const TAB_BACKGROUND_TEXT_COLOR = [207, 221, 192, 0.9]; + +function hexToRGB(hex) { + if (!hex) { + return null; + } + hex = parseInt(hex.indexOf("#") > -1 ? hex.substring(1) : hex, 16); + return [hex >> 16, (hex & 0x00ff00) >> 8, hex & 0x0000ff]; +} + +function rgbToCSS(rgb) { + return `rgb(${rgb.join(", ")})`; +} + +function hexToCSS(hex) { + if (!hex) { + return null; + } + return rgbToCSS(hexToRGB(hex)); +} + +function imageBufferFromDataURI(encodedImageData) { + let decodedImageData = atob(encodedImageData); + return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer; +} + +function waitForTransition(element, propertyName) { + return BrowserTestUtils.waitForEvent( + element, + "transitionend", + false, + event => { + return event.target == element && event.propertyName == propertyName; + } + ); +} + +function testBorderColor(element, expected) { + let computedStyle = window.getComputedStyle(element); + Assert.equal( + computedStyle.borderLeftColor, + hexToCSS(expected), + "Element left border color should be set." + ); + Assert.equal( + computedStyle.borderRightColor, + hexToCSS(expected), + "Element right border color should be set." + ); + Assert.equal( + computedStyle.borderTopColor, + hexToCSS(expected), + "Element top border color should be set." + ); + Assert.equal( + computedStyle.borderBottomColor, + hexToCSS(expected), + "Element bottom border color should be set." + ); +} + +function loadTestSubscript(filePath) { + Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this); +} + +/** + * Windows 7 and 8 set the window's background-color on :root instead of + * #navigator-toolbox to avoid bug 1695280. When that bug is fixed, this + * function and the assertions it gates can be removed. + * + * @returns {boolean} True if the window's background-color is set on :root + * rather than #navigator-toolbox. + */ +function backgroundColorSetOnRoot() { + const os = ClientEnvironmentBase.os; + if (!os.isWindows) { + return false; + } + return os.windowsVersion < 10; +} + +// Persistent Listener test functionality +const { assertPersistentListeners } = ExtensionTestUtils.testAssertions; diff --git a/toolkit/components/extensions/test/browser/head_serviceworker.js b/toolkit/components/extensions/test/browser/head_serviceworker.js new file mode 100644 index 0000000000..012dcfe284 --- /dev/null +++ b/toolkit/components/extensions/test/browser/head_serviceworker.js @@ -0,0 +1,123 @@ +"use strict"; + +/* exported assert_background_serviceworker_pref_enabled, + * getBackgroundServiceWorkerRegistration, + * getServiceWorkerInfo, getServiceWorkerState, + * waitForServiceWorkerRegistrationsRemoved, waitForServiceWorkerTerminated + */ + +async function assert_background_serviceworker_pref_enabled() { + is( + WebExtensionPolicy.backgroundServiceWorkerEnabled, + true, + "Expect extensions.backgroundServiceWorker.enabled to be true" + ); +} + +// Return the name of the enum corresponding to the worker's state (ex: "STATE_ACTIVATED") +// because nsIServiceWorkerInfo doesn't currently provide a comparable string-returning getter. +function getServiceWorkerState(workerInfo) { + const map = Object.keys(workerInfo) + .filter(k => k.startsWith("STATE_")) + .reduce((map, name) => { + map.set(workerInfo[name], name); + return map; + }, new Map()); + return map.has(workerInfo.state) + ? map.get(workerInfo.state) + : "state: ${workerInfo.state}"; +} + +function getServiceWorkerInfo(swRegInfo) { + const { + evaluatingWorker, + installingWorker, + waitingWorker, + activeWorker, + } = swRegInfo; + return evaluatingWorker || installingWorker || waitingWorker || activeWorker; +} + +async function waitForServiceWorkerTerminated(swRegInfo) { + info(`Wait all ${swRegInfo.scope} workers to be terminated`); + + try { + await BrowserTestUtils.waitForCondition( + () => !getServiceWorkerInfo(swRegInfo) + ); + } catch (err) { + const workerInfo = getServiceWorkerInfo(swRegInfo); + if (workerInfo) { + ok( + false, + `Error while waiting for workers for scope ${swRegInfo.scope} to be terminated. ` + + `Found a worker in state: ${getServiceWorkerState(workerInfo)}` + ); + return; + } + + throw err; + } +} + +function getBackgroundServiceWorkerRegistration(extension) { + const policy = WebExtensionPolicy.getByHostname(extension.uuid); + const expectedSWScope = policy.getURL("/"); + const expectedScriptURL = policy.extension.backgroundWorkerScript || ""; + + ok( + expectedScriptURL.startsWith(expectedSWScope), + `Extension does include a valid background.service_worker: ${expectedScriptURL}` + ); + + const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + + let swReg; + let regs = swm.getAllRegistrations(); + + for (let i = 0; i < regs.length; i++) { + let reg = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo); + if (reg.scriptSpec === expectedScriptURL) { + swReg = reg; + break; + } + } + + ok(swReg, `Found service worker registration for ${expectedScriptURL}`); + + is( + swReg.scope, + expectedSWScope, + "The extension background worker registration has the expected scope URL" + ); + + return swReg; +} + +async function waitForServiceWorkerRegistrationsRemoved(extension) { + info(`Wait ${extension.id} service worker registration to be deleted`); + const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + let baseURI = Services.io.newURI(`moz-extension://${extension.uuid}/`); + let principal = Services.scriptSecurityManager.createContentPrincipal( + baseURI, + {} + ); + + await BrowserTestUtils.waitForCondition(() => { + let regs = swm.getAllRegistrations(); + + for (let i = 0; i < regs.length; i++) { + let reg = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo); + if (principal.equals(reg.principal)) { + return false; + } + } + + info(`All ${extension.id} service worker registrations are gone`); + return true; + }, `All ${extension.id} service worker registrations should be deleted`); +} diff --git a/toolkit/components/extensions/test/marionette/data/extension-with-bg-sw/manifest.json b/toolkit/components/extensions/test/marionette/data/extension-with-bg-sw/manifest.json new file mode 100644 index 0000000000..a4366130d4 --- /dev/null +++ b/toolkit/components/extensions/test/marionette/data/extension-with-bg-sw/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 2, + "name": "Test Extension with Background Service Worker", + "version": "1", + "browser_specific_settings": { + "gecko": { "id": "extension-with-bg-sw@test" } + }, + "background": { + "service_worker": "sw.js" + } +} \ No newline at end of file diff --git a/toolkit/components/extensions/test/marionette/data/extension-with-bg-sw/sw.js b/toolkit/components/extensions/test/marionette/data/extension-with-bg-sw/sw.js new file mode 100644 index 0000000000..2282e6a64b --- /dev/null +++ b/toolkit/components/extensions/test/marionette/data/extension-with-bg-sw/sw.js @@ -0,0 +1,3 @@ +"use strict"; + +dump("extension-with-bg-sw: sw.js loaded"); diff --git a/toolkit/components/extensions/test/marionette/manifest.ini b/toolkit/components/extensions/test/marionette/manifest.ini new file mode 100644 index 0000000000..8073096b9e --- /dev/null +++ b/toolkit/components/extensions/test/marionette/manifest.ini @@ -0,0 +1,2 @@ +[test_extension_serviceworkers_purged_on_pref_disabled.py] +[test_temporary_extension_serviceworkers_not_persisted.py] diff --git a/toolkit/components/extensions/test/marionette/service_worker_testutils.py b/toolkit/components/extensions/test/marionette/service_worker_testutils.py new file mode 100644 index 0000000000..b1fda926c0 --- /dev/null +++ b/toolkit/components/extensions/test/marionette/service_worker_testutils.py @@ -0,0 +1,48 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from marionette_harness import MarionetteTestCase + +EXT_ID = "extension-with-bg-sw@test" +EXT_DIR_PATH = "extension-with-bg-sw" +PREF_BG_SW_ENABLED = "extensions.backgroundServiceWorker.enabled" +PREF_PERSIST_TEMP_ADDONS = ( + "dom.serviceWorkers.testing.persistTemporarilyInstalledAddons" +) + + +class MarionetteServiceWorkerTestCase(MarionetteTestCase): + def get_extension_url(self, path="/"): + with self.marionette.using_context("chrome"): + return self.marionette.execute_script( + """ + let policy = WebExtensionPolicy.getByID(arguments[0]); + return policy.getURL(arguments[1]) + """, + script_args=(self.test_extension_id, path), + ) + + @property + def is_extension_service_worker_registered(self): + with self.marionette.using_context("chrome"): + return self.marionette.execute_script( + """ + let serviceWorkerManager = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (sw.scope == arguments[0]) { + return true; + } + } + return false; + """, + script_args=(self.test_extension_base_url,), + ) diff --git a/toolkit/components/extensions/test/marionette/test_extension_serviceworkers_purged_on_pref_disabled.py b/toolkit/components/extensions/test/marionette/test_extension_serviceworkers_purged_on_pref_disabled.py new file mode 100644 index 0000000000..ff2184c692 --- /dev/null +++ b/toolkit/components/extensions/test/marionette/test_extension_serviceworkers_purged_on_pref_disabled.py @@ -0,0 +1,56 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +from marionette_driver import Wait +from marionette_driver.addons import Addons + +# Add this directory to the import path. +sys.path.append(os.path.dirname(__file__)) + +from service_worker_testutils import ( + EXT_DIR_PATH, + EXT_ID, + PREF_BG_SW_ENABLED, + PREF_PERSIST_TEMP_ADDONS, + MarionetteServiceWorkerTestCase, +) + + +class PurgeExtensionServiceWorkersOnPrefDisabled(MarionetteServiceWorkerTestCase): + def setUp(self): + super(PurgeExtensionServiceWorkersOnPrefDisabled, self).setUp() + self.test_extension_id = EXT_ID + # Flip the "mirror: once" pref and restart Firefox to be able + # to run the extension successfully. + self.marionette.set_pref(PREF_BG_SW_ENABLED, True) + self.marionette.set_pref(PREF_PERSIST_TEMP_ADDONS, True) + self.marionette.restart(in_app=True) + + def tearDown(self): + self.marionette.restart(in_app=False, clean=True) + super(PurgeExtensionServiceWorkersOnPrefDisabled, self).tearDown() + + def test_unregistering_service_worker_when_clearing_data(self): + self.install_extension_with_service_worker() + + # Flip the pref to false and restart again to verify that the + # service worker registration has been removed as expected. + self.marionette.set_pref(PREF_BG_SW_ENABLED, False) + self.marionette.restart(in_app=True) + self.assertFalse(self.is_extension_service_worker_registered) + + def install_extension_with_service_worker(self): + addons = Addons(self.marionette) + test_extension_path = os.path.join( + os.path.dirname(self.filepath), "data", EXT_DIR_PATH + ) + addons.install(test_extension_path, temp=True) + self.test_extension_base_url = self.get_extension_url() + Wait(self.marionette).until( + lambda _: self.is_extension_service_worker_registered, + message="Wait the extension service worker to be registered", + ) diff --git a/toolkit/components/extensions/test/marionette/test_temporary_extension_serviceworkers_not_persisted.py b/toolkit/components/extensions/test/marionette/test_temporary_extension_serviceworkers_not_persisted.py new file mode 100644 index 0000000000..57c0696385 --- /dev/null +++ b/toolkit/components/extensions/test/marionette/test_temporary_extension_serviceworkers_not_persisted.py @@ -0,0 +1,54 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +from marionette_driver import Wait +from marionette_driver.addons import Addons + +# Add this directory to the import path. +sys.path.append(os.path.dirname(__file__)) + +from service_worker_testutils import ( + EXT_DIR_PATH, + EXT_ID, + PREF_BG_SW_ENABLED, + MarionetteServiceWorkerTestCase, +) + + +class TemporarilyInstalledAddonServiceWorkerNotPersisted( + MarionetteServiceWorkerTestCase +): + def setUp(self): + super(TemporarilyInstalledAddonServiceWorkerNotPersisted, self).setUp() + self.test_extension_id = EXT_ID + # Flip the "mirror: once" pref and restart Firefox to be able + # to run the extension successfully. + self.marionette.set_pref(PREF_BG_SW_ENABLED, True) + self.marionette.restart(in_app=True) + + def tearDown(self): + self.marionette.restart(in_app=False, clean=True) + super(TemporarilyInstalledAddonServiceWorkerNotPersisted, self).tearDown() + + def test_temporarily_installed_addon_serviceWorkers_not_persisted(self): + self.install_temporary_extension_with_service_worker() + # Make sure the extension worker registration is persisted + # across restarts when the pref stays set to true. + self.marionette.restart(in_app=True) + self.assertFalse(self.is_extension_service_worker_registered) + + def install_temporary_extension_with_service_worker(self): + addons = Addons(self.marionette) + test_extension_path = os.path.join( + os.path.dirname(self.filepath), "data", EXT_DIR_PATH + ) + addons.install(test_extension_path, temp=True) + self.test_extension_base_url = self.get_extension_url() + Wait(self.marionette).until( + lambda _: self.is_extension_service_worker_registered, + message="Wait the extension service worker to be registered", + ) diff --git a/toolkit/components/extensions/test/mochitest/.eslintrc.js b/toolkit/components/extensions/test/mochitest/.eslintrc.js new file mode 100644 index 0000000000..a776405c9d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/.eslintrc.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = { + env: { + browser: true, + webextensions: true, + }, + + rules: { + "no-shadow": 0, + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/chrome.ini b/toolkit/components/extensions/test/mochitest/chrome.ini new file mode 100644 index 0000000000..0a844760c2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome.ini @@ -0,0 +1,38 @@ +[DEFAULT] +support-files = + chrome_cleanup_script.js + head.js + head_cookies.js + file_image_good.png + file_image_great.png + file_sample.html + file_with_images.html + webrequest_chromeworker.js + webrequest_test.jsm +prefs = + security.mixed_content.upgrade_display_content=false +tags = webextensions in-process-webextensions + +# NO NEW TESTS. mochitest-chrome does not run under e10s, avoid adding new +# tests here unless absolutely necessary. + +[test_chrome_ext_contentscript_data_uri.html] +[test_chrome_ext_contentscript_telemetry.html] +[test_chrome_ext_contentscript_unrecognizedprop_warning.html] +[test_chrome_ext_downloads_open.html] +[test_chrome_ext_downloads_saveAs.html] +skip-if = (verify && !debug && (os == 'win')) || (os == 'android') || (os == 'win' && os_version == '10.0') # Bug 1695612 +[test_chrome_ext_downloads_uniquify.html] +skip-if = os == 'win' && os_version == '10.0' # Bug 1695612 +[test_chrome_ext_permissions.html] +skip-if = os == 'android' # Bug 1350559 +[test_chrome_ext_svg_context_fill.html] +[test_chrome_ext_trackingprotection.html] +[test_chrome_ext_webnavigation_resolved_urls.html] +[test_chrome_ext_webrequest_background_events.html] +[test_chrome_ext_webrequest_host_permissions.html] +skip-if = verify +[test_chrome_ext_webrequest_mozextension.html] +skip-if = true # Bug 1404172 +[test_chrome_native_messaging_paths.html] +skip-if = os != "mac" && os != "linux" diff --git a/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js b/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js new file mode 100644 index 0000000000..9afa95f302 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js @@ -0,0 +1,65 @@ +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +let listener = msg => { + void (msg instanceof Ci.nsIConsoleMessage); + dump(`Console message: ${msg}\n`); +}; + +Services.console.registerListener(listener); + +let getBrowserApp, getTabBrowser; +if (AppConstants.MOZ_BUILD_APP === "mobile/android") { + getBrowserApp = win => win.BrowserApp; + getTabBrowser = tab => tab.browser; +} else { + getBrowserApp = win => win.gBrowser; + getTabBrowser = tab => tab.linkedBrowser; +} + +function* iterBrowserWindows() { + for (let win of Services.wm.getEnumerator("navigator:browser")) { + if (!win.closed && getBrowserApp(win)) { + yield win; + } + } +} + +let initialTabs = new Map(); +for (let win of iterBrowserWindows()) { + initialTabs.set(win, new Set(getBrowserApp(win).tabs)); +} + +addMessageListener("check-cleanup", extensionId => { + Services.console.unregisterListener(listener); + + let results = { + extraWindows: [], + extraTabs: [], + }; + + for (let win of iterBrowserWindows()) { + if (initialTabs.has(win)) { + let tabs = initialTabs.get(win); + + for (let tab of getBrowserApp(win).tabs) { + if (!tabs.has(tab)) { + results.extraTabs.push(getTabBrowser(tab).currentURI.spec); + } + } + } else { + results.extraWindows.push( + Array.from(win.gBrowser.tabs, tab => getTabBrowser(tab).currentURI.spec) + ); + } + } + + initialTabs = null; + + sendAsyncMessage("cleanup-results", results); +}); diff --git a/toolkit/components/extensions/test/mochitest/chrome_head.js b/toolkit/components/extensions/test/mochitest/chrome_head.js new file mode 100644 index 0000000000..3918c74e44 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome_head.js @@ -0,0 +1 @@ +"use strict"; diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html new file mode 100644 index 0000000000..663ebc6112 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html @@ -0,0 +1,12 @@ + + + + + + + +
+
+ + + diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html new file mode 100644 index 0000000000..cc1acc83d6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html new file mode 100644 index 0000000000..a0a26a2e9d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html @@ -0,0 +1,9 @@ + + + + + +click me + + + diff --git a/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html new file mode 100644 index 0000000000..24c7a42986 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_contains_iframe.html b/toolkit/components/extensions/test/mochitest/file_contains_iframe.html new file mode 100644 index 0000000000..e905b5a224 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contains_iframe.html @@ -0,0 +1,13 @@ + + + + +file contains iframe + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_contains_img.html b/toolkit/components/extensions/test/mochitest/file_contains_img.html new file mode 100644 index 0000000000..2b0c3137d6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contains_img.html @@ -0,0 +1,12 @@ + + + + +file contains img + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html new file mode 100644 index 0000000000..6c1675cb47 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html new file mode 100644 index 0000000000..3b102b3d67 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html b/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html new file mode 100644 index 0000000000..670bad1360 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_green.html b/toolkit/components/extensions/test/mochitest/file_green.html new file mode 100644 index 0000000000..20755c5b56 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_green.html @@ -0,0 +1,3 @@ + +Super green test page + diff --git a/toolkit/components/extensions/test/mochitest/file_green_blue.html b/toolkit/components/extensions/test/mochitest/file_green_blue.html new file mode 100644 index 0000000000..9266b637ba --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_green_blue.html @@ -0,0 +1,16 @@ + +Upper square green, rest blue + +
diff --git a/toolkit/components/extensions/test/mochitest/file_image_bad.png b/toolkit/components/extensions/test/mochitest/file_image_bad.png new file mode 100644 index 0000000000..4c3be50847 Binary files /dev/null and b/toolkit/components/extensions/test/mochitest/file_image_bad.png differ diff --git a/toolkit/components/extensions/test/mochitest/file_image_good.png b/toolkit/components/extensions/test/mochitest/file_image_good.png new file mode 100644 index 0000000000..769c636340 Binary files /dev/null and b/toolkit/components/extensions/test/mochitest/file_image_good.png differ diff --git a/toolkit/components/extensions/test/mochitest/file_image_great.png b/toolkit/components/extensions/test/mochitest/file_image_great.png new file mode 100644 index 0000000000..769c636340 Binary files /dev/null and b/toolkit/components/extensions/test/mochitest/file_image_great.png differ diff --git a/toolkit/components/extensions/test/mochitest/file_image_redirect.png b/toolkit/components/extensions/test/mochitest/file_image_redirect.png new file mode 100644 index 0000000000..4c3be50847 Binary files /dev/null and b/toolkit/components/extensions/test/mochitest/file_image_redirect.png differ diff --git a/toolkit/components/extensions/test/mochitest/file_indexedDB.html b/toolkit/components/extensions/test/mochitest/file_indexedDB.html new file mode 100644 index 0000000000..65b7e0ad2f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_indexedDB.html @@ -0,0 +1,28 @@ + + + + + + + + This is a test page. + + diff --git a/toolkit/components/extensions/test/mochitest/file_mixed.html b/toolkit/components/extensions/test/mochitest/file_mixed.html new file mode 100644 index 0000000000..f3c7dda580 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_mixed.html @@ -0,0 +1,13 @@ + + + + + + + + +
Sample text
+ + + + diff --git a/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html b/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html new file mode 100644 index 0000000000..b8fda2369a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html @@ -0,0 +1,30 @@ + + + + 1450965 Skip Cors Check for Early WebExtention Redirects + + +
+    Fetching ...
+  
+ + + diff --git a/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html b/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html new file mode 100644 index 0000000000..fe8e5bea44 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html @@ -0,0 +1,9 @@ + + + + Bug 1434357: Allow Web Request API to redirect to data: URI + + +
foo
+ + diff --git a/toolkit/components/extensions/test/mochitest/file_remote_frame.html b/toolkit/components/extensions/test/mochitest/file_remote_frame.html new file mode 100644 index 0000000000..f1b9240092 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_remote_frame.html @@ -0,0 +1,20 @@ + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_sample.html b/toolkit/components/extensions/test/mochitest/file_sample.html new file mode 100644 index 0000000000..aa1ef6e6f4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.html @@ -0,0 +1,13 @@ + + + + + +file sample + + + +
Sample text
+ + + diff --git a/toolkit/components/extensions/test/mochitest/file_sample.txt b/toolkit/components/extensions/test/mochitest/file_sample.txt new file mode 100644 index 0000000000..c02cd532b1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.txt @@ -0,0 +1 @@ +Sample \ No newline at end of file diff --git a/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ b/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ new file mode 100644 index 0000000000..cb762eff80 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/toolkit/components/extensions/test/mochitest/file_script_bad.js b/toolkit/components/extensions/test/mochitest/file_script_bad.js new file mode 100644 index 0000000000..c425122c71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_bad.js @@ -0,0 +1,3 @@ +"use strict"; + +window.failure = true; diff --git a/toolkit/components/extensions/test/mochitest/file_script_good.js b/toolkit/components/extensions/test/mochitest/file_script_good.js new file mode 100644 index 0000000000..14e959aa5c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_good.js @@ -0,0 +1,12 @@ +"use strict"; + +window.success = window.success ? window.success + 1 : 1; + +{ + let scripts = document.getElementsByTagName("script"); + let url = new URL(scripts[scripts.length - 1].src); + let flag = url.searchParams.get("q"); + if (flag) { + window.postMessage(flag, "*"); + } +} diff --git a/toolkit/components/extensions/test/mochitest/file_script_redirect.js b/toolkit/components/extensions/test/mochitest/file_script_redirect.js new file mode 100644 index 0000000000..c425122c71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_redirect.js @@ -0,0 +1,3 @@ +"use strict"; + +window.failure = true; diff --git a/toolkit/components/extensions/test/mochitest/file_script_xhr.js b/toolkit/components/extensions/test/mochitest/file_script_xhr.js new file mode 100644 index 0000000000..ad01f74253 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_xhr.js @@ -0,0 +1,9 @@ +"use strict"; + +var request = new XMLHttpRequest(); +request.open( + "get", + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/xhr_resource", + false +); +request.send(); diff --git a/toolkit/components/extensions/test/mochitest/file_serviceWorker.html b/toolkit/components/extensions/test/mochitest/file_serviceWorker.html new file mode 100644 index 0000000000..d2b99769cc --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_serviceWorker.html @@ -0,0 +1,16 @@ + + + + + + + + This is a test page. + + diff --git a/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html new file mode 100644 index 0000000000..909a1f9e36 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html new file mode 100644 index 0000000000..a0a437d0eb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr.html new file mode 100644 index 0000000000..f6ef67277d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr.html @@ -0,0 +1,19 @@ + + + + + + + + + + +`); + } + response.write(``); + response.finish(); + }, + DELAY, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/toolkit/components/extensions/test/mochitest/file_streamfilter.txt b/toolkit/components/extensions/test/mochitest/file_streamfilter.txt new file mode 100644 index 0000000000..56cdd85e1d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_streamfilter.txt @@ -0,0 +1 @@ +Middle diff --git a/toolkit/components/extensions/test/mochitest/file_style_bad.css b/toolkit/components/extensions/test/mochitest/file_style_bad.css new file mode 100644 index 0000000000..8dbc8dc7a4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_bad.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/components/extensions/test/mochitest/file_style_good.css b/toolkit/components/extensions/test/mochitest/file_style_good.css new file mode 100644 index 0000000000..46f9774b5f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_good.css @@ -0,0 +1,3 @@ +#test { + color: red; +} diff --git a/toolkit/components/extensions/test/mochitest/file_style_redirect.css b/toolkit/components/extensions/test/mochitest/file_style_redirect.css new file mode 100644 index 0000000000..8dbc8dc7a4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_redirect.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html new file mode 100644 index 0000000000..63f503ad3c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html @@ -0,0 +1,10 @@ + + + + + The Title + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html new file mode 100644 index 0000000000..87ac7a2f64 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html @@ -0,0 +1,11 @@ + + + + + Another Title + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_third_party.html b/toolkit/components/extensions/test/mochitest/file_third_party.html new file mode 100644 index 0000000000..fc5a326297 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_third_party.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html b/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html new file mode 100644 index 0000000000..6ebd54d9a3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html @@ -0,0 +1,9 @@ + + + + + + +   + + diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html new file mode 100644 index 0000000000..cba3043f71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html new file mode 100644 index 0000000000..c5b436979f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ new file mode 100644 index 0000000000..574a392a15 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ @@ -0,0 +1 @@ +Refresh: 1;url=dummy_page.html diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html new file mode 100644 index 0000000000..d360bcbb13 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html @@ -0,0 +1,12 @@ + + + + + + + +
+
+ + + diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html new file mode 100644 index 0000000000..06dbd43741 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html @@ -0,0 +1,12 @@ + + + + + + + +
+
+ + + diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html new file mode 100644 index 0000000000..307990714b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html @@ -0,0 +1,12 @@ + + + + + + + +
+
+ + + diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html new file mode 100644 index 0000000000..55bb7aa6ae --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html @@ -0,0 +1,8 @@ + + + + +

page1

+ page2 + + diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html new file mode 100644 index 0000000000..8f589f8bbd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html @@ -0,0 +1,7 @@ + + + + +

page2

+ + diff --git a/toolkit/components/extensions/test/mochitest/file_with_about_blank.html b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html new file mode 100644 index 0000000000..af51c2e52a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_with_images.html b/toolkit/components/extensions/test/mochitest/file_with_images.html new file mode 100644 index 0000000000..6a3c090be2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_images.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_with_subframes_and_embed.html b/toolkit/components/extensions/test/mochitest/file_with_subframes_and_embed.html new file mode 100644 index 0000000000..348c51f16c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_subframes_and_embed.html @@ -0,0 +1,21 @@ + + + +Load a bunch of iframes with subframes. +

+ + + + +

+Load an embed frame. +

+ + +

+And an object. +

+ + +

+Done. diff --git a/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html b/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html new file mode 100644 index 0000000000..25c60df078 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html @@ -0,0 +1,6 @@ + + + + +Load a cross-origin iframe from example.net

+ diff --git a/toolkit/components/extensions/test/mochitest/head.js b/toolkit/components/extensions/test/mochitest/head.js new file mode 100644 index 0000000000..3676a40540 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head.js @@ -0,0 +1,124 @@ +"use strict"; + +/* exported AppConstants, Assert, AppTestDelegate */ + +var { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +var { AppTestDelegate } = SpecialPowers.ChromeUtils.importESModule( + "resource://specialpowers/AppTestDelegate.sys.mjs" +); + +let remote = SpecialPowers.getBoolPref("extensions.webextensions.remote"); +if (remote) { + // 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. + SpecialPowers.setIntPref("dom.ipc.keepProcessesAlive.extension", 1); +} + +{ + let chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("chrome_cleanup_script.js") + ); + + SimpleTest.registerCleanupFunction(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + + chromeScript.sendAsyncMessage("check-cleanup"); + + let results = await chromeScript.promiseOneMessage("cleanup-results"); + chromeScript.destroy(); + + if (results.extraWindows.length || results.extraTabs.length) { + ok( + false, + `Test left extra windows or tabs: ${JSON.stringify(results)}\n` + ); + } + }); +} + +let Assert = { + // Cut-down version based on Assert.sys.mjs. Only supports regexp and objects as + // the expected variables. + rejects(promise, expected, msg) { + return promise.then( + () => { + ok(false, msg); + }, + actual => { + let matched = false; + if (Object.prototype.toString.call(expected) == "[object RegExp]") { + if (expected.test(actual)) { + matched = true; + } + } else if (actual instanceof expected) { + matched = true; + } + + if (matched) { + ok(true, msg); + } else { + ok(false, `Unexpected exception for "${msg}": ${actual}`); + } + } + ); + }, +}; + +/* exported waitForLoad */ + +function waitForLoad(win) { + return new Promise(resolve => { + win.addEventListener( + "load", + function() { + resolve(); + }, + { capture: true, once: true } + ); + }); +} + +/* exported loadChromeScript */ +function loadChromeScript(fn) { + let wrapper = ` +(${fn.toString()})();`; + + return SpecialPowers.loadChromeScript(new Function(wrapper)); +} + +/* exported consoleMonitor */ +let consoleMonitor = { + start(messages) { + this.chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("mochitest_console.js") + ); + this.chromeScript.sendAsyncMessage("consoleStart", messages); + }, + + async finished() { + let done = this.chromeScript.promiseOneMessage("consoleDone").then(done => { + this.chromeScript.destroy(); + return done; + }); + this.chromeScript.sendAsyncMessage("waitForConsole"); + let test = await done; + ok(test.ok, test.message); + }, +}; +/* exported waitForState */ + +function waitForState(sw, state) { + return new Promise(resolve => { + if (sw.state === state) { + return resolve(); + } + sw.addEventListener("statechange", function onStateChange() { + if (sw.state === state) { + sw.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); +} diff --git a/toolkit/components/extensions/test/mochitest/head_cookies.js b/toolkit/components/extensions/test/mochitest/head_cookies.js new file mode 100644 index 0000000000..610c800c94 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_cookies.js @@ -0,0 +1,287 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported testCookies */ +/* import-globals-from head.js */ + +async function testCookies(options) { + // Changing the options object is a bit of a hack, but it allows us to easily + // pass an expiration date to the background script. + options.expiry = Date.now() / 1000 + 3600; + + async function background(backgroundOptions) { + // Ask the parent scope to change some cookies we may or may not have + // permission for. + let awaitChanges = new Promise(resolve => { + browser.test.onMessage.addListener(msg => { + browser.test.assertEq("cookies-changed", msg, "browser.test.onMessage"); + resolve(); + }); + }); + + let changed = []; + browser.cookies.onChanged.addListener(event => { + changed.push(`${event.cookie.name}:${event.cause}`); + }); + browser.test.sendMessage("change-cookies"); + + // Try to access some cookies in various ways. + let { url, domain, secure } = backgroundOptions; + + let failures = 0; + let tallyFailure = error => { + failures++; + }; + + try { + await awaitChanges; + + let cookie = await browser.cookies.get({ url, name: "foo" }); + browser.test.assertEq( + backgroundOptions.shouldPass, + cookie != null, + "should pass == get cookie" + ); + + let cookies = await browser.cookies.getAll({ domain }); + if (backgroundOptions.shouldPass) { + browser.test.assertEq(2, cookies.length, "expected number of cookies"); + } else { + browser.test.assertEq(0, cookies.length, "expected number of cookies"); + } + + await Promise.all([ + browser.cookies + .set({ + url, + domain, + secure, + name: "foo", + value: "baz", + expirationDate: backgroundOptions.expiry, + }) + .catch(tallyFailure), + browser.cookies + .set({ + url, + domain, + secure, + name: "bar", + value: "quux", + expirationDate: backgroundOptions.expiry, + }) + .catch(tallyFailure), + browser.cookies.remove({ url, name: "deleted" }), + ]); + + if (backgroundOptions.shouldPass) { + // The order of eviction events isn't guaranteed, so just check that + // it's there somewhere. + let evicted = changed.indexOf("evicted:evicted"); + if (evicted < 0) { + browser.test.fail("got no eviction event"); + } else { + browser.test.succeed("got eviction event"); + changed.splice(evicted, 1); + } + + browser.test.assertEq( + "x:explicit,x:overwrite,x:explicit,x:explicit,foo:overwrite,foo:explicit,bar:explicit,deleted:explicit", + changed.join(","), + "expected changes" + ); + } else { + browser.test.assertEq("", changed.join(","), "expected no changes"); + } + + if (!(backgroundOptions.shouldPass || backgroundOptions.shouldWrite)) { + browser.test.assertEq(2, failures, "Expected failures"); + } else { + browser.test.assertEq(0, failures, "Expected no failures"); + } + + browser.test.notifyPass("cookie-permissions"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("cookie-permissions"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: options.permissions, + }, + + background: `(${background})(${JSON.stringify(options)})`, + }); + + let stepOne = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage } = this; + addMessageListener("options", options => { + let domain = options.domain.replace(/^\.?/, "."); + // This will be evicted after we add a fourth cookie. + Services.cookies.add( + domain, + "/", + "evicted", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + // This will be modified by the background script. + Services.cookies.add( + domain, + "/", + "foo", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + // This will be deleted by the background script. + Services.cookies.add( + domain, + "/", + "deleted", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + sendAsyncMessage("done"); + }); + }); + stepOne.sendAsyncMessage("options", options); + await stepOne.promiseOneMessage("done"); + stepOne.destroy(); + + await extension.startup(); + + await extension.awaitMessage("change-cookies"); + + let stepTwo = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage } = this; + addMessageListener("options", options => { + let domain = options.domain.replace(/^\.?/, "."); + + Services.cookies.add( + domain, + "/", + "x", + "y", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + Services.cookies.add( + domain, + "/", + "x", + "z", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + Services.cookies.remove(domain, "x", "/", {}); + sendAsyncMessage("done"); + }); + }); + stepTwo.sendAsyncMessage("options", options); + await stepTwo.promiseOneMessage("done"); + stepTwo.destroy(); + + extension.sendMessage("cookies-changed"); + + await extension.awaitFinish("cookie-permissions"); + await extension.unload(); + + let stepThree = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage, assert } = this; + let cookieSvc = Services.cookies; + + function getCookies(host) { + let cookies = []; + for (let cookie of cookieSvc.getCookiesFromHost(host, {})) { + cookies.push(cookie); + } + return cookies.sort((a, b) => a.name.localeCompare(b.name)); + } + + addMessageListener("options", options => { + let cookies = getCookies(options.domain); + + if (options.shouldPass) { + assert.equal(cookies.length, 2, "expected two cookies for host"); + + assert.equal(cookies[0].name, "bar", "correct cookie name"); + assert.equal(cookies[0].value, "quux", "correct cookie value"); + + assert.equal(cookies[1].name, "foo", "correct cookie name"); + assert.equal(cookies[1].value, "baz", "correct cookie value"); + } else if (options.shouldWrite) { + // Note: |shouldWrite| applies only when |shouldPass| is false. + // This is necessary because, unfortunately, websites (and therefore web + // extensions) are allowed to write some cookies which they're not allowed + // to read. + assert.equal(cookies.length, 3, "expected three cookies for host"); + + assert.equal(cookies[0].name, "bar", "correct cookie name"); + assert.equal(cookies[0].value, "quux", "correct cookie value"); + + assert.equal(cookies[1].name, "deleted", "correct cookie name"); + + assert.equal(cookies[2].name, "foo", "correct cookie name"); + assert.equal(cookies[2].value, "baz", "correct cookie value"); + } else { + assert.equal(cookies.length, 2, "expected two cookies for host"); + + assert.equal(cookies[0].name, "deleted", "correct second cookie name"); + + assert.equal(cookies[1].name, "foo", "correct cookie name"); + assert.equal(cookies[1].value, "bar", "correct cookie value"); + } + + for (let cookie of cookies) { + cookieSvc.remove(cookie.host, cookie.name, "/", {}); + } + // Make sure we don't silently poison subsequent tests if something goes wrong. + assert.equal(getCookies(options.domain).length, 0, "cookies cleared"); + sendAsyncMessage("done"); + }); + }); + stepThree.sendAsyncMessage("options", options); + await stepThree.promiseOneMessage("done"); + stepThree.destroy(); +} diff --git a/toolkit/components/extensions/test/mochitest/head_notifications.js b/toolkit/components/extensions/test/mochitest/head_notifications.js new file mode 100644 index 0000000000..73d233380b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_notifications.js @@ -0,0 +1,167 @@ +"use strict"; + +/* exported MockAlertsService */ + +function mockServicesChromeScript() { + /* eslint-env mozilla/chrome-script */ + + const MOCK_ALERTS_CID = Components.ID( + "{48068bc2-40ab-4904-8afd-4cdfb3a385f3}" + ); + const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1"; + + const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" + ); + const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + + let activeNotifications = Object.create(null); + + const mockAlertsService = { + showPersistentNotification: function(persistentData, alert, alertListener) { + this.showAlert(alert, alertListener); + }, + + showAlert: function(alert, listener) { + activeNotifications[alert.name] = { + listener: listener, + cookie: alert.cookie, + title: alert.title, + }; + + // fake async alert show event + if (listener) { + setTimeout(function() { + listener.observe(null, "alertshow", alert.cookie); + }, 100); + } + }, + + showAlertNotification: function( + imageUrl, + title, + text, + textClickable, + cookie, + alertListener, + name + ) { + this.showAlert( + { + name: name, + cookie: cookie, + title: title, + }, + alertListener + ); + }, + + closeAlert: function(name) { + let alertNotification = activeNotifications[name]; + if (alertNotification) { + if (alertNotification.listener) { + alertNotification.listener.observe( + null, + "alertfinished", + alertNotification.cookie + ); + } + delete activeNotifications[name]; + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIAlertsService"]), + + createInstance: function(iid) { + return this.QueryInterface(iid); + }, + }; + + registrar.registerFactory( + MOCK_ALERTS_CID, + "alerts service", + ALERTS_SERVICE_CONTRACT_ID, + mockAlertsService + ); + + function clickNotifications(doClose) { + // Until we need to close a specific notification, just click them all. + for (let [name, notification] of Object.entries(activeNotifications)) { + let { listener, cookie } = notification; + listener.observe(null, "alertclickcallback", cookie); + if (doClose) { + mockAlertsService.closeAlert(name); + } + } + } + + function closeAllNotifications() { + for (let alertName of Object.keys(activeNotifications)) { + mockAlertsService.closeAlert(alertName); + } + } + + const { addMessageListener, sendAsyncMessage } = this; + + addMessageListener("mock-alert-service:unregister", () => { + closeAllNotifications(); + activeNotifications = null; + registrar.unregisterFactory(MOCK_ALERTS_CID, mockAlertsService); + sendAsyncMessage("mock-alert-service:unregistered"); + }); + + addMessageListener( + "mock-alert-service:click-notifications", + clickNotifications + ); + + addMessageListener( + "mock-alert-service:close-notifications", + closeAllNotifications + ); + + sendAsyncMessage("mock-alert-service:registered"); +} + +const MockAlertsService = { + async register() { + if (this._chromeScript) { + throw new Error("MockAlertsService already registered"); + } + this._chromeScript = SpecialPowers.loadChromeScript( + mockServicesChromeScript + ); + await this._chromeScript.promiseOneMessage("mock-alert-service:registered"); + }, + async unregister() { + if (!this._chromeScript) { + throw new Error("MockAlertsService not registered"); + } + this._chromeScript.sendAsyncMessage("mock-alert-service:unregister"); + return this._chromeScript + .promiseOneMessage("mock-alert-service:unregistered") + .then(() => { + this._chromeScript.destroy(); + this._chromeScript = null; + }); + }, + async clickNotifications() { + // Most implementations of the nsIAlertsService automatically close upon click. + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:click-notifications", + true + ); + }, + async clickNotificationsWithoutClose() { + // The implementation on macOS does not automatically close the notification. + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:click-notifications", + false + ); + }, + async closeNotifications() { + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:close-notifications" + ); + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js b/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js new file mode 100644 index 0000000000..dfec90b3e0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js @@ -0,0 +1,45 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported checkSitePermissions */ + +const { Services } = SpecialPowers; +const { NetUtil } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/NetUtil.jsm" +); + +function checkSitePermissions(uuid, expectedPermAction, assertMessage) { + if (!uuid) { + throw new Error( + "checkSitePermissions should not be called with an undefined uuid" + ); + } + + const baseURI = NetUtil.newURI(`moz-extension://${uuid}/`); + const principal = Services.scriptSecurityManager.createContentPrincipal( + baseURI, + {} + ); + + const sitePermissions = { + webextUnlimitedStorage: Services.perms.testPermissionFromPrincipal( + principal, + "WebExtensions-unlimitedStorage" + ), + persistentStorage: Services.perms.testPermissionFromPrincipal( + principal, + "persistent-storage" + ), + }; + + for (const [sitePermissionName, actualPermAction] of Object.entries( + sitePermissions + )) { + is( + actualPermAction, + expectedPermAction, + `The extension "${sitePermissionName}" SitePermission ${assertMessage} as expected` + ); + } +} diff --git a/toolkit/components/extensions/test/mochitest/head_webrequest.js b/toolkit/components/extensions/test/mochitest/head_webrequest.js new file mode 100644 index 0000000000..f6c6530e41 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js @@ -0,0 +1,482 @@ +"use strict"; + +let commonEvents = { + onBeforeRequest: [{ urls: [""] }, ["blocking"]], + onBeforeSendHeaders: [ + { urls: [""] }, + ["blocking", "requestHeaders"], + ], + onSendHeaders: [{ urls: [""] }, ["requestHeaders"]], + onBeforeRedirect: [{ urls: [""] }], + onHeadersReceived: [ + { urls: [""] }, + ["blocking", "responseHeaders"], + ], + // Auth tests will need to set their own events object + // "onAuthRequired": [{urls: [""]}, ["blocking", "responseHeaders"]], + onResponseStarted: [{ urls: [""] }], + onCompleted: [{ urls: [""] }, ["responseHeaders"]], + onErrorOccurred: [{ urls: [""] }], +}; + +function background(events) { + const IP_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + + let expect; + let ignore; + let defaultOrigin; + let watchAuth = Object.keys(events).includes("onAuthRequired"); + let expectedIp = null; + + browser.test.onMessage.addListener((msg, expected) => { + if (msg !== "set-expected") { + return; + } + expect = expected.expect; + defaultOrigin = expected.origin; + ignore = expected.ignore; + let promises = []; + // Initialize some stuff we'll need in the tests. + for (let entry of Object.values(expect)) { + // a place for the test infrastructure to store some state. + entry.test = {}; + // Each entry in expected gets a Promise that will be resolved in the + // last event for that entry. This will either be onCompleted, or the + // last entry if an events list was provided. + promises.push( + new Promise(resolve => { + entry.test.resolve = resolve; + }) + ); + // If events was left undefined, we're expecting all normal events we're + // listening for, exclude onBeforeRedirect and onErrorOccurred + if (entry.events === undefined) { + entry.events = Object.keys(events).filter( + name => name != "onErrorOccurred" && name != "onBeforeRedirect" + ); + } + if (entry.optional_events === undefined) { + entry.optional_events = []; + } + } + // When every expected entry has finished our test is done. + Promise.all(promises).then(() => { + browser.test.sendMessage("done"); + }); + browser.test.sendMessage("continue"); + }); + + // Retrieve the per-file/test expected values. + function getExpected(details) { + let url = new URL(details.url); + let filename = url.pathname.split("/").pop(); + if (ignore && ignore.includes(filename)) { + return; + } + let expected = expect[filename]; + if (!expected) { + browser.test.fail(`unexpected request ${filename}`); + return; + } + // Save filename for redirect verification. + expected.test.filename = filename; + return expected; + } + + // Process any test header modifications that can happen in request or response phases. + // If a test includes headers, it needs a complete header object, no undefined + // objects even if empty: + // request: { + // add: {"HeaderName": "value",}, + // modify: {"HeaderName": "value",}, + // remove: ["HeaderName",], + // }, + // response: { + // add: {"HeaderName": "value",}, + // modify: {"HeaderName": "value",}, + // remove: ["HeaderName",], + // }, + function processHeaders(phase, expected, details) { + // This should only happen once per phase [request|response]. + browser.test.assertFalse( + !!expected.test[phase], + `First processing of headers for ${phase}` + ); + expected.test[phase] = true; + + let headers = details[`${phase}Headers`]; + browser.test.assertTrue( + Array.isArray(headers), + `${phase}Headers array present` + ); + + let { add, modify, remove } = expected.headers[phase]; + + for (let name in add) { + browser.test.assertTrue( + !headers.find(h => h.name === name), + `header ${name} to be added not present yet in ${phase}Headers` + ); + let header = { name: name }; + if (name.endsWith("-binary")) { + header.binaryValue = Array.from(add[name], c => c.charCodeAt(0)); + } else { + header.value = add[name]; + } + headers.push(header); + } + + let modifiedAny = false; + for (let header of headers) { + if (header.name.toLowerCase() in modify) { + header.value = modify[header.name.toLowerCase()]; + modifiedAny = true; + } + } + browser.test.assertTrue( + modifiedAny, + `at least one ${phase}Headers element to modify` + ); + + let deletedAny = false; + for (let j = headers.length; j-- > 0; ) { + if (remove.includes(headers[j].name.toLowerCase())) { + headers.splice(j, 1); + deletedAny = true; + } + } + browser.test.assertTrue( + deletedAny, + `at least one ${phase}Headers element to delete` + ); + + return headers; + } + + // phase is request or response. + function checkHeaders(phase, expected, details) { + if (!/^https?:/.test(details.url)) { + return; + } + + let headers = details[`${phase}Headers`]; + browser.test.assertTrue( + Array.isArray(headers), + `valid ${phase}Headers array` + ); + + let { add, modify, remove } = expected.headers[phase]; + for (let name in add) { + let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()) + .value; + browser.test.assertEq( + value, + add[name], + `header ${name} correctly injected in ${phase}Headers` + ); + } + + for (let name in modify) { + let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()) + .value; + browser.test.assertEq( + value, + modify[name], + `header ${name} matches modified value` + ); + } + + for (let name of remove) { + let found = headers.find( + h => h.name.toLowerCase() === name.toLowerCase() + ); + browser.test.assertFalse( + !!found, + `deleted header ${name} still found in ${phase}Headers` + ); + } + } + + let listeners = { + onBeforeRequest(expected, details, result) { + // Save some values to test request consistency in later events. + browser.test.assertTrue( + details.tabId !== undefined, + `tabId ${details.tabId}` + ); + browser.test.assertTrue( + details.requestId !== undefined, + `requestId ${details.requestId}` + ); + // Validate requestId if it's already set, this happens with redirects. + if (expected.test.requestId !== undefined) { + browser.test.assertEq( + "string", + typeof expected.test.requestId, + `requestid ${expected.test.requestId} is string` + ); + browser.test.assertEq( + "string", + typeof details.requestId, + `requestid ${details.requestId} is string` + ); + browser.test.assertEq( + "number", + typeof parseInt(details.requestId, 10), + "parsed requestid is number" + ); + browser.test.assertEq( + expected.test.requestId, + details.requestId, + "redirects will keep the same requestId" + ); + } else { + // Save any values we want to validate in later events. + expected.test.requestId = details.requestId; + expected.test.tabId = details.tabId; + } + // Tests we don't need to do every event. + browser.test.assertTrue( + details.type.toUpperCase() in browser.webRequest.ResourceType, + `valid resource type ${details.type}` + ); + if (details.type == "main_frame") { + browser.test.assertEq( + 0, + details.frameId, + "frameId is zero when type is main_frame, see bug 1329299" + ); + } + }, + onBeforeSendHeaders(expected, details, result) { + if (expected.headers && expected.headers.request) { + result.requestHeaders = processHeaders("request", expected, details); + } + if (expected.redirect) { + browser.test.log(`${name} redirect request`); + result.redirectUrl = details.url.replace( + expected.test.filename, + expected.redirect + ); + } + }, + onBeforeRedirect() {}, + onSendHeaders(expected, details, result) { + if (expected.headers && expected.headers.request) { + checkHeaders("request", expected, details); + } + }, + onResponseStarted() {}, + onHeadersReceived(expected, details, result) { + let expectedStatus = expected.status || 200; + // If authentication is being requested we don't fail on the status code. + if (watchAuth && [401, 407].includes(details.statusCode)) { + expectedStatus = details.statusCode; + } + browser.test.assertEq( + expectedStatus, + details.statusCode, + `expected HTTP status received for ${details.url} ${details.statusLine}` + ); + if (expected.headers && expected.headers.response) { + result.responseHeaders = processHeaders("response", expected, details); + } + }, + onAuthRequired(expected, details, result) { + result.authCredentials = expected.authInfo; + }, + onCompleted(expected, details, result) { + // If we have already completed a GET request for this url, + // and it was found, we expect for the response to come fromCache. + // expected.cached may be undefined, force boolean. + if (typeof expected.cached === "boolean") { + let expectCached = + expected.cached && + details.method === "GET" && + details.statusCode != 404; + browser.test.assertEq( + expectCached, + details.fromCache, + "fromCache is correct" + ); + } + // We can only tell IPs for non-cached HTTP requests. + if (!details.fromCache && /^https?:/.test(details.url)) { + browser.test.assertTrue( + IP_PATTERN.test(details.ip), + `IP for ${details.url} looks IP-ish: ${details.ip}` + ); + + // We can't easily predict the IP ahead of time, so just make + // sure they're all consistent. + expectedIp = expectedIp || details.ip; + browser.test.assertEq( + expectedIp, + details.ip, + `correct ip for ${details.url}` + ); + } + if (expected.headers && expected.headers.response) { + checkHeaders("response", expected, details); + } + }, + onErrorOccurred(expected, details, result) { + if (expected.error) { + if (Array.isArray(expected.error)) { + browser.test.assertTrue( + expected.error.includes(details.error), + "expected error message received in onErrorOccurred" + ); + } else { + browser.test.assertEq( + expected.error, + details.error, + "expected error message received in onErrorOccurred" + ); + } + } + }, + }; + + function getListener(name) { + return details => { + let result = {}; + browser.test.log(`${name} ${details.requestId} ${details.url}`); + let expected = getExpected(details); + if (!expected) { + return result; + } + let expectedEvent = expected.events[0] == name; + if (expectedEvent) { + expected.events.shift(); + } else { + // e10s vs. non-e10s errors can end with either onCompleted or onErrorOccurred + expectedEvent = expected.optional_events.includes(name); + } + browser.test.assertTrue(expectedEvent, `received ${name}`); + browser.test.assertEq( + expected.type, + details.type, + "resource type is correct" + ); + browser.test.assertEq( + expected.origin || defaultOrigin, + details.originUrl, + "origin is correct" + ); + + if (name != "onBeforeRequest") { + // On events after onBeforeRequest, check the previous values. + browser.test.assertEq( + expected.test.requestId, + details.requestId, + "correct requestId" + ); + browser.test.assertEq( + expected.test.tabId, + details.tabId, + "correct tabId" + ); + } + try { + listeners[name](expected, details, result); + } catch (e) { + browser.test.fail(`unexpected webrequest failure ${name} ${e}`); + } + + if (expected.cancel && expected.cancel == name) { + browser.test.log(`${name} cancel request`); + browser.test.sendMessage("cancelled"); + result.cancel = true; + } + // If we've used up all the events for this test, resolve the promise. + // If something wrong happens and more events come through, there will be + // failures. + if (expected.events.length <= 0) { + expected.test.resolve(); + } + return result; + }; + } + + for (let [name, args] of Object.entries(events)) { + browser.test.log(`adding listener for ${name}`); + try { + browser.webRequest[name].addListener(getListener(name), ...args); + } catch (e) { + browser.test.assertTrue( + /\brequestBody\b/.test(e.message), + "Request body is unsupported" + ); + + // RequestBody is disabled in release builds. + if (!/\brequestBody\b/.test(e.message)) { + throw e; + } + + args.splice(args.indexOf("requestBody"), 1); + browser.webRequest[name].addListener(getListener(name), ...args); + } + } +} + +/* exported makeExtension */ + +function makeExtension(events = commonEvents) { + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", ""], + }, + background: `(${background})(${JSON.stringify(events)})`, + }); +} + +/* exported addStylesheet */ + +function addStylesheet(file) { + let link = document.createElement("link"); + link.setAttribute("rel", "stylesheet"); + link.setAttribute("href", file); + document.body.appendChild(link); +} + +/* exported addLink */ + +function addLink(file) { + let a = document.createElement("a"); + a.setAttribute("href", file); + a.setAttribute("target", "_blank"); + a.setAttribute("rel", "opener"); + document.body.appendChild(a); + return a; +} + +/* exported addImage */ + +function addImage(file) { + let img = document.createElement("img"); + img.setAttribute("src", file); + document.body.appendChild(img); +} + +/* exported addScript */ + +function addScript(file) { + let script = document.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("src", file); + document + .getElementsByTagName("head") + .item(0) + .appendChild(script); +} + +/* exported addFrame */ + +function addFrame(file) { + let frame = document.createElement("iframe"); + frame.setAttribute("width", "200"); + frame.setAttribute("height", "200"); + frame.setAttribute("src", file); + document.body.appendChild(frame); +} diff --git a/toolkit/components/extensions/test/mochitest/hsts.sjs b/toolkit/components/extensions/test/mochitest/hsts.sjs new file mode 100644 index 0000000000..52b9dd340b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/hsts.sjs @@ -0,0 +1,10 @@ +"use strict"; + +function handleRequest(request, response) { + let page = "

HSTS page

"; + response.setStatusLine(request.httpVersion, "200", "OK"); + response.setHeader("Strict-Transport-Security", "max-age=60"); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Length", page.length + "", false); + response.write(page); +} diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.ini b/toolkit/components/extensions/test/mochitest/mochitest-common.ini new file mode 100644 index 0000000000..7566192eee --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini @@ -0,0 +1,239 @@ +[DEFAULT] +tags = condprof +support-files = + chrome_cleanup_script.js + file_WebNavigation_page1.html + file_WebNavigation_page2.html + file_WebNavigation_page3.html + file_WebRequest_page3.html + file_contains_img.html + file_contains_iframe.html + file_green.html + file_green_blue.html + file_contentscript_activeTab.html + file_contentscript_activeTab2.html + file_contentscript_iframe.html + file_image_bad.png + file_image_good.png + file_image_great.png + file_image_redirect.png + file_indexedDB.html + file_mixed.html + file_remote_frame.html + file_sample.html + file_sample.txt + file_sample.txt^headers^ + file_script_bad.js + file_script_good.js + file_script_redirect.js + file_script_xhr.js + file_serviceWorker.html + file_simple_sandboxed_frame.html + file_simple_sandboxed_subframe.html + file_simple_xhr.html + file_simple_xhr_frame.html + file_simple_xhr_frame2.html + file_slowed_document.sjs + file_streamfilter.txt + file_style_bad.css + file_style_good.css + file_style_redirect.css + file_third_party.html + file_to_drawWindow.html + file_webNavigation_clientRedirect.html + file_webNavigation_clientRedirect_httpHeaders.html + file_webNavigation_clientRedirect_httpHeaders.html^headers^ + file_webNavigation_frameClientRedirect.html + file_webNavigation_frameRedirect.html + file_webNavigation_manualSubframe.html + file_webNavigation_manualSubframe_page1.html + file_webNavigation_manualSubframe_page2.html + file_with_about_blank.html + file_with_subframes_and_embed.html + file_with_xorigin_frame.html + head.js + head_cookies.js + head_notifications.js + head_unlimitedStorage.js + head_webrequest.js + hsts.sjs + mochitest_console.js + oauth.html + redirect_auto.sjs + redirection.sjs + return_headers.sjs + serviceWorker.js + slow_response.sjs + webrequest_worker.js + !/dom/tests/mochitest/geolocation/network_geolocation.sjs + !/toolkit/components/passwordmgr/test/authenticate.sjs + file_redirect_data_uri.html + file_redirect_cors_bypass.html + file_tabs_permission_page1.html + file_tabs_permission_page2.html +prefs = + security.mixed_content.upgrade_display_content=false + browser.chrome.guess_favicon=true + +[test_check_startupcache.html] +[test_ext_action.html] +[test_ext_activityLog.html] +skip-if = + os == 'android' + tsan # Times out on TSan, bug 1612707 + xorigin # Inconsistent pass/fail in opt and debug +[test_ext_async_clipboard.html] +skip-if = toolkit == 'android' || tsan # near-permafail after landing bug 1270059: Bug 1523131. tsan: bug 1612707 +[test_ext_background_canvas.html] +[test_ext_background_page.html] +skip-if = (toolkit == 'android') # android doesn't have devtools +[test_ext_background_page_dpi.html] +[test_ext_browserAction_openPopup.html] +[test_ext_browserAction_openPopup_incognito_window.html] +skip-if = os == "android" # cannot open private windows - bug 1372178 +[test_ext_browserAction_openPopup_windowId.html] +skip-if = os == "android" # only the current window is supported - bug 1795956 +[test_ext_browserAction_openPopup_without_pref.html] +[test_ext_browsingData_indexedDB.html] +[test_ext_browsingData_localStorage.html] +[test_ext_browsingData_pluginData.html] +[test_ext_browsingData_serviceWorkers.html] +skip-if = condprof # "Wait for 2 service workers to be registered - timed out after 50 tries." +[test_ext_browsingData_settings.html] +[test_ext_canvas_resistFingerprinting.html] +[test_ext_clipboard.html] +skip-if = os == 'android' +[test_ext_clipboard_image.html] +skip-if = headless # Bug 1405872 +[test_ext_contentscript_about_blank.html] +skip-if = os == 'android' # bug 1369440 + condprof #: "exactly 7 more scripts ran - got 11, expected 10" +[test_ext_contentscript_activeTab.html] +[test_ext_contentscript_cache.html] +skip-if = (os == 'linux' && debug) || (toolkit == 'android' && debug) # bug 1348241 +fail-if = xorigin # TypeError: can't access property "staticScripts", ext is undefined - Should not throw any errors +[test_ext_contentscript_canvas.html] +skip-if = (os == 'android') || (verify && debug && (os == 'linux')) # Bug 1617062 +[test_ext_contentscript_devtools_metadata.html] +[test_ext_contentscript_fission_frame.html] +[test_ext_contentscript_getFrameId.html] +[test_ext_contentscript_incognito.html] +skip-if = os == 'android' # Android does not support multiple windows. +[test_ext_contentscript_permission.html] +skip-if = tsan # Times out on TSan, bug 1612707 +[test_ext_cookies.html] +skip-if = os == 'android' || tsan # Times out on TSan intermittently, bug 1615184; not supported on Android yet + condprof #: "one tabId returned for store - Expected: 1, Actual: 3" +[test_ext_cookies_containers.html] +[test_ext_cookies_expiry.html] +[test_ext_cookies_first_party.html] +[test_ext_cookies_incognito.html] +skip-if = os == 'android' # Bug 1513544 Android does not support multiple windows. +[test_ext_cookies_permissions_bad.html] +[test_ext_cookies_permissions_good.html] +[test_ext_dnr_tabIds.html] +[test_ext_dnr_upgradeScheme.html] +[test_ext_downloads_download.html] +[test_ext_embeddedimg_iframe_frameAncestors.html] +[test_ext_exclude_include_globs.html] +[test_ext_extension_iframe_messaging.html] +[test_ext_external_messaging.html] +[test_ext_generate.html] +[test_ext_geolocation.html] +skip-if = os == 'android' # Android support Bug 1336194 +[test_ext_identity.html] +skip-if = os == 'android' || tsan # unsupported. tsan: bug 1612707 +[test_ext_idle.html] +skip-if = tsan # Times out on TSan, bug 1612707 +[test_ext_inIncognitoContext_window.html] +skip-if = os == 'android' # Android does not support multiple windows. +[test_ext_listener_proxies.html] +[test_ext_new_tab_processType.html] +skip-if = verify && debug && (os == 'linux' || os == 'mac') + condprof #: Page URL should match - got "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_serviceWorker.html", expected "https://example.com/" +[test_ext_notifications.html] +skip-if = os == 'android' # Not supported on Android yet +[test_ext_optional_permissions.html] +[test_ext_protocolHandlers.html] +skip-if = (toolkit == 'android') # bug 1342577 +[test_ext_redirect_jar.html] +skip-if = os == 'win' && (debug || asan) # Bug 1563440 +[test_ext_request_urlClassification.html] +skip-if = os == 'android' # Bug 1615427 +[test_ext_runtime_connect.html] +[test_ext_runtime_connect_iframe.html] +[test_ext_runtime_connect_twoway.html] +[test_ext_runtime_connect2.html] +[test_ext_runtime_disconnect.html] +[test_ext_script_filenames.html] +[test_ext_scripting_contentScripts.html] +[test_ext_scripting_executeScript.html] +[test_ext_scripting_executeScript_activeTab.html] +[test_ext_scripting_executeScript_injectImmediately.html] +[test_ext_scripting_insertCSS.html] +[test_ext_scripting_permissions.html] +[test_ext_scripting_removeCSS.html] +[test_ext_sendmessage_doublereply.html] +[test_ext_sendmessage_frameId.html] +[test_ext_sendmessage_no_receiver.html] +[test_ext_sendmessage_reply.html] +[test_ext_sendmessage_reply2.html] +skip-if = os == 'android' +[test_ext_storage_manager_capabilities.html] +skip-if = xorigin # JavaScript Error: "SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object" {file: "https://example.com/tests/SimpleTest/TestRunner.js" line: 157} +scheme=https +[test_ext_storage_smoke_test.html] +[test_ext_streamfilter_multiple.html] +skip-if = + !debug # Bug 1628642 + os == 'linux' # Bug 1628642 +[test_ext_streamfilter_processswitch.html] +[test_ext_subframes_privileges.html] +skip-if = os == 'android' || verify # bug 1489771 +[test_ext_tabs_captureTab.html] +[test_ext_tabs_executeScript_good.html] +[test_ext_tabs_create_cookieStoreId.html] +[test_ext_tabs_query_popup.html] +[test_ext_tabs_permissions.html] +[test_ext_tabs_sendMessage.html] +[test_ext_test.html] +[test_ext_unlimitedStorage.html] +skip-if = os == 'android' +[test_ext_web_accessible_resources.html] +skip-if = (os == 'android' && debug) || (os == "linux" && bits == 64) # bug 1397615, bug 1618231 +[test_ext_web_accessible_incognito.html] +skip-if = (os == 'android') # bug 1397615, bug 1513544 +[test_ext_webnavigation.html] +skip-if = (os == 'android' && debug) # bug 1397615 +[test_ext_webnavigation_filters.html] +skip-if = (os == 'android' && debug) || (verify && (os == 'linux' || os == 'mac')) # bug 1397615 +[test_ext_webnavigation_incognito.html] +skip-if = os == 'android' # bug 1513544 +[test_ext_webrequest_and_proxy_filter.html] +[test_ext_webrequest_auth.html] +skip-if = os == 'android' +[test_ext_webrequest_background_events.html] +[test_ext_webrequest_basic.html] +skip-if = + os == 'android' && debug # bug 1397615 + tsan # bug 1612707 + xorigin # JavaScript Error: "SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object" {file: "http://mochi.false-test:8888/tests/SimpleTest/TestRunner.js" line: 157}] + os == "linux" && bits == 64 && !debug && asan # Bug 1633189 +[test_ext_webrequest_errors.html] +skip-if = tsan +[test_ext_webrequest_filter.html] +skip-if = + os == 'android' && debug || tsan # bug 1452348. tsan: bug 1612707 + os == 'linux' && bits == 64 && !debug && xorigin # Bug 1756023 +[test_ext_webrequest_frameId.html] +skip-if = os == 'linux' # Bug 1482983 caused by Bug 1480951 +[test_ext_webrequest_hsts.html] +skip-if = os == 'android' || os == 'linux' || os == 'mac' #Bug 1605515 +[test_ext_webrequest_upgrade.html] +[test_ext_webrequest_upload.html] +skip-if = os == 'android' # Currently fails in emulator tests +[test_ext_webrequest_redirect_bypass_cors.html] +[test_ext_webrequest_redirect_data_uri.html] +[test_ext_window_postMessage.html] +# test_startup_canary.html is at the bottom to minimize the time spent waiting in the test. +[test_startup_canary.html] diff --git a/toolkit/components/extensions/test/mochitest/mochitest-remote.ini b/toolkit/components/extensions/test/mochitest/mochitest-remote.ini new file mode 100644 index 0000000000..a71ff6ad65 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-remote.ini @@ -0,0 +1,8 @@ +[DEFAULT] +tags = webextensions remote-webextensions +skip-if = os == 'android' # Bug 1620091: disable on android until extension process is done +prefs = + extensions.webextensions.remote=true + +[test_verify_remote_mode.html] +[include:mochitest-common.ini] diff --git a/toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini b/toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini new file mode 100644 index 0000000000..c0f4f3005b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini @@ -0,0 +1,24 @@ +[DEFAULT] +tags = webextensions sw-webextensions condprof +skip-if = + !e10s # Thunderbird does still run in non e10s mode (and so also with in-process-webextensions mode) + (os == 'android') # Bug 1620091: disable on android until extension process is done + +prefs = + extensions.webextensions.remote=true + extensions.backgroundServiceWorker.enabled=true + extensions.backgroundServiceWorker.forceInTestExtension=true + +dupe-manifest = true + +# `test_verify_sw_mode.html` should be the first one, even if it breaks the +# alphabetical order. +[test_verify_sw_mode.html] +[test_ext_scripting_contentScripts.html] +[test_ext_scripting_executeScript.html] +skip-if = true # Bug 1748315 - Add WebIDL bindings for `scripting.executeScript()` +[test_ext_scripting_insertCSS.html] +skip-if = true # Bug 1748318 - Add WebIDL bindings for `tabs` +[test_ext_scripting_removeCSS.html] +skip-if = true # Bug 1748318 - Add WebIDL bindings for `tabs` +[test_ext_test.html] diff --git a/toolkit/components/extensions/test/mochitest/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..f2f6117726 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest.ini @@ -0,0 +1,13 @@ +[DEFAULT] +tags = webextensions in-process-webextensions +prefs = + extensions.webextensions.remote=false + javascript.options.asyncstack_capture_debuggee_only=false +dupe-manifest = true + +[test_verify_non_remote_mode.html] +[test_ext_storage_cleanup.html] +# Bug 1426514 storage_cleanup: clearing localStorage fails with oop + +[include:mochitest-common.ini] +skip-if = os == 'win' # Windows WebExtensions always run OOP diff --git a/toolkit/components/extensions/test/mochitest/mochitest_console.js b/toolkit/components/extensions/test/mochitest/mochitest_console.js new file mode 100644 index 0000000000..582e12b48f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest_console.js @@ -0,0 +1,54 @@ +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { addMessageListener, sendAsyncMessage } = this; + +// Much of the console monitoring code is copied from TestUtils but simplified +// to our needs. +function monitorConsole(msgs) { + function msgMatches(msg, pat) { + for (let k in pat) { + if (!(k in msg)) { + return false; + } + if (pat[k] instanceof RegExp && typeof msg[k] === "string") { + if (!pat[k].test(msg[k])) { + return false; + } + } else if (msg[k] !== pat[k]) { + return false; + } + } + return true; + } + + let counter = 0; + function listener(msg) { + if (msgMatches(msg, msgs[counter])) { + counter++; + } + } + addMessageListener("waitForConsole", () => { + sendAsyncMessage("consoleDone", { + ok: counter >= msgs.length, + message: `monitorConsole | messages left expected at least ${msgs.length} got ${counter}`, + }); + Services.console.unregisterListener(listener); + }); + + Services.console.registerListener(listener); +} + +addMessageListener("consoleStart", messages => { + for (let msg of messages) { + // Message might be a RegExp object from a different compartment, but + // instanceof RegExp will fail. If we have an object, lets just make + // sure. + let message = msg.message; + if (typeof message == "object" && !(message instanceof RegExp)) { + msg.message = new RegExp(message); + } + } + monitorConsole(messages); +}); diff --git a/toolkit/components/extensions/test/mochitest/oauth.html b/toolkit/components/extensions/test/mochitest/oauth.html new file mode 100644 index 0000000000..8b9b1d65ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/oauth.html @@ -0,0 +1,26 @@ + + + + + + +
+
+ + diff --git a/toolkit/components/extensions/test/mochitest/redirect_auto.sjs b/toolkit/components/extensions/test/mochitest/redirect_auto.sjs new file mode 100644 index 0000000000..bf7af2556b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/redirect_auto.sjs @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; +Cu.importGlobalProperties(["URLSearchParams", "URL"]); + +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + if (params.has("no_redirect")) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write("ok"); + } else { + if (request.method == "POST") { + response.setStatusLine(request.httpVersion, 303, "Redirected"); + } else { + response.setStatusLine(request.httpVersion, 302, "Moved Temporarily"); + } + let url = new URL( + params.get("redirect_uri") || params.get("default_redirect") + ); + url.searchParams.set("access_token", "here ya go"); + response.setHeader("Location", url.href); + } +} diff --git a/toolkit/components/extensions/test/mochitest/redirection.sjs b/toolkit/components/extensions/test/mochitest/redirection.sjs new file mode 100644 index 0000000000..873a3d41ba --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/redirection.sjs @@ -0,0 +1,6 @@ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 302); + response.setHeader("Location", "./dummy_page.html"); +} diff --git a/toolkit/components/extensions/test/mochitest/return_headers.sjs b/toolkit/components/extensions/test/mochitest/return_headers.sjs new file mode 100644 index 0000000000..46beab8185 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/return_headers.sjs @@ -0,0 +1,19 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported handleRequest */ + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + + let headers = {}; + // Why on earth is this a nsISimpleEnumerator... + let enumerator = request.headers; + while (enumerator.hasMoreElements()) { + let header = enumerator.getNext().data; + headers[header.toLowerCase()] = request.getHeader(header); + } + + response.write(JSON.stringify(headers)); +} diff --git a/toolkit/components/extensions/test/mochitest/serviceWorker.js b/toolkit/components/extensions/test/mochitest/serviceWorker.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/toolkit/components/extensions/test/mochitest/slow_response.sjs b/toolkit/components/extensions/test/mochitest/slow_response.sjs new file mode 100644 index 0000000000..d39c4c0bf0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/slow_response.sjs @@ -0,0 +1,60 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +/* eslint-disable no-unused-vars */ + +let { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const DELAY = AppConstants.DEBUG ? 4000 : 800; + +let nsTimer = Components.Constructor( + "@mozilla.org/timer;1", + "nsITimer", + "initWithCallback" +); + +let timer; +function delay() { + return new Promise(resolve => { + timer = nsTimer(resolve, DELAY, Ci.nsITimer.TYPE_ONE_SHOT); + }); +} + +const PARTS = [ + ` + + + + + + `, + "Lorem ipsum dolor sit amet,
", + "consectetur adipiscing elit,
", + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
", + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
", + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
", + "Excepteur sint occaecat cupidatat non proident,
", + "sunt in culpa qui officia deserunt mollit anim id est laborum.
", + ` + + `, +]; + +async function handleRequest(request, response) { + response.processAsync(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + + await delay(); + + for (let part of PARTS) { + response.write(`${part}\n`); + await delay(); + } + + response.finish(); +} diff --git a/toolkit/components/extensions/test/mochitest/test_check_startupcache.html b/toolkit/components/extensions/test/mochitest/test_check_startupcache.html new file mode 100644 index 0000000000..01d361ca5e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_check_startupcache.html @@ -0,0 +1,61 @@ + + + + Check StartupCache + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html new file mode 100644 index 0000000000..42950c50ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html @@ -0,0 +1,104 @@ + + + + Test content script matching a data: URI + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html new file mode 100644 index 0000000000..224e806288 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html @@ -0,0 +1,68 @@ + + + + Test for telemetry for content script injection + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html new file mode 100644 index 0000000000..40403dea2b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html @@ -0,0 +1,80 @@ + + + + Test for content script unrecognized property on manifest + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html new file mode 100644 index 0000000000..530937c1ac --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html @@ -0,0 +1,114 @@ + + + + Test for permissions + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html new file mode 100644 index 0000000000..64cfcfd289 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html @@ -0,0 +1,257 @@ + + + + Test downloads.download() saveAs option + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html new file mode 100644 index 0000000000..b5fedee7ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html @@ -0,0 +1,116 @@ + + + + Test downloads.download() uniquify option + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html new file mode 100644 index 0000000000..65bf0a50d0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html @@ -0,0 +1,172 @@ + + + + Test for permissions + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_svg_context_fill.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_svg_context_fill.html new file mode 100644 index 0000000000..3b022be5ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_svg_context_fill.html @@ -0,0 +1,204 @@ + + + + Test for permissions + + + + + + + +

Testing on:

+ + + + + + + + + + + + + + + +
webext imageallowed refdisallowed ref
+ + +
+
+
+
+ + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html new file mode 100644 index 0000000000..580ea5e793 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html @@ -0,0 +1,98 @@ + + + + Test for simple WebExtension + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html new file mode 100644 index 0000000000..7e876694a0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html @@ -0,0 +1,81 @@ + + + + Test for simple WebExtension + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html new file mode 100644 index 0000000000..a9dfb0a902 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html @@ -0,0 +1,94 @@ + + + + Test for simple WebExtension + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html new file mode 100644 index 0000000000..19c812f59f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html @@ -0,0 +1,89 @@ + + + + Test webRequest checks host permissions + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html new file mode 100644 index 0000000000..6a41b9cf08 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html @@ -0,0 +1,193 @@ + + + + Test moz-extension protocol use + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html new file mode 100644 index 0000000000..0300d7c1b5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html @@ -0,0 +1,55 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_action.html b/toolkit/components/extensions/test/mochitest/test_ext_action.html new file mode 100644 index 0000000000..1899475723 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_action.html @@ -0,0 +1,51 @@ + + + + + Action with MV3 + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html b/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html new file mode 100644 index 0000000000..c426913373 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html @@ -0,0 +1,390 @@ + + + + WebExtension activityLog test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js new file mode 100644 index 0000000000..4c82a4575c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js @@ -0,0 +1,245 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Tests whether not too many APIs are visible by default. +// This file is used by test_ext_all_apis.html in browser/ and mobile/android/, +// which may modify the following variables to add or remove expected APIs. +/* globals expectedContentApisTargetSpecific */ +/* globals expectedBackgroundApisTargetSpecific */ + +// Generates a list of expectations. +function generateExpectations(list) { + return list + .reduce((allApis, path) => { + return allApis.concat(`browser.${path}`, `chrome.${path}`); + }, []) + .sort(); +} + +let expectedCommonApis = [ + "extension.getURL", + "extension.inIncognitoContext", + "extension.lastError", + "i18n.detectLanguage", + "i18n.getAcceptLanguages", + "i18n.getMessage", + "i18n.getUILanguage", + "runtime.OnInstalledReason", + "runtime.OnRestartRequiredReason", + "runtime.PlatformArch", + "runtime.PlatformOs", + "runtime.RequestUpdateCheckStatus", + "runtime.getManifest", + "runtime.connect", + "runtime.getFrameId", + "runtime.getURL", + "runtime.id", + "runtime.lastError", + "runtime.onConnect", + "runtime.onMessage", + "runtime.sendMessage", + // browser.test is only available in xpcshell or when + // Cu.isInAutomation is true. + "test.assertDeepEq", + "test.assertEq", + "test.assertFalse", + "test.assertRejects", + "test.assertThrows", + "test.assertTrue", + "test.fail", + "test.log", + "test.notifyFail", + "test.notifyPass", + "test.onMessage", + "test.sendMessage", + "test.succeed", + "test.withHandlingUserInput", +]; + +let expectedContentApis = [ + ...expectedCommonApis, + ...expectedContentApisTargetSpecific, +]; + +let expectedBackgroundApis = [ + ...expectedCommonApis, + ...expectedBackgroundApisTargetSpecific, + "contentScripts.register", + "experiments.APIChildScope", + "experiments.APIEvent", + "experiments.APIParentScope", + "extension.ViewType", + "extension.getBackgroundPage", + "extension.getViews", + "extension.isAllowedFileSchemeAccess", + "extension.isAllowedIncognitoAccess", + // Note: extensionTypes is not visible in Chrome. + "extensionTypes.CSSOrigin", + "extensionTypes.ImageFormat", + "extensionTypes.RunAt", + "management.ExtensionDisabledReason", + "management.ExtensionInstallType", + "management.ExtensionType", + "management.getSelf", + "management.uninstallSelf", + "permissions.getAll", + "permissions.contains", + "permissions.request", + "permissions.remove", + "permissions.onAdded", + "permissions.onRemoved", + "runtime.getBackgroundPage", + "runtime.getBrowserInfo", + "runtime.getPlatformInfo", + "runtime.onConnectExternal", + "runtime.onInstalled", + "runtime.onMessageExternal", + "runtime.onStartup", + "runtime.onSuspend", + "runtime.onSuspendCanceled", + "runtime.onUpdateAvailable", + "runtime.openOptionsPage", + "runtime.reload", + "runtime.setUninstallURL", + "theme.getCurrent", + "theme.onUpdated", + "types.LevelOfControl", + "types.SettingScope", +]; + +// APIs that are exposed to MV2 by default, but not to MV3. +const mv2onlyBackgroundApis = new Set([ + "extension.getURL", + "extension.lastError", + "contentScripts.register", + "tabs.executeScript", + "tabs.insertCSS", + "tabs.removeCSS", +]); +let expectedBackgroundApisMV3 = expectedBackgroundApis.filter( + path => !mv2onlyBackgroundApis.has(path) +); + +function sendAllApis() { + function isEvent(key, val) { + if (!/^on[A-Z]/.test(key)) { + return false; + } + let eventKeys = []; + for (let prop in val) { + eventKeys.push(prop); + } + eventKeys = eventKeys.sort().join(); + return eventKeys === "addListener,hasListener,removeListener"; + } + // Some items are removed from the namespaces in the lazy getters after the first get. This + // in one case, the events namespace, leaves a namespace that is empty. Make sure we don't + // consider those as a part of our testing. + function isEmptyObject(val) { + return val !== null && typeof val == "object" && !Object.keys(val).length; + } + function mayRecurse(key, val) { + if (Object.keys(val).filter(k => !/^[A-Z\-0-9_]+$/.test(k)).length === 0) { + // Don't recurse on constants and empty objects. + return false; + } + return !isEvent(key, val); + } + + let results = []; + function diveDeeper(path, obj) { + for (const [key, val] of Object.entries(obj)) { + if (typeof val == "object" && val !== null && mayRecurse(key, val)) { + diveDeeper(`${path}.${key}`, val); + } else if (val !== undefined && !isEmptyObject(val)) { + results.push(`${path}.${key}`); + } + } + } + diveDeeper("browser", browser); + diveDeeper("chrome", chrome); + browser.test.sendMessage("allApis", results.sort()); + browser.test.sendMessage("namespaces", browser === chrome); +} + +add_task(async function setup() { + // This test enumerates all APIs and may access a deprecated API. Just log a + // warning instead of throwing. + await ExtensionTestUtils.failOnSchemaWarnings(false); +}); + +add_task(async function test_enumerate_content_script_apis() { + let extensionData = { + manifest: { + content_scripts: [ + { + matches: ["http://mochi.test/*/file_sample.html"], + js: ["contentscript.js"], + run_at: "document_start", + }, + ], + }, + files: { + "contentscript.js": sendAllApis, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + let actualApis = await extension.awaitMessage("allApis"); + win.close(); + let expectedApis = generateExpectations(expectedContentApis); + isDeeply(actualApis, expectedApis, "content script APIs"); + + let sameness = await extension.awaitMessage("namespaces"); + ok(sameness, "namespaces are same object"); + + await extension.unload(); +}); + +add_task(async function test_enumerate_background_script_apis() { + let extensionData = { + background: sendAllApis, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let actualApis = await extension.awaitMessage("allApis"); + let expectedApis = generateExpectations(expectedBackgroundApis); + isDeeply(actualApis, expectedApis, "background script APIs"); + + let sameness = await extension.awaitMessage("namespaces"); + ok(!sameness, "namespaces are different objects"); + + await extension.unload(); +}); + +add_task(async function test_enumerate_background_script_apis_mv3() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); + let extensionData = { + background: sendAllApis, + manifest: { + manifest_version: 3, + + // Features that expose APIs in MV2, but should not do anything with MV3. + browser_action: {}, + user_scripts: {}, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let actualApis = await extension.awaitMessage("allApis"); + let expectedApis = generateExpectations(expectedBackgroundApisMV3); + isDeeply(actualApis, expectedApis, "background script APIs in MV3"); + + let sameness = await extension.awaitMessage("namespaces"); + ok(sameness, "namespaces are same object"); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html new file mode 100644 index 0000000000..c3b8da8a8c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html @@ -0,0 +1,401 @@ + + + + Async Clipboard permissions tests + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html new file mode 100644 index 0000000000..e7745f08c5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html @@ -0,0 +1,42 @@ + + + + Test for background page canvas rendering + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_page.html b/toolkit/components/extensions/test/mochitest/test_ext_background_page.html new file mode 100644 index 0000000000..2f4fe3b96c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_page.html @@ -0,0 +1,84 @@ + + + + WebExtension test + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_page_dpi.html b/toolkit/components/extensions/test/mochitest/test_ext_background_page_dpi.html new file mode 100644 index 0000000000..40772402b1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_page_dpi.html @@ -0,0 +1,46 @@ + + +DPI of background page + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup.html b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup.html new file mode 100644 index 0000000000..0d038da897 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup.html @@ -0,0 +1,183 @@ + + + + action.openPopup Test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_incognito_window.html b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_incognito_window.html new file mode 100644 index 0000000000..8036d97398 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_incognito_window.html @@ -0,0 +1,151 @@ + + + + action.openPopup Incognito Test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_windowId.html b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_windowId.html new file mode 100644 index 0000000000..c528028901 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_windowId.html @@ -0,0 +1,162 @@ + + + + action.openPopup Window ID Test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_without_pref.html b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_without_pref.html new file mode 100644 index 0000000000..aa7285d5f5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_without_pref.html @@ -0,0 +1,58 @@ + + + + action.openPopup Preference Test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html new file mode 100644 index 0000000000..f8ea41ddab --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html @@ -0,0 +1,159 @@ + + + + + Test browsingData.remove indexedDB + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html new file mode 100644 index 0000000000..0b61ce341f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html @@ -0,0 +1,323 @@ + + + + + Test browsingData.remove indexedDB + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html new file mode 100644 index 0000000000..bf4bd8fe80 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html @@ -0,0 +1,69 @@ + + + + + Test browsingData.remove indexedDB + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html new file mode 100644 index 0000000000..900546f32c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html @@ -0,0 +1,141 @@ + + + + + Test browsingData.remove indexedDB + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html new file mode 100644 index 0000000000..11c690e5bf --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html @@ -0,0 +1,65 @@ + + + + + Test browsingData.settings + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html b/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html new file mode 100644 index 0000000000..7116d03235 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html @@ -0,0 +1,64 @@ + + + + WebExtension test + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html b/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html new file mode 100644 index 0000000000..77ac767391 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html @@ -0,0 +1,210 @@ + + + + Clipboard permissions tests + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html b/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html new file mode 100644 index 0000000000..b5d5f6764a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html @@ -0,0 +1,262 @@ + + + + Clipboard permissions tests + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html new file mode 100644 index 0000000000..127305715f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html @@ -0,0 +1,116 @@ + + + + Test content script match_about_blank option + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html new file mode 100644 index 0000000000..96091fd959 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html @@ -0,0 +1,711 @@ + + + + Test for content script + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html new file mode 100644 index 0000000000..6e2420e1c5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html @@ -0,0 +1,117 @@ + + + + Test for content script caching + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html new file mode 100644 index 0000000000..8659d8c409 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html @@ -0,0 +1,134 @@ + + + + Test content script access to canvas drawWindow() + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html new file mode 100644 index 0000000000..d7030258b3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html @@ -0,0 +1,77 @@ + + + + Test for Sandbox metadata on WebExtensions ContentScripts + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html new file mode 100644 index 0000000000..6e03c3e9cd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html @@ -0,0 +1,109 @@ + + + Test content script in cross-origin frame + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_getFrameId.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_getFrameId.html new file mode 100644 index 0000000000..d679634030 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_getFrameId.html @@ -0,0 +1,189 @@ + + + Test content script runtime.getFrameId + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html new file mode 100644 index 0000000000..de2a2571a9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html @@ -0,0 +1,101 @@ + + + + Test for content script private browsing ID + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html new file mode 100644 index 0000000000..8ab4b1fb28 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html @@ -0,0 +1,59 @@ + + + + Test for content script + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html new file mode 100644 index 0000000000..cdec628975 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html @@ -0,0 +1,367 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html new file mode 100644 index 0000000000..d4bbd61177 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html @@ -0,0 +1,98 @@ + + + + WebExtension test + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html new file mode 100644 index 0000000000..fa118f5271 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html @@ -0,0 +1,72 @@ + + + + WebExtension cookies test + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html new file mode 100644 index 0000000000..7e33f4731d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html @@ -0,0 +1,316 @@ + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html new file mode 100644 index 0000000000..b33ceecf06 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html @@ -0,0 +1,107 @@ + + + + WebExtension test + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html new file mode 100644 index 0000000000..0bd2852075 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html @@ -0,0 +1,115 @@ + + + + WebExtension test + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html new file mode 100644 index 0000000000..bd76f2b9c0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html @@ -0,0 +1,89 @@ + + + + WebExtension test + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_dnr_tabIds.html b/toolkit/components/extensions/test/mochitest/test_ext_dnr_tabIds.html new file mode 100644 index 0000000000..0278a8ccc8 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_dnr_tabIds.html @@ -0,0 +1,137 @@ + + + + + DNR with tabIds condition + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html b/toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html new file mode 100644 index 0000000000..028bed32aa --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html @@ -0,0 +1,132 @@ + + + + + DNR with upgradeScheme action + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html b/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html new file mode 100644 index 0000000000..ea163db0de --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html @@ -0,0 +1,90 @@ + + + + + Downloads Test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html b/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html new file mode 100644 index 0000000000..d6702da4d3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html @@ -0,0 +1,94 @@ + + + + Test checking webRequest.onBeforeRequest details object + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html new file mode 100644 index 0000000000..f87b5620d6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html @@ -0,0 +1,91 @@ + + + + Test for content script + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_extension_iframe_messaging.html b/toolkit/components/extensions/test/mochitest/test_ext_extension_iframe_messaging.html new file mode 100644 index 0000000000..403782ab7d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_extension_iframe_messaging.html @@ -0,0 +1,124 @@ + + + + Test moz-extension iframe messaging + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html b/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html new file mode 100644 index 0000000000..639cacef28 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html @@ -0,0 +1,110 @@ + + + + WebExtension external messaging + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_generate.html b/toolkit/components/extensions/test/mochitest/test_ext_generate.html new file mode 100644 index 0000000000..ba88d16ca3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_generate.html @@ -0,0 +1,48 @@ + + + + Test for generating WebExtensions + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html b/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html new file mode 100644 index 0000000000..9f326372bb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_identity.html b/toolkit/components/extensions/test/mochitest/test_ext_identity.html new file mode 100644 index 0000000000..7aa590ec22 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_identity.html @@ -0,0 +1,390 @@ + + + + Test for WebExtension Identity + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_idle.html b/toolkit/components/extensions/test/mochitest/test_ext_idle.html new file mode 100644 index 0000000000..381687ee38 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_idle.html @@ -0,0 +1,68 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html new file mode 100644 index 0000000000..5b36902581 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html @@ -0,0 +1,49 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html new file mode 100644 index 0000000000..cc161f735f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html @@ -0,0 +1,62 @@ + + + + Test for content script + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html b/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html new file mode 100644 index 0000000000..2c5ae1c0ba --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html @@ -0,0 +1,168 @@ + + + + Test for opening links in new tabs from extension frames + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_notifications.html b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html new file mode 100644 index 0000000000..7a91320373 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html @@ -0,0 +1,340 @@ + + + + Test for notifications + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_optional_permissions.html b/toolkit/components/extensions/test/mochitest/test_ext_optional_permissions.html new file mode 100644 index 0000000000..659a55f5c9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_optional_permissions.html @@ -0,0 +1,98 @@ + + + + + optional permissions and preloaded processes + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html new file mode 100644 index 0000000000..41db82eff7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html @@ -0,0 +1,580 @@ + + + + + Test for protocol handlers + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html b/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html new file mode 100644 index 0000000000..18ff14a6de --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html b/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html new file mode 100644 index 0000000000..a5c7024a83 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html @@ -0,0 +1,130 @@ + + + + Test for WebRequest urlClassification + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html new file mode 100644 index 0000000000..85f98d5034 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html @@ -0,0 +1,83 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html new file mode 100644 index 0000000000..13b9029c48 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html @@ -0,0 +1,102 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html new file mode 100644 index 0000000000..9c64635063 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html @@ -0,0 +1,136 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html new file mode 100644 index 0000000000..b671cba23d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html @@ -0,0 +1,126 @@ + + + + WebExtension test + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html new file mode 100644 index 0000000000..f18190bf8b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html @@ -0,0 +1,77 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_script_filenames.html b/toolkit/components/extensions/test/mochitest/test_ext_script_filenames.html new file mode 100644 index 0000000000..de0993c33d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_script_filenames.html @@ -0,0 +1,62 @@ + + + + Script Filenames Test + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html new file mode 100644 index 0000000000..c8457b6d36 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html @@ -0,0 +1,1532 @@ + + + + + Tests scripting.*ContentScripts() + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript.html new file mode 100644 index 0000000000..a2d741606f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript.html @@ -0,0 +1,1479 @@ + + + + + Tests scripting.executeScript() + + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html new file mode 100644 index 0000000000..5eb2193409 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html @@ -0,0 +1,144 @@ + + + + + Tests scripting.executeScript() and activeTab + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_injectImmediately.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_injectImmediately.html new file mode 100644 index 0000000000..9d05925adc --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_injectImmediately.html @@ -0,0 +1,215 @@ + + + + + Tests scripting.executeScript() and injectImmediately + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_insertCSS.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_insertCSS.html new file mode 100644 index 0000000000..3e2cef8721 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_insertCSS.html @@ -0,0 +1,395 @@ + + + + + Tests scripting.insertCSS() + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_permissions.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_permissions.html new file mode 100644 index 0000000000..e3e6552290 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_permissions.html @@ -0,0 +1,149 @@ + + + + + Tests scripting APIs and permissions + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_removeCSS.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_removeCSS.html new file mode 100644 index 0000000000..3036e49761 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_removeCSS.html @@ -0,0 +1,135 @@ + + + + + Tests scripting.removeCSS() + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html new file mode 100644 index 0000000000..ffdbc90efb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html @@ -0,0 +1,100 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html new file mode 100644 index 0000000000..6b42073031 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html @@ -0,0 +1,45 @@ + + + Test sendMessage frameId + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html new file mode 100644 index 0000000000..a18b003e48 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html @@ -0,0 +1,115 @@ + + + + WebExtension test + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html new file mode 100644 index 0000000000..a7f6314efd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html @@ -0,0 +1,78 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html new file mode 100644 index 0000000000..8cce833b49 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html @@ -0,0 +1,202 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html new file mode 100644 index 0000000000..d4dc7a013f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html @@ -0,0 +1,235 @@ + + + + WebExtension test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html new file mode 100644 index 0000000000..135d2b9589 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html @@ -0,0 +1,130 @@ + + + + Test Storage API + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html new file mode 100644 index 0000000000..e68caa7e55 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html @@ -0,0 +1,108 @@ + + + + WebExtension test + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html new file mode 100644 index 0000000000..d1bfbd824b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html @@ -0,0 +1,91 @@ + + + + Test for multiple extensions trying to filterResponseData on the same request + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html new file mode 100644 index 0000000000..049178cad0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html @@ -0,0 +1,76 @@ + + + + Test for using filterResponseData to intercept a cross-origin navigation that will involve a process switch with fission + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html new file mode 100644 index 0000000000..fd034f0b65 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html @@ -0,0 +1,340 @@ + + + + + WebExtension test + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html new file mode 100644 index 0000000000..ab06a965ed --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html @@ -0,0 +1,324 @@ + + + + + Tests tabs.captureTab and tabs.captureVisibleTab + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_create_cookieStoreId.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_create_cookieStoreId.html new file mode 100644 index 0000000000..331faca016 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_create_cookieStoreId.html @@ -0,0 +1,210 @@ + + + + Test tabs.create(cookieStoreId) + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_executeScript_good.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_executeScript_good.html new file mode 100644 index 0000000000..ab3b9de5a3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_executeScript_good.html @@ -0,0 +1,162 @@ + + + + Tabs executeScript Test + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html new file mode 100644 index 0000000000..217139f12b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html @@ -0,0 +1,752 @@ + + + + Tabs permissions test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html new file mode 100644 index 0000000000..80b6def0ab --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html @@ -0,0 +1,102 @@ + + + + Tabs create Test + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html new file mode 100644 index 0000000000..4b230c258c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html @@ -0,0 +1,152 @@ + + + + Test tabs.sendMessage + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_test.html b/toolkit/components/extensions/test/mochitest/test_ext_test.html new file mode 100644 index 0000000000..bf68786465 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html @@ -0,0 +1,341 @@ + + + + Testing test + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html new file mode 100644 index 0000000000..db0f512ac3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html @@ -0,0 +1,138 @@ + + + + Test for simple WebExtension + + + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html new file mode 100644 index 0000000000..d1c41d2030 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html @@ -0,0 +1,170 @@ + + + + Test the web_accessible_resources incognito + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html new file mode 100644 index 0000000000..c13e40e265 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html @@ -0,0 +1,567 @@ + + + + Test the web_accessible_resources manifest directive + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html new file mode 100644 index 0000000000..12c90f8350 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html @@ -0,0 +1,610 @@ + + + + Test for simple WebExtension + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html new file mode 100644 index 0000000000..19cb6539d7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html @@ -0,0 +1,313 @@ + + + + Test for simple WebExtension + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html new file mode 100644 index 0000000000..45147365ee --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html @@ -0,0 +1,105 @@ + + + + Test for simple WebExtension + + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html new file mode 100644 index 0000000000..b28cbb7635 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html new file mode 100644 index 0000000000..f260f040a1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html @@ -0,0 +1,181 @@ + + + + + + + + + + + + + +
Authorization Test
+ + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html new file mode 100644 index 0000000000..86cec62fb4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html @@ -0,0 +1,120 @@ + + + + Test for simple WebExtension + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html new file mode 100644 index 0000000000..4df42ade60 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html @@ -0,0 +1,447 @@ + + + + + + + + + + + + + +
Sample text
+ + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html new file mode 100644 index 0000000000..cbfc5c17e7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html @@ -0,0 +1,59 @@ + + + + Test for WebRequest errors + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html new file mode 100644 index 0000000000..077708ea24 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html new file mode 100644 index 0000000000..c9d0865997 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html @@ -0,0 +1,215 @@ + + + + + + + + + + + + + +
Sample text
+ + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html new file mode 100644 index 0000000000..51ffc1e4f6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html new file mode 100644 index 0000000000..87dbbd6598 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html @@ -0,0 +1,75 @@ + + + + Bug 1450965: Skip Cors Check for Early WebExtention Redirects + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html new file mode 100644 index 0000000000..5d58549c46 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html @@ -0,0 +1,83 @@ + + + + Bug 1434357: Allow Web Request API to redirect to data: URI + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html new file mode 100644 index 0000000000..f086d29d02 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html @@ -0,0 +1,139 @@ + + + + Test for simple WebExtension + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html new file mode 100644 index 0000000000..30ecb0aa78 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html @@ -0,0 +1,265 @@ + + + + + + + + + + + + +
+ + + + +
+ +
+ + + +
+ + +
+ + +
+ + + diff --git a/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html new file mode 100644 index 0000000000..53b19d0ead --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html @@ -0,0 +1,104 @@ + + + + Test for content script + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_startup_canary.html b/toolkit/components/extensions/test/mochitest/test_startup_canary.html new file mode 100644 index 0000000000..1f705940c2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_startup_canary.html @@ -0,0 +1,76 @@ + + + + Check StartupCache + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html b/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html new file mode 100644 index 0000000000..3713243c1b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html @@ -0,0 +1,32 @@ + + + + Verify non-remote mode + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html b/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html new file mode 100644 index 0000000000..2be0e19179 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html @@ -0,0 +1,22 @@ + + + + Verify remote mode + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html b/toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html new file mode 100644 index 0000000000..5aea44b62b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html @@ -0,0 +1,24 @@ + + + + Verify WebExtension background service worker mode + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js new file mode 100644 index 0000000000..6a44fcac2e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js @@ -0,0 +1,9 @@ +"use strict"; + +/* eslint-env worker */ + +onmessage = function(event) { + fetch("https://example.com/example.txt").then(() => { + postMessage("Done!"); + }); +}; diff --git a/toolkit/components/extensions/test/mochitest/webrequest_test.jsm b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm new file mode 100644 index 0000000000..50496524fc --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm @@ -0,0 +1,20 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["webrequest_test"]; + +var webrequest_test = { + testFetch(url) { + return fetch(url); + }, + + testXHR(url) { + return new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("HEAD", url); + xhr.onload = () => { + resolve(); + }; + xhr.send(); + }); + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/webrequest_worker.js b/toolkit/components/extensions/test/mochitest/webrequest_worker.js new file mode 100644 index 0000000000..dcffd08578 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +fetch("https://example.com/example.txt"); diff --git a/toolkit/components/extensions/test/xpcshell/.eslintrc.js b/toolkit/components/extensions/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..60d784b53c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/.eslintrc.js @@ -0,0 +1,13 @@ +"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, + // Many parts of WebExtensions test definitions (e.g. content scripts) also + // interact with the browser environment, so define that here as we don't + // have an easy way to handle per-function/scope usage yet. + browser: true, + }, +}; diff --git a/toolkit/components/extensions/test/xpcshell/data/TestWorkerWatcherChild.jsm b/toolkit/components/extensions/test/xpcshell/data/TestWorkerWatcherChild.jsm new file mode 100644 index 0000000000..47120687e0 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/TestWorkerWatcherChild.jsm @@ -0,0 +1,68 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["TestWorkerWatcherChild"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "wdm", + "@mozilla.org/dom/workers/workerdebuggermanager;1", + "nsIWorkerDebuggerManager" +); + +class TestWorkerWatcherChild extends JSProcessActorChild { + async receiveMessage(msg) { + switch (msg.name) { + case "Test:StartWatchingWorkers": + this.startWatchingWorkers(); + break; + case "Test:StopWatchingWorkers": + this.stopWatchingWorkers(); + break; + default: + // Ensure the test case will fail if this JSProcessActorChild does receive + // unexpected messages. + return Promise.reject( + new Error(`Unexpected message received: ${msg.name}`) + ); + } + } + + startWatchingWorkers() { + if (!this._workerDebuggerListener) { + const actor = this; + this._workerDebuggerListener = { + onRegister(dbg) { + actor.sendAsyncMessage("Test:WorkerSpawned", { + workerType: dbg.type, + workerUrl: dbg.url, + }); + }, + onUnregister(dbg) { + actor.sendAsyncMessage("Test:WorkerTerminated", { + workerType: dbg.type, + workerUrl: dbg.url, + }); + }, + }; + + lazy.wdm.addListener(this._workerDebuggerListener); + } + } + + stopWatchingWorkers() { + if (this._workerDebuggerListener) { + lazy.wdm.removeListener(this._workerDebuggerListener); + this._workerDebuggerListener = null; + } + } + + willDestroy() { + this.stopWatchingWorkers(); + } +} diff --git a/toolkit/components/extensions/test/xpcshell/data/TestWorkerWatcherParent.jsm b/toolkit/components/extensions/test/xpcshell/data/TestWorkerWatcherParent.jsm new file mode 100644 index 0000000000..bf7836385c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/TestWorkerWatcherParent.jsm @@ -0,0 +1,24 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["TestWorkerWatcherParent"]; + +class TestWorkerWatcherParent extends JSProcessActorParent { + constructor() { + super(); + // This is set by the test helper that does use these process actors. + this.eventEmitter = null; + } + + receiveMessage(msg) { + switch (msg.name) { + case "Test:WorkerSpawned": + this.eventEmitter?.emit("worker-spawned", msg.data); + break; + case "Test:WorkerTerminated": + this.eventEmitter?.emit("worker-terminated", msg.data); + break; + default: + throw new Error(`Unexpected message received: ${msg.name}`); + } + } +} diff --git a/toolkit/components/extensions/test/xpcshell/data/dummy_page.html b/toolkit/components/extensions/test/xpcshell/data/dummy_page.html new file mode 100644 index 0000000000..c1c9a4e043 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/dummy_page.html @@ -0,0 +1,7 @@ + + + + +

Page

+ + diff --git a/toolkit/components/extensions/test/xpcshell/data/empty_file_download.txt b/toolkit/components/extensions/test/xpcshell/data/empty_file_download.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/toolkit/components/extensions/test/xpcshell/data/file download.txt b/toolkit/components/extensions/test/xpcshell/data/file download.txt new file mode 100644 index 0000000000..6293c7af79 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file download.txt @@ -0,0 +1 @@ +This is a sample file used in download tests. diff --git a/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_page2.html b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_page2.html new file mode 100644 index 0000000000..b2cf48f9e1 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_page2.html @@ -0,0 +1,25 @@ + + + + + + + + + + + +
Sample text
+ + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_original.html b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_original.html new file mode 100644 index 0000000000..f6b5142c4d --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_original.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_original.js b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_original.js new file mode 100644 index 0000000000..2981108b64 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_original.js @@ -0,0 +1,2 @@ +"use strict"; +window.testScript = "original"; diff --git a/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_redirected.html b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_redirected.html new file mode 100644 index 0000000000..0979593f7b --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_redirected.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_redirected.js b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_redirected.js new file mode 100644 index 0000000000..06fd42aa40 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_WebRequest_permission_redirected.js @@ -0,0 +1,2 @@ +"use strict"; +window.testScript = "redirected"; diff --git a/toolkit/components/extensions/test/xpcshell/data/file_content_script_errors.html b/toolkit/components/extensions/test/xpcshell/data/file_content_script_errors.html new file mode 100644 index 0000000000..da1d1c32bc --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_content_script_errors.html @@ -0,0 +1,7 @@ + + + + + + Content script errors + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_csp.html b/toolkit/components/extensions/test/xpcshell/data/file_csp.html new file mode 100644 index 0000000000..9f5cf92f5a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_csp.html @@ -0,0 +1,14 @@ + + + + + + + + +
Sample text
+ + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_csp.html^headers^ b/toolkit/components/extensions/test/xpcshell/data/file_csp.html^headers^ new file mode 100644 index 0000000000..4c6fa3c26a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_csp.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' diff --git a/toolkit/components/extensions/test/xpcshell/data/file_do_load_script_subresource.html b/toolkit/components/extensions/test/xpcshell/data/file_do_load_script_subresource.html new file mode 100644 index 0000000000..c74dec5f5a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_do_load_script_subresource.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_document_open.html b/toolkit/components/extensions/test/xpcshell/data/file_document_open.html new file mode 100644 index 0000000000..dae5e90667 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_document_open.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_document_write.html b/toolkit/components/extensions/test/xpcshell/data/file_document_write.html new file mode 100644 index 0000000000..f8369ae574 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_document_write.html @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_download.html b/toolkit/components/extensions/test/xpcshell/data/file_download.html new file mode 100644 index 0000000000..d970c63259 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_download.html @@ -0,0 +1,12 @@ + + + + + + + + +
Download HTML File
+ + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_download.txt b/toolkit/components/extensions/test/xpcshell/data/file_download.txt new file mode 100644 index 0000000000..6293c7af79 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_download.txt @@ -0,0 +1 @@ +This is a sample file used in download tests. diff --git a/toolkit/components/extensions/test/xpcshell/data/file_iframe.html b/toolkit/components/extensions/test/xpcshell/data/file_iframe.html new file mode 100644 index 0000000000..0cd68be586 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_iframe.html @@ -0,0 +1,9 @@ + + + + + Iframe document + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_image_bad.png b/toolkit/components/extensions/test/xpcshell/data/file_image_bad.png new file mode 100644 index 0000000000..4c3be50847 Binary files /dev/null and b/toolkit/components/extensions/test/xpcshell/data/file_image_bad.png differ diff --git a/toolkit/components/extensions/test/xpcshell/data/file_image_good.png b/toolkit/components/extensions/test/xpcshell/data/file_image_good.png new file mode 100644 index 0000000000..769c636340 Binary files /dev/null and b/toolkit/components/extensions/test/xpcshell/data/file_image_good.png differ diff --git a/toolkit/components/extensions/test/xpcshell/data/file_image_redirect.png b/toolkit/components/extensions/test/xpcshell/data/file_image_redirect.png new file mode 100644 index 0000000000..4c3be50847 Binary files /dev/null and b/toolkit/components/extensions/test/xpcshell/data/file_image_redirect.png differ diff --git a/toolkit/components/extensions/test/xpcshell/data/file_page_xhr.html b/toolkit/components/extensions/test/xpcshell/data/file_page_xhr.html new file mode 100644 index 0000000000..387b5285f5 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_page_xhr.html @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_permission_xhr.html b/toolkit/components/extensions/test/xpcshell/data/file_permission_xhr.html new file mode 100644 index 0000000000..6f1bb4648b --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_permission_xhr.html @@ -0,0 +1,61 @@ + + + + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_privilege_escalation.html b/toolkit/components/extensions/test/xpcshell/data/file_privilege_escalation.html new file mode 100644 index 0000000000..258f7058d9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_privilege_escalation.html @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_sample.html b/toolkit/components/extensions/test/xpcshell/data/file_sample.html new file mode 100644 index 0000000000..a20e49a1f0 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_sample.html @@ -0,0 +1,12 @@ + + + + + + + + +
Sample text
+ + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_sample_registered_styles.html b/toolkit/components/extensions/test/xpcshell/data/file_sample_registered_styles.html new file mode 100644 index 0000000000..9f5c5d5a6a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_sample_registered_styles.html @@ -0,0 +1,13 @@ + + + + + + + + +
Registered Extension URL style
+
Registered Extension Text style
+ + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_script.html b/toolkit/components/extensions/test/xpcshell/data/file_script.html new file mode 100644 index 0000000000..8d192b7d8e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_script.html @@ -0,0 +1,14 @@ + + + + + + + + + + +
Sample text
+ + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_script_bad.js b/toolkit/components/extensions/test/xpcshell/data/file_script_bad.js new file mode 100644 index 0000000000..ff4572865b --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_script_bad.js @@ -0,0 +1,12 @@ +"use strict"; + +window.failure = true; +window.addEventListener( + "load", + () => { + let el = document.createElement("div"); + el.setAttribute("id", "bad"); + document.body.appendChild(el); + }, + { once: true } +); diff --git a/toolkit/components/extensions/test/xpcshell/data/file_script_good.js b/toolkit/components/extensions/test/xpcshell/data/file_script_good.js new file mode 100644 index 0000000000..bf47fb36d2 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_script_good.js @@ -0,0 +1,12 @@ +"use strict"; + +window.success = window.success ? window.success + 1 : 1; +window.addEventListener( + "load", + () => { + let el = document.createElement("div"); + el.setAttribute("id", "good"); + document.body.appendChild(el); + }, + { once: true } +); diff --git a/toolkit/components/extensions/test/xpcshell/data/file_script_redirect.js b/toolkit/components/extensions/test/xpcshell/data/file_script_redirect.js new file mode 100644 index 0000000000..c425122c71 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_script_redirect.js @@ -0,0 +1,3 @@ +"use strict"; + +window.failure = true; diff --git a/toolkit/components/extensions/test/xpcshell/data/file_script_xhr.js b/toolkit/components/extensions/test/xpcshell/data/file_script_xhr.js new file mode 100644 index 0000000000..24a26cb8d1 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_script_xhr.js @@ -0,0 +1,9 @@ +"use strict"; + +var request = new XMLHttpRequest(); +request.open( + "get", + "http://example.com/browser/toolkit/modules/tests/browser/xhr_resource", + false +); +request.send(); diff --git a/toolkit/components/extensions/test/xpcshell/data/file_shadowdom.html b/toolkit/components/extensions/test/xpcshell/data/file_shadowdom.html new file mode 100644 index 0000000000..c4e7db14e7 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_shadowdom.html @@ -0,0 +1,13 @@ + + + + + + +
host
+ + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_style_bad.css b/toolkit/components/extensions/test/xpcshell/data/file_style_bad.css new file mode 100644 index 0000000000..8dbc8dc7a4 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_style_bad.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/components/extensions/test/xpcshell/data/file_style_good.css b/toolkit/components/extensions/test/xpcshell/data/file_style_good.css new file mode 100644 index 0000000000..46f9774b5f --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_style_good.css @@ -0,0 +1,3 @@ +#test { + color: red; +} diff --git a/toolkit/components/extensions/test/xpcshell/data/file_style_redirect.css b/toolkit/components/extensions/test/xpcshell/data/file_style_redirect.css new file mode 100644 index 0000000000..8dbc8dc7a4 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_style_redirect.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache.css b/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache.css new file mode 100644 index 0000000000..6a9140d97e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache.css @@ -0,0 +1 @@ +:root { color: green; } diff --git a/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache.html b/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache.html new file mode 100644 index 0000000000..6d6d187a27 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache.html @@ -0,0 +1,3 @@ + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache_2.html b/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache_2.html new file mode 100644 index 0000000000..07a4324c44 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_stylesheet_cache_2.html @@ -0,0 +1,19 @@ + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_toplevel.html b/toolkit/components/extensions/test/xpcshell/data/file_toplevel.html new file mode 100644 index 0000000000..d93813d0f5 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_toplevel.html @@ -0,0 +1,12 @@ + + + + + Top-level frame document + + + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_with_iframe.html b/toolkit/components/extensions/test/xpcshell/data/file_with_iframe.html new file mode 100644 index 0000000000..705350d55c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_with_iframe.html @@ -0,0 +1,11 @@ + + + + + file with iframe + + +
+ + + diff --git a/toolkit/components/extensions/test/xpcshell/data/file_with_xorigin_frame.html b/toolkit/components/extensions/test/xpcshell/data/file_with_xorigin_frame.html new file mode 100644 index 0000000000..199c2ce4d4 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_with_xorigin_frame.html @@ -0,0 +1,10 @@ + + + + + Document with example.org frame + + + + + diff --git a/toolkit/components/extensions/test/xpcshell/data/lorem.html.gz b/toolkit/components/extensions/test/xpcshell/data/lorem.html.gz new file mode 100644 index 0000000000..9eb8d73d50 Binary files /dev/null and b/toolkit/components/extensions/test/xpcshell/data/lorem.html.gz differ diff --git a/toolkit/components/extensions/test/xpcshell/data/pixel_green.gif b/toolkit/components/extensions/test/xpcshell/data/pixel_green.gif new file mode 100644 index 0000000000..baf8166dae Binary files /dev/null and b/toolkit/components/extensions/test/xpcshell/data/pixel_green.gif differ diff --git a/toolkit/components/extensions/test/xpcshell/data/pixel_red.gif b/toolkit/components/extensions/test/xpcshell/data/pixel_red.gif new file mode 100644 index 0000000000..48f97f74bd Binary files /dev/null and b/toolkit/components/extensions/test/xpcshell/data/pixel_red.gif differ diff --git a/toolkit/components/extensions/test/xpcshell/head.js b/toolkit/components/extensions/test/xpcshell/head.js new file mode 100644 index 0000000000..14d8b74b66 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head.js @@ -0,0 +1,353 @@ +"use strict"; +/* exported createHttpServer, cleanupDir, clearCache, optionalPermissionsPromptHandler, promiseConsoleOutput, + promiseQuotaManagerServiceReset, promiseQuotaManagerServiceClear, + runWithPrefs, testEnv, withHandlingUserInput, resetHandlingUserInput, + assertPersistentListeners, promiseExtensionEvent, assertHasPersistedScriptsCachedFlag, + assertIsPersistedScriptsCachedFlag +*/ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { + clearInterval, + clearTimeout, + setInterval, + setIntervalWithTarget, + setTimeout, + setTimeoutWithTarget, +} = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs"); +var { AddonTestUtils, MockAsyncShutdown } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); + +ChromeUtils.defineESModuleGetters(this, { + ContentTask: "resource://testing-common/ContentTask.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + PromiseTestUtils: "resource://testing-common/PromiseTestUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + Extension: "resource://gre/modules/Extension.jsm", + ExtensionData: "resource://gre/modules/Extension.jsm", + ExtensionParent: "resource://gre/modules/ExtensionParent.jsm", + ExtensionTestUtils: "resource://testing-common/ExtensionXPCShellUtils.jsm", + MessageChannel: "resource://testing-common/MessageChannel.jsm", + NetUtil: "resource://gre/modules/NetUtil.jsm", + Schemas: "resource://gre/modules/Schemas.jsm", +}); + +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); + +// Persistent Listener test functionality +const { assertPersistentListeners } = ExtensionTestUtils.testAssertions; + +// https_first automatically upgrades http to https, but the tests are not +// designed to expect that. And it is not easy to change that because +// nsHttpServer does not support https (bug 1742061). So disable https_first. +Services.prefs.setBoolPref("dom.security.https_first", false); + +// These values may be changed in later head files and tested in check_remote +// below. +Services.prefs.setBoolPref("browser.tabs.remote.autostart", false); +Services.prefs.setBoolPref("extensions.webextensions.remote", false); +const testEnv = { + expectRemote: false, +}; + +add_setup(function check_remote() { + Assert.equal( + WebExtensionPolicy.useRemoteWebExtensions, + testEnv.expectRemote, + "useRemoteWebExtensions matches" + ); + Assert.equal( + WebExtensionPolicy.isExtensionProcess, + !testEnv.expectRemote, + "testing from extension process" + ); +}); + +ExtensionTestUtils.init(this); + +var createHttpServer = (...args) => { + AddonTestUtils.maybeInit(this); + return AddonTestUtils.createHttpServer(...args); +}; + +if (AppConstants.platform === "android") { + Services.io.offline = true; +} + +/** + * Clears the HTTP and content image caches. + */ +function clearCache() { + Services.cache2.clear(); + + let imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(null); + imageCache.clearCache(false); +} + +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); + void (msg instanceof Ci.nsIScriptError); + 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); + } +}; + +// Attempt to remove a directory. If the Windows OS is still using the +// file sometimes remove() will fail. So try repeatedly until we can +// remove it or we give up. +function cleanupDir(dir) { + let count = 0; + return new Promise((resolve, reject) => { + function tryToRemoveDir() { + count += 1; + try { + dir.remove(true); + } catch (e) { + // ignore + } + if (!dir.exists()) { + return resolve(); + } + if (count >= 25) { + return reject(`Failed to cleanup directory: ${dir}`); + } + setTimeout(tryToRemoveDir, 100); + } + tryToRemoveDir(); + }); +} + +// Run a test with the specified preferences and then restores their initial values +// right after the test function run (whether it passes or fails). +async function runWithPrefs(prefsToSet, testFn) { + const setPrefs = prefs => { + for (let [pref, value] of prefs) { + if (value === undefined) { + // Clear any pref that didn't have a user value. + info(`Clearing pref "${pref}"`); + Services.prefs.clearUserPref(pref); + continue; + } + + info(`Setting pref "${pref}": ${value}`); + switch (typeof value) { + case "boolean": + Services.prefs.setBoolPref(pref, value); + break; + case "number": + Services.prefs.setIntPref(pref, value); + break; + case "string": + Services.prefs.setStringPref(pref, value); + break; + default: + throw new Error("runWithPrefs doesn't support this pref type yet"); + } + } + }; + + const getPrefs = prefs => { + return prefs.map(([pref, value]) => { + info(`Getting initial pref value for "${pref}"`); + if (!Services.prefs.prefHasUserValue(pref)) { + // Check if the pref doesn't have a user value. + return [pref, undefined]; + } + switch (typeof value) { + case "boolean": + return [pref, Services.prefs.getBoolPref(pref)]; + case "number": + return [pref, Services.prefs.getIntPref(pref)]; + case "string": + return [pref, Services.prefs.getStringPref(pref)]; + default: + throw new Error("runWithPrefs doesn't support this pref type yet"); + } + }); + }; + + let initialPrefsValues = []; + + try { + initialPrefsValues = getPrefs(prefsToSet); + + setPrefs(prefsToSet); + + await testFn(); + } finally { + info("Restoring initial preferences values on exit"); + setPrefs(initialPrefsValues); + } +} + +// "Handling User Input" test helpers. + +let extensionHandlers = new WeakSet(); + +function handlingUserInputFrameScript() { + /* globals content */ + // eslint-disable-next-line no-shadow + const { MessageChannel } = ChromeUtils.import( + "resource://testing-common/MessageChannel.jsm" + ); + + let handle; + MessageChannel.addListener(this, "ExtensionTest:HandleUserInput", { + receiveMessage({ name, data }) { + if (data) { + handle = content.windowUtils.setHandlingUserInput(true); + } else if (handle) { + handle.destruct(); + handle = null; + } + }, + }); +} + +// If you use withHandlingUserInput then restart the addon manager, +// you need to reset this before using withHandlingUserInput again. +function resetHandlingUserInput() { + extensionHandlers = new WeakSet(); +} + +async function withHandlingUserInput(extension, fn) { + let { messageManager } = extension.extension.groupFrameLoader; + + if (!extensionHandlers.has(extension)) { + messageManager.loadFrameScript( + `data:,(${encodeURI(handlingUserInputFrameScript)}).call(this)`, + false, + true + ); + extensionHandlers.add(extension); + } + + await MessageChannel.sendMessage( + messageManager, + "ExtensionTest:HandleUserInput", + true + ); + await fn(); + await MessageChannel.sendMessage( + messageManager, + "ExtensionTest:HandleUserInput", + false + ); +} + +// QuotaManagerService test helpers. + +function promiseQuotaManagerServiceReset() { + info("Calling QuotaManagerService.reset to enforce new test storage limits"); + return new Promise(resolve => { + Services.qms.reset().callback = resolve; + }); +} + +function promiseQuotaManagerServiceClear() { + info( + "Calling QuotaManagerService.clear to empty the test data and refresh test storage limits" + ); + return new Promise(resolve => { + Services.qms.clear().callback = resolve; + }); +} + +// Optional Permission prompt handling +const optionalPermissionsPromptHandler = { + sawPrompt: false, + acceptPrompt: false, + + init() { + Services.prefs.setBoolPref( + "extensions.webextOptionalPermissionPrompts", + true + ); + Services.obs.addObserver(this, "webextension-optional-permission-prompt"); + registerCleanupFunction(() => { + Services.obs.removeObserver( + this, + "webextension-optional-permission-prompt" + ); + Services.prefs.clearUserPref( + "extensions.webextOptionalPermissionPrompts" + ); + }); + }, + + observe(subject, topic, data) { + if (topic == "webextension-optional-permission-prompt") { + this.sawPrompt = true; + let { resolve } = subject.wrappedJSObject; + resolve(this.acceptPrompt); + } + }, +}; + +function promiseExtensionEvent(wrapper, event) { + return new Promise(resolve => { + wrapper.extension.once(event, (...args) => resolve(args)); + }); +} + +async function assertHasPersistedScriptsCachedFlag(ext) { + const { StartupCache } = ExtensionParent; + const allCachedGeneral = StartupCache._data.get("general"); + equal( + allCachedGeneral + .get(ext.id) + ?.get(ext.version) + ?.get("scripting") + ?.has("hasPersistedScripts"), + true, + "Expect the StartupCache to include hasPersistedScripts flag" + ); +} + +async function assertIsPersistentScriptsCachedFlag(ext, expectedValue) { + const { StartupCache } = ExtensionParent; + const allCachedGeneral = StartupCache._data.get("general"); + equal( + allCachedGeneral + .get(ext.id) + ?.get(ext.version) + ?.get("scripting") + ?.get("hasPersistedScripts"), + expectedValue, + "Expected cached value set on hasPersistedScripts flag" + ); +} diff --git a/toolkit/components/extensions/test/xpcshell/head_dnr.js b/toolkit/components/extensions/test/xpcshell/head_dnr.js new file mode 100644 index 0000000000..c6856fde4a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_dnr.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* exported assertDNRStoreData, getDNRRule, getSchemaNormalizedRule, getSchemaNormalizedRules + */ + +XPCOMUtils.defineLazyModuleGetters(this, { + Schemas: "resource://gre/modules/Schemas.jsm", +}); + +function getDNRRule({ + id = 1, + priority = 1, + action = {}, + condition = {}, +} = {}) { + return { + id, + priority, + action: { + type: "block", + ...action, + }, + condition: { + ...condition, + }, + }; +} + +const getSchemaNormalizedRule = (extensionTestWrapper, value) => { + const { extension } = extensionTestWrapper; + const validationContext = { + url: extension.baseURI.spec, + principal: extension.principal, + logError: err => { + // We don't expect this test helper function to be called on invalid rules, + // and so we trigger an explicit test failure if we ever hit any. + Assert.ok( + false, + `Unexpected logError on normalizing DNR rule ${JSON.stringify( + value + )} - ${err}` + ); + }, + preprocessors: {}, + manifestVersion: extension.manifestVersion, + }; + + return Schemas.normalize( + value, + "declarativeNetRequest.Rule", + validationContext + ); +}; + +const getSchemaNormalizedRules = (extensionTestWrapper, rules) => { + return rules.map(rule => { + const normalized = getSchemaNormalizedRule(extensionTestWrapper, rule); + if (normalized.error) { + throw new Error( + `Unexpected DNR Rule normalization error: ${normalized.error}` + ); + } + return normalized.value; + }); +}; + +const assertDNRStoreData = async ( + dnrStore, + extensionTestWrapper, + expectedRulesets, + { assertIndividualRules = true } = {} +) => { + const extUUID = extensionTestWrapper.uuid; + const rule_resources = + extensionTestWrapper.extension.manifest.declarative_net_request + ?.rule_resources; + const expectedRulesetIds = Array.from(Object.keys(expectedRulesets)); + const expectedRulesetIndexesMap = expectedRulesetIds.reduce((acc, rsId) => { + acc.set( + rsId, + rule_resources.findIndex(rr => rr.id === rsId) + ); + return acc; + }, new Map()); + + ok( + dnrStore._dataPromises.has(extUUID), + "Got promise for the test extension DNR data being loaded" + ); + + await dnrStore._dataPromises.get(extUUID); + + ok(dnrStore._data.has(extUUID), "Got data for the test extension"); + + const dnrExtData = dnrStore._data.get(extUUID); + Assert.deepEqual( + { + schemaVersion: dnrExtData.schemaVersion, + extVersion: dnrExtData.extVersion, + }, + { + schemaVersion: dnrExtData.constructor.VERSION, + extVersion: extensionTestWrapper.extension.version, + }, + "Got the expected data schema version and extension version in the store data" + ); + Assert.deepEqual( + Array.from(dnrExtData.staticRulesets.keys()), + expectedRulesetIds, + "Got the enabled rulesets in the stored data staticRulesets Map" + ); + + for (const rulesetId of expectedRulesetIds) { + const expectedRulesetIdx = expectedRulesetIndexesMap.get(rulesetId); + const expectedRulesetRules = getSchemaNormalizedRules( + extensionTestWrapper, + expectedRulesets[rulesetId] + ); + const actualData = dnrExtData.staticRulesets.get(rulesetId); + equal( + actualData.idx, + expectedRulesetIdx, + `Got the expected ruleset index for ruleset id ${rulesetId}` + ); + + // Asserting an entire array of rules all at once will produce + // a big enough output to don't be immediately useful to investigate + // failures, asserting each rule individually would produce more + // readable assertion failure logs. + const assertRuleAtIdx = ruleIdx => + Assert.deepEqual( + actualData.rules[ruleIdx], + expectedRulesetRules[ruleIdx], + `Got the expected rule at index ${ruleIdx} for ruleset id "${rulesetId}"` + ); + + // Some tests may be using a big enough number of rules that + // the assertiongs would be producing a huge amount of log spam, + // and so for those tests we only explicitly assert the first + // and last rule and that the total amount of rules matches the + // expected number of rules (there are still other tests explicitly + // asserting all loaded rules). + if (assertIndividualRules) { + info( + `Verify the each individual rule loaded for ruleset id "${rulesetId}"` + ); + for (let ruleIdx = 0; ruleIdx < expectedRulesetRules.length; ruleIdx++) { + assertRuleAtIdx(ruleIdx); + } + } else { + // NOTE: Only asserting the first and last rule also helps to speed up + // the test is some slower builds when the number of expected rules is + // big enough (e.g. the test task verifying the enforced rule count limits + // was timing out in tsan build because asserting all indidual rules was + // taking long enough and the event page was being suspended on the idle + // timeout by the time we did run all these assertion and proceeding with + // the rest of the test task assertions), we still confirm that the total + // number of expected vs actual rules also matches right after these + // assertions. + info( + `Verify the first and last rules loaded for ruleset id "${rulesetId}"` + ); + const lastExpectedRuleIdx = expectedRulesetRules.length - 1; + for (const ruleIdx of [0, lastExpectedRuleIdx]) { + assertRuleAtIdx(ruleIdx); + } + } + + equal( + actualData.rules.length, + expectedRulesetRules.length, + `Got the expected number of rules loaded for ruleset id "${rulesetId}"` + ); + } +}; diff --git a/toolkit/components/extensions/test/xpcshell/head_e10s.js b/toolkit/components/extensions/test/xpcshell/head_e10s.js new file mode 100644 index 0000000000..196afae7c9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_e10s.js @@ -0,0 +1,8 @@ +"use strict"; + +/* globals ExtensionTestUtils */ + +// xpcshell disables e10s by default. Turn it on. +Services.prefs.setBoolPref("browser.tabs.remote.autostart", true); + +ExtensionTestUtils.remoteContentScripts = true; diff --git a/toolkit/components/extensions/test/xpcshell/head_legacy_ep.js b/toolkit/components/extensions/test/xpcshell/head_legacy_ep.js new file mode 100644 index 0000000000..01f16ec54c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_legacy_ep.js @@ -0,0 +1,13 @@ +"use strict"; + +// Bug 1646182: Test the legacy ExtensionPermission backend until we fully +// migrate to rkv + +{ + const { ExtensionPermissions } = ChromeUtils.import( + "resource://gre/modules/ExtensionPermissions.jsm" + ); + + ExtensionPermissions._useLegacyStorageBackend = true; + ExtensionPermissions._uninit(); +} diff --git a/toolkit/components/extensions/test/xpcshell/head_native_messaging.js b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js new file mode 100644 index 0000000000..c9e507ec79 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_native_messaging.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"; + +/* globals AppConstants, FileUtils */ +/* exported getSubprocessCount, setupHosts, waitForSubprocessExit */ + +ChromeUtils.defineESModuleGetters(this, { + MockRegistry: "resource://testing-common/MockRegistry.sys.mjs", +}); +ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); +if (AppConstants.platform == "win") { + ChromeUtils.defineESModuleGetters(this, { + SubprocessImpl: "resource://gre/modules/subprocess/subprocess_win.sys.mjs", + }); +} else { + ChromeUtils.defineESModuleGetters(this, { + SubprocessImpl: "resource://gre/modules/subprocess/subprocess_unix.sys.mjs", + }); +} + +const { Subprocess } = ChromeUtils.importESModule( + "resource://gre/modules/Subprocess.sys.mjs" +); + +// It's important that we use a space in this directory name to make sure we +// correctly handle executing batch files with spaces in their path. +let tmpDir = FileUtils.getDir("TmpD", ["Native Messaging"]); +tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +const TYPE_SLUG = + AppConstants.platform === "linux" + ? "native-messaging-hosts" + : "NativeMessagingHosts"; +OS.File.makeDir(OS.Path.join(tmpDir.path, TYPE_SLUG)); + +registerCleanupFunction(() => { + tmpDir.remove(true); +}); + +function getPath(filename) { + return OS.Path.join(tmpDir.path, TYPE_SLUG, filename); +} + +const ID = "native@tests.mozilla.org"; + +async function setupHosts(scripts) { + const PERMS = { unixMode: 0o755 }; + + const pythonPath = await Subprocess.pathSearch(Services.env.get("PYTHON")); + + async function writeManifest(script, scriptPath, path) { + let body = `#!${pythonPath} -u\n${script.script}`; + + await OS.File.writeAtomic(scriptPath, body); + await OS.File.setPermissions(scriptPath, PERMS); + + let manifest = { + name: script.name, + description: script.description, + path, + type: "stdio", + allowed_extensions: [ID], + }; + + // Optionally, allow the test to change the manifest before writing. + script._hookModifyManifest?.(manifest); + + let manifestPath = getPath(`${script.name}.json`); + await OS.File.writeAtomic(manifestPath, JSON.stringify(manifest)); + + return manifestPath; + } + + switch (AppConstants.platform) { + case "macosx": + case "linux": + let dirProvider = { + getFile(property) { + if (property == "XREUserNativeManifests") { + return tmpDir.clone(); + } else if (property == "XRESysNativeManifests") { + return tmpDir.clone(); + } + return null; + }, + }; + + Services.dirsvc.registerProvider(dirProvider); + registerCleanupFunction(() => { + Services.dirsvc.unregisterProvider(dirProvider); + }); + + for (let script of scripts) { + let path = getPath(`${script.name}.py`); + + await writeManifest(script, path, path); + } + break; + + case "win": + const REGKEY = String.raw`Software\Mozilla\NativeMessagingHosts`; + + let registry = new MockRegistry(); + registerCleanupFunction(() => { + registry.shutdown(); + }); + + for (let script of scripts) { + let { scriptExtension = "bat" } = script; + + // It's important that we use a space in this filename. See directory + // name comment above. + let batPath = getPath(`batch ${script.name}.${scriptExtension}`); + let scriptPath = getPath(`${script.name}.py`); + + let batBody = `@ECHO OFF\n${pythonPath} -u "${scriptPath}" %*\n`; + await OS.File.writeAtomic(batPath, batBody); + + let manifestPath = await writeManifest(script, scriptPath, batPath); + + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + `${REGKEY}\\${script.name}`, + "", + manifestPath + ); + } + break; + + default: + ok( + false, + `Native messaging is not supported on ${AppConstants.platform}` + ); + } +} + +function getSubprocessCount() { + return SubprocessImpl.Process.getWorker() + .call("getProcesses", []) + .then(result => result.size); +} +function waitForSubprocessExit() { + return SubprocessImpl.Process.getWorker() + .call("waitForNoProcesses", []) + .then(() => { + // Return to the main event loop to give IO handlers enough time to consume + // their remaining buffered input. + return new Promise(resolve => setTimeout(resolve, 0)); + }); +} diff --git a/toolkit/components/extensions/test/xpcshell/head_remote.js b/toolkit/components/extensions/test/xpcshell/head_remote.js new file mode 100644 index 0000000000..f9c31144c9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_remote.js @@ -0,0 +1,7 @@ +"use strict"; + +Services.prefs.setBoolPref("extensions.webextensions.remote", true); +Services.prefs.setIntPref("dom.ipc.keepProcessesAlive.extension", 1); + +/* globals testEnv */ +testEnv.expectRemote = true; // tested in head_test.js diff --git a/toolkit/components/extensions/test/xpcshell/head_schemas.js b/toolkit/components/extensions/test/xpcshell/head_schemas.js new file mode 100644 index 0000000000..aeba2011fc --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_schemas.js @@ -0,0 +1,127 @@ +"use strict"; + +/* exported Schemas, LocalAPIImplementation, SchemaAPIInterface, getContextWrapper */ + +const { Schemas } = ChromeUtils.import("resource://gre/modules/Schemas.jsm"); + +const { ExtensionCommon } = ChromeUtils.import( + "resource://gre/modules/ExtensionCommon.jsm" +); + +let { LocalAPIImplementation, SchemaAPIInterface } = ExtensionCommon; + +const contextCloneScope = this; + +class TallyingAPIImplementation extends SchemaAPIInterface { + constructor(context, namespace, name) { + super(); + this.namespace = namespace; + this.name = name; + this.context = context; + } + + callFunction(args) { + this.context.tally("call", this.namespace, this.name, args); + if (this.name === "sub_foo") { + return 13; + } + } + + callFunctionNoReturn(args) { + this.context.tally("call", this.namespace, this.name, args); + } + + getProperty() { + this.context.tally("get", this.namespace, this.name); + } + + setProperty(value) { + this.context.tally("set", this.namespace, this.name, value); + } + + addListener(listener, args) { + this.context.tally("addListener", this.namespace, this.name, [ + listener, + args, + ]); + } + + removeListener(listener) { + this.context.tally("removeListener", this.namespace, this.name, [listener]); + } + + hasListener(listener) { + this.context.tally("hasListener", this.namespace, this.name, [listener]); + } +} + +function getContextWrapper(manifestVersion = 2) { + return { + url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/", + + cloneScope: contextCloneScope, + + manifestVersion, + + permissions: new Set(), + tallied: null, + talliedErrors: [], + + tally(kind, ns, name, args) { + this.tallied = [kind, ns, name, args]; + }, + + verify(...args) { + Assert.equal(JSON.stringify(this.tallied), JSON.stringify(args)); + this.tallied = null; + }, + + checkErrors(errors) { + let { talliedErrors } = this; + Assert.equal( + talliedErrors.length, + errors.length, + "Got expected number of errors" + ); + for (let [i, error] of errors.entries()) { + Assert.ok( + i in talliedErrors && String(talliedErrors[i]).includes(error), + `${JSON.stringify(error)} is a substring of error ${JSON.stringify( + talliedErrors[i] + )}` + ); + } + + talliedErrors.length = 0; + }, + + checkLoadURL(url) { + return !url.startsWith("chrome:"); + }, + + preprocessors: { + localize(value, context) { + return value.replace( + /__MSG_(.*?)__/g, + (m0, m1) => `${m1.toUpperCase()}` + ); + }, + }, + + logError(message) { + this.talliedErrors.push(message); + }, + + hasPermission(permission) { + return this.permissions.has(permission); + }, + + shouldInject(ns, name, allowedContexts) { + return name != "do-not-inject"; + }, + + getImplementation(namespace, name) { + return new TallyingAPIImplementation(this, namespace, name); + }, + }; +} diff --git a/toolkit/components/extensions/test/xpcshell/head_service_worker.js b/toolkit/components/extensions/test/xpcshell/head_service_worker.js new file mode 100644 index 0000000000..cefd26f6af --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_service_worker.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"; + +/* exported TestWorkerWatcher */ + +XPCOMUtils.defineLazyModuleGetters(this, { + ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm", +}); + +// Ensure that the profile-after-change message has been notified, +// so that ServiceWokerRegistrar is going to be initialized, +// otherwise tests using a background service worker will fail. +// in debug builds because of an assertion failure triggered +// by ServiceWorkerRegistrar.cpp (due to not being initialized +// automatically on startup as in a real Firefox instance). +Services.obs.notifyObservers( + null, + "profile-after-change", + "force-serviceworkerrestart-init" +); + +// A test utility class used in the test case to watch for a given extension +// service worker being spawned and terminated (using the same kind of Firefox DevTools +// internals that about:debugging is using to watch the workers activity). +// +// NOTE: this helper class does also depends from the two jsm files where the +// Parent and Child TestWorkerWatcher actor is defined: +// +// - data/TestWorkerWatcherParent.jsm +// - data/TestWorkerWatcherChild.jsm +class TestWorkerWatcher extends ExtensionCommon.EventEmitter { + JS_ACTOR_NAME = "TestWorkerWatcher"; + + constructor(dataRelPath = "./data") { + super(); + this.dataRelPath = dataRelPath; + this.extensionProcess = null; + this.extensionProcessActor = null; + this.registerProcessActor(); + this.getAndWatchExtensionProcess(); + // Observer child process creation and shutdown if the extension + // are meant to run in a child process. + Services.obs.addObserver(this, "ipc:content-created"); + Services.obs.addObserver(this, "ipc:content-shutdown"); + } + + async destroy() { + await this.stopWatchingWorkers(); + ChromeUtils.unregisterProcessActor(this.JS_ACTOR_NAME); + } + + get swm() { + return Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + } + + getRegistration(extension) { + return this.swm.getRegistrationByPrincipal( + extension.extension.principal, + extension.extension.principal.spec + ); + } + + watchExtensionServiceWorker(extension) { + // These events are emitted by TestWatchExtensionWorkersParent. + const promiseWorkerSpawned = this.waitForEvent("worker-spawned", extension); + const promiseWorkerTerminated = this.waitForEvent( + "worker-terminated", + extension + ); + + // Terminate the worker sooner by settng the idle_timeout to 0, + // then clear the pref as soon as the worker has been terminated. + const terminate = () => { + promiseWorkerTerminated.then(() => { + Services.prefs.clearUserPref("dom.serviceWorkers.idle_timeout"); + }); + Services.prefs.setIntPref("dom.serviceWorkers.idle_timeout", 0); + const swReg = this.getRegistration(extension); + // If the active worker is already active, we have to make sure the new value + // set on the idle_timeout pref is picked up by ServiceWorkerPrivate::ResetIdleTimeout. + swReg.activeWorker?.attachDebugger(); + swReg.activeWorker?.detachDebugger(); + return promiseWorkerTerminated; + }; + + return { + promiseWorkerSpawned, + promiseWorkerTerminated, + terminate, + }; + } + + // Methods only used internally. + + waitForEvent(event, extension) { + return new Promise(resolve => { + const listener = (_eventName, data) => { + if (!data.workerUrl.startsWith(extension.extension?.principal.spec)) { + return; + } + this.off(event, listener); + resolve(data); + }; + + this.on(event, listener); + }); + } + + registerProcessActor() { + const { JS_ACTOR_NAME } = this; + ChromeUtils.registerProcessActor(JS_ACTOR_NAME, { + parent: { + moduleURI: `resource://testing-common/${JS_ACTOR_NAME}Parent.jsm`, + }, + child: { + moduleURI: `resource://testing-common/${JS_ACTOR_NAME}Child.jsm`, + }, + }); + } + + startWatchingWorkers() { + if (!this.extensionProcessActor) { + return; + } + this.extensionProcessActor.eventEmitter = this; + return this.extensionProcessActor.sendQuery("Test:StartWatchingWorkers"); + } + + stopWatchingWorkers() { + if (!this.extensionProcessActor) { + return; + } + this.extensionProcessActor.eventEmitter = null; + return this.extensionProcessActor.sendQuery("Test:StopWatchingWorkers"); + } + + getAndWatchExtensionProcess() { + const extensionProcess = ChromeUtils.getAllDOMProcesses().find(p => { + return p.remoteType === "extension"; + }); + if (extensionProcess !== this.extensionProcess) { + this.extensionProcess = extensionProcess; + this.extensionProcessActor = extensionProcess + ? extensionProcess.getActor(this.JS_ACTOR_NAME) + : null; + this.startWatchingWorkers(); + } + } + + observe(subject, topic, childIDString) { + // Keep the watched process and related test child process actor updated + // when a process is created or destroyed. + this.getAndWatchExtensionProcess(); + } +} diff --git a/toolkit/components/extensions/test/xpcshell/head_storage.js b/toolkit/components/extensions/test/xpcshell/head_storage.js new file mode 100644 index 0000000000..dca0780367 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_storage.js @@ -0,0 +1,1330 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* import-globals-from head.js */ + +const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled"; + +// Test implementations and utility functions that are used against multiple +// storage areas (eg, a test which is run against browser.storage.local and +// browser.storage.sync, or a test against browser.storage.sync but needs to +// be run against both the kinto and rust implementations.) + +/** + * Utility function to ensure that all supported APIs for getting are + * tested. + * + * @param {string} areaName + * either "local" or "sync" according to what we want to test + * @param {string} prop + * "key" to look up using the storage API + * @param {object} value + * "value" to compare against + */ +async function checkGetImpl(areaName, prop, value) { + let storage = browser.storage[areaName]; + + let data = await storage.get(); + browser.test.assertEq( + value, + data[prop], + `unspecified getter worked for ${prop} in ${areaName}` + ); + + data = await storage.get(null); + browser.test.assertEq( + value, + data[prop], + `null getter worked for ${prop} in ${areaName}` + ); + + data = await storage.get(prop); + browser.test.assertEq( + value, + data[prop], + `string getter worked for ${prop} in ${areaName}` + ); + browser.test.assertEq( + Object.keys(data).length, + 1, + `string getter should return an object with a single property` + ); + + data = await storage.get([prop]); + browser.test.assertEq( + value, + data[prop], + `array getter worked for ${prop} in ${areaName}` + ); + browser.test.assertEq( + Object.keys(data).length, + 1, + `array getter with a single key should return an object with a single property` + ); + + data = await storage.get({ [prop]: undefined }); + browser.test.assertEq( + value, + data[prop], + `object getter worked for ${prop} in ${areaName}` + ); + browser.test.assertEq( + Object.keys(data).length, + 1, + `object getter with a single key should return an object with a single property` + ); +} + +function test_config_flag_needed() { + async function testFn() { + function background() { + let promises = []; + let apiTests = [ + { method: "get", args: ["foo"] }, + { method: "set", args: [{ foo: "bar" }] }, + { method: "remove", args: ["foo"] }, + { method: "clear", args: [] }, + ]; + apiTests.forEach(testDef => { + promises.push( + browser.test.assertRejects( + browser.storage.sync[testDef.method](...testDef.args), + "Please set webextensions.storage.sync.enabled to true in about:config", + `storage.sync.${testDef.method} is behind a flag` + ) + ); + }); + + Promise.all(promises).then(() => browser.test.notifyPass("flag needed")); + } + + ok( + !Services.prefs.getBoolPref(STORAGE_SYNC_PREF, false), + "The `${STORAGE_SYNC_PREF}` should be set to false" + ); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["storage"], + }, + background, + }); + + await extension.startup(); + await extension.awaitFinish("flag needed"); + await extension.unload(); + } + + return runWithPrefs([[STORAGE_SYNC_PREF, false]], testFn); +} + +function test_sync_reloading_extensions_works() { + async function testFn() { + // Just some random extension ID that we can re-use + const extensionId = "my-extension-id@1"; + + function loadExtension() { + async function background() { + browser.test.sendMessage( + "initialItems", + await browser.storage.sync.get(null) + ); + await browser.storage.sync.set({ a: "b" }); + browser.test.notifyPass("set-works"); + } + + return ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: extensionId } }, + permissions: ["storage"], + }, + background: `(${background})()`, + }); + } + + ok( + Services.prefs.getBoolPref(STORAGE_SYNC_PREF, false), + "The `${STORAGE_SYNC_PREF}` should be set to true" + ); + + let extension1 = loadExtension(); + + await extension1.startup(); + + Assert.deepEqual( + await extension1.awaitMessage("initialItems"), + {}, + "No stored items at first" + ); + + await extension1.awaitFinish("set-works"); + await extension1.unload(); + + let extension2 = loadExtension(); + + await extension2.startup(); + + Assert.deepEqual( + await extension2.awaitMessage("initialItems"), + { a: "b" }, + "Stored items available after restart" + ); + + await extension2.awaitFinish("set-works"); + await extension2.unload(); + } + + return runWithPrefs([[STORAGE_SYNC_PREF, true]], testFn); +} + +async function test_background_page_storage(testAreaName) { + async function backgroundScript(checkGet) { + let globalChanges, gResolve; + function clearGlobalChanges() { + globalChanges = new Promise(resolve => { + gResolve = resolve; + }); + } + clearGlobalChanges(); + let expectedAreaName; + + browser.storage.onChanged.addListener((changes, areaName) => { + browser.test.assertEq( + expectedAreaName, + areaName, + "Expected area name received by listener" + ); + gResolve(changes); + }); + + async function checkChanges(areaName, changes, message) { + function checkSub(obj1, obj2) { + for (let prop in obj1) { + browser.test.assertTrue( + obj1[prop] !== undefined, + `checkChanges ${areaName} ${prop} is missing (${message})` + ); + browser.test.assertTrue( + obj2[prop] !== undefined, + `checkChanges ${areaName} ${prop} is missing (${message})` + ); + browser.test.assertEq( + obj1[prop].oldValue, + obj2[prop].oldValue, + `checkChanges ${areaName} ${prop} old (${message})` + ); + browser.test.assertEq( + obj1[prop].newValue, + obj2[prop].newValue, + `checkChanges ${areaName} ${prop} new (${message})` + ); + } + } + + const recentChanges = await globalChanges; + checkSub(changes, recentChanges); + checkSub(recentChanges, changes); + clearGlobalChanges(); + } + + // Regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=1645598 + async function testNonExistingKeys(storage, storageAreaDesc) { + let data = await storage.get({ test6: 6 }); + browser.test.assertEq( + `{"test6":6}`, + JSON.stringify(data), + `Use default value when not stored for ${storageAreaDesc}` + ); + + data = await storage.get({ test6: null }); + browser.test.assertEq( + `{"test6":null}`, + JSON.stringify(data), + `Use default value, even if null for ${storageAreaDesc}` + ); + + data = await storage.get("test6"); + browser.test.assertEq( + `{}`, + JSON.stringify(data), + `Empty result if key is not found for ${storageAreaDesc}` + ); + + data = await storage.get(["test6", "test7"]); + browser.test.assertEq( + `{}`, + JSON.stringify(data), + `Empty result if list of keys is not found for ${storageAreaDesc}` + ); + } + + async function testFalseyValues(areaName) { + let storage = browser.storage[areaName]; + const dataInitial = { + "test-falsey-value-bool": false, + "test-falsey-value-string": "", + "test-falsey-value-number": 0, + }; + const dataUpdate = { + "test-falsey-value-bool": true, + "test-falsey-value-string": "non-empty-string", + "test-falsey-value-number": 10, + }; + + // Compute the expected changes. + const onSetInitial = { + "test-falsey-value-bool": { newValue: false }, + "test-falsey-value-string": { newValue: "" }, + "test-falsey-value-number": { newValue: 0 }, + }; + const onRemovedFalsey = { + "test-falsey-value-bool": { oldValue: false }, + "test-falsey-value-string": { oldValue: "" }, + "test-falsey-value-number": { oldValue: 0 }, + }; + const onUpdatedFalsey = { + "test-falsey-value-bool": { newValue: true, oldValue: false }, + "test-falsey-value-string": { + newValue: "non-empty-string", + oldValue: "", + }, + "test-falsey-value-number": { newValue: 10, oldValue: 0 }, + }; + const keys = Object.keys(dataInitial); + + // Test on removing falsey values. + await storage.set(dataInitial); + await checkChanges(areaName, onSetInitial, "set falsey values"); + await storage.remove(keys); + await checkChanges(areaName, onRemovedFalsey, "remove falsey value"); + + // Test on updating falsey values. + await storage.set(dataInitial); + await checkChanges(areaName, onSetInitial, "set falsey values"); + await storage.set(dataUpdate); + await checkChanges(areaName, onUpdatedFalsey, "set non-falsey values"); + + // Clear the storage state. + await testNonExistingKeys(storage, `${areaName} before clearing`); + await storage.clear(); + await testNonExistingKeys(storage, `${areaName} after clearing`); + await globalChanges; + clearGlobalChanges(); + } + + function CustomObj() { + this.testKey1 = "testValue1"; + } + + CustomObj.prototype.toString = function() { + return '{"testKey2":"testValue2"}'; + }; + + CustomObj.prototype.toJSON = function customObjToJSON() { + return { testKey1: "testValue3" }; + }; + + /* eslint-disable dot-notation */ + async function runTests(areaName) { + expectedAreaName = areaName; + let storage = browser.storage[areaName]; + // Set some data and then test getters. + try { + await storage.set({ "test-prop1": "value1", "test-prop2": "value2" }); + await checkChanges( + areaName, + { + "test-prop1": { newValue: "value1" }, + "test-prop2": { newValue: "value2" }, + }, + "set (a)" + ); + + await checkGet(areaName, "test-prop1", "value1"); + await checkGet(areaName, "test-prop2", "value2"); + + let data = await storage.get({ + "test-prop1": undefined, + "test-prop2": undefined, + other: "default", + }); + browser.test.assertEq( + "value1", + data["test-prop1"], + "prop1 correct (a)" + ); + browser.test.assertEq( + "value2", + data["test-prop2"], + "prop2 correct (a)" + ); + browser.test.assertEq("default", data["other"], "other correct"); + + data = await storage.get(["test-prop1", "test-prop2", "other"]); + browser.test.assertEq( + "value1", + data["test-prop1"], + "prop1 correct (b)" + ); + browser.test.assertEq( + "value2", + data["test-prop2"], + "prop2 correct (b)" + ); + browser.test.assertFalse("other" in data, "other correct"); + + // Remove data in various ways. + await storage.remove("test-prop1"); + await checkChanges( + areaName, + { "test-prop1": { oldValue: "value1" } }, + "remove string" + ); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse( + "test-prop1" in data, + "prop1 absent (remove string)" + ); + browser.test.assertTrue( + "test-prop2" in data, + "prop2 present (remove string)" + ); + + await storage.set({ "test-prop1": "value1" }); + await checkChanges( + areaName, + { "test-prop1": { newValue: "value1" } }, + "set (c)" + ); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertEq( + data["test-prop1"], + "value1", + "prop1 correct (c)" + ); + browser.test.assertEq( + data["test-prop2"], + "value2", + "prop2 correct (c)" + ); + + await storage.remove(["test-prop1", "test-prop2"]); + await checkChanges( + areaName, + { + "test-prop1": { oldValue: "value1" }, + "test-prop2": { oldValue: "value2" }, + }, + "remove array" + ); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse( + "test-prop1" in data, + "prop1 absent (remove array)" + ); + browser.test.assertFalse( + "test-prop2" in data, + "prop2 absent (remove array)" + ); + + await testFalseyValues(areaName); + + // test storage.clear + await storage.set({ "test-prop1": "value1", "test-prop2": "value2" }); + // Make sure that set() handler happened before we clear the + // promise again. + await globalChanges; + + clearGlobalChanges(); + await storage.clear(); + + await checkChanges( + areaName, + { + "test-prop1": { oldValue: "value1" }, + "test-prop2": { oldValue: "value2" }, + }, + "clear" + ); + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)"); + browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)"); + + // Make sure we can store complex JSON data. + // known previous values + await storage.set({ "test-prop1": "value1", "test-prop2": "value2" }); + + // Make sure the set() handler landed. + await globalChanges; + + let date = new Date(0); + + clearGlobalChanges(); + await storage.set({ + "test-prop1": { + str: "hello", + bool: true, + null: null, + undef: undefined, + obj: {}, + nestedObj: { + testKey: {}, + }, + intKeyObj: { + 4: "testValue1", + 3: "testValue2", + 99: "testValue3", + }, + floatKeyObj: { + 1.4: "testValue1", + 5.5: "testValue2", + }, + customObj: new CustomObj(), + arr: [1, 2], + nestedArr: [1, [2, 3]], + date, + regexp: /regexp/, + }, + }); + + await browser.test.assertRejects( + storage.set({ + window, + }), + /DataCloneError|cyclic object value/ + ); + + await browser.test.assertRejects( + storage.set({ "test-prop2": function func() {} }), + /DataCloneError/ + ); + + const recentChanges = await globalChanges; + + browser.test.assertEq( + "value1", + recentChanges["test-prop1"].oldValue, + "oldValue correct" + ); + browser.test.assertEq( + "object", + typeof recentChanges["test-prop1"].newValue, + "newValue is obj" + ); + clearGlobalChanges(); + + data = await storage.get({ + "test-prop1": undefined, + "test-prop2": undefined, + }); + let obj = data["test-prop1"]; + + browser.test.assertEq( + "object", + typeof obj.customObj, + "custom object part correct" + ); + browser.test.assertEq( + 1, + Object.keys(obj.customObj).length, + "customObj keys correct" + ); + + if (areaName === "local") { + browser.test.assertEq( + String(date), + String(obj.date), + "date part correct" + ); + browser.test.assertEq( + "/regexp/", + obj.regexp.toString(), + "regexp part correct" + ); + // storage.local doesn't call toJSON + browser.test.assertEq( + "testValue1", + obj.customObj.testKey1, + "customObj keys correct" + ); + } else { + browser.test.assertEq( + "1970-01-01T00:00:00.000Z", + String(obj.date), + "date part correct" + ); + + browser.test.assertEq( + "object", + typeof obj.regexp, + "regexp part is an object" + ); + browser.test.assertEq( + 0, + Object.keys(obj.regexp).length, + "regexp part is an empty object" + ); + // storage.sync does call toJSON + browser.test.assertEq( + "testValue3", + obj.customObj.testKey1, + "customObj keys correct" + ); + } + + browser.test.assertEq("hello", obj.str, "string part correct"); + browser.test.assertEq(true, obj.bool, "bool part correct"); + browser.test.assertEq(null, obj.null, "null part correct"); + browser.test.assertEq(undefined, obj.undef, "undefined part correct"); + browser.test.assertEq(undefined, obj.window, "window part correct"); + browser.test.assertEq("object", typeof obj.obj, "object part correct"); + browser.test.assertEq( + "object", + typeof obj.nestedObj, + "nested object part correct" + ); + browser.test.assertEq( + "object", + typeof obj.nestedObj.testKey, + "nestedObj.testKey part correct" + ); + browser.test.assertEq( + "object", + typeof obj.intKeyObj, + "int key object part correct" + ); + browser.test.assertEq( + "testValue1", + obj.intKeyObj[4], + "intKeyObj[4] part correct" + ); + browser.test.assertEq( + "testValue2", + obj.intKeyObj[3], + "intKeyObj[3] part correct" + ); + browser.test.assertEq( + "testValue3", + obj.intKeyObj[99], + "intKeyObj[99] part correct" + ); + browser.test.assertEq( + "object", + typeof obj.floatKeyObj, + "float key object part correct" + ); + browser.test.assertEq( + "testValue1", + obj.floatKeyObj[1.4], + "floatKeyObj[1.4] part correct" + ); + browser.test.assertEq( + "testValue2", + obj.floatKeyObj[5.5], + "floatKeyObj[5.5] part correct" + ); + + browser.test.assertTrue(Array.isArray(obj.arr), "array part present"); + browser.test.assertEq(1, obj.arr[0], "arr[0] part correct"); + browser.test.assertEq(2, obj.arr[1], "arr[1] part correct"); + browser.test.assertEq(2, obj.arr.length, "arr.length part correct"); + browser.test.assertTrue( + Array.isArray(obj.nestedArr), + "nested array part present" + ); + browser.test.assertEq( + 2, + obj.nestedArr.length, + "nestedArr.length part correct" + ); + browser.test.assertEq(1, obj.nestedArr[0], "nestedArr[0] part correct"); + browser.test.assertTrue( + Array.isArray(obj.nestedArr[1]), + "nestedArr[1] part present" + ); + browser.test.assertEq( + 2, + obj.nestedArr[1].length, + "nestedArr[1].length part correct" + ); + browser.test.assertEq( + 2, + obj.nestedArr[1][0], + "nestedArr[1][0] part correct" + ); + browser.test.assertEq( + 3, + obj.nestedArr[1][1], + "nestedArr[1][1] part correct" + ); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("storage"); + } + } + + browser.test.onMessage.addListener(msg => { + let promise; + if (msg === "test-local") { + promise = runTests("local"); + } else if (msg === "test-sync") { + promise = runTests("sync"); + } + promise.then(() => browser.test.sendMessage("test-finished")); + }); + + browser.test.sendMessage("ready"); + } + + let extensionData = { + background: `(${backgroundScript})(${checkGetImpl})`, + manifest: { + permissions: ["storage"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitMessage("ready"); + + extension.sendMessage(`test-${testAreaName}`); + await extension.awaitMessage("test-finished"); + + await extension.unload(); +} + +function test_storage_sync_requires_real_id() { + async function testFn() { + async function background() { + const EXCEPTION_MESSAGE = + "The storage API will not work with a temporary addon ID. " + + "Please add an explicit addon ID to your manifest. " + + "For more information see https://mzl.la/3lPk1aE."; + + await browser.test.assertRejects( + browser.storage.sync.set({ foo: "bar" }), + EXCEPTION_MESSAGE + ); + + browser.test.notifyPass("exception correct"); + } + + let extensionData = { + background, + manifest: { + permissions: ["storage"], + }, + useAddonManager: "temporary", + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitFinish("exception correct"); + + await extension.unload(); + } + + return runWithPrefs([[STORAGE_SYNC_PREF, true]], testFn); +} + +// Test for storage areas which don't support getBytesInUse() nor QUOTA +// constants. +async function check_storage_area_no_bytes_in_use(area) { + let impl = browser.storage[area]; + + browser.test.assertEq( + typeof impl.getBytesInUse, + "undefined", + "getBytesInUse API method should not be available" + ); + browser.test.sendMessage("test-complete"); +} + +async function test_background_storage_area_no_bytes_in_use(area) { + const EXT_ID = "test-gbiu@mozilla.org"; + + const extensionDef = { + manifest: { + permissions: ["storage"], + browser_specific_settings: { gecko: { id: EXT_ID } }, + }, + background: `(${check_storage_area_no_bytes_in_use})("${area}")`, + }; + + const extension = ExtensionTestUtils.loadExtension(extensionDef); + + await extension.startup(); + await extension.awaitMessage("test-complete"); + await extension.unload(); +} + +async function test_contentscript_storage_area_no_bytes_in_use(area) { + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/data/file_sample.html" + ); + + function contentScript(checkImpl) { + browser.test.onMessage.addListener(msg => { + if (msg === "test-local") { + checkImpl("local"); + } else if (msg === "test-sync") { + checkImpl("sync"); + } else { + browser.test.fail(`Unexpected test message received: ${msg}`); + browser.test.sendMessage("test-complete"); + } + }); + browser.test.sendMessage("ready"); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + matches: ["http://example.com/data/file_sample.html"], + js: ["content_script.js"], + run_at: "document_idle", + }, + ], + + permissions: ["storage"], + }, + + files: { + "content_script.js": `(${contentScript})(${check_storage_area_no_bytes_in_use})`, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitMessage("ready"); + + extension.sendMessage(`test-${area}`); + await extension.awaitMessage("test-complete"); + + await extension.unload(); + await contentPage.close(); +} + +// Test for storage areas which do support getBytesInUse() (but which may or may +// not support enforcement of the quota) +async function check_storage_area_with_bytes_in_use(area, expectQuota) { + let impl = browser.storage[area]; + + // QUOTA_* constants aren't currently exposed - see bug 1396810. + // However, the quotas are still enforced, so test them here. + // (Note that an implication of this is that we can't test area other than + // 'sync', because its limits are different - so for completeness...) + browser.test.assertEq( + area, + "sync", + "Running test on storage.sync API as expected" + ); + const QUOTA_BYTES_PER_ITEM = 8192; + const MAX_ITEMS = 512; + + // bytes is counted as "length of key as a string, length of value as + // JSON" - ie, quotes not counted in the key, but are in the value. + let value = "x".repeat(QUOTA_BYTES_PER_ITEM - 3); + + await impl.set({ x: value }); // Shouldn't reject on either kinto or rust-based storage.sync. + browser.test.assertEq(await impl.getBytesInUse(null), QUOTA_BYTES_PER_ITEM); + // kinto does implement getBytesInUse() but doesn't enforce a quota. + if (expectQuota) { + await browser.test.assertRejects( + impl.set({ x: value + "x" }), + /QuotaExceededError/, + "Got a rejection with the expected error message" + ); + // MAX_ITEMS + await impl.clear(); + let ob = {}; + for (let i = 0; i < MAX_ITEMS; i++) { + ob[`key-${i}`] = "x"; + } + await impl.set(ob); // should work. + await browser.test.assertRejects( + impl.set({ straw: "camel's back" }), // exceeds MAX_ITEMS + /QuotaExceededError/, + "Got a rejection with the expected error message" + ); + // QUOTA_BYTES is being already tested for the underlying StorageSyncService + // so we don't duplicate those tests here. + } else { + // Exceeding quota should work on the previous kinto-based storage.sync implementation + await impl.set({ x: value + "x" }); // exceeds quota but should work. + browser.test.assertEq( + await impl.getBytesInUse(null), + QUOTA_BYTES_PER_ITEM + 1, + "Got the expected result from getBytesInUse" + ); + } + browser.test.sendMessage("test-complete"); +} + +async function test_background_storage_area_with_bytes_in_use( + area, + expectQuota +) { + const EXT_ID = "test-gbiu@mozilla.org"; + + const extensionDef = { + manifest: { + permissions: ["storage"], + browser_specific_settings: { gecko: { id: EXT_ID } }, + }, + background: `(${check_storage_area_with_bytes_in_use})("${area}", ${expectQuota})`, + }; + + const extension = ExtensionTestUtils.loadExtension(extensionDef); + + await extension.startup(); + await extension.awaitMessage("test-complete"); + await extension.unload(); +} + +async function test_contentscript_storage_area_with_bytes_in_use( + area, + expectQuota +) { + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/data/file_sample.html" + ); + + function contentScript(checkImpl) { + browser.test.onMessage.addListener(([area, expectQuota]) => { + if ( + !["local", "sync"].includes(area) || + typeof expectQuota !== "boolean" + ) { + browser.test.fail(`Unexpected test message: [${area}, ${expectQuota}]`); + // Let the test to fail immediately instead of wait for a timeout failure. + browser.test.sendMessage("test-complete"); + return; + } + checkImpl(area, expectQuota); + }); + browser.test.sendMessage("ready"); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + matches: ["http://example.com/data/file_sample.html"], + js: ["content_script.js"], + run_at: "document_idle", + }, + ], + + permissions: ["storage"], + }, + + files: { + "content_script.js": `(${contentScript})(${check_storage_area_with_bytes_in_use})`, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitMessage("ready"); + + extension.sendMessage([area, expectQuota]); + await extension.awaitMessage("test-complete"); + + await extension.unload(); + await contentPage.close(); +} + +// A couple of common tests for checking content scripts. +async function testStorageContentScript(checkGet) { + let globalChanges, gResolve; + function clearGlobalChanges() { + globalChanges = new Promise(resolve => { + gResolve = resolve; + }); + } + clearGlobalChanges(); + let expectedAreaName; + + browser.storage.onChanged.addListener((changes, areaName) => { + browser.test.assertEq( + expectedAreaName, + areaName, + "Expected area name received by listener" + ); + gResolve(changes); + }); + + async function checkChanges(areaName, changes, message) { + function checkSub(obj1, obj2) { + for (let prop in obj1) { + browser.test.assertTrue( + obj1[prop] !== undefined, + `checkChanges ${areaName} ${prop} is missing (${message})` + ); + browser.test.assertTrue( + obj2[prop] !== undefined, + `checkChanges ${areaName} ${prop} is missing (${message})` + ); + browser.test.assertEq( + obj1[prop].oldValue, + obj2[prop].oldValue, + `checkChanges ${areaName} ${prop} old (${message})` + ); + browser.test.assertEq( + obj1[prop].newValue, + obj2[prop].newValue, + `checkChanges ${areaName} ${prop} new (${message})` + ); + } + } + + const recentChanges = await globalChanges; + checkSub(changes, recentChanges); + checkSub(recentChanges, changes); + clearGlobalChanges(); + } + + /* eslint-disable dot-notation */ + async function runTests(areaName) { + expectedAreaName = areaName; + let storage = browser.storage[areaName]; + // Set some data and then test getters. + try { + await storage.set({ "test-prop1": "value1", "test-prop2": "value2" }); + await checkChanges( + areaName, + { + "test-prop1": { newValue: "value1" }, + "test-prop2": { newValue: "value2" }, + }, + "set (a)" + ); + + await checkGet(areaName, "test-prop1", "value1"); + await checkGet(areaName, "test-prop2", "value2"); + + let data = await storage.get({ + "test-prop1": undefined, + "test-prop2": undefined, + other: "default", + }); + browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)"); + browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)"); + browser.test.assertEq("default", data["other"], "other correct"); + + data = await storage.get(["test-prop1", "test-prop2", "other"]); + browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)"); + browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)"); + browser.test.assertFalse("other" in data, "other correct"); + + // Remove data in various ways. + await storage.remove("test-prop1"); + await checkChanges( + areaName, + { "test-prop1": { oldValue: "value1" } }, + "remove string" + ); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse( + "test-prop1" in data, + "prop1 absent (remove string)" + ); + browser.test.assertTrue( + "test-prop2" in data, + "prop2 present (remove string)" + ); + + await storage.set({ "test-prop1": "value1" }); + await checkChanges( + areaName, + { "test-prop1": { newValue: "value1" } }, + "set (c)" + ); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)"); + browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)"); + + await storage.remove(["test-prop1", "test-prop2"]); + await checkChanges( + areaName, + { + "test-prop1": { oldValue: "value1" }, + "test-prop2": { oldValue: "value2" }, + }, + "remove array" + ); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse( + "test-prop1" in data, + "prop1 absent (remove array)" + ); + browser.test.assertFalse( + "test-prop2" in data, + "prop2 absent (remove array)" + ); + + // test storage.clear + await storage.set({ "test-prop1": "value1", "test-prop2": "value2" }); + // Make sure that set() handler happened before we clear the + // promise again. + await globalChanges; + + clearGlobalChanges(); + await storage.clear(); + + await checkChanges( + areaName, + { + "test-prop1": { oldValue: "value1" }, + "test-prop2": { oldValue: "value2" }, + }, + "clear" + ); + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)"); + browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)"); + + // Make sure we can store complex JSON data. + // known previous values + await storage.set({ "test-prop1": "value1", "test-prop2": "value2" }); + + // Make sure the set() handler landed. + await globalChanges; + + let date = new Date(0); + + clearGlobalChanges(); + await storage.set({ + "test-prop1": { + str: "hello", + bool: true, + null: null, + undef: undefined, + obj: {}, + arr: [1, 2], + date: new Date(0), + regexp: /regexp/, + }, + }); + + await browser.test.assertRejects( + storage.set({ + window, + }), + /DataCloneError|cyclic object value/ + ); + + await browser.test.assertRejects( + storage.set({ "test-prop2": function func() {} }), + /DataCloneError/ + ); + + const recentChanges = await globalChanges; + + browser.test.assertEq( + "value1", + recentChanges["test-prop1"].oldValue, + "oldValue correct" + ); + browser.test.assertEq( + "object", + typeof recentChanges["test-prop1"].newValue, + "newValue is obj" + ); + clearGlobalChanges(); + + data = await storage.get({ + "test-prop1": undefined, + "test-prop2": undefined, + }); + let obj = data["test-prop1"]; + + if (areaName === "local") { + browser.test.assertEq( + String(date), + String(obj.date), + "date part correct" + ); + browser.test.assertEq( + "/regexp/", + obj.regexp.toString(), + "regexp part correct" + ); + } else { + browser.test.assertEq( + "1970-01-01T00:00:00.000Z", + String(obj.date), + "date part correct" + ); + + browser.test.assertEq( + "object", + typeof obj.regexp, + "regexp part is an object" + ); + browser.test.assertEq( + 0, + Object.keys(obj.regexp).length, + "regexp part is an empty object" + ); + } + + browser.test.assertEq("hello", obj.str, "string part correct"); + browser.test.assertEq(true, obj.bool, "bool part correct"); + browser.test.assertEq(null, obj.null, "null part correct"); + browser.test.assertEq(undefined, obj.undef, "undefined part correct"); + browser.test.assertEq(undefined, obj.window, "window part correct"); + browser.test.assertEq("object", typeof obj.obj, "object part correct"); + browser.test.assertTrue(Array.isArray(obj.arr), "array part present"); + browser.test.assertEq(1, obj.arr[0], "arr[0] part correct"); + browser.test.assertEq(2, obj.arr[1], "arr[1] part correct"); + browser.test.assertEq(2, obj.arr.length, "arr.length part correct"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("storage"); + } + } + + browser.test.onMessage.addListener(msg => { + let promise; + if (msg === "test-local") { + promise = runTests("local"); + } else if (msg === "test-sync") { + promise = runTests("sync"); + } + promise.then(() => browser.test.sendMessage("test-finished")); + }); + + browser.test.sendMessage("ready"); +} + +async function test_contentscript_storage(storageType) { + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/data/file_sample.html" + ); + + let extensionData = { + manifest: { + content_scripts: [ + { + matches: ["http://example.com/data/file_sample.html"], + js: ["content_script.js"], + run_at: "document_idle", + }, + ], + + permissions: ["storage"], + }, + + files: { + "content_script.js": `(${testStorageContentScript})(${checkGetImpl})`, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitMessage("ready"); + + extension.sendMessage(`test-${storageType}`); + await extension.awaitMessage("test-finished"); + + await extension.unload(); + await contentPage.close(); +} + +async function test_storage_change_event_page(areaName) { + async function testOnChanged(targetIsStorageArea) { + function backgroundTestStorageTopNamespace(areaName) { + browser.storage.onChanged.addListener((changes, area) => { + browser.test.assertEq(area, areaName, "Expected areaName"); + browser.test.assertEq( + JSON.stringify(changes), + `{"storageKey":{"newValue":"newStorageValue"}}`, + "Expected changes" + ); + browser.test.sendMessage("onChanged_was_fired"); + }); + } + function backgroundTestStorageAreaNamespace(areaName) { + browser.storage[areaName].onChanged.addListener((changes, ...args) => { + browser.test.assertEq(args.length, 0, "no more args after changes"); + browser.test.assertEq( + JSON.stringify(changes), + `{"storageKey":{"newValue":"newStorageValue"}}`, + `Expected changes via ${areaName}.onChanged event` + ); + browser.test.sendMessage("onChanged_was_fired"); + }); + } + let background, onChangedName; + if (targetIsStorageArea) { + // Test storage.local.onChanged / storage.sync.onChanged. + background = backgroundTestStorageAreaNamespace; + onChangedName = `${areaName}.onChanged`; + } else { + background = backgroundTestStorageTopNamespace; + onChangedName = "onChanged"; + } + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["storage"], + background: { persistent: false }, + }, + background: `(${background})("${areaName}")`, + files: { + "trigger-change.html": ` + + + `, + "trigger-change.js": async () => { + let areaName = location.search.slice(1); + await browser.storage[areaName].set({ + storageKey: "newStorageValue", + }); + browser.test.sendMessage("tried_to_trigger_change"); + }, + }, + }); + await extension.startup(); + assertPersistentListeners(extension, "storage", onChangedName, { + primed: false, + }); + + await extension.terminateBackground(); + assertPersistentListeners(extension, "storage", onChangedName, { + primed: true, + }); + + // Now trigger the event + let contentPage = await ExtensionTestUtils.loadContentPage( + `moz-extension://${extension.uuid}/trigger-change.html?${areaName}` + ); + await extension.awaitMessage("tried_to_trigger_change"); + await contentPage.close(); + await extension.awaitMessage("onChanged_was_fired"); + + assertPersistentListeners(extension, "storage", onChangedName, { + primed: false, + }); + await extension.unload(); + } + + async function testFn() { + // Test browser.storage.onChanged.addListener + await testOnChanged(/* targetIsStorageArea */ false); + // Test browser.storage.local.onChanged.addListener + // and browser.storage.sync.onChanged.addListener, depending on areaName. + await testOnChanged(/* targetIsStorageArea */ true); + } + + return runWithPrefs([["extensions.eventPages.enabled", true]], testFn); +} diff --git a/toolkit/components/extensions/test/xpcshell/head_sync.js b/toolkit/components/extensions/test/xpcshell/head_sync.js new file mode 100644 index 0000000000..cec03a6a4e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_sync.js @@ -0,0 +1,66 @@ +/* This Source Code Form is 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"; + +/* exported withSyncContext */ + +const { ExtensionCommon } = ChromeUtils.import( + "resource://gre/modules/ExtensionCommon.jsm" +); + +class KintoExtContext extends ExtensionCommon.BaseContext { + constructor(principal) { + let fakeExtension = { id: "test@web.extension", manifestVersion: 2 }; + super("addon_parent", fakeExtension); + Object.defineProperty(this, "principal", { + value: principal, + configurable: true, + }); + this.sandbox = Cu.Sandbox(principal, { wantXrays: false }); + } + + get cloneScope() { + return this.sandbox; + } +} + +/** + * Call the given function with a newly-constructed context. + * Unload the context on the way out. + * + * @param {Function} f the function to call + */ +async function withContext(f) { + const ssm = Services.scriptSecurityManager; + const PRINCIPAL1 = ssm.createContentPrincipalFromOrigin( + "http://www.example.org" + ); + const context = new KintoExtContext(PRINCIPAL1); + try { + await f(context); + } finally { + await context.unload(); + } +} + +/** + * Like withContext(), but also turn on the "storage.sync" pref for + * the duration of the function. + * Calls to this function can be replaced with calls to withContext + * once the pref becomes on by default. + * + * @param {Function} f the function to call + */ +async function withSyncContext(f) { + const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled"; + let prefs = Services.prefs; + + try { + prefs.setBoolPref(STORAGE_SYNC_PREF, true); + await withContext(f); + } finally { + prefs.clearUserPref(STORAGE_SYNC_PREF); + } +} diff --git a/toolkit/components/extensions/test/xpcshell/head_telemetry.js b/toolkit/components/extensions/test/xpcshell/head_telemetry.js new file mode 100644 index 0000000000..ccbdd4d787 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_telemetry.js @@ -0,0 +1,172 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported IS_OOP, valueSum, clearHistograms, getSnapshots, promiseTelemetryRecorded */ + +ChromeUtils.defineESModuleGetters(this, { + ContentTaskUtils: "resource://testing-common/ContentTaskUtils.sys.mjs", +}); + +// Allows to run xpcshell telemetry test also on products (e.g. Thunderbird) where +// that telemetry wouldn't be actually collected in practice (but to be sure +// that it will work on those products as well by just adding the product in +// the telemetry metric definitions if it turns out we want to). +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +const IS_OOP = Services.prefs.getBoolPref("extensions.webextensions.remote"); + +const WEBEXT_EVENTPAGE_RUNNING_TIME_MS = "WEBEXT_EVENTPAGE_RUNNING_TIME_MS"; +const WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID = + "WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID"; +const WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT = "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT"; +const WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID = + "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID"; + +// Keep this in sync with the order in Histograms.json for "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT": +// the position of the category string determines the index of the values collected in the categorial +// histogram and so the existing labels should be kept in the exact same order and any new category +// to be added in the future should be appended to the existing ones. +const HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES = [ + "suspend", + "reset_other", + "reset_event", + "reset_listeners", + "reset_nativeapp", + "reset_streamfilter", +]; + +function valueSum(arr) { + return Object.values(arr).reduce((a, b) => a + b, 0); +} + +function clearHistograms() { + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getSnapshotForKeyedHistograms("main", true /* clear */); +} + +function getSnapshots(process) { + return Services.telemetry.getSnapshotForHistograms("main", false /* clear */)[ + process + ]; +} + +function getKeyedSnapshots(process) { + return Services.telemetry.getSnapshotForKeyedHistograms( + "main", + false /* clear */ + )[process]; +} + +// TODO Bug 1357509: There is no good way to make sure that the parent received +// the histogram entries from the extension and content processes. Let's stick +// to the ugly, spinning the event loop until we have a good approach. +function promiseTelemetryRecorded(id, process, expectedCount) { + let condition = () => { + let snapshot = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + )[process][id]; + return snapshot && valueSum(snapshot.values) >= expectedCount; + }; + return ContentTaskUtils.waitForCondition(condition); +} + +function promiseKeyedTelemetryRecorded( + id, + process, + expectedKey, + expectedCount +) { + let condition = () => { + let snapshot = Services.telemetry.getSnapshotForKeyedHistograms( + "main", + false /* clear */ + )[process][id]; + return ( + snapshot && + snapshot[expectedKey] && + valueSum(snapshot[expectedKey].values) >= expectedCount + ); + }; + return ContentTaskUtils.waitForCondition(condition); +} + +function assertHistogramSnapshot( + histogramId, + { keyed, processSnapshot, expectedValue }, + msg +) { + let histogram; + + if (keyed) { + histogram = Services.telemetry.getKeyedHistogramById(histogramId); + } else { + histogram = Services.telemetry.getHistogramById(histogramId); + } + + let res = processSnapshot(histogram.snapshot()); + Assert.deepEqual(res, expectedValue, msg); + return res; +} + +function assertHistogramEmpty(histogramId) { + assertHistogramSnapshot( + histogramId, + { + processSnapshot: snapshot => snapshot.sum, + expectedValue: 0, + }, + `No data recorded for histogram: ${histogramId}.` + ); +} + +function assertKeyedHistogramEmpty(histogramId) { + assertHistogramSnapshot( + histogramId, + { + keyed: true, + processSnapshot: snapshot => Object.keys(snapshot).length, + expectedValue: 0, + }, + `No data recorded for histogram: ${histogramId}.` + ); +} + +function assertHistogramCategoryNotEmpty( + histogramId, + { category, categories, keyed, key }, + msg +) { + let message = msg; + + if (!msg) { + message = `Data recorded for histogram: ${histogramId}, category "${category}"`; + if (keyed) { + message += `, key "${key}"`; + } + } + + assertHistogramSnapshot( + histogramId, + { + keyed, + processSnapshot: snapshot => { + const categoryIndex = categories.indexOf(category); + if (keyed) { + return { + [key]: snapshot[key] + ? snapshot[key].values[categoryIndex] > 0 + : null, + }; + } + return snapshot.values[categoryIndex] > 0; + }, + expectedValue: keyed ? { [key]: true } : true, + }, + message + ); +} diff --git a/toolkit/components/extensions/test/xpcshell/native_messaging.ini b/toolkit/components/extensions/test/xpcshell/native_messaging.ini new file mode 100644 index 0000000000..06702670bc --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/native_messaging.ini @@ -0,0 +1,19 @@ +[DEFAULT] +head = head.js head_e10s.js head_native_messaging.js head_telemetry.js +tail = +firefox-appdir = browser +skip-if = appname == "thunderbird" || os == "android" +subprocess = true +support-files = + data/** +tags = webextensions + +[test_ext_native_messaging.js] +skip-if = + (os == "win" && processor == "aarch64") # bug 1530841 + apple_silicon # bug 1729540 +run-sequentially = very high failure rate in parallel +[test_ext_native_messaging_perf.js] +skip-if = tsan # Unreasonably slow, bug 1612707 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[test_ext_native_messaging_unresponsive.js] diff --git a/toolkit/components/extensions/test/xpcshell/test_ExtensionShortcutKeyMap.js b/toolkit/components/extensions/test/xpcshell/test_ExtensionShortcutKeyMap.js new file mode 100644 index 0000000000..d059d8606b --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ExtensionShortcutKeyMap.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"; + +const { ExtensionShortcutKeyMap } = ChromeUtils.import( + "resource://gre/modules/ExtensionShortcuts.jsm" +); + +add_task(function test_ExtensionShortcutKeymap() { + const shortcutsMap = new ExtensionShortcutKeyMap(); + + shortcutsMap.recordShortcut("Ctrl+Shift+1", "Addon1", "Command1"); + shortcutsMap.recordShortcut("Ctrl+Shift+1", "Addon2", "Command2"); + shortcutsMap.recordShortcut("Ctrl+Alt+2", "Addon2", "Command3"); + // Empty shortcut not expected to be recorded, just ignored. + shortcutsMap.recordShortcut("", "Addon3", "Command4"); + + Assert.equal( + shortcutsMap.size, + 2, + "Got the expected number of shortcut entries" + ); + Assert.deepEqual( + { + shortcutWithTwoExtensions: shortcutsMap.getFirstAddonName("Ctrl+Shift+1"), + shortcutWithOnlyOneExtension: shortcutsMap.getFirstAddonName( + "Ctrl+Alt+2" + ), + shortcutWithNoExtension: shortcutsMap.getFirstAddonName(""), + }, + { + shortcutWithTwoExtensions: "Addon1", + shortcutWithOnlyOneExtension: "Addon2", + shortcutWithNoExtension: null, + }, + "Got the expected results from getFirstAddonName calls" + ); + + Assert.deepEqual( + { + shortcutWithTwoExtensions: shortcutsMap.has("Ctrl+Shift+1"), + shortcutWithOnlyOneExtension: shortcutsMap.has("Ctrl+Alt+2"), + shortcutWithNoExtension: shortcutsMap.has(""), + }, + { + shortcutWithTwoExtensions: true, + shortcutWithOnlyOneExtension: true, + shortcutWithNoExtension: false, + }, + "Got the expected results from `has` calls" + ); + + shortcutsMap.removeShortcut("Ctrl+Shift+1", "Addon1", "Command1"); + Assert.equal( + shortcutsMap.has("Ctrl+Shift+1"), + true, + "Expect shortcut to already exist after removing one duplicate" + ); + Assert.equal( + shortcutsMap.getFirstAddonName("Ctrl+Shift+1"), + "Addon2", + "Expect getFirstAddonName to return the remaining addon name" + ); + + shortcutsMap.removeShortcut("Ctrl+Shift+1", "Addon2", "Command2"); + Assert.equal( + shortcutsMap.has("Ctrl+Shift+1"), + false, + "Expect shortcut to not exist anymore after removing last entry" + ); + Assert.equal(shortcutsMap.size, 1, "Got only one shortcut as expected"); + + shortcutsMap.clear(); + Assert.equal( + shortcutsMap.size, + 0, + "Got no shortcut as expected after clearing the map" + ); +}); + +// This test verify that ExtensionShortcutKeyMap does catch duplicated +// shortcut when the two modifiers strings are associated to the same +// key (in particular on macOS where Ctrl and Command keys are both translated +// in the same modifier in the keyboard shortcuts). +add_task(function test_PlatformShortcutString() { + const shortcutsMap = new ExtensionShortcutKeyMap(); + + // Make the class instance behave like it would while running on macOS. + // (this is just for unit testing purpose, there is a separate integration + // test exercising this behavior in a real "Manage Extension Shortcut" + // about:addons view and only running on macOS, skipped on other platforms). + shortcutsMap._os = "mac"; + + shortcutsMap.recordShortcut("Ctrl+Shift+1", "Addon1", "MacCommand1"); + + Assert.deepEqual( + { + hasWithCtrl: shortcutsMap.has("Ctrl+Shift+1"), + hasWithCommand: shortcutsMap.has("Command+Shift+1"), + }, + { + hasWithCtrl: true, + hasWithCommand: true, + }, + "Got the expected results from `has` calls" + ); + + Assert.deepEqual( + { + nameWithCtrl: shortcutsMap.getFirstAddonName("Ctrl+Shift+1"), + nameWithCommand: shortcutsMap.getFirstAddonName("Command+Shift+1"), + }, + { + nameWithCtrl: "Addon1", + nameWithCommand: "Addon1", + }, + "Got the expected results from `getFirstAddonName` calls" + ); + + // Add a duplicate shortcut using Command instead of Ctrl and + // verify the expected behaviors. + shortcutsMap.recordShortcut("Command+Shift+1", "Addon2", "MacCommand2"); + Assert.equal(shortcutsMap.size, 1, "Got still one shortcut as expected"); + shortcutsMap.removeShortcut("Ctrl+Shift+1", "Addon1", "MacCommand1"); + Assert.equal(shortcutsMap.size, 1, "Got still one shortcut as expected"); + Assert.deepEqual( + { + nameWithCtrl: shortcutsMap.getFirstAddonName("Ctrl+Shift+1"), + nameWithCommand: shortcutsMap.getFirstAddonName("Command+Shift+1"), + }, + { + nameWithCtrl: "Addon2", + nameWithCommand: "Addon2", + }, + "Got the expected results from `getFirstAddonName` calls" + ); + + // Remove the entry added with a shortcut using "Command" by using the + // equivalent shortcut using Ctrl. + shortcutsMap.removeShortcut("Ctrl+Shift+1", "Addon2", "MacCommand2"); + Assert.equal(shortcutsMap.size, 0, "Got no shortcut as expected"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ExtensionStorageSync_migration_kinto.js b/toolkit/components/extensions/test/xpcshell/test_ExtensionStorageSync_migration_kinto.js new file mode 100644 index 0000000000..1dc6239d9c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ExtensionStorageSync_migration_kinto.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"; + +// Import the rust-based and kinto-based implementations +const { extensionStorageSync: rustImpl } = ChromeUtils.import( + "resource://gre/modules/ExtensionStorageSync.jsm" +); +const { extensionStorageSyncKinto: kintoImpl } = ChromeUtils.import( + "resource://gre/modules/ExtensionStorageSyncKinto.jsm" +); + +Services.prefs.setBoolPref("webextensions.storage.sync.kinto", false); + +add_task(async function test_sync_migration() { + // There's no good reason to perform this test via test extensions - we just + // call the underlying APIs directly. + + // Set some stuff using the kinto-based impl. + let e1 = { id: "test@mozilla.com" }; + let c1 = { extension: e1, callOnClose() {} }; + await kintoImpl.set(e1, { foo: "bar" }, c1); + + let e2 = { id: "test-2@mozilla.com" }; + let c2 = { extension: e2, callOnClose() {} }; + await kintoImpl.set(e2, { second: "2nd" }, c2); + + let e3 = { id: "test-3@mozilla.com" }; + let c3 = { extension: e3, callOnClose() {} }; + + // And all the data should be magically migrated. + Assert.deepEqual(await rustImpl.get(e1, "foo", c1), { foo: "bar" }); + Assert.deepEqual(await rustImpl.get(e2, null, c2), { second: "2nd" }); + + // Sanity check we really are doing what we think we are - set a value in our + // new one, it should not be reflected by kinto. + await rustImpl.set(e3, { third: "3rd" }, c3); + Assert.deepEqual(await rustImpl.get(e3, null, c3), { third: "3rd" }); + Assert.deepEqual(await kintoImpl.get(e3, null, c3), {}); + // cleanup. + await kintoImpl.clear(e1, c1); + await kintoImpl.clear(e2, c2); + await kintoImpl.clear(e3, c3); + await rustImpl.clear(e1, c1); + await rustImpl.clear(e2, c2); + await rustImpl.clear(e3, c3); +}); + +// It would be great to have failure tests, but that seems impossible to have +// in automated tests given the conditions under which we migrate - it would +// basically require us to arrange for zero free disk space or to somehow +// arrange for sqlite to see an io error. Specially crafted "corrupt" +// sqlite files doesn't help because that file must not exist for us to even +// attempt migration. +// +// But - what we can test is that if .migratedOk on the new impl ever goes to +// false we delegate correctly. +add_task(async function test_sync_migration_delgates() { + let e1 = { id: "test@mozilla.com" }; + let c1 = { extension: e1, callOnClose() {} }; + await kintoImpl.set(e1, { foo: "bar" }, c1); + + // We think migration went OK - `get` shouldn't see kinto. + Assert.deepEqual(rustImpl.get(e1, null, c1), {}); + + info( + "Setting migration failure flag to ensure we delegate to kinto implementation" + ); + rustImpl.migrationOk = false; + // get should now be seeing kinto. + Assert.deepEqual(await rustImpl.get(e1, null, c1), { foo: "bar" }); + // check everything else delegates. + + await rustImpl.set(e1, { foo: "foo" }, c1); + Assert.deepEqual(await kintoImpl.get(e1, null, c1), { foo: "foo" }); + + Assert.equal(await rustImpl.getBytesInUse(e1, null, c1), 8); + + await rustImpl.remove(e1, "foo", c1); + Assert.deepEqual(await kintoImpl.get(e1, null, c1), {}); + + await rustImpl.set(e1, { foo: "foo" }, c1); + Assert.deepEqual(await kintoImpl.get(e1, null, c1), { foo: "foo" }); + await rustImpl.clear(e1, c1); + Assert.deepEqual(await kintoImpl.get(e1, null, c1), {}); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_MatchPattern.js b/toolkit/components/extensions/test/xpcshell/test_MatchPattern.js new file mode 100644 index 0000000000..1a57d0870e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_MatchPattern.js @@ -0,0 +1,602 @@ +/* -*- 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_MatchPattern_matches() { + function test(url, pattern, normalized = pattern, options = {}, explicit) { + let uri = Services.io.newURI(url); + + pattern = Array.prototype.concat.call(pattern); + normalized = Array.prototype.concat.call(normalized); + + let patterns = pattern.map(pat => new MatchPattern(pat, options)); + + let set = new MatchPatternSet(pattern, options); + let set2 = new MatchPatternSet(patterns, options); + + deepEqual( + set2.patterns, + patterns, + "Patterns in set should equal the input patterns" + ); + + equal( + set.matches(uri, explicit), + set2.matches(uri, explicit), + "Single pattern and pattern set should return the same match" + ); + + for (let [i, pat] of patterns.entries()) { + equal( + pat.pattern, + normalized[i], + "Pattern property should contain correct normalized pattern value" + ); + } + + if (patterns.length == 1) { + equal( + patterns[0].matches(uri, explicit), + set.matches(uri, explicit), + "Single pattern and string set should return the same match" + ); + } + + return set.matches(uri, explicit); + } + + function pass({ url, pattern, normalized, options, explicit }) { + ok( + test(url, pattern, normalized, options, explicit), + `Expected match: ${JSON.stringify(pattern)}, ${url}` + ); + } + + function fail({ url, pattern, normalized, options, explicit }) { + ok( + !test(url, pattern, normalized, options, explicit), + `Expected no match: ${JSON.stringify(pattern)}, ${url}` + ); + } + + function invalid({ pattern }) { + Assert.throws( + () => new MatchPattern(pattern), + /.*/, + `Invalid pattern '${pattern}' should throw` + ); + Assert.throws( + () => new MatchPatternSet([pattern]), + /.*/, + `Invalid pattern '${pattern}' should throw` + ); + } + + // Invalid pattern. + invalid({ pattern: "" }); + + // Pattern must include trailing slash. + invalid({ pattern: "http://mozilla.org" }); + + // Protocol not allowed. + invalid({ pattern: "gopher://wuarchive.wustl.edu/" }); + + pass({ url: "http://mozilla.org", pattern: "http://mozilla.org/" }); + pass({ url: "http://mozilla.org/", pattern: "http://mozilla.org/" }); + + pass({ url: "http://mozilla.org/", pattern: "*://mozilla.org/" }); + pass({ url: "https://mozilla.org/", pattern: "*://mozilla.org/" }); + fail({ url: "file://mozilla.org/", pattern: "*://mozilla.org/" }); + fail({ url: "ftp://mozilla.org/", pattern: "*://mozilla.org/" }); + + fail({ url: "http://mozilla.com", pattern: "http://*mozilla.com*/" }); + fail({ url: "http://mozilla.com", pattern: "http://mozilla.*/" }); + invalid({ pattern: "http:/mozilla.com/" }); + + pass({ url: "http://google.com", pattern: "http://*.google.com/" }); + pass({ url: "http://docs.google.com", pattern: "http://*.google.com/" }); + + pass({ url: "http://mozilla.org:8080", pattern: "http://mozilla.org/" }); + pass({ url: "http://mozilla.org:8080", pattern: "*://mozilla.org/" }); + fail({ url: "http://mozilla.org:8080", pattern: "http://mozilla.org:8080/" }); + + // Now try with * in the path. + pass({ url: "http://mozilla.org", pattern: "http://mozilla.org/*" }); + pass({ url: "http://mozilla.org/", pattern: "http://mozilla.org/*" }); + + pass({ url: "http://mozilla.org/", pattern: "*://mozilla.org/*" }); + pass({ url: "https://mozilla.org/", pattern: "*://mozilla.org/*" }); + fail({ url: "file://mozilla.org/", pattern: "*://mozilla.org/*" }); + fail({ url: "http://mozilla.com", pattern: "http://mozilla.*/*" }); + + pass({ url: "http://google.com", pattern: "http://*.google.com/*" }); + pass({ url: "http://docs.google.com", pattern: "http://*.google.com/*" }); + + // Check path stuff. + fail({ url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/" }); + pass({ url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*" }); + pass({ + url: "http://mozilla.com/abc/def", + pattern: "http://mozilla.com/a*f", + }); + pass({ url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*" }); + pass({ url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*f" }); + fail({ url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*e" }); + fail({ url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*c" }); + + invalid({ pattern: "http:///a.html" }); + pass({ url: "file:///foo", pattern: "file:///foo*" }); + pass({ url: "file:///foo/bar.html", pattern: "file:///foo*" }); + + pass({ url: "http://mozilla.org/a", pattern: "" }); + pass({ url: "https://mozilla.org/a", pattern: "" }); + pass({ url: "ftp://mozilla.org/a", pattern: "" }); + pass({ url: "file:///a", pattern: "" }); + fail({ url: "gopher://wuarchive.wustl.edu/a", pattern: "" }); + + // Multiple patterns. + pass({ url: "http://mozilla.org", pattern: ["http://mozilla.org/"] }); + pass({ + url: "http://mozilla.org", + pattern: ["http://mozilla.org/", "http://mozilla.com/"], + }); + pass({ + url: "http://mozilla.com", + pattern: ["http://mozilla.org/", "http://mozilla.com/"], + }); + fail({ + url: "http://mozilla.biz", + pattern: ["http://mozilla.org/", "http://mozilla.com/"], + }); + + // Match url with fragments. + pass({ + url: "http://mozilla.org/base#some-fragment", + pattern: "http://mozilla.org/base", + }); + + // Match data:-URLs. + pass({ url: "data:text/plain,foo", pattern: ["data:text/plain,foo"] }); + pass({ url: "data:text/plain,foo", pattern: ["data:text/plain,*"] }); + pass({ + url: "data:text/plain;charset=utf-8,foo", + pattern: ["data:text/plain;charset=utf-8,foo"], + }); + fail({ + url: "data:text/plain,foo", + pattern: ["data:text/plain;charset=utf-8,foo"], + }); + fail({ + url: "data:text/plain;charset=utf-8,foo", + pattern: ["data:text/plain,foo"], + }); + + // Privileged matchers: + invalid({ pattern: "about:foo" }); + invalid({ pattern: "resource://foo/*" }); + + pass({ + url: "about:foo", + pattern: ["about:foo", "about:foo*"], + options: { restrictSchemes: false }, + }); + pass({ + url: "about:foo", + pattern: ["about:foo*"], + options: { restrictSchemes: false }, + }); + pass({ + url: "about:foobar", + pattern: ["about:foo*"], + options: { restrictSchemes: false }, + }); + + pass({ + url: "resource://foo/bar", + pattern: ["resource://foo/bar"], + options: { restrictSchemes: false }, + }); + fail({ + url: "resource://fog/bar", + pattern: ["resource://foo/bar"], + options: { restrictSchemes: false }, + }); + fail({ + url: "about:foo", + pattern: ["about:meh"], + options: { restrictSchemes: false }, + }); + + // Matchers for schemes without host should ignore ignorePath. + pass({ + url: "about:reader?http://e.com/", + pattern: ["about:reader*"], + options: { ignorePath: true, restrictSchemes: false }, + }); + pass({ url: "data:,", pattern: ["data:,*"], options: { ignorePath: true } }); + + // Matchers for schems without host should still match even if the explicit (host) flag is set. + pass({ + url: "about:reader?explicit", + pattern: ["about:reader*"], + options: { restrictSchemes: false }, + explicit: true, + }); + pass({ + url: "about:reader?explicit", + pattern: ["about:reader?explicit"], + options: { restrictSchemes: false }, + explicit: true, + }); + pass({ url: "data:,explicit", pattern: ["data:,explicit"], explicit: true }); + pass({ url: "data:,explicit", pattern: ["data:,*"], explicit: true }); + + // Matchers without "//" separator in the pattern. + pass({ url: "data:text/plain;charset=utf-8,foo", pattern: ["data:*"] }); + pass({ + url: "about:blank", + pattern: ["about:*"], + options: { restrictSchemes: false }, + }); + pass({ + url: "view-source:https://example.com", + pattern: ["view-source:*"], + options: { restrictSchemes: false }, + }); + invalid({ pattern: ["chrome:*"], options: { restrictSchemes: false } }); + invalid({ pattern: "http:*" }); + + // Matchers for unrecognized schemes. + invalid({ pattern: "unknown-scheme:*" }); + pass({ + url: "unknown-scheme:foo", + pattern: ["unknown-scheme:foo"], + options: { restrictSchemes: false }, + }); + pass({ + url: "unknown-scheme:foo", + pattern: ["unknown-scheme:*"], + options: { restrictSchemes: false }, + }); + pass({ + url: "unknown-scheme://foo", + pattern: ["unknown-scheme://foo"], + options: { restrictSchemes: false }, + }); + pass({ + url: "unknown-scheme://foo", + pattern: ["unknown-scheme://*"], + options: { restrictSchemes: false }, + }); + pass({ + url: "unknown-scheme://foo", + pattern: ["unknown-scheme:*"], + options: { restrictSchemes: false }, + }); + fail({ + url: "unknown-scheme://foo", + pattern: ["unknown-scheme:foo"], + options: { restrictSchemes: false }, + }); + fail({ + url: "unknown-scheme:foo", + pattern: ["unknown-scheme://foo"], + options: { restrictSchemes: false }, + }); + fail({ + url: "unknown-scheme:foo", + pattern: ["unknown-scheme://*"], + options: { restrictSchemes: false }, + }); + + // Matchers for IPv6 + pass({ url: "http://[::1]/", pattern: ["http://[::1]/"] }); + pass({ + url: "http://[2a03:4000:6:310e:216:3eff:fe53:99b]/", + pattern: ["http://[2a03:4000:6:310e:216:3eff:fe53:99b]/"], + }); + fail({ + url: "http://[2:4:6:3:2:3:f:b]/", + pattern: ["http://[2a03:4000:6:310e:216:3eff:fe53:99b]/"], + }); + + // Before fixing Bug 1529230, the only way to match a specific IPv6 url is by droping the brackets in pattern, + // thus we keep this pattern valid for the sake of backward compatibility + pass({ url: "http://[::1]/", pattern: ["http://::1/"] }); + pass({ + url: "http://[2a03:4000:6:310e:216:3eff:fe53:99b]/", + pattern: ["http://2a03:4000:6:310e:216:3eff:fe53:99b/"], + }); +}); + +add_task(async function test_MatchPattern_overlaps() { + function test(filter, hosts, optional) { + filter = Array.prototype.concat.call(filter); + hosts = Array.prototype.concat.call(hosts); + optional = Array.prototype.concat.call(optional); + + const set = new MatchPatternSet([...hosts, ...optional]); + const pat = new MatchPatternSet(filter); + return set.overlapsAll(pat); + } + + function pass({ filter = [], hosts = [], optional = [] }) { + ok( + test(filter, hosts, optional), + `Expected overlap: ${filter}, ${hosts} (${optional})` + ); + } + + function fail({ filter = [], hosts = [], optional = [] }) { + ok( + !test(filter, hosts, optional), + `Expected no overlap: ${filter}, ${hosts} (${optional})` + ); + } + + // Direct comparison. + pass({ hosts: "http://ab.cd/", filter: "http://ab.cd/" }); + fail({ hosts: "http://ab.cd/", filter: "ftp://ab.cd/" }); + + // Wildcard protocol. + pass({ hosts: "*://ab.cd/", filter: "https://ab.cd/" }); + fail({ hosts: "*://ab.cd/", filter: "ftp://ab.cd/" }); + + // Wildcard subdomain. + pass({ hosts: "http://*.ab.cd/", filter: "http://ab.cd/" }); + pass({ hosts: "http://*.ab.cd/", filter: "http://www.ab.cd/" }); + fail({ hosts: "http://*.ab.cd/", filter: "http://ab.cd.ef/" }); + fail({ hosts: "http://*.ab.cd/", filter: "http://www.cd/" }); + + // Wildcard subsumed. + pass({ hosts: "http://*.ab.cd/", filter: "http://*.cd/" }); + fail({ hosts: "http://*.cd/", filter: "http://*.xy/" }); + + // Subdomain vs substring. + fail({ hosts: "http://*.ab.cd/", filter: "http://fake-ab.cd/" }); + fail({ hosts: "http://*.ab.cd/", filter: "http://*.fake-ab.cd/" }); + + // Wildcard domain. + pass({ hosts: "http://*/", filter: "http://ab.cd/" }); + fail({ hosts: "http://*/", filter: "https://ab.cd/" }); + + // Wildcard wildcards. + pass({ hosts: "", filter: "ftp://ab.cd/" }); + fail({ hosts: "" }); + + // Multiple hosts. + pass({ hosts: ["http://ab.cd/"], filter: ["http://ab.cd/"] }); + pass({ hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.cd/" }); + pass({ hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.xy/" }); + fail({ hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.zz/" }); + + // Multiple Multiples. + pass({ + hosts: ["http://*.ab.cd/"], + filter: ["http://ab.cd/", "http://www.ab.cd/"], + }); + pass({ + hosts: ["http://ab.cd/", "http://ab.xy/"], + filter: ["http://ab.cd/", "http://ab.xy/"], + }); + fail({ + hosts: ["http://ab.cd/", "http://ab.xy/"], + filter: ["http://ab.cd/", "http://ab.zz/"], + }); + + // Optional. + pass({ hosts: [], optional: "http://ab.cd/", filter: "http://ab.cd/" }); + pass({ + hosts: "http://ab.cd/", + optional: "http://ab.xy/", + filter: ["http://ab.cd/", "http://ab.xy/"], + }); + fail({ + hosts: "http://ab.cd/", + optional: "https://ab.xy/", + filter: "http://ab.xy/", + }); +}); + +add_task(async function test_MatchGlob() { + function test(url, pattern) { + let m = new MatchGlob(pattern[0]); + return m.matches(Services.io.newURI(url).spec); + } + + function pass({ url, pattern }) { + ok( + test(url, pattern), + `Expected match: ${JSON.stringify(pattern)}, ${url}` + ); + } + + function fail({ url, pattern }) { + ok( + !test(url, pattern), + `Expected no match: ${JSON.stringify(pattern)}, ${url}` + ); + } + + let moz = "http://mozilla.org"; + + pass({ url: moz, pattern: ["*"] }); + pass({ url: moz, pattern: ["http://*"] }); + pass({ url: moz, pattern: ["*mozilla*"] }); + // pass({url: moz, pattern: ["*example*", "*mozilla*"]}); + + pass({ url: moz, pattern: ["*://*"] }); + pass({ url: "https://mozilla.org", pattern: ["*://*"] }); + + // Documentation example + pass({ + url: "http://www.example.com/foo/bar", + pattern: ["http://???.example.com/foo/*"], + }); + pass({ + url: "http://the.example.com/foo/", + pattern: ["http://???.example.com/foo/*"], + }); + fail({ + url: "http://my.example.com/foo/bar", + pattern: ["http://???.example.com/foo/*"], + }); + fail({ + url: "http://example.com/foo/", + pattern: ["http://???.example.com/foo/*"], + }); + fail({ + url: "http://www.example.com/foo", + pattern: ["http://???.example.com/foo/*"], + }); + + // Matches path + let path = moz + "/abc/def"; + pass({ url: path, pattern: ["*def"] }); + pass({ url: path, pattern: ["*c/d*"] }); + pass({ url: path, pattern: ["*org/abc*"] }); + fail({ url: path + "/", pattern: ["*def"] }); + + // Trailing slash + pass({ url: moz, pattern: ["*.org/"] }); + fail({ url: moz, pattern: ["*.org"] }); + + // Wrong TLD + fail({ url: moz, pattern: ["*oz*.com/"] }); + // Case sensitive + fail({ url: moz, pattern: ["*.ORG/"] }); +}); + +add_task(async function test_MatchGlob_redundant_wildcards_backtracking() { + const slow_build = + AppConstants.DEBUG || AppConstants.TSAN || AppConstants.ASAN; + const first_limit = slow_build ? 200 : 20; + { + // Bug 1570868 - repeated * in tabs.query glob causes too much backtracking. + let title = `Monster${"*".repeat(99)}Mash`; + + // The first run could take longer than subsequent runs, as the DFA is lazily created. + let first_start = Date.now(); + let glob = new MatchGlob(title); + let first_matches = glob.matches(title); + let first_duration = Date.now() - first_start; + ok(first_matches, `Expected match: ${title}, ${title}`); + ok( + first_duration < first_limit, + `First matching duration: ${first_duration}ms (limit: ${first_limit}ms)` + ); + + let start = Date.now(); + let matches = glob.matches(title); + let duration = Date.now() - start; + + ok(matches, `Expected match: ${title}, ${title}`); + ok(duration < 10, `Matching duration: ${duration}ms`); + } + { + // Similarly with any continuous combination of ?**???****? wildcards. + let title = `Monster${"?*".repeat(99)}Mash`; + + // The first run could take longer than subsequent runs, as the DFA is lazily created. + let first_start = Date.now(); + let glob = new MatchGlob(title); + let first_matches = glob.matches(title); + let first_duration = Date.now() - first_start; + ok(first_matches, `Expected match: ${title}, ${title}`); + ok( + first_duration < first_limit, + `First matching duration: ${first_duration}ms (limit: ${first_limit}ms)` + ); + + let start = Date.now(); + let matches = glob.matches(title); + let duration = Date.now() - start; + + ok(matches, `Expected match: ${title}, ${title}`); + ok(duration < 10, `Matching duration: ${duration}ms`); + } +}); + +add_task(async function test_MatchPattern_subsumes() { + function test(oldPat, newPat) { + let m = new MatchPatternSet(oldPat); + return m.subsumes(new MatchPattern(newPat)); + } + + function pass({ oldPat, newPat }) { + ok(test(oldPat, newPat), `${JSON.stringify(oldPat)} subsumes "${newPat}"`); + } + + function fail({ oldPat, newPat }) { + ok( + !test(oldPat, newPat), + `${JSON.stringify(oldPat)} doesn't subsume "${newPat}"` + ); + } + + pass({ oldPat: [""], newPat: "*://*/*" }); + pass({ oldPat: [""], newPat: "http://*/*" }); + pass({ oldPat: [""], newPat: "http://*.example.com/*" }); + + pass({ oldPat: ["*://*/*"], newPat: "http://*/*" }); + pass({ oldPat: ["*://*/*"], newPat: "wss://*/*" }); + pass({ oldPat: ["*://*/*"], newPat: "http://*.example.com/*" }); + + pass({ oldPat: ["*://*.example.com/*"], newPat: "http://*.example.com/*" }); + pass({ oldPat: ["*://*.example.com/*"], newPat: "*://sub.example.com/*" }); + + pass({ oldPat: ["https://*/*"], newPat: "https://*.example.com/*" }); + pass({ + oldPat: ["http://*.example.com/*"], + newPat: "http://subdomain.example.com/*", + }); + pass({ + oldPat: ["http://*.sub.example.com/*"], + newPat: "http://sub.example.com/*", + }); + pass({ + oldPat: ["http://*.sub.example.com/*"], + newPat: "http://sec.sub.example.com/*", + }); + pass({ + oldPat: ["http://www.example.com/*"], + newPat: "http://www.example.com/path/*", + }); + pass({ + oldPat: ["http://www.example.com/path/*"], + newPat: "http://www.example.com/*", + }); + + fail({ oldPat: ["*://*/*"], newPat: "" }); + fail({ oldPat: ["*://*/*"], newPat: "ftp://*/*" }); + fail({ oldPat: ["*://*/*"], newPat: "file://*/*" }); + + fail({ oldPat: ["http://example.com/*"], newPat: "*://example.com/*" }); + fail({ oldPat: ["http://example.com/*"], newPat: "https://example.com/*" }); + fail({ + oldPat: ["http://example.com/*"], + newPat: "http://otherexample.com/*", + }); + fail({ oldPat: ["http://example.com/*"], newPat: "http://*.example.com/*" }); + fail({ + oldPat: ["http://example.com/*"], + newPat: "http://subdomain.example.com/*", + }); + + fail({ + oldPat: ["http://subdomain.example.com/*"], + newPat: "http://example.com/*", + }); + fail({ + oldPat: ["http://subdomain.example.com/*"], + newPat: "http://*.example.com/*", + }); + fail({ + oldPat: ["http://sub.example.com/*"], + newPat: "http://*.sub.example.com/*", + }); + + fail({ oldPat: ["ws://example.com/*"], newPat: "wss://example.com/*" }); + fail({ oldPat: ["http://example.com/*"], newPat: "ws://example.com/*" }); + fail({ oldPat: ["https://example.com/*"], newPat: "wss://example.com/*" }); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_StorageSyncService.js b/toolkit/components/extensions/test/xpcshell/test_StorageSyncService.js new file mode 100644 index 0000000000..ef55ed37e8 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_StorageSyncService.js @@ -0,0 +1,274 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const NS_ERROR_DOM_QUOTA_EXCEEDED_ERR = 0x80530016; + +XPCOMUtils.defineLazyServiceGetter( + this, + "StorageSyncService", + "@mozilla.org/extensions/storage/sync;1", + "nsIInterfaceRequestor" +); + +function promisify(func, ...params) { + return new Promise((resolve, reject) => { + let changes = []; + func(...params, { + QueryInterface: ChromeUtils.generateQI([ + "mozIExtensionStorageListener", + "mozIExtensionStorageCallback", + "mozIBridgedSyncEngineCallback", + "mozIBridgedSyncEngineApplyCallback", + ]), + onChanged(extId, json) { + changes.push({ extId, changes: JSON.parse(json) }); + }, + handleSuccess(value) { + resolve({ + changes, + value: typeof value == "string" ? JSON.parse(value) : value, + }); + }, + handleError(code, message) { + reject(Components.Exception(message, code)); + }, + }); + }); +} + +add_task(async function setup_storage_sync() { + // So that we can write to the profile directory. + do_get_profile(); +}); + +add_task(async function test_storage_sync_service() { + const service = StorageSyncService.getInterface(Ci.mozIExtensionStorageArea); + { + let { changes, value } = await promisify( + service.set, + "ext-1", + JSON.stringify({ + hi: "hello! 💖", + bye: "adiós", + }) + ); + deepEqual( + changes, + [ + { + extId: "ext-1", + changes: { + hi: { + newValue: "hello! 💖", + }, + bye: { + newValue: "adiós", + }, + }, + }, + ], + "`set` should notify listeners about changes" + ); + ok(!value, "`set` should not return a value"); + } + + { + let { changes, value } = await promisify( + service.get, + "ext-1", + JSON.stringify(["hi"]) + ); + deepEqual(changes, [], "`get` should not notify listeners"); + deepEqual( + value, + { + hi: "hello! 💖", + }, + "`get` with key should return value" + ); + + let { value: allValues } = await promisify(service.get, "ext-1", "null"); + deepEqual( + allValues, + { + hi: "hello! 💖", + bye: "adiós", + }, + "`get` without a key should return all values" + ); + } + + { + await promisify( + service.set, + "ext-2", + JSON.stringify({ + hi: "hola! 👋", + }) + ); + await promisify(service.clear, "ext-1"); + let { value: allValues } = await promisify(service.get, "ext-1", "null"); + deepEqual(allValues, {}, "clear removed ext-1"); + + let { value: allValues2 } = await promisify(service.get, "ext-2", "null"); + deepEqual(allValues2, { hi: "hola! 👋" }, "clear didn't remove ext-2"); + // We need to clear data for ext-2 too, so later tests don't fail due to + // this data. + await promisify(service.clear, "ext-2"); + } +}); + +add_task(async function test_storage_sync_bridged_engine() { + const area = StorageSyncService.getInterface(Ci.mozIExtensionStorageArea); + const engine = StorageSyncService.getInterface(Ci.mozIBridgedSyncEngine); + + info("Add some local items"); + await promisify(area.set, "ext-1", JSON.stringify({ a: "abc" })); + await promisify(area.set, "ext-2", JSON.stringify({ b: "xyz" })); + + info("Start a sync"); + await promisify(engine.syncStarted); + + info("Store some incoming synced items"); + let incomingEnvelopesAsJSON = [ + { + id: "guidAAA", + modified: 0.1, + payload: JSON.stringify({ + extId: "ext-2", + data: JSON.stringify({ + c: 1234, + }), + }), + }, + { + id: "guidBBB", + modified: 0.1, + payload: JSON.stringify({ + extId: "ext-3", + data: JSON.stringify({ + d: "new! ✨", + }), + }), + }, + ].map(e => JSON.stringify(e)); + await promisify(area.storeIncoming, incomingEnvelopesAsJSON); + + info("Merge"); + // Three levels of JSON wrapping: each outgoing envelope, the cleartext in + // each envelope, and the extension storage data in each cleartext payload. + let { value: outgoingEnvelopesAsJSON } = await promisify(area.apply); + let outgoingEnvelopes = outgoingEnvelopesAsJSON.map(json => JSON.parse(json)); + let parsedCleartexts = outgoingEnvelopes.map(e => JSON.parse(e.payload)); + let parsedData = parsedCleartexts.map(c => JSON.parse(c.data)); + + let { changes } = await promisify( + area.QueryInterface(Ci.mozISyncedExtensionStorageArea) + .fetchPendingSyncChanges + ); + deepEqual( + changes, + [ + { + extId: "ext-2", + changes: { + c: { newValue: 1234 }, + }, + }, + { + extId: "ext-3", + changes: { + d: { newValue: "new! ✨" }, + }, + }, + ], + "Should return pending synced changes for observers" + ); + + // ext-1 doesn't exist remotely yet, so the Rust sync layer will generate + // a GUID for it. We don't know what it is, so we find it by the extension + // ID. + let ext1Index = parsedCleartexts.findIndex(c => c.extId == "ext-1"); + greater(ext1Index, -1, "Should find envelope for ext-1"); + let ext1Guid = outgoingEnvelopes[ext1Index].id; + + // ext-2 has a remote GUID that we set in the test above. + let ext2Index = outgoingEnvelopes.findIndex(c => c.id == "guidAAA"); + greater(ext2Index, -1, "Should find envelope for ext-2"); + + equal(outgoingEnvelopes.length, 2, "Should upload ext-1 and ext-2"); + deepEqual( + parsedData[ext1Index], + { + a: "abc", + }, + "Should upload new data for ext-1" + ); + deepEqual( + parsedData[ext2Index], + { + b: "xyz", + c: 1234, + }, + "Should merge local and remote data for ext-2" + ); + + info("Mark all extensions as uploaded"); + await promisify(engine.setUploaded, 0, [ext1Guid, "guidAAA"]); + + info("Finish sync"); + await promisify(engine.syncFinished); + + // Try fetching values for the remote-only extension we just synced. + let { value: ext3Value } = await promisify(area.get, "ext-3", "null"); + deepEqual( + ext3Value, + { + d: "new! ✨", + }, + "Should return new keys for ext-3" + ); + + info("Try applying a second time"); + let secondApply = await promisify(area.apply); + deepEqual(secondApply.value, {}, "Shouldn't merge anything on second apply"); + + info("Wipe all items"); + await promisify(engine.wipe); + + for (let extId of ["ext-1", "ext-2", "ext-3"]) { + // `get` always returns an object, even if there are no keys for the + // extension ID. + let { value } = await promisify(area.get, extId, "null"); + deepEqual(value, {}, `Wipe should remove all values for ${extId}`); + } +}); + +add_task(async function test_storage_sync_quota() { + const service = StorageSyncService.getInterface(Ci.mozIExtensionStorageArea); + const engine = StorageSyncService.getInterface(Ci.mozIBridgedSyncEngine); + await promisify(engine.wipe); + await promisify(service.set, "ext-1", JSON.stringify({ x: "hi" })); + await promisify(service.set, "ext-1", JSON.stringify({ longer: "value" })); + + let { value: v1 } = await promisify(service.getBytesInUse, "ext-1", '"x"'); + Assert.equal(v1, 5); // key len without quotes, value len with quotes. + let { value: v2 } = await promisify(service.getBytesInUse, "ext-1", "null"); + // 5 from 'x', plus 'longer' (6 for key, 7 for value = 13) = 18. + Assert.equal(v2, 18); + + // Now set something greater than our quota. + await Assert.rejects( + promisify( + service.set, + "ext-1", + JSON.stringify({ + big: "x".repeat(Ci.mozIExtensionStorageArea.SYNC_QUOTA_BYTES), + }) + ), + ex => ex.result == NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, + "should reject with NS_ERROR_DOM_QUOTA_EXCEEDED_ERR" + ); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js b/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js new file mode 100644 index 0000000000..2427c3c1be --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js @@ -0,0 +1,321 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { newURI } = Services.io; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerDirectory("/data/", do_get_file("data")); + +async function test_url_matching({ + manifestVersion = 2, + allowedOrigins = [], + checkPermissions, + expectMatches, +}) { + let policy = new WebExtensionPolicy({ + id: "foo@bar.baz", + mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2", + baseURL: "file:///foo", + + manifestVersion, + allowedOrigins: new MatchPatternSet(allowedOrigins), + localizeCallback() {}, + }); + + let contentScript = new WebExtensionContentScript(policy, { + checkPermissions, + + matches: new MatchPatternSet(["http://*.foo.com/bar", "*://bar.com/baz/*"]), + + excludeMatches: new MatchPatternSet(["*://bar.com/baz/quux"]), + + includeGlobs: ["*flerg*", "*.com/bar", "*/quux"].map( + glob => new MatchGlob(glob) + ), + + excludeGlobs: ["*glorg*"].map(glob => new MatchGlob(glob)), + }); + + equal( + expectMatches, + contentScript.matchesURI(newURI("http://www.foo.com/bar")), + `Simple matches include should ${expectMatches ? "" : "not "} match.` + ); + + equal( + expectMatches, + contentScript.matchesURI(newURI("https://bar.com/baz/xflergx")), + `Simple matches include should ${expectMatches ? "" : "not "} match.` + ); + + ok( + !contentScript.matchesURI(newURI("https://bar.com/baz/xx")), + "Failed includeGlobs match pattern should not match" + ); + + ok( + !contentScript.matchesURI(newURI("https://bar.com/baz/quux")), + "Excluded match pattern should not match" + ); + + ok( + !contentScript.matchesURI(newURI("https://bar.com/baz/xflergxglorgx")), + "Excluded match glob should not match" + ); +} + +add_task(function test_WebExtensionContentScript_urls_mv2() { + return test_url_matching({ manifestVersion: 2, expectMatches: true }); +}); + +add_task(function test_WebExtensionContentScript_urls_mv2_checkPermissions() { + return test_url_matching({ + manifestVersion: 2, + checkPermissions: true, + expectMatches: false, + }); +}); + +add_task(function test_WebExtensionContentScript_urls_mv2_with_permissions() { + return test_url_matching({ + manifestVersion: 2, + checkPermissions: true, + allowedOrigins: [""], + expectMatches: true, + }); +}); + +add_task(function test_WebExtensionContentScript_urls_mv3() { + // checkPermissions ignored here because it's forced for MV3. + return test_url_matching({ + manifestVersion: 3, + checkPermissions: false, + expectMatches: false, + }); +}); + +add_task(function test_WebExtensionContentScript_mv3_all_urls() { + return test_url_matching({ + manifestVersion: 3, + allowedOrigins: [""], + expectMatches: true, + }); +}); + +add_task(function test_WebExtensionContentScript_mv3_wildcards() { + return test_url_matching({ + manifestVersion: 3, + allowedOrigins: ["*://*.foo.com/*", "*://*.bar.com/*"], + expectMatches: true, + }); +}); + +add_task(function test_WebExtensionContentScript_mv3_specific() { + return test_url_matching({ + manifestVersion: 3, + allowedOrigins: ["http://www.foo.com/*", "https://bar.com/*"], + expectMatches: true, + }); +}); + +add_task(function test_WebExtensionContentScript_restricted() { + let tests = [ + { + manifestVersion: 2, + permissions: [], + expect: false, + }, + { + manifestVersion: 2, + permissions: ["mozillaAddons"], + expect: true, + }, + { + manifestVersion: 3, + permissions: [], + expect: false, + }, + { + manifestVersion: 3, + permissions: ["mozillaAddons"], + expect: true, + }, + ]; + + for (let { manifestVersion, permissions, expect } of tests) { + let policy = new WebExtensionPolicy({ + id: "foo@bar.baz", + mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2", + baseURL: "file:///foo", + + manifestVersion, + permissions, + allowedOrigins: new MatchPatternSet([""]), + localizeCallback() {}, + }); + let contentScript = new WebExtensionContentScript(policy, { + checkPermissions: true, + matches: new MatchPatternSet([""]), + }); + + // AMO is on the extensions.webextensions.restrictedDomains list. + equal( + expect, + contentScript.matchesURI(newURI("https://addons.mozilla.org/foo")), + `Expect extension with [${permissions}] to ${expect ? "" : "not"} match` + ); + } +}); + +async function test_frame_matching(meta) { + if (AppConstants.platform == "linux") { + // The windowless browser currently does not load correctly on Linux on + // infra. + return; + } + + let baseURL = `http://example.com/data`; + let urls = { + topLevel: `${baseURL}/file_toplevel.html`, + iframe: `${baseURL}/file_iframe.html`, + srcdoc: "about:srcdoc", + aboutBlank: "about:blank", + }; + + let contentPage = await ExtensionTestUtils.loadContentPage(urls.topLevel); + + let tests = [ + { + matches: ["http://example.com/data/*"], + contentScript: {}, + topLevel: true, + iframe: false, + aboutBlank: false, + srcdoc: false, + }, + + { + matches: ["http://example.com/data/*"], + contentScript: { + frameID: 0, + }, + topLevel: true, + iframe: false, + aboutBlank: false, + srcdoc: false, + }, + + { + matches: ["http://example.com/data/*"], + contentScript: { + allFrames: true, + }, + topLevel: true, + iframe: true, + aboutBlank: false, + srcdoc: false, + }, + + { + matches: ["http://example.com/data/*"], + contentScript: { + allFrames: true, + matchAboutBlank: true, + }, + topLevel: true, + iframe: true, + aboutBlank: true, + srcdoc: true, + }, + + { + matches: ["http://foo.com/data/*"], + contentScript: { + allFrames: true, + matchAboutBlank: true, + }, + topLevel: false, + iframe: false, + aboutBlank: false, + srcdoc: false, + }, + ]; + + // matchesWindowGlobal tests against content frames + await contentPage.spawn({ tests, urls, meta }, args => { + let { manifestVersion = 2, allowedOrigins = [], expectMatches } = args.meta; + + this.windows = new Map(); + this.windows.set(this.content.location.href, this.content); + for (let c of Array.from(this.content.frames)) { + this.windows.set(c.location.href, c); + } + this.policy = new WebExtensionPolicy({ + id: "foo@bar.baz", + mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2", + baseURL: "file:///foo", + + manifestVersion, + allowedOrigins: new MatchPatternSet(allowedOrigins), + localizeCallback() {}, + }); + + let tests = args.tests.map(t => { + t.contentScript.matches = new MatchPatternSet(t.matches); + t.script = new WebExtensionContentScript(this.policy, t.contentScript); + return t; + }); + for (let [i, test] of tests.entries()) { + for (let [frame, url] of Object.entries(args.urls)) { + let should = test[frame] ? "should" : "should not"; + let wgc = this.windows.get(url).windowGlobalChild; + Assert.equal( + test.script.matchesWindowGlobal(wgc), + test[frame] && expectMatches, + `Script ${i} ${should} match the ${frame} frame` + ); + } + } + }); + + await contentPage.close(); +} + +add_task(function test_WebExtensionContentScript_frames_mv2() { + return test_frame_matching({ + manifestVersion: 2, + expectMatches: true, + }); +}); + +add_task(function test_WebExtensionContentScript_frames_mv3() { + return test_frame_matching({ + manifestVersion: 3, + expectMatches: false, + }); +}); + +add_task(function test_WebExtensionContentScript_frames_mv3_all_urls() { + return test_frame_matching({ + manifestVersion: 3, + allowedOrigins: [""], + expectMatches: true, + }); +}); + +add_task(function test_WebExtensionContentScript_frames_mv3_wildcards() { + return test_frame_matching({ + manifestVersion: 3, + allowedOrigins: ["*://*.example.com/*"], + expectMatches: true, + }); +}); + +add_task(function test_WebExtensionContentScript_frames_mv3_specific() { + return test_frame_matching({ + manifestVersion: 3, + allowedOrigins: ["http://example.com/*"], + expectMatches: true, + }); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js b/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js new file mode 100644 index 0000000000..ff2cc3c2ac --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js @@ -0,0 +1,620 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { newURI } = Services.io; + +add_task(async function test_WebExtensionPolicy() { + const id = "foo@bar.baz"; + const uuid = "ca9d3f23-125c-4b24-abfc-1ca2692b0610"; + + const baseURL = "file:///foo/"; + const mozExtURL = `moz-extension://${uuid}/`; + const mozExtURI = newURI(mozExtURL); + + let policy = new WebExtensionPolicy({ + id, + mozExtensionHostname: uuid, + baseURL, + + localizeCallback(str) { + return `<${str}>`; + }, + + allowedOrigins: new MatchPatternSet(["http://foo.bar/", "*://*.baz/"], { + ignorePath: true, + }), + permissions: [""], + webAccessibleResources: [ + { + resources: ["/foo/*", "/bar.baz"].map(glob => new MatchGlob(glob)), + }, + ], + }); + + equal(policy.active, false, "Active attribute should initially be false"); + + // GetURL + + equal( + policy.getURL(), + mozExtURL, + "getURL() should return the correct root URL" + ); + equal( + policy.getURL("path/foo.html"), + `${mozExtURL}path/foo.html`, + "getURL(path) should return the correct URL" + ); + + // Permissions + + deepEqual( + policy.permissions, + [""], + "Initial permissions should be correct" + ); + + ok( + policy.hasPermission(""), + "hasPermission should match existing permission" + ); + ok( + !policy.hasPermission("history"), + "hasPermission should not match nonexistent permission" + ); + + Assert.throws( + () => { + policy.permissions[0] = "foo"; + }, + TypeError, + "Permissions array should be frozen" + ); + + policy.permissions = ["history"]; + deepEqual( + policy.permissions, + ["history"], + "Permissions should be updateable as a set" + ); + + ok( + policy.hasPermission("history"), + "hasPermission should match existing permission" + ); + ok( + !policy.hasPermission(""), + "hasPermission should not match nonexistent permission" + ); + + // Origins + + ok( + policy.canAccessURI(newURI("http://foo.bar/quux")), + "Should be able to access permitted URI" + ); + ok( + policy.canAccessURI(newURI("https://x.baz/foo")), + "Should be able to access permitted URI" + ); + + ok( + !policy.canAccessURI(newURI("https://foo.bar/quux")), + "Should not be able to access non-permitted URI" + ); + + policy.allowedOrigins = new MatchPatternSet(["https://foo.bar/"], { + ignorePath: true, + }); + + ok( + policy.canAccessURI(newURI("https://foo.bar/quux")), + "Should be able to access updated permitted URI" + ); + ok( + !policy.canAccessURI(newURI("https://x.baz/foo")), + "Should not be able to access removed permitted URI" + ); + + // Web-accessible resources + + ok( + policy.isWebAccessiblePath("/foo/bar"), + "Web-accessible glob should be web-accessible" + ); + ok( + policy.isWebAccessiblePath("/bar.baz"), + "Web-accessible path should be web-accessible" + ); + ok( + !policy.isWebAccessiblePath("/bar.baz/quux"), + "Non-web-accessible path should not be web-accessible" + ); + + ok( + policy.sourceMayAccessPath(mozExtURI, "/bar.baz"), + "Web-accessible path should be web-accessible to self" + ); + + // Localization + + equal( + policy.localize("foo"), + "", + "Localization callback should work as expected" + ); + + // Protocol and lookups. + + let proto = Services.io + .getProtocolHandler("moz-extension", uuid) + .QueryInterface(Ci.nsISubstitutingProtocolHandler); + + deepEqual( + WebExtensionPolicy.getActiveExtensions(), + [], + "Should have no active extensions" + ); + equal( + WebExtensionPolicy.getByID(id), + null, + "ID lookup should not return extension when not active" + ); + equal( + WebExtensionPolicy.getByHostname(uuid), + null, + "Hostname lookup should not return extension when not active" + ); + Assert.throws( + () => proto.resolveURI(mozExtURI), + /NS_ERROR_NOT_AVAILABLE/, + "URL should not resolve when not active" + ); + + policy.active = true; + equal(policy.active, true, "Active attribute should be updated"); + + let exts = WebExtensionPolicy.getActiveExtensions(); + equal(exts.length, 1, "Should have one active extension"); + equal(exts[0], policy, "Should have the correct active extension"); + + equal( + WebExtensionPolicy.getByID(id), + policy, + "ID lookup should return extension when active" + ); + equal( + WebExtensionPolicy.getByHostname(uuid), + policy, + "Hostname lookup should return extension when active" + ); + + equal( + proto.resolveURI(mozExtURI), + baseURL, + "URL should resolve correctly while active" + ); + + policy.active = false; + equal(policy.active, false, "Active attribute should be updated"); + + deepEqual( + WebExtensionPolicy.getActiveExtensions(), + [], + "Should have no active extensions" + ); + equal( + WebExtensionPolicy.getByID(id), + null, + "ID lookup should not return extension when not active" + ); + equal( + WebExtensionPolicy.getByHostname(uuid), + null, + "Hostname lookup should not return extension when not active" + ); + Assert.throws( + () => proto.resolveURI(mozExtURI), + /NS_ERROR_NOT_AVAILABLE/, + "URL should not resolve when not active" + ); + + // Conflicting policies. + + // This asserts in debug builds, so only test in non-debug builds. + if (!AppConstants.DEBUG) { + policy.active = true; + + let attrs = [ + { id, uuid }, + { id, uuid: "d916886c-cfdf-482e-b7b1-d7f5b0facfa5" }, + { id: "foo@quux", uuid }, + ]; + + // eslint-disable-next-line no-shadow + for (let { id, uuid } of attrs) { + let policy2 = new WebExtensionPolicy({ + id, + mozExtensionHostname: uuid, + baseURL: "file://bar/", + + localizeCallback() {}, + + allowedOrigins: new MatchPatternSet([]), + }); + + Assert.throws( + () => { + policy2.active = true; + }, + /NS_ERROR_UNEXPECTED/, + `Should not be able to activate conflicting policy: ${id} ${uuid}` + ); + } + + policy.active = false; + } +}); + +// mozExtensionHostname is normalized to lower case when using +// policy.getURL whereas using policy.getByHostname does +// not. Tests below will fail without case insensitive +// comparisons in ExtensionPolicyService +add_task(async function test_WebExtensionPolicy_case_sensitivity() { + const id = "policy-case@mochitest"; + const uuid = "BAD93A23-125C-4B24-ABFC-1CA2692B0610"; + + const baseURL = "file:///foo/"; + const mozExtURL = `moz-extension://${uuid}/`; + const mozExtURI = newURI(mozExtURL); + + let policy = new WebExtensionPolicy({ + id: id, + mozExtensionHostname: uuid, + baseURL, + localizeCallback() {}, + allowedOrigins: new MatchPatternSet([]), + permissions: [""], + }); + policy.active = true; + + equal( + WebExtensionPolicy.getByHostname(uuid)?.mozExtensionHostname, + policy.mozExtensionHostname, + "Hostname lookup should match policy" + ); + + equal( + WebExtensionPolicy.getByHostname(uuid.toLowerCase())?.mozExtensionHostname, + policy.mozExtensionHostname, + "Hostname lookup should match policy" + ); + + equal(policy.getURL(), mozExtURI.spec, "Urls should match policy"); + ok( + policy.sourceMayAccessPath(mozExtURI, "/bar.baz"), + "Extension path should be accessible to self" + ); + + policy.active = false; +}); + +add_task(async function test_WebExtensionPolicy_V3() { + const id = "foo@bar.baz"; + const uuid = "ca9d3f23-125c-4b24-abfc-1ca2692b0610"; + const id2 = "foo-2@bar.baz"; + const uuid2 = "89383c45-7db4-4999-83f7-f4cc246372cd"; + const id3 = "foo-3@bar.baz"; + const uuid3 = "56652231-D7E2-45D1-BDBD-BD3BFF80927E"; + + const baseURL = "file:///foo/"; + const mozExtURL = `moz-extension://${uuid}/`; + const mozExtURI = newURI(mozExtURL); + const fooSite = newURI("http://foo.bar/"); + const exampleSite = newURI("https://example.com/"); + + let policy = new WebExtensionPolicy({ + id, + mozExtensionHostname: uuid, + baseURL, + manifestVersion: 3, + + localizeCallback(str) { + return `<${str}>`; + }, + + allowedOrigins: new MatchPatternSet(["http://foo.bar/", "*://*.baz/"], { + ignorePath: true, + }), + permissions: [""], + webAccessibleResources: [ + { + resources: ["/foo/*", "/bar.baz"].map(glob => new MatchGlob(glob)), + matches: ["http://foo.bar/"], + extension_ids: [id3], + }, + { + resources: ["/foo.bar.baz"].map(glob => new MatchGlob(glob)), + extension_ids: ["*"], + }, + ], + }); + policy.active = true; + equal( + WebExtensionPolicy.getByHostname(uuid), + policy, + "Hostname lookup should match policy" + ); + + let policy2 = new WebExtensionPolicy({ + id: id2, + mozExtensionHostname: uuid2, + baseURL, + localizeCallback() {}, + allowedOrigins: new MatchPatternSet([]), + permissions: [""], + }); + policy2.active = true; + equal( + WebExtensionPolicy.getByHostname(uuid2), + policy2, + "Hostname lookup should match policy" + ); + + let policy3 = new WebExtensionPolicy({ + id: id3, + mozExtensionHostname: uuid3, + baseURL, + localizeCallback() {}, + allowedOrigins: new MatchPatternSet([]), + permissions: [""], + }); + policy3.active = true; + equal( + WebExtensionPolicy.getByHostname(uuid3), + policy3, + "Hostname lookup should match policy" + ); + + ok( + policy.isWebAccessiblePath("/bar.baz"), + "Web-accessible path should be web-accessible" + ); + ok( + !policy.isWebAccessiblePath("/bar.baz/quux"), + "Non-web-accessible path should not be web-accessible" + ); + // Extension can always access itself + ok( + policy.sourceMayAccessPath(mozExtURI, "/bar.baz"), + "Web-accessible path should be accessible to self" + ); + ok( + policy.sourceMayAccessPath(mozExtURI, "/foo.bar.baz"), + "Web-accessible path should be accessible to self" + ); + + ok( + !policy.sourceMayAccessPath(newURI(`https://${uuid}/`), "/bar.baz"), + "Web-accessible path should not be accessible due to scheme mismatch" + ); + + // non-matching site cannot access url + ok( + policy.sourceMayAccessPath(fooSite, "/bar.baz"), + "Web-accessible path should be accessible to foo.bar site" + ); + ok( + !policy.sourceMayAccessPath(fooSite, "/foo.bar.baz"), + "Web-accessible path should not be accessible to foo.bar site" + ); + + // non-matching site cannot access url + ok( + !policy.sourceMayAccessPath(exampleSite, "/bar.baz"), + "Web-accessible path should not be accessible to example.com" + ); + ok( + !policy.sourceMayAccessPath(exampleSite, "/foo.bar.baz"), + "Web-accessible path should not be accessible to example.com" + ); + + let extURI = newURI(policy2.getURL("")); + ok( + !policy.sourceMayAccessPath(extURI, "/bar.baz"), + "Web-accessible path should not be accessible to other extension" + ); + ok( + policy.sourceMayAccessPath(extURI, "/foo.bar.baz"), + "Web-accessible path should be accessible to other extension" + ); + + extURI = newURI(policy3.getURL("")); + ok( + policy.sourceMayAccessPath(extURI, "/bar.baz"), + "Web-accessible path should be accessible to other extension" + ); + ok( + policy.sourceMayAccessPath(extURI, "/foo.bar.baz"), + "Web-accessible path should be accessible to other extension" + ); + + policy.active = false; + policy2.active = false; + policy3.active = false; +}); + +add_task(async function test_WebExtensionPolicy_registerContentScripts() { + const id = "foo@bar.baz"; + const uuid = "77a7b9d3-e73c-4cf3-97fb-1824868fe00f"; + + const id2 = "foo-2@bar.baz"; + const uuid2 = "89383c45-7db4-4999-83f7-f4cc246372cd"; + + const baseURL = "file:///foo/"; + + const mozExtURL = `moz-extension://${uuid}/`; + const mozExtURL2 = `moz-extension://${uuid2}/`; + + let policy = new WebExtensionPolicy({ + id, + mozExtensionHostname: uuid, + baseURL, + localizeCallback() {}, + allowedOrigins: new MatchPatternSet([]), + permissions: [""], + }); + + let policy2 = new WebExtensionPolicy({ + id: id2, + mozExtensionHostname: uuid2, + baseURL, + localizeCallback() {}, + allowedOrigins: new MatchPatternSet([]), + permissions: [""], + }); + + let script1 = new WebExtensionContentScript(policy, { + run_at: "document_end", + js: [`${mozExtURL}/registered-content-script.js`], + matches: new MatchPatternSet(["http://localhost/data/*"]), + }); + + let script2 = new WebExtensionContentScript(policy, { + run_at: "document_end", + css: [`${mozExtURL}/registered-content-style.css`], + matches: new MatchPatternSet(["http://localhost/data/*"]), + }); + + let script3 = new WebExtensionContentScript(policy2, { + run_at: "document_end", + css: [`${mozExtURL2}/registered-content-style.css`], + matches: new MatchPatternSet(["http://localhost/data/*"]), + }); + + deepEqual( + policy.contentScripts, + [], + "The policy contentScripts is initially empty" + ); + + policy.registerContentScript(script1); + + deepEqual( + policy.contentScripts, + [script1], + "script1 has been added to the policy contentScripts" + ); + + Assert.throws( + () => policy.registerContentScript(script1), + e => e.result == Cr.NS_ERROR_ILLEGAL_VALUE, + "Got the expected NS_ERROR_ILLEGAL_VALUE when trying to register a script more than once" + ); + + Assert.throws( + () => policy.registerContentScript(script3), + e => e.result == Cr.NS_ERROR_ILLEGAL_VALUE, + "Got the expected NS_ERROR_ILLEGAL_VALUE when trying to register a script related to " + + "a different extension" + ); + + Assert.throws( + () => policy.unregisterContentScript(script3), + e => e.result == Cr.NS_ERROR_ILLEGAL_VALUE, + "Got the expected NS_ERROR_ILLEGAL_VALUE when trying to unregister a script related to " + + "a different extension" + ); + + deepEqual( + policy.contentScripts, + [script1], + "script1 has not been added twice" + ); + + policy.registerContentScript(script2); + + deepEqual( + policy.contentScripts, + [script1, script2], + "script2 has the last item of the policy contentScripts array" + ); + + policy.unregisterContentScript(script1); + + deepEqual( + policy.contentScripts, + [script2], + "script1 has been removed from the policy contentscripts" + ); + + Assert.throws( + () => policy.unregisterContentScript(script1), + e => e.result == Cr.NS_ERROR_ILLEGAL_VALUE, + "Got the expected NS_ERROR_ILLEGAL_VALUE when trying to unregister a script more than once" + ); + + deepEqual( + policy.contentScripts, + [script2], + "the policy contentscripts is unmodified when unregistering an unknown contentScript" + ); + + policy.unregisterContentScript(script2); + + deepEqual( + policy.contentScripts, + [], + "script2 has been removed from the policy contentScripts" + ); +}); + +add_task(async function test_WebExtensionPolicy_static_themes_resources() { + const uuid = "0e7ae607-b5b3-4204-9838-c2138c14bc3c"; + const mozExtURL = `moz-extension://${uuid}/`; + const mozExtURI = newURI(mozExtURL); + + let policy = new WebExtensionPolicy({ + id: "test-extension@mochitest", + mozExtensionHostname: uuid, + baseURL: "file:///foo/foo/", + localizeCallback() {}, + allowedOrigins: new MatchPatternSet([]), + permissions: [], + }); + policy.active = true; + + let staticThemePolicy = new WebExtensionPolicy({ + id: "statictheme@bar.baz", + mozExtensionHostname: "164d05dc-b45b-4731-aefc-7c1691bae9a4", + baseURL: "file:///static_theme/", + type: "theme", + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, + }); + + staticThemePolicy.active = true; + + ok( + staticThemePolicy.sourceMayAccessPath(mozExtURI, "/someresource.ext"), + "Active extensions should be allowed to access the static themes resources" + ); + + policy.active = false; + + ok( + !staticThemePolicy.sourceMayAccessPath(mozExtURI, "/someresource.ext"), + "Disabled extensions should be disallowed the static themes resources" + ); + + ok( + !staticThemePolicy.sourceMayAccessPath( + Services.io.newURI("http://example.com"), + "/someresource.ext" + ), + "Web content should be disallowed the static themes resources" + ); + + staticThemePolicy.active = false; +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_change_remote_mode.js b/toolkit/components/extensions/test/xpcshell/test_change_remote_mode.js new file mode 100644 index 0000000000..a6d22e8703 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_change_remote_mode.js @@ -0,0 +1,20 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function change_remote() { + let remote = Services.prefs.getBoolPref("extensions.webextensions.remote"); + Assert.equal( + WebExtensionPolicy.useRemoteWebExtensions, + remote, + "value of useRemoteWebExtensions matches the pref" + ); + + Services.prefs.setBoolPref("extensions.webextensions.remote", !remote); + + Assert.equal( + WebExtensionPolicy.useRemoteWebExtensions, + remote, + "value of useRemoteWebExtensions is still the same after changing the pref" + ); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js new file mode 100644 index 0000000000..c860d73cc8 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js @@ -0,0 +1,303 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +const ADDON_ID = "test@web.extension"; + +const aps = Cc["@mozilla.org/addons/policy-service;1"].getService( + Ci.nsIAddonPolicyService +); + +const v2_csp = Preferences.get( + "extensions.webextensions.base-content-security-policy" +); +const v3_csp = Preferences.get( + "extensions.webextensions.base-content-security-policy.v3" +); + +add_task(async function test_invalid_addon_csp() { + await Assert.throws( + () => aps.getBaseCSP("invalid@missing"), + /NS_ERROR_ILLEGAL_VALUE/, + "no base csp for non-existent addon" + ); + await Assert.throws( + () => aps.getExtensionPageCSP("invalid@missing"), + /NS_ERROR_ILLEGAL_VALUE/, + "no extension page csp for non-existent addon" + ); +}); + +add_task(async function test_policy_csp() { + equal( + aps.defaultCSP, + Preferences.get("extensions.webextensions.default-content-security-policy"), + "Expected default CSP value" + ); + + const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp"; + + let tests = [ + { + name: "manifest version 2, no custom policy", + policyData: {}, + expectedPolicy: aps.defaultCSP, + }, + { + name: "manifest version 2, no custom policy", + policyData: { + manifestVersion: 2, + }, + expectedPolicy: aps.defaultCSP, + }, + { + name: "version 2 custom extension policy", + policyData: { + extensionPageCSP: CUSTOM_POLICY, + }, + expectedPolicy: CUSTOM_POLICY, + }, + { + name: "manifest version 2 set, custom extension policy", + policyData: { + manifestVersion: 2, + extensionPageCSP: CUSTOM_POLICY, + }, + expectedPolicy: CUSTOM_POLICY, + }, + { + name: "manifest version 3, no custom policy", + policyData: { + manifestVersion: 3, + }, + expectedPolicy: aps.defaultCSPV3, + }, + { + name: "manifest 3 version set, custom extensionPage policy", + policyData: { + manifestVersion: 3, + extensionPageCSP: CUSTOM_POLICY, + }, + expectedPolicy: CUSTOM_POLICY, + }, + ]; + + let policy = null; + + function setExtensionCSP({ manifestVersion, extensionPageCSP }) { + if (policy) { + policy.active = false; + } + + policy = new WebExtensionPolicy({ + id: ADDON_ID, + mozExtensionHostname: ADDON_ID, + baseURL: "file:///", + + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, + + manifestVersion, + extensionPageCSP, + }); + + policy.active = true; + } + + for (let test of tests) { + info(test.name); + setExtensionCSP(test.policyData); + equal( + aps.getBaseCSP(ADDON_ID), + test.policyData.manifestVersion == 3 ? v3_csp : v2_csp, + "baseCSP is correct" + ); + equal( + aps.getExtensionPageCSP(ADDON_ID), + test.expectedPolicy, + "extensionPageCSP is correct" + ); + } +}); + +add_task(async function test_extension_csp() { + Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + + ExtensionTestUtils.failOnSchemaWarnings(false); + + let extension_pages = "script-src 'self'; img-src 'none'"; + + let tests = [ + { + name: "manifest_v2 invalid csp results in default csp used", + manifest: { + content_security_policy: `script-src 'none'`, + }, + expectedPolicy: aps.defaultCSP, + }, + { + name: "manifest_v2 allows https protocol", + manifest: { + manifest_version: 2, + content_security_policy: `script-src 'self' https://example.com`, + }, + expectedPolicy: `script-src 'self' https://example.com`, + }, + { + name: "manifest_v2 allows unsafe-eval", + manifest: { + manifest_version: 2, + content_security_policy: `script-src 'self' 'unsafe-eval'`, + }, + expectedPolicy: `script-src 'self' 'unsafe-eval'`, + }, + { + name: "manifest_v2 allows wasm-unsafe-eval", + manifest: { + manifest_version: 2, + content_security_policy: `script-src 'self' 'wasm-unsafe-eval'`, + }, + expectedPolicy: `script-src 'self' 'wasm-unsafe-eval'`, + }, + { + // object-src used to require local sources, but now we accept anything. + name: "manifest_v2 allows object-src, with non-local sources", + manifest: { + manifest_version: 2, + content_security_policy: `script-src 'self'; object-src https:'`, + }, + expectedPolicy: `script-src 'self'; object-src https:'`, + }, + { + name: "manifest_v3 invalid csp results in default csp used", + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'none'`, + }, + }, + expectedPolicy: aps.defaultCSPV3, + }, + { + name: "manifest_v3 forbidden protocol results in default csp used", + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'self' https://*`, + }, + }, + expectedPolicy: aps.defaultCSPV3, + }, + { + name: "manifest_v3 forbidden eval results in default csp used", + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'self' 'unsafe-eval'`, + }, + }, + expectedPolicy: aps.defaultCSPV3, + }, + { + name: "manifest_v3 disallows localhost", + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'self' https://localhost`, + }, + }, + expectedPolicy: aps.defaultCSPV3, + }, + { + name: "manifest_v3 disallows 127.0.0.1", + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'self' https://127.0.0.1`, + }, + }, + expectedPolicy: aps.defaultCSPV3, + }, + { + name: "manifest_v3 allows wasm-unsafe-eval", + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'self' 'wasm-unsafe-eval'`, + }, + }, + expectedPolicy: `script-src 'self' 'wasm-unsafe-eval'`, + }, + { + // object-src used to require local sources, but now we accept anything. + name: "manifest_v3 allows object-src, with non-local sources", + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'self'; object-src https:'`, + }, + }, + expectedPolicy: `script-src 'self'; object-src https:'`, + }, + { + name: "manifest_v2 csp", + manifest: { + manifest_version: 2, + content_security_policy: extension_pages, + }, + expectedPolicy: extension_pages, + }, + { + name: "manifest_v2 with no csp, expect default", + manifest: { + manifest_version: 2, + }, + expectedPolicy: aps.defaultCSP, + }, + { + name: "manifest_v3 used with no csp, expect default", + manifest: { + manifest_version: 3, + }, + expectedPolicy: aps.defaultCSPV3, + }, + { + name: "manifest_v3 syntax used", + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages, + }, + }, + expectedPolicy: extension_pages, + }, + ]; + + for (let test of tests) { + info(test.name); + let extension = ExtensionTestUtils.loadExtension({ + manifest: test.manifest, + }); + await extension.startup(); + let policy = WebExtensionPolicy.getByID(extension.id); + equal( + policy.baseCSP, + test.manifest.manifest_version == 3 ? v3_csp : v2_csp, + "baseCSP is correct" + ); + equal( + policy.extensionPageCSP, + test.expectedPolicy, + "extensionPageCSP is correct." + ); + await extension.unload(); + } + + ExtensionTestUtils.failOnSchemaWarnings(true); + + Services.prefs.clearUserPref("extensions.manifestV3.enabled"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_validator.js b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js new file mode 100644 index 0000000000..12ba3f93e9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js @@ -0,0 +1,322 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const cps = Cc["@mozilla.org/addons/content-policy;1"].getService( + Ci.nsIAddonContentPolicy +); + +add_task(async function test_csp_validator_flags() { + let checkPolicy = (policy, flags, expectedResult, message = null) => { + info(`Checking policy: ${policy}`); + + let result = cps.validateAddonCSP(policy, flags); + equal(result, expectedResult); + }; + + let flags = Ci.nsIAddonContentPolicy; + + checkPolicy( + "default-src 'self'; script-src 'self' http://localhost", + 0, + "\u2018script-src\u2019 directive contains a forbidden http: protocol source", + "localhost disallowed" + ); + checkPolicy( + "default-src 'self'; script-src 'self' http://localhost", + flags.CSP_ALLOW_LOCALHOST, + null, + "localhost allowed" + ); + + checkPolicy( + "default-src 'self'; script-src 'self' 'unsafe-eval'", + 0, + "\u2018script-src\u2019 directive contains a forbidden 'unsafe-eval' keyword", + "eval disallowed" + ); + checkPolicy( + "default-src 'self'; script-src 'self' 'unsafe-eval'", + flags.CSP_ALLOW_EVAL, + null, + "eval allowed" + ); + + checkPolicy( + "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'", + 0, + "\u2018script-src\u2019 directive contains a forbidden 'wasm-unsafe-eval' keyword", + "wasm disallowed" + ); + checkPolicy( + "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'", + flags.CSP_ALLOW_WASM, + null, + "wasm allowed" + ); + checkPolicy( + "default-src 'self'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'", + flags.CSP_ALLOW_EVAL, + null, + "wasm and eval allowed" + ); + + checkPolicy( + "default-src 'self'; script-src 'self' https://example.com", + 0, + "\u2018script-src\u2019 directive contains a forbidden https: protocol source", + "remote disallowed" + ); + checkPolicy( + "default-src 'self'; script-src 'self' https://example.com", + flags.CSP_ALLOW_REMOTE, + null, + "remote allowed" + ); +}); + +add_task(async function test_csp_validator() { + let checkPolicy = (policy, expectedResult, message = null) => { + info(`Checking policy: ${policy}`); + + let result = cps.validateAddonCSP( + policy, + Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY + ); + equal(result, expectedResult); + }; + + checkPolicy("script-src 'self';", null); + + // In the past, object-src was required to be secure and defaulted to 'self'. + // But that is no longer required (see bug 1766881). + checkPolicy("script-src 'self'; object-src 'self';", null); + checkPolicy("script-src 'self'; object-src https:;", null); + + let hash = + "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='"; + + checkPolicy( + `script-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash} 'unsafe-eval'; ` + + `object-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash}`, + null + ); + + checkPolicy( + "", + "Policy is missing a required \u2018script-src\u2019 directive" + ); + + checkPolicy( + "object-src 'none';", + "Policy is missing a required \u2018script-src\u2019 directive" + ); + + checkPolicy( + "default-src 'self' http:", + "Policy is missing a required \u2018script-src\u2019 directive", + "A strict default-src is required as a fallback if script-src is missing" + ); + + checkPolicy( + "default-src 'self' http:; script-src 'self'", + null, + "A valid script-src removes the need for a strict default-src fallback" + ); + + checkPolicy( + "default-src 'self'", + null, + "A valid default-src should count as a valid script-src" + ); + + checkPolicy( + "default-src 'self'; script-src 'self'", + null, + "A valid default-src should count as a valid script-src" + ); + + checkPolicy( + "default-src 'self'; script-src http://example.com", + "\u2018script-src\u2019 directive contains a forbidden http: protocol source", + "A valid default-src should not allow an invalid script-src directive" + ); + + checkPolicy( + "script-src 'none'", + "\u2018script-src\u2019 must include the source 'self'" + ); + + checkPolicy( + "script-src 'self' 'unsafe-inline'", + "\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword" + ); + + // Localhost is always valid + for (let src of [ + "http://localhost", + "https://localhost", + "http://127.0.0.1", + "https://127.0.0.1", + ]) { + checkPolicy(`script-src 'self' ${src};`, null); + } + + let directives = ["script-src", "worker-src"]; + + for (let [directive, other] of [directives, directives.slice().reverse()]) { + for (let src of ["https://*", "https://*.blogspot.com", "https://*"]) { + checkPolicy( + `${directive} 'self' ${src}; ${other} 'self';`, + `https: wildcard sources in \u2018${directive}\u2019 directives must include at least one non-generic sub-domain (e.g., *.example.com rather than *.com)` + ); + } + + for (let protocol of ["http", "https"]) { + checkPolicy( + `${directive} 'self' ${protocol}:; ${other} 'self';`, + `${protocol}: protocol requires a host in \u2018${directive}\u2019 directives` + ); + } + + checkPolicy( + `${directive} 'self' http://example.com; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden http: protocol source` + ); + + for (let protocol of ["ftp", "meh"]) { + checkPolicy( + `${directive} 'self' ${protocol}:; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source` + ); + } + + checkPolicy( + `${directive} 'self' 'nonce-01234'; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden 'nonce-*' keyword` + ); + } +}); + +add_task(async function test_csp_validator_extension_pages() { + let checkPolicy = (policy, expectedResult, message = null) => { + info(`Checking policy: ${policy}`); + + // While Schemas.jsm uses Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM, we don't + // pass that here because we are only verifying that remote scripts are + // blocked here. + let result = cps.validateAddonCSP(policy, 0); + equal(result, expectedResult); + }; + + checkPolicy("script-src 'self';", null); + checkPolicy("script-src 'self'; worker-src 'none'", null); + checkPolicy("script-src 'self'; worker-src 'self'", null); + + // In the past, object-src was required to be secure and defaulted to 'self'. + // But that is no longer required (see bug 1766881). + checkPolicy("script-src 'self'; object-src 'self';", null); + checkPolicy("script-src 'self'; object-src https:;", null); + + let hash = + "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='"; + + checkPolicy( + `script-src 'self' moz-extension://09abcdef blob: filesystem: ${hash}; `, + null + ); + + for (let policy of ["", "script-src-elem 'none';", "worker-src 'none';"]) { + checkPolicy( + policy, + "Policy is missing a required \u2018script-src\u2019 directive" + ); + } + + checkPolicy( + "default-src 'self' http:; script-src 'self'", + null, + "A valid script-src removes the need for a strict default-src fallback" + ); + + checkPolicy( + "default-src 'self'", + null, + "A valid default-src should count as a valid script-src" + ); + + for (let directive of ["script-src", "worker-src"]) { + checkPolicy( + `default-src 'self'; ${directive} 'self'`, + null, + `A valid default-src should count as a valid ${directive}` + ); + checkPolicy( + `default-src 'self'; ${directive} http://example.com`, + `\u2018${directive}\u2019 directive contains a forbidden http: protocol source`, + `A valid default-src should not allow an invalid ${directive} directive` + ); + } + + checkPolicy( + "script-src 'none'", + "\u2018script-src\u2019 must include the source 'self'" + ); + + checkPolicy( + "script-src 'self' 'unsafe-inline';", + "\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword" + ); + + checkPolicy( + "script-src 'self' 'unsafe-eval';", + "\u2018script-src\u2019 directive contains a forbidden 'unsafe-eval' keyword" + ); + + // Localhost is invalid + for (let src of [ + "http://localhost", + "https://localhost", + "http://127.0.0.1", + "https://127.0.0.1", + ]) { + const protocol = src.split(":")[0]; + checkPolicy( + `script-src 'self' ${src};`, + `\u2018script-src\u2019 directive contains a forbidden ${protocol}: protocol source` + ); + } + + let directives = ["script-src", "worker-src"]; + + for (let [directive, other] of [directives, directives.slice().reverse()]) { + for (let protocol of ["http", "https"]) { + checkPolicy( + `${directive} 'self' ${protocol}:; ${other} 'self';`, + `${protocol}: protocol requires a host in \u2018${directive}\u2019 directives` + ); + } + + checkPolicy( + `${directive} 'self' https://example.com; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden https: protocol source` + ); + + checkPolicy( + `${directive} 'self' http://example.com; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden http: protocol source` + ); + + for (let protocol of ["ftp", "meh"]) { + checkPolicy( + `${directive} 'self' ${protocol}:; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source` + ); + } + + checkPolicy( + `${directive} 'self' 'nonce-01234'; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden 'nonce-*' keyword` + ); + } +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_MessageManagerProxy.js b/toolkit/components/extensions/test/xpcshell/test_ext_MessageManagerProxy.js new file mode 100644 index 0000000000..07b688b406 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_MessageManagerProxy.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"; + +const { MessageManagerProxy } = ChromeUtils.import( + "resource://gre/modules/MessageManagerProxy.jsm" +); +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +class TestMessageManagerProxy extends MessageManagerProxy { + constructor(contentPage, identifier) { + super(contentPage.browser); + this.identifier = identifier; + this.contentPage = contentPage; + this.deferred = null; + } + + // Registers message listeners. Call dispose() once you've finished. + async setupPingPongListeners() { + await this.contentPage.loadFrameScript(`() => { + this.addMessageListener("test:MessageManagerProxy:Ping", ({data}) => { + this.sendAsyncMessage("test:MessageManagerProxy:Pong", "${this.identifier}:" + data); + }); + }`); + + // Register the listener here instead of during testPingPong, to make sure + // that the listener is correctly registered during the whole test. + this.addMessageListener("test:MessageManagerProxy:Pong", event => { + ok( + this.deferred, + `[${this.identifier}] expected to be waiting for ping-pong` + ); + this.deferred.resolve(event.data); + this.deferred = null; + }); + } + + async testPingPong(description) { + equal(this.deferred, null, "should not be waiting for a message"); + this.deferred = PromiseUtils.defer(); + this.sendAsyncMessage("test:MessageManagerProxy:Ping", description); + let result = await this.deferred.promise; + equal(result, `${this.identifier}:${description}`, "Expected ping-pong"); + } +} + +// Tests that MessageManagerProxy continues to proxy messages after docshells +// have been swapped. +add_task(async function test_message_after_swapdocshells() { + let page1 = await ExtensionTestUtils.loadContentPage("about:blank"); + let page2 = await ExtensionTestUtils.loadContentPage("about:blank"); + + let testProxyOne = new TestMessageManagerProxy(page1, "page1"); + let testProxyTwo = new TestMessageManagerProxy(page2, "page2"); + + await testProxyOne.setupPingPongListeners(); + await testProxyTwo.setupPingPongListeners(); + + await testProxyOne.testPingPong("after setup (to 1)"); + await testProxyTwo.testPingPong("after setup (to 2)"); + + page1.browser.swapDocShells(page2.browser); + + await testProxyOne.testPingPong("after docshell swap (to 1)"); + await testProxyTwo.testPingPong("after docshell swap (to 2)"); + + // Swap again to verify that listeners are repeatedly moved when needed. + page1.browser.swapDocShells(page2.browser); + + await testProxyOne.testPingPong("after another docshell swap (to 1)"); + await testProxyTwo.testPingPong("after another docshell swap (to 2)"); + + // Verify that dispose() works regardless of the browser's validity. + await testProxyOne.dispose(); + await page1.close(); + await page2.close(); + await testProxyTwo.dispose(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_activityLog.js b/toolkit/components/extensions/test/xpcshell/test_ext_activityLog.js new file mode 100644 index 0000000000..c60b24b2b4 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_activityLog.js @@ -0,0 +1,77 @@ +"use strict"; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.usePrivilegedSignatures = false; +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" +); + +add_setup(async () => { + await AddonTestUtils.promiseStartupManager(); +}); + +// This test should produce a warning, but still startup +add_task(async function test_api_restricted() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "activityLog-permission@tests.mozilla.org" }, + }, + permissions: ["activityLog"], + }, + async background() { + browser.test.assertEq( + undefined, + browser.activityLog, + "activityLog is privileged" + ); + }, + useAddonManager: "permanent", + }); + await extension.startup(); + await extension.unload(); +}); + +// This test should produce a error and not startup +add_task( + { + // Some builds (e.g. thunderbird) have experiments enabled by default. + pref_set: [["extensions.experiments.enabled", false]], + }, + async function test_api_restricted_temporary_without_privilege() { + let extension = ExtensionTestUtils.loadExtension({ + temporarilyInstalled: true, + isPrivileged: false, + manifest: { + browser_specific_settings: { + gecko: { id: "activityLog-permission@tests.mozilla.org" }, + }, + permissions: ["activityLog"], + }, + }); + 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 'activityLog' requires a privileged add-on/, + }, + ], + }, + true + ); + } +); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_private_field_xrays.js b/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_private_field_xrays.js new file mode 100644 index 0000000000..2d8b02bcd9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_private_field_xrays.js @@ -0,0 +1,160 @@ +"use strict"; + +// ExtensionContent.jsm needs to know when it's running from xpcshell, +// to use the right timeout for content scripts executed at document_idle. +ExtensionTestUtils.mockAppInfo(); +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; + +add_task(async function test_contentscript_private_field_xrays() { + async function contentScript() { + let node = window.document.createElement("div"); + + class Base { + constructor(o) { + return o; + } + } + + class A extends Base { + #x = 5; + static gx(o) { + return o.#x; + } + static sx(o, v) { + o.#x = v; + } + } + + browser.test.log(A.toString()); + + // Stamp node with A's private field. + new A(node); + + browser.test.log("stamped"); + + browser.test.assertEq( + A.gx(node), + 5, + "We should be able to see our expando private field" + ); + browser.test.log("Read"); + browser.test.assertThrows( + () => A.gx(node.wrappedJSObject), + /can't access private field or method/, + "Underlying object should not have our private field" + ); + + browser.test.log("threw"); + window.frames[0].document.adoptNode(node); + browser.test.log("adopted"); + browser.test.assertEq( + A.gx(node), + 5, + "Adoption should not change expando private field" + ); + browser.test.log("read"); + browser.test.assertThrows( + () => A.gx(node.wrappedJSObject), + /can't access private field or method/, + "Adoption should really not change expandos private fields" + ); + browser.test.log("threw2"); + + // Repeat but now with an object that has a reference from the + // window it's being cloned into. + node = window.document.createElement("div"); + // Stamp node with A's private field. + new A(node); + A.sx(node, 6); + + browser.test.assertEq( + A.gx(node), + 6, + "We should be able to see our expando (2)" + ); + browser.test.assertThrows( + () => A.gx(node.wrappedJSObject), + /can't access private field or method/, + "Underlying object should not have exxpando. (2)" + ); + + window.frames[0].wrappedJSObject.incoming = node.wrappedJSObject; + window.frames[0].document.adoptNode(node); + + browser.test.assertEq( + A.gx(node), + 6, + "We should be able to see our expando (3)" + ); + browser.test.assertThrows( + () => A.gx(node.wrappedJSObject), + /can't access private field or method/, + "Underlying object should not have exxpando. (3)" + ); + + // Repeat once more, now with an expando that refers to the object itself + node = window.document.createElement("div"); + new A(node); + A.sx(node, node); + + browser.test.assertEq( + A.gx(node), + node, + "We should be able to see our self-referential expando (4)" + ); + browser.test.assertThrows( + () => A.gx(node.wrappedJSObject), + /can't access private field or method/, + "Underlying object should not have exxpando. (4)" + ); + + window.frames[0].document.adoptNode(node); + + browser.test.assertEq( + A.gx(node), + node, + "Adoption should not change our self-referential expando (4)" + ); + browser.test.assertThrows( + () => A.gx(node.wrappedJSObject), + /can't access private field or method/, + "Adoption should not change underlying object. (4)" + ); + + // And test what happens if we now set document.domain and cause + // wrapper remapping. + let doc = window.frames[0].document; + // eslint-disable-next-line no-self-assign + doc.domain = doc.domain; + + browser.test.notifyPass("privateFieldXRayAdoption"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://*/*/file_toplevel.html"], + js: ["content_script.js"], + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }); + + await extension.startup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + `${BASE_URL}/file_toplevel.html` + ); + + await extension.awaitFinish("privateFieldXRayAdoption"); + + await contentPage.close(); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_xrays.js b/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_xrays.js new file mode 100644 index 0000000000..9655c157d1 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_xrays.js @@ -0,0 +1,129 @@ +"use strict"; + +// ExtensionContent.jsm needs to know when it's running from xpcshell, +// to use the right timeout for content scripts executed at document_idle. +ExtensionTestUtils.mockAppInfo(); +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; + +add_task(async function test_contentscript_xrays() { + async function contentScript() { + let node = window.document.createElement("div"); + node.expando = 5; + + browser.test.assertEq( + node.expando, + 5, + "We should be able to see our expando" + ); + browser.test.assertEq( + node.wrappedJSObject.expando, + undefined, + "Underlying object should not have our expando" + ); + + window.frames[0].document.adoptNode(node); + browser.test.assertEq( + node.expando, + 5, + "Adoption should not change expandos" + ); + browser.test.assertEq( + node.wrappedJSObject.expando, + undefined, + "Adoption should really not change expandos" + ); + + // Repeat but now with an object that has a reference from the + // window it's being cloned into. + node = window.document.createElement("div"); + node.expando = 6; + + browser.test.assertEq( + node.expando, + 6, + "We should be able to see our expando (2)" + ); + browser.test.assertEq( + node.wrappedJSObject.expando, + undefined, + "Underlying object should not have our expando (2)" + ); + + window.frames[0].wrappedJSObject.incoming = node.wrappedJSObject; + + window.frames[0].document.adoptNode(node); + browser.test.assertEq( + node.expando, + 6, + "Adoption should not change expandos (2)" + ); + browser.test.assertEq( + node.wrappedJSObject.expando, + undefined, + "Adoption should really not change expandos (2)" + ); + + // Repeat once more, now with an expando that refers to the object itself. + node = window.document.createElement("div"); + node.expando = node; + + browser.test.assertEq( + node.expando, + node, + "We should be able to see our self-referential expando (3)" + ); + browser.test.assertEq( + node.wrappedJSObject.expando, + undefined, + "Underlying object should not have our self-referential expando (3)" + ); + + window.frames[0].document.adoptNode(node); + browser.test.assertEq( + node.expando, + node, + "Adoption should not change self-referential expando (3)" + ); + browser.test.assertEq( + node.wrappedJSObject.expando, + undefined, + "Adoption should really not change self-referential expando (3)" + ); + + // And test what happens if we now set document.domain and cause + // wrapper remapping. + let doc = window.frames[0].document; + // eslint-disable-next-line no-self-assign + doc.domain = doc.domain; + + browser.test.notifyPass("contentScriptAdoptionWithXrays"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://*/*/file_toplevel.html"], + js: ["content_script.js"], + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }); + + await extension.startup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + `${BASE_URL}/file_toplevel.html` + ); + + await extension.awaitFinish("contentScriptAdoptionWithXrays"); + + await contentPage.close(); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js new file mode 100644 index 0000000000..892a82e2e3 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js @@ -0,0 +1,346 @@ +/* -*- 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"; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "43" +); + +add_task( + { + // TODO(Bug 1725478): remove the skip if once webidl API bindings will be hidden based on permissions. + skip_if: () => ExtensionTestUtils.isInBackgroundServiceWorkerTests(), + }, + async function test_alarm_without_permissions() { + function backgroundScript() { + browser.test.assertTrue( + !browser.alarms, + "alarm API is not available when the alarm permission is not required" + ); + browser.test.notifyPass("alarms_permission"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: [], + }, + }); + + await extension.startup(); + await extension.awaitFinish("alarms_permission"); + await extension.unload(); + } +); + +add_task(async function test_alarm_clear_non_matching_name() { + async function backgroundScript() { + let ALARM_NAME = "test_ext_alarms"; + + browser.alarms.create(ALARM_NAME, { when: Date.now() + 2000000 }); + + let wasCleared = await browser.alarms.clear(ALARM_NAME + "1"); + browser.test.assertFalse(wasCleared, "alarm was not cleared"); + + let alarms = await browser.alarms.getAll(); + browser.test.assertEq(1, alarms.length, "alarm was not removed"); + browser.test.notifyPass("alarm-clear"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("alarm-clear"); + await extension.unload(); +}); + +add_task(async function test_alarm_get_and_clear_single_argument() { + async function backgroundScript() { + browser.alarms.create({ when: Date.now() + 2000000 }); + + let alarm = await browser.alarms.get(); + browser.test.assertEq("", alarm.name, "expected alarm returned"); + + let wasCleared = await browser.alarms.clear(); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + let alarms = await browser.alarms.getAll(); + browser.test.assertEq(0, alarms.length, "alarm was removed"); + + browser.test.notifyPass("alarm-single-arg"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("alarm-single-arg"); + await extension.unload(); +}); + +// This test case covers the behavior of browser.alarms.create when the +// first optional argument (the alarm name) is passed explicitly as null +// or undefined instead of being omitted. +add_task(async function test_alarm_name_arg_null_or_undefined() { + async function backgroundScript(alarmName) { + browser.alarms.create(alarmName, { when: Date.now() + 2000000 }); + + let alarm = await browser.alarms.get(); + browser.test.assertTrue(alarm, "got an alarm"); + browser.test.assertEq("", alarm.name, "expected alarm returned"); + + let wasCleared = await browser.alarms.clear(); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + let alarms = await browser.alarms.getAll(); + browser.test.assertEq(0, alarms.length, "alarm was removed"); + + browser.test.notifyPass("alarm-test-done"); + } + + for (const alarmName of [null, undefined]) { + info(`Test alarm.create with alarm name ${alarmName}`); + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})(${alarmName})`, + manifest: { + permissions: ["alarms"], + }, + }); + await extension.startup(); + await extension.awaitFinish("alarm-test-done"); + await extension.unload(); + } +}); + +add_task(async function test_get_get_all_clear_all_alarms() { + async function backgroundScript() { + const ALARM_NAME = "test_alarm"; + + let suffixes = [0, 1, 2]; + + for (let suffix of suffixes) { + browser.alarms.create(ALARM_NAME + suffix, { + when: Date.now() + (suffix + 1) * 10000, + }); + } + + let alarms = await browser.alarms.getAll(); + browser.test.assertEq( + suffixes.length, + alarms.length, + "expected number of alarms were found" + ); + alarms.forEach((alarm, index) => { + browser.test.assertEq( + ALARM_NAME + index, + alarm.name, + "alarm has the expected name" + ); + }); + + for (let suffix of suffixes) { + let alarm = await browser.alarms.get(ALARM_NAME + suffix); + browser.test.assertEq( + ALARM_NAME + suffix, + alarm.name, + "alarm has the expected name" + ); + browser.test.sendMessage(`get-${suffix}`); + } + + let wasCleared = await browser.alarms.clear(ALARM_NAME + suffixes[0]); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + alarms = await browser.alarms.getAll(); + browser.test.assertEq(2, alarms.length, "alarm was removed"); + + let alarm = await browser.alarms.get(ALARM_NAME + suffixes[0]); + browser.test.assertEq(undefined, alarm, "non-existent alarm is undefined"); + browser.test.sendMessage(`get-invalid`); + + wasCleared = await browser.alarms.clearAll(); + browser.test.assertTrue(wasCleared, "alarms were cleared"); + + alarms = await browser.alarms.getAll(); + browser.test.assertEq(0, alarms.length, "no alarms exist"); + browser.test.sendMessage("clearAll"); + browser.test.sendMessage("clear"); + browser.test.sendMessage("getAll"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + await Promise.all([ + extension.startup(), + extension.awaitMessage("getAll"), + extension.awaitMessage("get-0"), + extension.awaitMessage("get-1"), + extension.awaitMessage("get-2"), + extension.awaitMessage("clear"), + extension.awaitMessage("get-invalid"), + extension.awaitMessage("clearAll"), + ]); + await extension.unload(); +}); + +function getAlarmExtension(alarmCreateOptions, extOpts = {}) { + info( + `Test alarms.create fires with options: ${JSON.stringify( + alarmCreateOptions + )}` + ); + + function backgroundScript(createOptions) { + let ALARM_NAME = "test_ext_alarms"; + let timer; + + browser.alarms.onAlarm.addListener(alarm => { + browser.test.assertEq( + ALARM_NAME, + alarm.name, + "alarm has the expected name" + ); + clearTimeout(timer); + browser.test.sendMessage("alarms-create-with-options"); + }); + + browser.alarms.create(ALARM_NAME, createOptions); + + timer = setTimeout(async () => { + browser.test.fail("alarm fired within expected time"); + let wasCleared = await browser.alarms.clear(ALARM_NAME); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + browser.test.sendMessage("alarms-create-with-options"); + }, 10000); + } + + let { persistent, useAddonManager } = extOpts; + return ExtensionTestUtils.loadExtension({ + useAddonManager, + // Pass the alarms.create options to the background page. + background: `(${backgroundScript})(${JSON.stringify(alarmCreateOptions)})`, + manifest: { + permissions: ["alarms"], + background: { persistent }, + }, + }); +} + +async function test_alarm_fires_with_options(alarmCreateOptions) { + let extension = getAlarmExtension(alarmCreateOptions); + + await extension.startup(); + await extension.awaitMessage("alarms-create-with-options"); + + // Defer unloading the extension so the asynchronous event listener + // reply finishes. + await new Promise(resolve => setTimeout(resolve, 0)); + await extension.unload(); +} + +add_task(async function test_alarm_fires() { + Services.prefs.setBoolPref( + "privacy.resistFingerprinting.reduceTimerPrecision.jitter", + false + ); + + await test_alarm_fires_with_options({ delayInMinutes: 0.01 }); + await test_alarm_fires_with_options({ when: Date.now() + 1000 }); + await test_alarm_fires_with_options({ delayInMinutes: -10 }); + await test_alarm_fires_with_options({ when: Date.now() - 1000 }); + + Services.prefs.clearUserPref( + "privacy.resistFingerprinting.reduceTimerPrecision.jitter" + ); +}); + +function trackEvents(wrapper) { + let events = new Map(); + for (let event of ["background-script-event", "start-background-script"]) { + events.set(event, false); + wrapper.extension.once(event, () => events.set(event, true)); + } + return events; +} + +add_task( + { + // TODO(Bug 1748665): remove the skip once background service worker is also + // woken up by persistent listeners. + skip_if: () => ExtensionTestUtils.isInBackgroundServiceWorkerTests(), + pref_set: [ + ["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false], + ["extensions.eventPages.enabled", true], + ], + }, + async function test_alarm_persists() { + await AddonTestUtils.promiseStartupManager(); + + let extension = getAlarmExtension( + { periodInMinutes: 0.01 }, + { useAddonManager: "permanent", persistent: false } + ); + info(`wait startup`); + await extension.startup(); + assertPersistentListeners(extension, "alarms", "onAlarm", { + primed: false, + }); + info(`wait first alarm`); + await extension.awaitMessage("alarms-create-with-options"); + + await extension.terminateBackground({ disableResetIdleForTest: true }); + ok( + !extension.extension.backgroundContext, + "Background Extension context should have been destroyed" + ); + + assertPersistentListeners(extension, "alarms", "onAlarm", { + primed: true, + }); + + // Test an early startup event + let events = trackEvents(extension); + ok( + !events.get("background-script-event"), + "Should not have received a background script event" + ); + ok( + !events.get("start-background-script"), + "Background script should not be started" + ); + + info("waiting for alarm to wake background"); + await extension.awaitMessage("alarms-create-with-options"); + ok( + events.get("background-script-event"), + "Should have received a background script event" + ); + ok( + events.get("start-background-script"), + "Background script should be started" + ); + + await extension.unload(); + await AddonTestUtils.promiseShutdownManager(); + } +); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js new file mode 100644 index 0000000000..fe385004ba --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js @@ -0,0 +1,34 @@ +/* -*- 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_cleared_alarm_does_not_fire() { + async function backgroundScript() { + let ALARM_NAME = "test_ext_alarms"; + + browser.alarms.onAlarm.addListener(alarm => { + browser.test.fail("cleared alarm does not fire"); + browser.test.notifyFail("alarm-cleared"); + }); + browser.alarms.create(ALARM_NAME, { when: Date.now() + 1000 }); + + let wasCleared = await browser.alarms.clear(ALARM_NAME); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + browser.test.notifyPass("alarm-cleared"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("alarm-cleared"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js new file mode 100644 index 0000000000..b78d6da649 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js @@ -0,0 +1,50 @@ +/* -*- 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_periodic_alarm_fires() { + function backgroundScript() { + const ALARM_NAME = "test_ext_alarms"; + let count = 0; + let timer; + + browser.alarms.onAlarm.addListener(alarm => { + browser.test.assertEq( + alarm.name, + ALARM_NAME, + "alarm has the expected name" + ); + if (count++ === 3) { + clearTimeout(timer); + browser.alarms.clear(ALARM_NAME).then(wasCleared => { + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + browser.test.notifyPass("alarm-periodic"); + }); + } + }); + + browser.alarms.create(ALARM_NAME, { periodInMinutes: 0.02 }); + + timer = setTimeout(async () => { + browser.test.fail("alarm fired expected number of times"); + + let wasCleared = await browser.alarms.clear(ALARM_NAME); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + browser.test.notifyFail("alarm-periodic"); + }, 30000); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("alarm-periodic"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js new file mode 100644 index 0000000000..0d7597fa5a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.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_duplicate_alarm_name_replaces_alarm() { + function backgroundScript() { + let count = 0; + + browser.alarms.onAlarm.addListener(async alarm => { + browser.test.assertEq( + "replaced alarm", + alarm.name, + "Expected last alarm" + ); + browser.test.assertEq( + 0, + count++, + "duplicate named alarm replaced existing alarm" + ); + let results = await browser.alarms.getAll(); + + // "replaced alarm" is expected to be replaced with a non-repeating + // alarm, so it should not appear in the list of alarms. + browser.test.assertEq(1, results.length, "exactly one alarms exists"); + browser.test.assertEq( + "unrelated alarm", + results[0].name, + "remaining alarm has the expected name" + ); + + browser.test.notifyPass("alarm-duplicate"); + }); + + // Alarm that is so far in the future that it is never triggered. + browser.alarms.create("unrelated alarm", { delayInMinutes: 60 }); + // Alarm that repeats. + browser.alarms.create("replaced alarm", { + delayInMinutes: 1 / 60, + periodInMinutes: 1 / 60, + }); + // Before the repeating alarm is triggered, it is immediately replaced with + // a non-repeating alarm. + browser.alarms.create("replaced alarm", { delayInMinutes: 3 / 60 }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("alarm-duplicate"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js new file mode 100644 index 0000000000..2a13a295a9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js @@ -0,0 +1,75 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { Management } = ChromeUtils.import( + "resource://gre/modules/Extension.jsm" +); +function getNextContext() { + return new Promise(resolve => { + Management.on("proxy-context-load", function listener(type, context) { + Management.off("proxy-context-load", listener); + resolve(context); + }); + }); +} + +add_task(async function test_storage_api_without_permissions() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + // Force API initialization. + try { + browser.storage.onChanged.addListener(() => {}); + } catch (e) { + // Ignore. + } + }, + + manifest: { + permissions: [], + }, + }); + + let contextPromise = getNextContext(); + await extension.startup(); + + let context = await contextPromise; + + // Force API initialization. + void context.apiObj; + + ok( + !("storage" in context.apiObj), + "The storage API should not be initialized" + ); + + await extension.unload(); +}); + +add_task(async function test_storage_api_with_permissions() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.storage.onChanged.addListener(() => {}); + }, + + manifest: { + permissions: ["storage"], + }, + }); + + let contextPromise = getNextContext(); + await extension.startup(); + + let context = await contextPromise; + + // Force API initialization. + void context.apiObj; + + equal( + typeof context.apiObj.storage, + "object", + "The storage API should be initialized" + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_asyncAPICall_isHandlingUserInput.js b/toolkit/components/extensions/test/xpcshell/test_ext_asyncAPICall_isHandlingUserInput.js new file mode 100644 index 0000000000..6c0ccd860d --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_asyncAPICall_isHandlingUserInput.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExtensionAPI } = ExtensionCommon; + +const API_CLASS = class extends ExtensionAPI { + getAPI(context) { + return { + testMockAPI: { + async anAsyncAPIMethod(...args) { + const callContextDataBeforeAwait = context.callContextData; + await Promise.resolve(); + const callContextDataAfterAwait = context.callContextData; + return { + args, + callContextDataBeforeAwait, + callContextDataAfterAwait, + }; + }, + }, + }; + } +}; + +const API_SCRIPT = ` + this.testMockAPI = ${API_CLASS.toString()}; +`; + +const API_SCHEMA = [ + { + namespace: "testMockAPI", + functions: [ + { + name: "anAsyncAPIMethod", + type: "function", + async: true, + parameters: [ + { + name: "param1", + type: "object", + additionalProperties: { + type: "string", + }, + }, + { + name: "param2", + type: "string", + }, + ], + }, + ], + }, +]; + +const MODULE_INFO = { + testMockAPI: { + schema: `data:,${JSON.stringify(API_SCHEMA)}`, + scopes: ["addon_parent"], + paths: [["testMockAPI"]], + url: URL.createObjectURL(new Blob([API_SCRIPT])), + }, +}; + +add_setup(async function() { + // The blob:-URL registered above in MODULE_INFO gets loaded at + // https://searchfox.org/mozilla-central/rev/0fec57c05d3996cc00c55a66f20dd5793a9bfb5d/toolkit/components/extensions/ExtensionCommon.jsm#1649 + Services.prefs.setBoolPref( + "security.allow_parent_unrestricted_js_loads", + true + ); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads"); + }); + + ExtensionParent.apiManager.registerModules(MODULE_INFO); +}); + +add_task( + async function test_propagated_isHandlingUserInput_on_async_api_methods_calls() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "@test-ext" } }, + }, + background() { + browser.test.onMessage.addListener(async (msg, args) => { + if (msg !== "async-method-call") { + browser.test.fail(`Unexpected test message: ${msg}`); + return; + } + + try { + let result = await browser.testMockAPI.anAsyncAPIMethod(...args); + browser.test.sendMessage("async-method-call:result", result); + } catch (err) { + browser.test.sendMessage("async-method-call:error", err.message); + } + }); + }, + }); + + await extension.startup(); + + const callArgs = [{ param1: "param1" }, "param2"]; + + info("Test API method called without handling user input"); + + extension.sendMessage("async-method-call", callArgs); + const result = await extension.awaitMessage("async-method-call:result"); + Assert.deepEqual( + result?.args, + callArgs, + "Got the expected parameters when called without handling user input" + ); + Assert.deepEqual( + result?.callContextDataBeforeAwait, + { isHandlingUserInput: false }, + "Got the expected callContextData before awaiting on a promise" + ); + Assert.deepEqual( + result?.callContextDataAfterAwait, + null, + "context.callContextData should have been nullified after awaiting on a promise" + ); + + await withHandlingUserInput(extension, async () => { + extension.sendMessage("async-method-call", callArgs); + const result = await extension.awaitMessage("async-method-call:result"); + Assert.deepEqual( + result?.args, + callArgs, + "Got the expected parameters when called while handling user input" + ); + Assert.deepEqual( + result?.callContextDataBeforeAwait, + { isHandlingUserInput: true }, + "Got the expected callContextData before awaiting on a promise" + ); + Assert.deepEqual( + result?.callContextDataAfterAwait, + null, + "context.callContextData should have been nullified after awaiting on a promise" + ); + }); + + await extension.unload(); + } +); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_api_injection.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_api_injection.js new file mode 100644 index 0000000000..a603b03a29 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_api_injection.js @@ -0,0 +1,35 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerDirectory("/data/", do_get_file("data")); + +add_task(async function testBackgroundWindow() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.log("background script executed"); + window.location = + "http://example.com/data/file_privilege_escalation.html"; + }, + }); + + let awaitConsole = new Promise(resolve => { + Services.console.registerListener(function listener(message) { + if (/WebExt Privilege Escalation/.test(message.message)) { + Services.console.unregisterListener(listener); + resolve(message); + } + }); + }); + + await extension.startup(); + + let message = await awaitConsole; + ok( + message.message.includes( + "WebExt Privilege Escalation: typeof(browser) = undefined" + ), + "Document does not have `browser` APIs." + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js new file mode 100644 index 0000000000..61157cba52 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.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"; + +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "43" +); + +let { + promiseRestartManager, + promiseShutdownManager, + promiseStartupManager, +} = AddonTestUtils; + +const { Management } = ChromeUtils.import( + "resource://gre/modules/Extension.jsm" +); + +// Crashes a 's remote process. +// Based on BrowserTestUtils.crashFrame. +function crashFrame(browser) { + if (!browser.isRemoteBrowser) { + // The browser should be remote, or the test runner would be killed. + throw new Error(" must be remote"); + } + + // Trigger crash by sending a message to BrowserTestUtils actor. + BrowserTestUtils.sendAsyncMessage( + browser.browsingContext, + "BrowserTestUtils:CrashFrame", + {} + ); +} + +// Verifies that a delayed background page is not loaded when an extension is +// shut down during startup. +add_task(async function test_unload_extension_before_background_page_startup() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background() { + browser.test.sendMessage("background_startup_observed"); + }, + }); + + // Delayed startup are only enabled for browser (re)starts, so we need to + // install the extension first, and then unload it. + + await extension.startup(); + await extension.awaitMessage("background_startup_observed"); + + // Now the actual test: Unloading an extension before the startup has + // finished should interrupt the start-up and abort pending delayed loads. + info("Starting extension whose startup will be interrupted"); + await promiseRestartManager({ earlyStartup: false }); + await extension.awaitStartup(); + + let extensionBrowserInsertions = 0; + let onExtensionBrowserInserted = () => ++extensionBrowserInsertions; + Management.on("extension-browser-inserted", onExtensionBrowserInserted); + + info("Unloading extension before the delayed background page starts loading"); + await extension.addon.disable(); + + // Re-enable the add-on to let enough time pass to load a whole background + // page. If at the end of this the original background page hasn't loaded, + // we can consider the test successful. + await extension.addon.enable(); + + // Trigger the notification that would load a background page. + info("Forcing pending delayed background page to load"); + AddonTestUtils.notifyLateStartup(); + + // This is the expected message from the re-enabled add-on. + await extension.awaitMessage("background_startup_observed"); + await extension.unload(); + + await promiseShutdownManager(); + + Management.off("extension-browser-inserted", onExtensionBrowserInserted); + Assert.equal( + extensionBrowserInsertions, + 1, + "Extension browser should have been inserted only once" + ); +}); + +// Verifies that the "build" method of BackgroundPage in ext-backgroundPage.js +// does not deadlock when startup is interrupted by extension shutdown. +add_task(async function test_unload_extension_during_background_page_startup() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background() { + browser.test.sendMessage("background_starting"); + }, + }); + + // Delayed startup are only enabled for browser (re)starts, so we need to + // install the extension first, and then reload it. + await extension.startup(); + await extension.awaitMessage("background_starting"); + + await promiseRestartManager({ lateStartup: false }); + await extension.awaitStartup(); + + let bgStartupPromise = new Promise(resolve => { + function onBackgroundPageDone(eventName) { + extension.extension.off( + "background-script-started", + onBackgroundPageDone + ); + extension.extension.off( + "background-script-aborted", + onBackgroundPageDone + ); + + if (eventName === "background-script-aborted") { + info("Background script startup was interrupted"); + resolve("bg_aborted"); + } else { + info("Background script startup finished normally"); + resolve("bg_fully_loaded"); + } + } + extension.extension.on("background-script-started", onBackgroundPageDone); + extension.extension.on("background-script-aborted", onBackgroundPageDone); + }); + + let bgStartingPromise = new Promise(resolve => { + let backgroundLoadCount = 0; + let backgroundPageUrl = extension.extension.baseURI.resolve( + "_generated_background_page.html" + ); + + // Prevent the background page from actually loading. + Management.once("extension-browser-inserted", (eventName, browser) => { + // Intercept background page load. + let browserLoadURI = browser.loadURI; + browser.loadURI = function() { + Assert.equal(++backgroundLoadCount, 1, "loadURI should be called once"); + Assert.equal( + arguments[0], + backgroundPageUrl, + "Expected background page" + ); + // Reset to "about:blank" to not load the actual background page. + arguments[0] = "about:blank"; + browserLoadURI.apply(this, arguments); + + // And force the extension process to crash. + if (browser.isRemote) { + crashFrame(browser); + } else { + // If extensions are not running in out-of-process mode, then the + // non-remote process should not be killed (or the test runner dies). + // Remove instead, to simulate the immediate disconnection + // of the message manager (that would happen if the process crashed). + browser.remove(); + } + resolve(); + }; + }); + }); + + // Force background page to initialize. + AddonTestUtils.notifyLateStartup(); + await bgStartingPromise; + + await extension.unload(); + await promiseShutdownManager(); + + // This part is the regression test for bug 1501375. It verifies that the + // background building completes eventually. + // If it does not, then the next line will cause a timeout. + info("Waiting for background builder to finish"); + let bgLoadState = await bgStartupPromise; + Assert.equal(bgLoadState, "bg_aborted", "Startup should be interrupted"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js new file mode 100644 index 0000000000..cac574b8ca --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js @@ -0,0 +1,23 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(async function test_DOMContentLoaded_in_generated_background_page() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + function reportListener(event) { + browser.test.sendMessage("eventname", event.type); + } + document.addEventListener("DOMContentLoaded", reportListener); + window.addEventListener("load", reportListener); + }, + }); + + await extension.startup(); + equal("DOMContentLoaded", await extension.awaitMessage("eventname")); + equal("load", await extension.awaitMessage("eventname")); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js new file mode 100644 index 0000000000..a22db9d582 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js @@ -0,0 +1,24 @@ +/* -*- 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_reload_generated_background_page() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + if (location.hash !== "#firstrun") { + browser.test.sendMessage("first run"); + location.hash = "#firstrun"; + browser.test.assertEq("#firstrun", location.hash); + location.reload(); + } else { + browser.test.notifyPass("second run"); + } + }, + }); + + await extension.startup(); + await extension.awaitMessage("first run"); + await extension.awaitFinish("second run"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js new file mode 100644 index 0000000000..19a918eff9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js @@ -0,0 +1,24 @@ +/* -*- 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" +); + +add_task(async function test_global_history() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.sendMessage("background-loaded", location.href); + }, + }); + + await extension.startup(); + + let backgroundURL = await extension.awaitMessage("background-loaded"); + + await extension.unload(); + + let exists = await PlacesTestUtils.isPageInDB(backgroundURL); + ok(!exists, "Background URL should not be in history database"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js new file mode 100644 index 0000000000..9ce80f3fda --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.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 test_background_incognito() { + info( + "Test background page incognito value with permanent private browsing enabled" + ); + + Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.privatebrowsing.autostart"); + }); + + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + async background() { + browser.test.assertEq( + window, + browser.extension.getBackgroundPage(), + "Caller should be able to access itself as a background page" + ); + browser.test.assertEq( + window, + await browser.runtime.getBackgroundPage(), + "Caller should be able to access itself as a background page" + ); + + browser.test.assertEq( + browser.extension.inIncognitoContext, + true, + "inIncognitoContext is true for permanent private browsing" + ); + + browser.test.notifyPass("incognito"); + }, + }); + + await extension.startup(); + + await extension.awaitFinish("incognito"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js new file mode 100644 index 0000000000..aa0976434b --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.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"; + +function backgroundScript() { + let received_ports_number = 0; + + const expected_received_ports_number = 1; + + function countReceivedPorts(port) { + received_ports_number++; + + if (port.name == "check-results") { + browser.runtime.onConnect.removeListener(countReceivedPorts); + + browser.test.assertEq( + expected_received_ports_number, + received_ports_number, + "invalid connect should not create a port" + ); + + browser.test.notifyPass("runtime.connect invalid params"); + } + } + + browser.runtime.onConnect.addListener(countReceivedPorts); + + let childFrame = document.createElement("iframe"); + childFrame.src = "extensionpage.html"; + document.body.appendChild(childFrame); +} + +function senderScript() { + let detected_invalid_connect_params = 0; + + const invalid_connect_params = [ + // too many params + [ + "fake-extensions-id", + { name: "fake-conn-name" }, + "unexpected third params", + ], + // invalid params format + [{}, {}], + ["fake-extensions-id", "invalid-connect-info-format"], + ]; + const expected_detected_invalid_connect_params = + invalid_connect_params.length; + + function assertInvalidConnectParamsException(params) { + try { + browser.runtime.connect(...params); + } catch (e) { + detected_invalid_connect_params++; + browser.test.assertTrue( + e.toString().includes("Incorrect argument types for runtime.connect."), + "exception message is correct" + ); + } + } + for (let params of invalid_connect_params) { + assertInvalidConnectParamsException(params); + } + browser.test.assertEq( + expected_detected_invalid_connect_params, + detected_invalid_connect_params, + "all invalid runtime.connect params detected" + ); + + browser.runtime.connect(browser.runtime.id, { name: "check-results" }); +} + +let extensionData = { + background: backgroundScript, + files: { + "senderScript.js": senderScript, + "extensionpage.html": ``, + }, +}; + +add_task(async function test_backgroundRuntimeConnectParams() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + await extension.awaitFinish("runtime.connect invalid params"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_service_worker.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_service_worker.js new file mode 100644 index 0000000000..cc1b0dd054 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_service_worker.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"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); + +AddonTestUtils.init(this); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" +); +AddonTestUtils.overrideCertDB(); + +add_task(async function setup() { + ok( + WebExtensionPolicy.useRemoteWebExtensions, + "Expect remote-webextensions mode enabled" + ); + ok( + WebExtensionPolicy.backgroundServiceWorkerEnabled, + "Expect remote-webextensions mode enabled" + ); + + await AddonTestUtils.promiseStartupManager(); + + Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled"); + Services.prefs.clearUserPref("dom.serviceWorkers.idle_timeout"); + }); +}); + +add_task( + async function test_fail_spawn_extension_worker_for_disabled_extension() { + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + version: "1.0", + background: { + service_worker: "sw.js", + }, + browser_specific_settings: { gecko: { id: "test-bg-sw@mochi.test" } }, + }, + files: { + "page.html": "", + "sw.js": "dump('Background ServiceWorker - executed\\n');", + }, + }); + + const testWorkerWatcher = new TestWorkerWatcher(); + let watcher = await testWorkerWatcher.watchExtensionServiceWorker( + extension + ); + + await extension.startup(); + + info("Wait for the background service worker to be spawned"); + + ok( + await watcher.promiseWorkerSpawned, + "The extension service worker has been spawned as expected" + ); + + info("Wait for the background service worker to be terminated"); + ok( + await watcher.terminate(), + "The extension service worker has been terminated as expected" + ); + + const swReg = testWorkerWatcher.getRegistration(extension); + ok(swReg, "Got a service worker registration"); + ok(swReg?.activeWorker, "Got an active worker"); + + info("Spawn the active worker by attaching the debugger"); + + watcher = await testWorkerWatcher.watchExtensionServiceWorker(extension); + + swReg.activeWorker.attachDebugger(); + info( + "Wait for the background service worker to be spawned after attaching the debugger" + ); + ok( + await watcher.promiseWorkerSpawned, + "The extension service worker has been spawned as expected" + ); + + swReg.activeWorker.detachDebugger(); + info( + "Wait for the background service worker to be terminated after detaching the debugger" + ); + ok( + await watcher.terminate(), + "The extension service worker has been terminated as expected" + ); + + info( + "Disabling the addon policy, and then double-check that the worker can't be spawned" + ); + const policy = WebExtensionPolicy.getByID(extension.id); + policy.active = false; + + await Assert.throws( + () => swReg.activeWorker.attachDebugger(), + /InvalidStateError/, + "Got the expected extension when trying to spawn a worker for a disabled addon" + ); + + info( + "Enabling the addon policy and double-check the worker is spawned successfully" + ); + policy.active = true; + + watcher = await testWorkerWatcher.watchExtensionServiceWorker(extension); + + swReg.activeWorker.attachDebugger(); + info( + "Wait for the background service worker to be spawned after attaching the debugger" + ); + ok( + await watcher.promiseWorkerSpawned, + "The extension service worker has been spawned as expected" + ); + + swReg.activeWorker.detachDebugger(); + info( + "Wait for the background service worker to be terminated after detaching the debugger" + ); + ok( + await watcher.terminate(), + "The extension service worker has been terminated as expected" + ); + + await testWorkerWatcher.destroy(); + await extension.unload(); + } +); + +add_task(async function test_serviceworker_lifecycle_events() { + async function assertLifecycleEvents({ extension, expected, message }) { + const getLifecycleEvents = async () => { + const { active } = await this.content.navigator.serviceWorker.ready; + const { port1, port2 } = new content.MessageChannel(); + + return new Promise(resolve => { + port1.onmessage = msg => resolve(msg.data.lifecycleEvents); + active.postMessage("test", [port2]); + }); + }; + const page = await ExtensionTestUtils.loadContentPage( + extension.extension.baseURI.resolve("page.html"), + { extension } + ); + Assert.deepEqual( + await page.spawn([], getLifecycleEvents), + expected, + `Got the expected lifecycle events on ${message}` + ); + await page.close(); + } + + const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + background: { + service_worker: "sw.js", + }, + browser_specific_settings: { gecko: { id: "test-bg-sw@mochi.test" } }, + }, + files: { + "page.html": "", + "sw.js": ` + dump('Background ServiceWorker - executed\\n'); + + const lifecycleEvents = []; + self.oninstall = () => { + dump('Background ServiceWorker - oninstall\\n'); + lifecycleEvents.push("install"); + }; + self.onactivate = () => { + dump('Background ServiceWorker - onactivate\\n'); + lifecycleEvents.push("activate"); + }; + self.onmessage = (evt) => { + dump('Background ServiceWorker - onmessage\\n'); + evt.ports[0].postMessage({ lifecycleEvents }); + dump('Background ServiceWorker - postMessage\\n'); + }; + `, + }, + }); + + const testWorkerWatcher = new TestWorkerWatcher(); + let watcher = await testWorkerWatcher.watchExtensionServiceWorker(extension); + + await extension.startup(); + + await assertLifecycleEvents({ + extension, + expected: ["install", "activate"], + message: "initial worker registration", + }); + + const file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("serviceworker.txt"); + await TestUtils.waitForCondition( + () => file.exists(), + "Wait for service worker registrations to have been dumped on disk" + ); + + const managerShutdownCompleted = AddonTestUtils.promiseShutdownManager(); + + const firstSwReg = swm.getRegistrationByPrincipal( + extension.extension.principal, + extension.extension.principal.spec + ); + // Force the worker shutdown (in normal condition the worker would have been + // terminated as part of the entire application shutting down). + firstSwReg.forceShutdown(); + + info( + "Wait for the background service worker to be terminated while the app is shutting down" + ); + ok( + await watcher.promiseWorkerTerminated, + "The extension service worker has been terminated as expected" + ); + await managerShutdownCompleted; + + Assert.equal( + firstSwReg, + swm.getRegistrationByPrincipal( + extension.extension.principal, + extension.extension.principal.spec + ), + "Expect the service worker to not be unregistered on application shutdown" + ); + + info("Restart AddonManager (mocking Browser instance restart)"); + // Start the addon manager with `earlyStartup: false` to keep the background service worker + // from being started right away: + // + // - the call to `swm.reloadRegistrationForTest()` that follows is making sure that + // the previously registered service worker is in the same state it would be when + // the entire browser is restarted. + // + // - if the background service worker is being spawned again by the time we call + // `swm.reloadRegistrationForTest()`, ServiceWorkerUpdateJob would fail and trigger + // an `mState == State::Started` diagnostic assertion from ServiceWorkerJob::Finish + // and the xpcshell test will fail for the crash triggered by the assertion. + await AddonTestUtils.promiseStartupManager({ lateStartup: false }); + await extension.awaitStartup(); + + info( + "Force reload ServiceWorkerManager registrations (mocking a Browser instance restart)" + ); + swm.reloadRegistrationsForTest(); + + info( + "trigger delayed call to nsIServiceWorkerManager.registerForAddonPrincipal" + ); + // complete the startup notifications, then start the background + AddonTestUtils.notifyLateStartup(); + extension.extension.emit("start-background-script"); + + info("Force activate the extension worker"); + const newSwReg = swm.getRegistrationByPrincipal( + extension.extension.principal, + extension.extension.principal.spec + ); + + Assert.notEqual( + newSwReg, + firstSwReg, + "Expect the service worker registration to have been recreated" + ); + + await assertLifecycleEvents({ + extension, + expected: [], + message: "on previous registration loaded", + }); + + const { principal } = extension.extension; + const addon = await AddonManager.getAddonByID(extension.id); + await addon.disable(); + + ok( + await watcher.promiseWorkerTerminated, + "The extension service worker has been terminated as expected" + ); + + Assert.throws( + () => swm.getRegistrationByPrincipal(principal, principal.spec), + /NS_ERROR_FAILURE/, + "Expect the service worker to have been unregistered on addon disabled" + ); + + await addon.enable(); + await assertLifecycleEvents({ + extension, + expected: ["install", "activate"], + message: "on disabled addon re-enabled", + }); + + await testWorkerWatcher.destroy(); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js new file mode 100644 index 0000000000..1c3180b1b6 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js @@ -0,0 +1,46 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function testBackgroundWindow() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.log("background script executed"); + + browser.test.sendMessage("background-script-load"); + + let img = document.createElement("img"); + img.src = + "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + document.body.appendChild(img); + + img.onload = () => { + browser.test.log("image loaded"); + + let iframe = document.createElement("iframe"); + iframe.src = "about:blank?1"; + + iframe.onload = () => { + browser.test.log("iframe loaded"); + setTimeout(() => { + browser.test.notifyPass("background sub-window test done"); + }, 0); + }; + document.body.appendChild(iframe); + }; + }, + }); + + let loadCount = 0; + extension.onMessage("background-script-load", () => { + loadCount++; + }); + + await extension.startup(); + + await extension.awaitFinish("background sub-window test done"); + + equal(loadCount, 1, "background script loaded only once"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_teardown.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_teardown.js new file mode 100644 index 0000000000..a44431682f --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_teardown.js @@ -0,0 +1,98 @@ +"use strict"; + +add_task(async function test_background_reload_and_unload() { + let events = []; + { + const { Management } = ChromeUtils.import( + "resource://gre/modules/Extension.jsm" + ); + let record = (type, extensionContext) => { + let eventType = type == "proxy-context-load" ? "load" : "unload"; + let url = extensionContext.uri.spec; + let extensionId = extensionContext.extension.id; + events.push({ eventType, url, extensionId }); + }; + + Management.on("proxy-context-load", record); + Management.on("proxy-context-unload", record); + registerCleanupFunction(() => { + Management.off("proxy-context-load", record); + Management.off("proxy-context-unload", record); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.onMessage.addListener(msg => { + browser.test.assertEq("reload-background", msg); + location.reload(); + }); + browser.test.sendMessage("background-url", location.href); + }, + }); + + await extension.startup(); + let backgroundUrl = await extension.awaitMessage("background-url"); + + let contextEvents = events.splice(0); + equal( + contextEvents.length, + 1, + "ExtensionContext state change after loading an extension" + ); + equal(contextEvents[0].eventType, "load"); + equal( + contextEvents[0].url, + backgroundUrl, + "The ExtensionContext should be the background page" + ); + + extension.sendMessage("reload-background"); + await extension.awaitMessage("background-url"); + + contextEvents = events.splice(0); + equal( + contextEvents.length, + 2, + "ExtensionContext state changes after reloading the background page" + ); + equal( + contextEvents[0].eventType, + "unload", + "Unload ExtensionContext of background page" + ); + equal( + contextEvents[0].url, + backgroundUrl, + "ExtensionContext URL = background" + ); + equal( + contextEvents[1].eventType, + "load", + "Create new ExtensionContext for background page" + ); + equal( + contextEvents[1].url, + backgroundUrl, + "ExtensionContext URL = background" + ); + + await extension.unload(); + + contextEvents = events.splice(0); + equal( + contextEvents.length, + 1, + "ExtensionContext state change after unloading the extension" + ); + equal( + contextEvents[0].eventType, + "unload", + "Unload ExtensionContext for background page after extension unloads" + ); + equal( + contextEvents[0].url, + backgroundUrl, + "ExtensionContext URL = background" + ); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_telemetry.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_telemetry.js new file mode 100644 index 0000000000..61c022ffc4 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_telemetry.js @@ -0,0 +1,99 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const HISTOGRAM = "WEBEXT_BACKGROUND_PAGE_LOAD_MS"; +const HISTOGRAM_KEYED = "WEBEXT_BACKGROUND_PAGE_LOAD_MS_BY_ADDONID"; + +add_task(async function test_telemetry() { + let extension1 = ExtensionTestUtils.loadExtension({ + background() { + browser.test.sendMessage("loaded"); + }, + }); + + let extension2 = ExtensionTestUtils.loadExtension({ + background() { + browser.test.sendMessage("loaded"); + }, + }); + + clearHistograms(); + + assertHistogramEmpty(HISTOGRAM); + assertKeyedHistogramEmpty(HISTOGRAM_KEYED); + + await extension1.startup(); + await extension1.awaitMessage("loaded"); + + const processSnapshot = snapshot => { + return snapshot.sum > 0; + }; + + const processKeyedSnapshot = snapshot => { + let res = {}; + for (let key of Object.keys(snapshot)) { + res[key] = snapshot[key].sum > 0; + } + return res; + }; + + assertHistogramSnapshot( + HISTOGRAM, + { processSnapshot, expectedValue: true }, + `Data recorded for first extension for histogram: ${HISTOGRAM}.` + ); + + assertHistogramSnapshot( + HISTOGRAM_KEYED, + { + keyed: true, + processSnapshot: processKeyedSnapshot, + expectedValue: { + [extension1.extension.id]: true, + }, + }, + `Data recorded for first extension for histogram ${HISTOGRAM_KEYED}` + ); + + let histogram = Services.telemetry.getHistogramById(HISTOGRAM); + let histogramKeyed = Services.telemetry.getKeyedHistogramById( + HISTOGRAM_KEYED + ); + let histogramSum = histogram.snapshot().sum; + let histogramSumExt1 = histogramKeyed.snapshot()[extension1.extension.id].sum; + + await extension2.startup(); + await extension2.awaitMessage("loaded"); + + assertHistogramSnapshot( + HISTOGRAM, + { + processSnapshot: snapshot => snapshot.sum > histogramSum, + expectedValue: true, + }, + `Data recorded for second extension for histogram: ${HISTOGRAM}.` + ); + + assertHistogramSnapshot( + HISTOGRAM_KEYED, + { + keyed: true, + processSnapshot: processKeyedSnapshot, + expectedValue: { + [extension1.extension.id]: true, + [extension2.extension.id]: true, + }, + }, + `Data recorded for second extension for histogram ${HISTOGRAM_KEYED}` + ); + + equal( + histogramKeyed.snapshot()[extension1.extension.id].sum, + histogramSumExt1, + `Data recorder for first extension is unchanged on the keyed histogram ${HISTOGRAM_KEYED}` + ); + + await extension1.unload(); + await extension2.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js new file mode 100644 index 0000000000..fb2ca27482 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js @@ -0,0 +1,41 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function testBackgroundWindowProperties() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + let expectedValues = { + screenX: 0, + screenY: 0, + outerWidth: 0, + outerHeight: 0, + }; + + for (let k in window) { + try { + if (k in expectedValues) { + browser.test.assertEq( + expectedValues[k], + window[k], + `should return the expected value for window property: ${k}` + ); + } else { + void window[k]; + } + } catch (e) { + browser.test.assertEq( + null, + e, + `unexpected exception accessing window property: ${k}` + ); + } + } + + browser.test.notifyPass("background.testWindowProperties.done"); + }, + }); + await extension.startup(); + await extension.awaitFinish("background.testWindowProperties.done"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_brokenlinks.js b/toolkit/components/extensions/test/xpcshell/test_ext_brokenlinks.js new file mode 100644 index 0000000000..c066147268 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_brokenlinks.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"; + +/* + * This test extension has a background script 'missing.js' that is missing + * from the XPI. Such an extension should install/uninstall cleanly without + * causing timeouts. + */ +add_task(async function testXPIMissingBackGroundScript() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + background: { + scripts: ["missing.js"], + }, + }, + }); + + await extension.startup(); + await extension.unload(); + ok(true, "load/unload completed without timing out"); +}); + +/* + * This test extension includes a page with a missing script. The + * extension should install/uninstall cleanly without causing hangs. + */ +add_task(async function testXPIMissingPageScript() { + async function pageScript() { + browser.test.sendMessage("pageReady"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.sendMessage("ready", browser.runtime.getURL("page.html")); + }, + files: { + "page.html": ` + + + `, + "page.js": pageScript, + }, + }); + + await extension.startup(); + let url = await extension.awaitMessage("ready"); + let contentPage = await ExtensionTestUtils.loadContentPage(url); + await extension.awaitMessage("pageReady"); + await extension.unload(); + await contentPage.close(); + + ok(true, "load/unload completed without timing out"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js b/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js new file mode 100644 index 0000000000..774a9d1dc5 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js @@ -0,0 +1,536 @@ +/* -*- 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", +}); + +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + +const SETTINGS_ID = "test_settings_staged_restart_webext@tests.mozilla.org"; + +const { + createAppInfo, + promiseShutdownManager, + promiseStartupManager, +} = AddonTestUtils; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42"); + +add_task(async function test_browser_settings() { + const PERM_DENY_ACTION = Services.perms.DENY_ACTION; + const PERM_UNKNOWN_ACTION = Services.perms.UNKNOWN_ACTION; + + // Create an object to hold the values to which we will initialize the prefs. + const PREFS = { + "browser.cache.disk.enable": true, + "browser.cache.memory.enable": true, + "dom.popup_allowed_events": Preferences.get("dom.popup_allowed_events"), + "image.animation_mode": "none", + "permissions.default.desktop-notification": PERM_UNKNOWN_ACTION, + "ui.context_menus.after_mouseup": false, + "browser.tabs.closeTabByDblclick": false, + "browser.tabs.loadBookmarksInTabs": false, + "browser.search.openintab": false, + "browser.tabs.insertRelatedAfterCurrent": true, + "browser.tabs.insertAfterCurrent": false, + "browser.display.document_color_use": 1, + "layout.css.prefers-color-scheme.content-override": 2, + "browser.display.use_document_fonts": 1, + "browser.zoom.full": true, + "browser.zoom.siteSpecific": true, + }; + + async function background() { + let listeners = new Set([]); + browser.test.onMessage.addListener(async (msg, apiName, value) => { + let apiObj = browser.browserSettings; + let apiNameSplit = apiName.split("."); + for (let apiPart of apiNameSplit) { + apiObj = apiObj[apiPart]; + } + if (msg == "get") { + browser.test.sendMessage("settingData", await apiObj.get({})); + return; + } + + // set and setNoOp + + // Don't add more than one listner per apiName. We leave the + // listener to ensure we do not get more calls than we expect. + if (!listeners.has(apiName)) { + apiObj.onChange.addListener(details => { + browser.test.sendMessage("onChange", { + details, + setting: apiName, + }); + }); + listeners.add(apiName); + } + let result = await apiObj.set({ value }); + if (msg === "set") { + browser.test.assertTrue(result, "set returns true."); + browser.test.sendMessage("settingData", await apiObj.get({})); + } else { + browser.test.assertFalse(result, "set returns false for a no-op."); + browser.test.sendMessage("no-op set"); + } + }); + } + + // Set prefs to our initial values. + for (let pref in PREFS) { + Preferences.set(pref, PREFS[pref]); + } + + registerCleanupFunction(() => { + // Reset the prefs. + for (let pref in PREFS) { + Preferences.reset(pref); + } + }); + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browserSettings"], + }, + useAddonManager: "temporary", + }); + + await promiseStartupManager(); + await extension.startup(); + + async function testSetting(setting, value, expected, expectedValue = value) { + extension.sendMessage("set", setting, value); + let data = await extension.awaitMessage("settingData"); + let dataChange = await extension.awaitMessage("onChange"); + equal(setting, dataChange.setting, "onChange fired"); + equal( + data.value, + dataChange.details.value, + "onChange fired with correct value" + ); + deepEqual( + data.value, + expectedValue, + `The ${setting} setting has the expected value.` + ); + equal( + data.levelOfControl, + "controlled_by_this_extension", + `The ${setting} setting has the expected levelOfControl.` + ); + for (let pref in expected) { + equal( + Preferences.get(pref), + expected[pref], + `${pref} set correctly for ${value}` + ); + } + } + + async function testNoOpSetting(setting, value, expected) { + extension.sendMessage("setNoOp", setting, value); + await extension.awaitMessage("no-op set"); + for (let pref in expected) { + equal( + Preferences.get(pref), + expected[pref], + `${pref} set correctly for ${value}` + ); + } + } + + await testSetting("cacheEnabled", false, { + "browser.cache.disk.enable": false, + "browser.cache.memory.enable": false, + }); + await testSetting("cacheEnabled", true, { + "browser.cache.disk.enable": true, + "browser.cache.memory.enable": true, + }); + + await testSetting("allowPopupsForUserEvents", false, { + "dom.popup_allowed_events": "", + }); + await testSetting("allowPopupsForUserEvents", true, { + "dom.popup_allowed_events": PREFS["dom.popup_allowed_events"], + }); + + for (let value of ["normal", "none", "once"]) { + await testSetting("imageAnimationBehavior", value, { + "image.animation_mode": value, + }); + } + + await testSetting("webNotificationsDisabled", true, { + "permissions.default.desktop-notification": PERM_DENY_ACTION, + }); + await testSetting("webNotificationsDisabled", false, { + // This pref is not defaulted on Android. + "permissions.default.desktop-notification": + AppConstants.MOZ_BUILD_APP !== "browser" + ? undefined + : PERM_UNKNOWN_ACTION, + }); + + // This setting is a no-op on Android. + if (AppConstants.platform === "android") { + await testNoOpSetting("contextMenuShowEvent", "mouseup", { + "ui.context_menus.after_mouseup": false, + }); + } else { + await testSetting("contextMenuShowEvent", "mouseup", { + "ui.context_menus.after_mouseup": true, + }); + } + + // "mousedown" is also a no-op on Windows. + if (["android", "win"].includes(AppConstants.platform)) { + await testNoOpSetting("contextMenuShowEvent", "mousedown", { + "ui.context_menus.after_mouseup": AppConstants.platform === "win", + }); + } else { + await testSetting("contextMenuShowEvent", "mousedown", { + "ui.context_menus.after_mouseup": false, + }); + } + + if (AppConstants.platform !== "android") { + await testSetting("closeTabsByDoubleClick", true, { + "browser.tabs.closeTabByDblclick": true, + }); + await testSetting("closeTabsByDoubleClick", false, { + "browser.tabs.closeTabByDblclick": false, + }); + } + + extension.sendMessage("get", "ftpProtocolEnabled"); + let data = await extension.awaitMessage("settingData"); + equal(data.value, false); + equal( + data.levelOfControl, + "not_controllable", + `ftpProtocolEnabled is not controllable.` + ); + + await testSetting("newTabPosition", "afterCurrent", { + "browser.tabs.insertRelatedAfterCurrent": false, + "browser.tabs.insertAfterCurrent": true, + }); + await testSetting("newTabPosition", "atEnd", { + "browser.tabs.insertRelatedAfterCurrent": false, + "browser.tabs.insertAfterCurrent": false, + }); + await testSetting("newTabPosition", "relatedAfterCurrent", { + "browser.tabs.insertRelatedAfterCurrent": true, + "browser.tabs.insertAfterCurrent": false, + }); + + await testSetting("openBookmarksInNewTabs", true, { + "browser.tabs.loadBookmarksInTabs": true, + }); + await testSetting("openBookmarksInNewTabs", false, { + "browser.tabs.loadBookmarksInTabs": false, + }); + + await testSetting("openSearchResultsInNewTabs", true, { + "browser.search.openintab": true, + }); + await testSetting("openSearchResultsInNewTabs", false, { + "browser.search.openintab": false, + }); + + await testSetting("openUrlbarResultsInNewTabs", true, { + "browser.urlbar.openintab": true, + }); + await testSetting("openUrlbarResultsInNewTabs", false, { + "browser.urlbar.openintab": false, + }); + + await testSetting("overrideDocumentColors", "high-contrast-only", { + "browser.display.document_color_use": 0, + }); + await testSetting("overrideDocumentColors", "never", { + "browser.display.document_color_use": 1, + }); + await testSetting("overrideDocumentColors", "always", { + "browser.display.document_color_use": 2, + }); + + await testSetting("overrideContentColorScheme", "dark", { + "layout.css.prefers-color-scheme.content-override": 0, + }); + await testSetting("overrideContentColorScheme", "light", { + "layout.css.prefers-color-scheme.content-override": 1, + }); + await testSetting("overrideContentColorScheme", "auto", { + "layout.css.prefers-color-scheme.content-override": 2, + }); + + await testSetting("useDocumentFonts", false, { + "browser.display.use_document_fonts": 0, + }); + await testSetting("useDocumentFonts", true, { + "browser.display.use_document_fonts": 1, + }); + + await testSetting("zoomFullPage", true, { + "browser.zoom.full": true, + }); + await testSetting("zoomFullPage", false, { + "browser.zoom.full": false, + }); + + await testSetting("zoomSiteSpecific", true, { + "browser.zoom.siteSpecific": true, + }); + await testSetting("zoomSiteSpecific", false, { + "browser.zoom.siteSpecific": false, + }); + + await testSetting("colorManagement.mode", "off", { + "gfx.color_management.mode": 0, + }); + await testSetting("colorManagement.mode", "full", { + "gfx.color_management.mode": 1, + }); + await testSetting("colorManagement.mode", "tagged_only", { + "gfx.color_management.mode": 2, + }); + + await testSetting("colorManagement.useNativeSRGB", false, { + "gfx.color_management.native_srgb": false, + }); + await testSetting("colorManagement.useNativeSRGB", true, { + "gfx.color_management.native_srgb": true, + }); + + await testSetting("colorManagement.useWebRenderCompositor", false, { + "gfx.webrender.compositor": false, + }); + await testSetting("colorManagement.useWebRenderCompositor", true, { + "gfx.webrender.compositor": true, + }); + + await extension.unload(); + await promiseShutdownManager(); +}); + +add_task(async function test_bad_value() { + async function background() { + await browser.test.assertRejects( + browser.browserSettings.contextMenuShowEvent.set({ value: "bad" }), + /bad is not a valid value for contextMenuShowEvent/, + "contextMenuShowEvent.set rejects with an invalid value." + ); + + await browser.test.assertRejects( + browser.browserSettings.overrideDocumentColors.set({ value: 2 }), + /2 is not a valid value for overrideDocumentColors/, + "overrideDocumentColors.set rejects with an invalid value." + ); + + await browser.test.assertRejects( + browser.browserSettings.overrideDocumentColors.set({ value: "bad" }), + /bad is not a valid value for overrideDocumentColors/, + "overrideDocumentColors.set rejects with an invalid value." + ); + + await browser.test.assertRejects( + browser.browserSettings.overrideContentColorScheme.set({ value: 0 }), + /0 is not a valid value for overrideContentColorScheme/, + "overrideContentColorScheme.set rejects with an invalid value." + ); + + await browser.test.assertRejects( + browser.browserSettings.overrideContentColorScheme.set({ value: "bad" }), + /bad is not a valid value for overrideContentColorScheme/, + "overrideContentColorScheme.set rejects with an invalid value." + ); + + await browser.test.assertRejects( + browser.browserSettings.zoomFullPage.set({ value: 0 }), + /0 is not a valid value for zoomFullPage/, + "zoomFullPage.set rejects with an invalid value." + ); + + await browser.test.assertRejects( + browser.browserSettings.zoomFullPage.set({ value: "bad" }), + /bad is not a valid value for zoomFullPage/, + "zoomFullPage.set rejects with an invalid value." + ); + + await browser.test.assertRejects( + browser.browserSettings.zoomSiteSpecific.set({ value: 0 }), + /0 is not a valid value for zoomSiteSpecific/, + "zoomSiteSpecific.set rejects with an invalid value." + ); + + await browser.test.assertRejects( + browser.browserSettings.zoomSiteSpecific.set({ value: "bad" }), + /bad is not a valid value for zoomSiteSpecific/, + "zoomSiteSpecific.set rejects with an invalid value." + ); + + browser.test.sendMessage("done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browserSettings"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_bad_value_android() { + if (AppConstants.platform !== "android") { + return; + } + + async function background() { + await browser.test.assertRejects( + browser.browserSettings.closeTabsByDoubleClick.set({ value: true }), + /android is not a supported platform for the closeTabsByDoubleClick setting/, + "closeTabsByDoubleClick.set rejects on Android." + ); + + await browser.test.assertRejects( + browser.browserSettings.closeTabsByDoubleClick.get({}), + /android is not a supported platform for the closeTabsByDoubleClick setting/, + "closeTabsByDoubleClick.get rejects on Android." + ); + + await browser.test.assertRejects( + browser.browserSettings.closeTabsByDoubleClick.clear({}), + /android is not a supported platform for the closeTabsByDoubleClick setting/, + "closeTabsByDoubleClick.clear rejects on Android." + ); + + browser.test.sendMessage("done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browserSettings"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +// Verifies settings remain after a staged update on restart. +add_task(async function delay_updates_settings_after_restart() { + let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + AddonTestUtils.registerJSON(server, "/test_update.json", { + addons: { + "test_settings_staged_restart_webext@tests.mozilla.org": { + updates: [ + { + version: "2.0", + update_link: + "http://example.com/addons/test_settings_staged_restart_v2.xpi", + }, + ], + }, + }, + }); + const update_xpi = AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 2, + name: "Delay Upgrade", + version: "2.0", + browser_specific_settings: { + gecko: { id: SETTINGS_ID }, + }, + permissions: ["browserSettings"], + }, + }); + server.registerFile( + `/addons/test_settings_staged_restart_v2.xpi`, + update_xpi + ); + + await AddonTestUtils.promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: SETTINGS_ID, + update_url: `http://example.com/test_update.json`, + }, + }, + permissions: ["browserSettings"], + }, + background() { + browser.runtime.onUpdateAvailable.addListener(async details => { + if (details) { + await browser.browserSettings.webNotificationsDisabled.set({ + value: true, + }); + if (details.version) { + // This should be the version of the pending update. + browser.test.assertEq("2.0", details.version, "correct version"); + browser.test.notifyPass("delay"); + } + } else { + browser.test.fail("no details object passed"); + } + }); + browser.test.sendMessage("ready"); + }, + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + let prefname = "permissions.default.desktop-notification"; + let val = Services.prefs.getIntPref(prefname); + Assert.notEqual(val, 2, "webNotificationsDisabled pref not set"); + + let update = await AddonTestUtils.promiseFindAddonUpdates(extension.addon); + let install = update.updateAvailable; + Assert.ok(install, `install is available ${update.error}`); + + await AddonTestUtils.promiseCompleteAllInstalls([install]); + + Assert.equal(install.state, AddonManager.STATE_POSTPONED); + await extension.awaitFinish("delay"); + + // restarting allows upgrade to proceed + await AddonTestUtils.promiseRestartManager(); + + await extension.awaitStartup(); + + // If an update is not handled correctly we would fail here. Bug 1639705. + val = Services.prefs.getIntPref(prefname); + Assert.equal(val, 2, "webNotificationsDisabled pref set"); + + await extension.unload(); + await AddonTestUtils.promiseShutdownManager(); + + val = Services.prefs.getIntPref(prefname); + Assert.notEqual(val, 2, "webNotificationsDisabled pref not set"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings_homepage.js b/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings_homepage.js new file mode 100644 index 0000000000..8d1d16c743 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings_homepage.js @@ -0,0 +1,36 @@ +/* -*- 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_homepage_get_without_set() { + async function background() { + let homepage = await browser.browserSettings.homepageOverride.get({}); + browser.test.sendMessage("homepage", homepage); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browserSettings"], + }, + }); + + let defaultHomepage = Services.prefs.getStringPref( + "browser.startup.homepage" + ); + + await extension.startup(); + let homepage = await extension.awaitMessage("homepage"); + equal( + homepage.value, + defaultHomepage, + "The homepageOverride setting has the expected value." + ); + equal( + homepage.levelOfControl, + "not_controllable", + "The homepageOverride setting has the expected levelOfControl." + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_browsingData.js b/toolkit/components/extensions/test/xpcshell/test_ext_browsingData.js new file mode 100644 index 0000000000..1df5e60478 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_browsingData.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 testInvalidArguments() { + async function background() { + const UNSUPPORTED_DATA_TYPES = ["appcache", "fileSystems", "webSQL"]; + + await browser.test.assertRejects( + browser.browsingData.remove( + { originTypes: { protectedWeb: true } }, + { cookies: true } + ), + "Firefox does not support protectedWeb or extension as originTypes.", + "Expected error received when using protectedWeb originType." + ); + + await browser.test.assertRejects( + browser.browsingData.removeCookies({ originTypes: { extension: true } }), + "Firefox does not support protectedWeb or extension as originTypes.", + "Expected error received when using extension originType." + ); + + for (let dataType of UNSUPPORTED_DATA_TYPES) { + let dataTypes = {}; + dataTypes[dataType] = true; + browser.test.assertThrows( + () => browser.browsingData.remove({}, dataTypes), + /Type error for parameter dataToRemove/, + `Expected error received when using ${dataType} dataType.` + ); + } + + browser.test.notifyPass("invalidArguments"); + } + + let extensionData = { + background: background, + manifest: { + permissions: ["browsingData"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitFinish("invalidArguments"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cache.js b/toolkit/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cache.js new file mode 100644 index 0000000000..612f2dd0f3 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cache.js @@ -0,0 +1,456 @@ +/* -*- 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 { SiteDataTestUtils } = ChromeUtils.import( + "resource://testing-common/SiteDataTestUtils.jsm" +); + +const COOKIE = { + host: "example.com", + name: "test_cookie", + path: "/", +}; +const COOKIE_NET = { + host: "example.net", + name: "test_cookie", + path: "/", +}; +const COOKIE_ORG = { + host: "example.org", + name: "test_cookie", + path: "/", +}; +let since, oldCookie; + +function addCookie(cookie) { + Services.cookies.add( + cookie.host, + cookie.path, + cookie.name, + "test", + false, + false, + false, + Date.now() / 1000 + 10000, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + ok( + Services.cookies.cookieExists(cookie.host, cookie.path, cookie.name, {}), + `Cookie ${cookie.name} was created.` + ); +} + +async function setUpCookies() { + Services.cookies.removeAll(); + + // Add a cookie which will end up with an older creationTime. + oldCookie = Object.assign({}, COOKIE, { name: Date.now() }); + addCookie(oldCookie); + await new Promise(resolve => setTimeout(resolve, 10)); + since = Date.now(); + await new Promise(resolve => setTimeout(resolve, 10)); + + // Add a cookie which will end up with a more recent creationTime. + addCookie(COOKIE); + + // Add cookies for different domains. + addCookie(COOKIE_NET); + addCookie(COOKIE_ORG); +} + +async function setUpCache() { + Services.cache2.clear(); + + // Add cache entries for different domains. + for (const domain of ["example.net", "example.org", "example.com"]) { + await SiteDataTestUtils.addCacheEntry(`http://${domain}/`, "disk"); + await SiteDataTestUtils.addCacheEntry(`http://${domain}/`, "memory"); + } +} + +function hasCacheEntry(domain) { + const disk = SiteDataTestUtils.hasCacheEntry(`http://${domain}/`, "disk"); + const memory = SiteDataTestUtils.hasCacheEntry(`http://${domain}/`, "memory"); + + equal( + disk, + memory, + `For ${domain} either either both or neither caches need to exists.` + ); + return disk; +} + +add_task(async function testCache() { + function background() { + browser.test.onMessage.addListener(async msg => { + if (msg == "removeCache") { + await browser.browsingData.removeCache({}); + } else { + await browser.browsingData.remove({}, { cache: true }); + } + browser.test.sendMessage("cacheRemoved"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + async function testRemovalMethod(method) { + await setUpCache(); + + extension.sendMessage(method); + await extension.awaitMessage("cacheRemoved"); + + ok(!hasCacheEntry("example.net"), "example.net cache was removed"); + ok(!hasCacheEntry("example.org"), "example.org cache was removed"); + ok(!hasCacheEntry("example.com"), "example.com cache was removed"); + } + + await extension.startup(); + + await testRemovalMethod("removeCache"); + await testRemovalMethod("remove"); + + await extension.unload(); +}); + +add_task(async function testCookies() { + // Above in setUpCookies we create an 'old' cookies, wait 10ms, then log a timestamp. + // Here we ask the browser to delete all cookies after the timestamp, with the intention + // that the 'old' cookie is not removed. The issue arises when the timer precision is + // low enough such that the timestamp that gets logged is the same as the 'old' cookie. + // We hardcode a precision value to ensure that there is time between the 'old' cookie + // and the timestamp generation. + Services.prefs.setBoolPref("privacy.reduceTimerPrecision", true); + Services.prefs.setIntPref( + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + 2000 + ); + + registerCleanupFunction(function() { + Services.prefs.clearUserPref("privacy.reduceTimerPrecision"); + Services.prefs.clearUserPref( + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds" + ); + }); + + function background() { + browser.test.onMessage.addListener(async (msg, options) => { + if (msg == "removeCookies") { + await browser.browsingData.removeCookies(options); + } else { + await browser.browsingData.remove(options, { cookies: true }); + } + browser.test.sendMessage("cookiesRemoved"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + async function testRemovalMethod(method) { + // Clear cookies with a recent since value. + await setUpCookies(); + extension.sendMessage(method, { since }); + await extension.awaitMessage("cookiesRemoved"); + + ok( + Services.cookies.cookieExists( + oldCookie.host, + oldCookie.path, + oldCookie.name, + {} + ), + "Old cookie was not removed." + ); + ok( + !Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + "Recent cookie was removed." + ); + + // Clear cookies with an old since value. + await setUpCookies(); + addCookie(COOKIE); + extension.sendMessage(method, { since: since - 100000 }); + await extension.awaitMessage("cookiesRemoved"); + + ok( + !Services.cookies.cookieExists( + oldCookie.host, + oldCookie.path, + oldCookie.name, + {} + ), + "Old cookie was removed." + ); + ok( + !Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + "Recent cookie was removed." + ); + + // Clear cookies with no since value and valid originTypes. + await setUpCookies(); + extension.sendMessage(method, { + originTypes: { unprotectedWeb: true, protectedWeb: false }, + }); + await extension.awaitMessage("cookiesRemoved"); + + ok( + !Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + `Cookie ${COOKIE.name} was removed.` + ); + ok( + !Services.cookies.cookieExists( + oldCookie.host, + oldCookie.path, + oldCookie.name, + {} + ), + `Cookie ${oldCookie.name} was removed.` + ); + } + + await extension.startup(); + + await testRemovalMethod("removeCookies"); + await testRemovalMethod("remove"); + + await extension.unload(); +}); + +add_task(async function testCacheAndCookies() { + function background() { + browser.test.onMessage.addListener(async options => { + await browser.browsingData.remove(options, { + cache: true, + cookies: true, + }); + browser.test.sendMessage("cacheAndCookiesRemoved"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + await extension.startup(); + + // Clear cache and cookies with a recent since value. + await setUpCookies(); + await setUpCache(); + extension.sendMessage({ since }); + await extension.awaitMessage("cacheAndCookiesRemoved"); + + ok( + Services.cookies.cookieExists( + oldCookie.host, + oldCookie.path, + oldCookie.name, + {} + ), + "Old cookie was not removed." + ); + ok( + !Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + "Recent cookie was removed." + ); + + // Cache does not support |since| and deletes everything! + ok(!hasCacheEntry("example.net"), "example.net cache was removed"); + ok(!hasCacheEntry("example.org"), "example.org cache was removed"); + ok(!hasCacheEntry("example.com"), "example.com cache was removed"); + + // Clear cache and cookies with an old since value. + await setUpCookies(); + await setUpCache(); + extension.sendMessage({ since: since - 100000 }); + await extension.awaitMessage("cacheAndCookiesRemoved"); + + // Cache does not support |since| and deletes everything! + ok(!hasCacheEntry("example.net"), "example.net cache was removed"); + ok(!hasCacheEntry("example.org"), "example.org cache was removed"); + ok(!hasCacheEntry("example.com"), "example.com cache was removed"); + + ok( + !Services.cookies.cookieExists( + oldCookie.host, + oldCookie.path, + oldCookie.name, + {} + ), + "Old cookie was removed." + ); + ok( + !Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + "Recent cookie was removed." + ); + + // Clear cache and cookies with hostnames value. + await setUpCookies(); + await setUpCache(); + extension.sendMessage({ + hostnames: ["example.net", "example.org", "unknown.com"], + }); + await extension.awaitMessage("cacheAndCookiesRemoved"); + + ok( + Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + `Cookie ${COOKIE.name} was not removed.` + ); + ok( + !Services.cookies.cookieExists( + COOKIE_NET.host, + COOKIE_NET.path, + COOKIE_NET.name, + {} + ), + `Cookie ${COOKIE_NET.name} was removed.` + ); + ok( + !Services.cookies.cookieExists( + COOKIE_ORG.host, + COOKIE_ORG.path, + COOKIE_ORG.name, + {} + ), + `Cookie ${COOKIE_ORG.name} was removed.` + ); + + ok(!hasCacheEntry("example.net"), "example.net cache was removed"); + ok(!hasCacheEntry("example.org"), "example.org cache was removed"); + ok(hasCacheEntry("example.com"), "example.com cache was not removed"); + + // Clear cache and cookies with (empty) hostnames value. + await setUpCookies(); + await setUpCache(); + extension.sendMessage({ hostnames: [] }); + await extension.awaitMessage("cacheAndCookiesRemoved"); + + ok( + Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + `Cookie ${COOKIE.name} was not removed.` + ); + ok( + Services.cookies.cookieExists( + COOKIE_NET.host, + COOKIE_NET.path, + COOKIE_NET.name, + {} + ), + `Cookie ${COOKIE_NET.name} was not removed.` + ); + ok( + Services.cookies.cookieExists( + COOKIE_ORG.host, + COOKIE_ORG.path, + COOKIE_ORG.name, + {} + ), + `Cookie ${COOKIE_ORG.name} was not removed.` + ); + + ok(hasCacheEntry("example.net"), "example.net cache was not removed"); + ok(hasCacheEntry("example.org"), "example.org cache was not removed"); + ok(hasCacheEntry("example.com"), "example.com cache was not removed"); + + // Clear cache and cookies with both hostnames and since values. + await setUpCache(); + await setUpCookies(); + extension.sendMessage({ hostnames: ["example.com"], since }); + await extension.awaitMessage("cacheAndCookiesRemoved"); + + ok( + Services.cookies.cookieExists( + oldCookie.host, + oldCookie.path, + oldCookie.name, + {} + ), + "Old cookie was not removed." + ); + ok( + !Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + "Recent cookie was removed." + ); + ok( + Services.cookies.cookieExists( + COOKIE_NET.host, + COOKIE_NET.path, + COOKIE_NET.name, + {} + ), + "Cookie with different hostname was not removed" + ); + ok( + Services.cookies.cookieExists( + COOKIE_ORG.host, + COOKIE_ORG.path, + COOKIE_ORG.name, + {} + ), + "Cookie with different hostname was not removed" + ); + + ok(hasCacheEntry("example.net"), "example.net cache was not removed"); + ok(hasCacheEntry("example.org"), "example.org cache was not removed"); + ok(!hasCacheEntry("example.com"), "example.com cache was removed"); + + // Clear cache and cookies with no since or hostnames value. + await setUpCache(); + await setUpCookies(); + extension.sendMessage({}); + await extension.awaitMessage("cacheAndCookiesRemoved"); + + ok( + !Services.cookies.cookieExists(COOKIE.host, COOKIE.path, COOKIE.name, {}), + `Cookie ${COOKIE.name} was removed.` + ); + ok( + !Services.cookies.cookieExists( + oldCookie.host, + oldCookie.path, + oldCookie.name, + {} + ), + `Cookie ${oldCookie.name} was removed.` + ); + ok( + !Services.cookies.cookieExists( + COOKIE_NET.host, + COOKIE_NET.path, + COOKIE_NET.name, + {} + ), + `Cookie ${COOKIE_NET.name} was removed.` + ); + ok( + !Services.cookies.cookieExists( + COOKIE_ORG.host, + COOKIE_ORG.path, + COOKIE_ORG.name, + {} + ), + `Cookie ${COOKIE_ORG.name} was removed.` + ); + + ok(!hasCacheEntry("example.net"), "example.net cache was removed"); + ok(!hasCacheEntry("example.org"), "example.org cache was removed"); + ok(!hasCacheEntry("example.com"), "example.com cache was removed"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cookieStoreId.js b/toolkit/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cookieStoreId.js new file mode 100644 index 0000000000..d3d066efd2 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cookieStoreId.js @@ -0,0 +1,192 @@ +/* -*- 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"; + +// "Normal" cookie +const COOKIE_NORMAL = { + host: "example.com", + name: "test_cookie", + path: "/", + originAttributes: {}, +}; +// Private browsing cookie +const COOKIE_PRIVATE = { + host: "example.net", + name: "test_cookie", + path: "/", + originAttributes: { + privateBrowsingId: 1, + }, +}; +// "firefox-container-1" cookie +const COOKIE_CONTAINER = { + host: "example.org", + name: "test_cookie", + path: "/", + originAttributes: { + userContextId: 1, + }, +}; + +function cookieExists(cookie) { + return Services.cookies.cookieExists( + cookie.host, + cookie.path, + cookie.name, + cookie.originAttributes + ); +} + +function addCookie(cookie) { + const THE_FUTURE = Date.now() + 5 * 60; + + Services.cookies.add( + cookie.host, + cookie.path, + cookie.name, + "test", + false, + false, + false, + THE_FUTURE, + cookie.originAttributes, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + ok(cookieExists(cookie), `Cookie ${cookie.name} was created.`); +} + +async function setUpCookies() { + Services.cookies.removeAll(); + + addCookie(COOKIE_NORMAL); + addCookie(COOKIE_PRIVATE); + addCookie(COOKIE_CONTAINER); +} + +add_task(async function testCookies() { + Services.prefs.setBoolPref("privacy.userContext.enabled", true); + + function background() { + browser.test.onMessage.addListener(async (msg, options) => { + if (msg == "removeCookies") { + await browser.browsingData.removeCookies(options); + } else { + await browser.browsingData.remove(options, { cookies: true }); + } + browser.test.sendMessage("cookiesRemoved"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + async function testRemovalMethod(method) { + // Clear only "normal"/default cookies. + await setUpCookies(); + + extension.sendMessage(method, { cookieStoreId: "firefox-default" }); + await extension.awaitMessage("cookiesRemoved"); + + ok(!cookieExists(COOKIE_NORMAL), "Normal cookie was removed"); + ok(cookieExists(COOKIE_PRIVATE), "Private cookie was not removed"); + ok(cookieExists(COOKIE_CONTAINER), "Container cookie was not removed"); + + // Clear container cookie + await setUpCookies(); + + extension.sendMessage(method, { cookieStoreId: "firefox-container-1" }); + await extension.awaitMessage("cookiesRemoved"); + + ok(cookieExists(COOKIE_NORMAL), "Normal cookie was not removed"); + ok(cookieExists(COOKIE_PRIVATE), "Private cookie was not removed"); + ok(!cookieExists(COOKIE_CONTAINER), "Container cookie was removed"); + + // Clear private cookie + await setUpCookies(); + + extension.sendMessage(method, { cookieStoreId: "firefox-private" }); + await extension.awaitMessage("cookiesRemoved"); + + ok(cookieExists(COOKIE_NORMAL), "Normal cookie was not removed"); + ok(!cookieExists(COOKIE_PRIVATE), "Private cookie was removed"); + ok(cookieExists(COOKIE_CONTAINER), "Container cookie was not removed"); + + // Clear container cookie with correct hostname + await setUpCookies(); + + extension.sendMessage(method, { + cookieStoreId: "firefox-container-1", + hostnames: ["example.org"], + }); + await extension.awaitMessage("cookiesRemoved"); + + ok(cookieExists(COOKIE_NORMAL), "Normal cookie was not removed"); + ok(cookieExists(COOKIE_PRIVATE), "Private cookie was not removed"); + ok(!cookieExists(COOKIE_CONTAINER), "Container cookie was removed"); + + // Clear container cookie with incorrect hostname; nothing is removed + await setUpCookies(); + + extension.sendMessage(method, { + cookieStoreId: "firefox-container-1", + hostnames: ["example.com"], + }); + await extension.awaitMessage("cookiesRemoved"); + + ok(cookieExists(COOKIE_NORMAL), "Normal cookie was not removed"); + ok(cookieExists(COOKIE_PRIVATE), "Private cookie was not removed"); + ok(cookieExists(COOKIE_CONTAINER), "Container cookie was not removed"); + + // Clear private cookie with correct hostname + await setUpCookies(); + + extension.sendMessage(method, { + cookieStoreId: "firefox-private", + hostnames: ["example.net"], + }); + await extension.awaitMessage("cookiesRemoved"); + + ok(cookieExists(COOKIE_NORMAL), "Normal cookie was not removed"); + ok(!cookieExists(COOKIE_PRIVATE), "Private cookie was removed"); + ok(cookieExists(COOKIE_CONTAINER), "Container cookie was not removed"); + + // Clear private cookie with incorrect hostname; nothing is removed + await setUpCookies(); + + extension.sendMessage(method, { + cookieStoreId: "firefox-private", + hostnames: ["example.com"], + }); + await extension.awaitMessage("cookiesRemoved"); + + ok(cookieExists(COOKIE_NORMAL), "Normal cookie was not removed"); + ok(cookieExists(COOKIE_PRIVATE), "Private cookie was not removed"); + ok(cookieExists(COOKIE_CONTAINER), "Container cookie was not removed"); + + // Clear private cookie by hostname + await setUpCookies(); + + extension.sendMessage(method, { + hostnames: ["example.net"], + }); + await extension.awaitMessage("cookiesRemoved"); + + ok(cookieExists(COOKIE_NORMAL), "Normal cookie was not removed"); + ok(!cookieExists(COOKIE_PRIVATE), "Private cookie was removed"); + ok(cookieExists(COOKIE_CONTAINER), "Container cookie was not removed"); + } + + await extension.startup(); + + await testRemovalMethod("removeCookies"); + await testRemovalMethod("remove"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_cache_api.js b/toolkit/components/extensions/test/xpcshell/test_ext_cache_api.js new file mode 100644 index 0000000000..277a69271d --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_cache_api.js @@ -0,0 +1,303 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +AddonTestUtils.init(this); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" +); + +const server = createHttpServer({ + hosts: ["example.com", "anotherdomain.com"], +}); +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write("test_ext_cache_api.js"); +}); + +add_task(async function test_cache_api_http_resource_allowed() { + async function background() { + try { + const BASE_URL = `http://example.com/dummy`; + + const cache = await window.caches.open("test-cache-api"); + browser.test.assertTrue( + await window.caches.has("test-cache-api"), + "CacheStorage.has should resolve to true" + ); + + // Test that adding and requesting cached http urls + // works as well. + await cache.add(BASE_URL); + browser.test.assertEq( + "test_ext_cache_api.js", + await cache.match(BASE_URL).then(res => res.text()), + "Got the expected content from the cached http url" + ); + + // Test that the http urls that the cache API is allowed + // to fetch and cache are limited by the host permissions + // associated to the extensions (same as when the extension + // for fetch from those urls using fetch or XHR). + await browser.test.assertRejects( + cache.add(`http://anotherdomain.com/dummy`), + "NetworkError when attempting to fetch resource.", + "Got the expected rejection of requesting an http not allowed by host permissions" + ); + + // Test that deleting the cache storage works as expected. + browser.test.assertTrue( + await window.caches.delete("test-cache-api"), + "Cache deleted successfully" + ); + browser.test.assertTrue( + !(await window.caches.has("test-cache-api")), + "CacheStorage.has should resolve to false" + ); + } catch (err) { + browser.test.fail(`Unexpected error on using Cache API: ${err}`); + throw err; + } finally { + browser.test.sendMessage("test-cache-api-allowed"); + } + } + + // Verify that Cache API support for http urls is available + // regardless of extensions.backgroundServiceWorker.enabled pref. + const extension = ExtensionTestUtils.loadExtension({ + manifest: { permissions: ["http://example.com/*"] }, + background, + }); + + await extension.startup(); + + await extension.awaitMessage("test-cache-api-allowed"); + await extension.unload(); +}); + +// This test is similar to `test_cache_api_http_resource_allowed` but it does +// exercise the Cache API from a moz-extension shared worker. +// We expect the cache API calls to be successfull when it is being used to +// cache an HTTP url that is allowed for the extensions based on its host +// permission, but to fail if the extension doesn't have the required host +// permission to fetch data from that url. +add_task(async function test_cache_api_from_ext_shared_worker() { + if (!WebExtensionPolicy.useRemoteWebExtensions) { + // Ensure RemoteWorkerService has been initialized in the main + // process. + Services.obs.notifyObservers(null, "profile-after-change"); + } + + const background = async function() { + const BASE_URL_OK = `http://example.com/dummy`; + const BASE_URL_KO = `http://anotherdomain.com/dummy`; + const worker = new SharedWorker("worker.js"); + const { data: resultOK } = await new Promise(resolve => { + worker.port.onmessage = resolve; + worker.port.postMessage(["worker-cacheapi-test-allowed", BASE_URL_OK]); + }); + browser.test.log( + `Got result from extension worker for allowed host url: ${JSON.stringify( + resultOK + )}` + ); + const { data: resultKO } = await new Promise(resolve => { + worker.port.onmessage = resolve; + worker.port.postMessage(["worker-cacheapi-test-disallowed", BASE_URL_KO]); + }); + browser.test.log( + `Got result from extension worker for disallowed host url: ${JSON.stringify( + resultKO + )}` + ); + + browser.test.assertTrue( + await window.caches.has("test-cache-api"), + "CacheStorage.has should resolve to true" + ); + const cache = await window.caches.open("test-cache-api"); + browser.test.assertEq( + "test_ext_cache_api.js", + await cache.match(BASE_URL_OK).then(res => res.text()), + "Got the expected content from the cached http url" + ); + browser.test.assertEq( + true, + await cache.match(BASE_URL_KO).then(res => res == undefined), + "Got no match for the http url that isn't allowed by host permissions" + ); + + browser.test.sendMessage("test-cacheapi-sharedworker:done", { + expectAllowed: resultOK, + expectDisallowed: resultKO, + }); + }; + + const extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { permissions: ["http://example.com/*"] }, + files: { + "worker.js": function() { + self.onconnect = evt => { + const port = evt.ports[0]; + port.onmessage = async evt => { + let result = {}; + let message; + try { + const [msg, BASE_URL] = evt.data; + message = msg; + const cache = await self.caches.open("test-cache-api"); + await cache.add(BASE_URL); + result.success = true; + } catch (err) { + result.error = err.message; + throw err; + } finally { + port.postMessage([`${message}:result`, result]); + } + }; + }; + }, + }, + }); + + await extension.startup(); + const { expectAllowed, expectDisallowed } = await extension.awaitMessage( + "test-cacheapi-sharedworker:done" + ); + // Increase the chance to have the error message related to an unexpected + // failure to be explicitly mention in the failure message. + Assert.deepEqual( + expectAllowed, + ["worker-cacheapi-test-allowed:result", { success: true }], + "Expect worker result to be successfull with the required host permission" + ); + Assert.deepEqual( + expectDisallowed, + [ + "worker-cacheapi-test-disallowed:result", + { error: "NetworkError when attempting to fetch resource." }, + ], + "Expect worker result to be unsuccessfull without the required host permission" + ); + + await extension.unload(); +}); + +add_task(async function test_cache_storage_evicted_on_addon_uninstalled() { + async function background() { + try { + const BASE_URL = `http://example.com/dummy`; + + const cache = await window.caches.open("test-cache-api"); + browser.test.assertTrue( + await window.caches.has("test-cache-api"), + "CacheStorage.has should resolve to true" + ); + + // Test that adding and requesting cached http urls + // works as well. + await cache.add(BASE_URL); + browser.test.assertEq( + "test_ext_cache_api.js", + await cache.match(BASE_URL).then(res => res.text()), + "Got the expected content from the cached http url" + ); + } catch (err) { + browser.test.fail(`Unexpected error on using Cache API: ${err}`); + throw err; + } finally { + browser.test.sendMessage("cache-storage-created"); + } + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { permissions: ["http://example.com/*"] }, + background, + // Necessary to be sure the expected extension stored data cleanup callback + // will be called when the extension is uninstalled from an AddonManager + // perspective. + useAddonManager: "temporary", + }); + + await AddonTestUtils.promiseStartupManager(); + await extension.startup(); + await extension.awaitMessage("cache-storage-created"); + + const extURL = `moz-extension://${extension.extension.uuid}`; + const extPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(extURL), + {} + ); + let extCacheStorage = new CacheStorage("content", extPrincipal); + + ok( + await extCacheStorage.has("test-cache-api"), + "Got the expected extension cache storage" + ); + + await extension.unload(); + + ok( + !(await extCacheStorage.has("test-cache-api")), + "The extension cache storage data should have been evicted on addon uninstall" + ); +}); + +add_task( + { + // Pref used to allow to use the Cache WebAPI related to a page loaded from http + // (otherwise Gecko will throw a SecurityError when trying to access the webpage + // cache storage from the content script, unless the webpage is loaded from https). + pref_set: [["dom.caches.testing.enabled", true]], + }, + async function test_cache_put_from_contentscript() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/*"], + js: ["content.js"], + }, + ], + }, + files: { + "content.js": async function() { + const cache = await caches.open("test-cachestorage"); + const request = "http://example.com"; + const response = await fetch(request); + await cache.put(request, response).catch(err => { + browser.test.sendMessage("cache-put-error", { + name: err.name, + message: err.message, + }); + }); + }, + }, + }); + + await extension.startup(); + + const page = await ExtensionTestUtils.loadContentPage("http://example.com"); + const actualError = await extension.awaitMessage("cache-put-error"); + equal( + actualError.name, + "SecurityError", + "Got a security error from cache.put call as expected" + ); + ok( + /Disallowed on WebExtension ContentScript Request/.test( + actualError.message + ), + `Got the expected error message: ${actualError.message}` + ); + + await page.close(); + await extension.unload(); + } +); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_captivePortal.js b/toolkit/components/extensions/test/xpcshell/test_ext_captivePortal.js new file mode 100644 index 0000000000..dfb5c4c415 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_captivePortal.js @@ -0,0 +1,202 @@ +"use strict"; + +Services.prefs.setBoolPref( + "extensions.webextensions.background-delayed-startup", + true +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "43" +); + +/** + * This duplicates the test from netwerk/test/unit/test_captive_portal_service.js + * however using an extension to gather the captive portal information. + */ + +const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled"; +const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode"; +const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval"; +const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL"; +const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost"; + +const SUCCESS_STRING = + ''; +let cpResponse = SUCCESS_STRING; + +const httpserver = createHttpServer(); +httpserver.registerPathHandler("/captive.txt", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain"); + response.write(cpResponse); +}); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED); + Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE); + Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT); + Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME); + Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST); +}); + +add_task(async function setup() { + Services.prefs.setCharPref( + PREF_CAPTIVE_ENDPOINT, + `http://localhost:${httpserver.identity.primaryPort}/captive.txt` + ); + Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true); + Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 0); + Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true); + + Services.prefs.setBoolPref("extensions.eventPages.enabled", true); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_captivePortal_basic() { + let cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService( + Ci.nsICaptivePortalService + ); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + permissions: ["captivePortal"], + background: { persistent: false }, + }, + isPrivileged: true, + async background() { + browser.captivePortal.onConnectivityAvailable.addListener(details => { + browser.test.log( + `onConnectivityAvailable received ${JSON.stringify(details)}` + ); + browser.test.sendMessage("connectivity", details); + }); + + browser.captivePortal.onStateChanged.addListener(details => { + browser.test.log(`onStateChanged received ${JSON.stringify(details)}`); + browser.test.sendMessage("state", details); + }); + + browser.captivePortal.canonicalURL.onChange.addListener(details => { + browser.test.sendMessage("url", details); + }); + + browser.test.onMessage.addListener(async msg => { + if (msg == "getstate") { + browser.test.sendMessage( + "getstate", + await browser.captivePortal.getState() + ); + } + }); + }, + }); + await extension.startup(); + + extension.sendMessage("getstate"); + let details = await extension.awaitMessage("getstate"); + equal(details, "unknown", "initial state"); + + // The captive portal service is started by nsIOService when the pref becomes true, so we + // toggle the pref. We cannot set to false before the extension loads above. + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + details = await extension.awaitMessage("connectivity"); + equal(details.status, "clear", "initial connectivity"); + extension.sendMessage("getstate"); + details = await extension.awaitMessage("getstate"); + equal(details, "not_captive", "initial state"); + + info("REFRESH to other"); + cpResponse = "other"; + cps.recheckCaptivePortal(); + details = await extension.awaitMessage("state"); + equal(details.state, "locked_portal", "state in portal"); + + info("REFRESH to success"); + cpResponse = SUCCESS_STRING; + cps.recheckCaptivePortal(); + details = await extension.awaitMessage("connectivity"); + equal(details.status, "captive", "final connectivity"); + + details = await extension.awaitMessage("state"); + equal(details.state, "unlocked_portal", "state after unlocking portal"); + + assertPersistentListeners( + extension, + "captivePortal", + "onConnectivityAvailable", + { + primed: false, + } + ); + + assertPersistentListeners(extension, "captivePortal", "onStateChanged", { + primed: false, + }); + + assertPersistentListeners(extension, "captivePortal", "captiveURL.onChange", { + primed: false, + }); + + info("Test event page terminate/waken"); + + await extension.terminateBackground({ disableResetIdleForTest: true }); + ok( + !extension.extension.backgroundContext, + "Background Extension context should have been destroyed" + ); + + assertPersistentListeners(extension, "captivePortal", "onStateChanged", { + primed: true, + }); + assertPersistentListeners( + extension, + "captivePortal", + "onConnectivityAvailable", + { + primed: true, + } + ); + + assertPersistentListeners(extension, "captivePortal", "captiveURL.onChange", { + primed: true, + }); + + info("REFRESH 2nd pass to other"); + cpResponse = "other"; + cps.recheckCaptivePortal(); + details = await extension.awaitMessage("state"); + equal(details.state, "locked_portal", "state in portal"); + + info("Test event page terminate/waken with settings"); + + await extension.terminateBackground({ disableResetIdleForTest: true }); + ok( + !extension.extension.backgroundContext, + "Background Extension context should have been destroyed" + ); + + assertPersistentListeners(extension, "captivePortal", "captiveURL.onChange", { + primed: true, + }); + + Services.prefs.setStringPref( + "captivedetect.canonicalURL", + "http://example.com" + ); + let url = await extension.awaitMessage("url"); + equal( + url.value, + "http://example.com", + "The canonicalURL setting has the expected value." + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_captivePortal_url.js b/toolkit/components/extensions/test/xpcshell/test_ext_captivePortal_url.js new file mode 100644 index 0000000000..7bd83b0572 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_captivePortal_url.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_url_get_without_set() { + async function background() { + browser.captivePortal.canonicalURL.onChange.addListener(details => { + browser.test.sendMessage("url", details); + }); + let url = await browser.captivePortal.canonicalURL.get({}); + browser.test.sendMessage("url", url); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["captivePortal"], + }, + }); + + let defaultURL = Services.prefs.getStringPref("captivedetect.canonicalURL"); + + await extension.startup(); + let url = await extension.awaitMessage("url"); + equal( + url.value, + defaultURL, + "The canonicalURL setting has the expected value." + ); + equal( + url.levelOfControl, + "not_controllable", + "The canonicalURL setting has the expected levelOfControl." + ); + + Services.prefs.setStringPref( + "captivedetect.canonicalURL", + "http://example.com" + ); + url = await extension.awaitMessage("url"); + equal( + url.value, + "http://example.com", + "The canonicalURL setting has the expected value." + ); + equal( + url.levelOfControl, + "not_controllable", + "The canonicalURL setting has the expected levelOfControl." + ); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_clear_cached_resources.js b/toolkit/components/extensions/test/xpcshell/test_ext_clear_cached_resources.js new file mode 100644 index 0000000000..715cc3c320 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_clear_cached_resources.js @@ -0,0 +1,417 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +Services.prefs.setBoolPref("extensions.blocklist.enabled", false); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "43" +); + +const { AddonManager } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); + +const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; + +const BASE64_R_PIXEL = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4z8DwHwAFAAH/F1FwBgAAAABJRU5ErkJggg=="; +const BASE64_G_PIXEL = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2Ng+M/wHwAEAQH/7yMK/gAAAABJRU5ErkJggg=="; +const BASE64_B_PIXEL = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYPj/HwADAgH/eL9GtQAAAABJRU5ErkJggg=="; + +const toArrayBuffer = b64data => + Uint8Array.from(atob(b64data), c => c.charCodeAt(0)); +const IMAGE_RED = toArrayBuffer(BASE64_R_PIXEL).buffer; +const IMAGE_GREEN = toArrayBuffer(BASE64_G_PIXEL).buffer; +const IMAGE_BLUE = toArrayBuffer(BASE64_B_PIXEL).buffer; + +const RGB_RED = "rgb(255, 0, 0)"; +const RGB_GREEN = "rgb(0, 255, 0)"; +const RGB_BLUE = "rgb(0, 0, 255)"; + +const CSS_RED_BG = `body { background-color: ${RGB_RED}; }`; +const CSS_GREEN_BG = `body { background-color: ${RGB_GREEN}; }`; +const CSS_BLUE_BG = `body { background-color: ${RGB_BLUE}; }`; + +const ADDON_ID = "test-cached-resources@test"; + +const manifest = { + version: "1", + browser_specific_settings: { gecko: { id: ADDON_ID } }, +}; + +const files = { + "extpage.html": ` + + + + + + + + + `, + "other_extpage.html": ` + + + + + `, + "extpage.css": CSS_RED_BG, + "image.png": IMAGE_RED, +}; + +const getBackgroundColor = () => { + return this.content.getComputedStyle(this.content.document.body) + .backgroundColor; +}; + +const hasCachedImage = imgUrl => { + const { document } = this.content; + + const imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(document); + + const imgCacheProps = imageCache.findEntryProperties( + Services.io.newURI(imgUrl), + document + ); + + // return true if the image was in the cache. + return !!imgCacheProps; +}; + +const getImageColor = () => { + const { document } = this.content; + const img = document.querySelector("img#test-image"); + const canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); // Draw without scaling. + const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; + if (a < 1) { + return `rgba(${r}, ${g}, ${b}, ${a})`; + } + return `rgb(${r}, ${g}, ${b})`; +}; + +async function assertBackgroundColor(page, color, message) { + equal( + await page.spawn([], getBackgroundColor), + color, + `Got the expected ${message}` + ); +} + +async function assertImageColor(page, color, message) { + equal(await page.spawn([], getImageColor), color, message); +} + +async function assertImageCached(page, imageUrl, message) { + ok(await page.spawn([imageUrl], hasCachedImage), message); +} + +// This test verifies that cached css are cleared across addon upgrades and downgrades +// for permanently installed addon (See Bug 1746841). +add_task(async function test_cached_resources_cleared_across_addon_updates() { + await AddonTestUtils.promiseStartupManager(); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest, + files, + }); + + await extension.startup(); + equal( + extension.version, + "1", + "Got the expected version for the initial extension" + ); + + const url = extension.extension.baseURI.resolve("extpage.html"); + let page = await ExtensionTestUtils.loadContentPage(url); + await assertBackgroundColor( + page, + RGB_RED, + "background color (initial extension version)" + ); + await assertImageColor(page, RGB_RED, "image (initial extension version)"); + + info("Verify extension page css and image after addon upgrade"); + + await extension.upgrade({ + useAddonManager: "permanent", + manifest: { + ...manifest, + version: "2", + }, + files: { + ...files, + "extpage.css": CSS_GREEN_BG, + "image.png": IMAGE_GREEN, + }, + }); + equal( + extension.version, + "2", + "Got the expected version for the upgraded extension" + ); + + await page.loadURL(url); + + await assertBackgroundColor( + page, + RGB_GREEN, + "background color (upgraded extension version)" + ); + await assertImageColor(page, RGB_GREEN, "image (upgraded extension version)"); + + info("Verify extension page css and image after addon downgrade"); + + await extension.upgrade({ + useAddonManager: "permanent", + manifest, + files, + }); + equal( + extension.version, + "1", + "Got the expected version for the downgraded extension" + ); + + await page.loadURL(url); + + await assertBackgroundColor( + page, + RGB_RED, + "background color (downgraded extension version)" + ); + await assertImageColor( + page, + RGB_RED, + "image color (downgraded extension version)" + ); + + await page.close(); + await extension.unload(); + await AddonTestUtils.promiseShutdownManager(); +}); + +// This test verifies that cached css are cleared if we are installing a new +// extension and we did not clear the cache for a previous one with the same uuid +// when it was uninstalled (See Bug 1746841). +add_task(async function test_cached_resources_cleared_on_addon_install() { + // Make sure the test addon installed without an AddonManager addon wrapper + // and the ones installed right after that using the AddonManager will share + // the same uuid (and so also the same moz-extension resource urls). + Services.prefs.setBoolPref(LEAVE_UUID_PREF, true); + registerCleanupFunction(() => Services.prefs.clearUserPref(LEAVE_UUID_PREF)); + + await AddonTestUtils.promiseStartupManager(); + + const nonAOMExtension = ExtensionTestUtils.loadExtension({ + manifest, + files: { + ...files, + // Override css with a different color from the one expected + // later in this test case. + "extpage.css": CSS_BLUE_BG, + "image.png": IMAGE_BLUE, + }, + }); + + await nonAOMExtension.startup(); + equal( + await AddonManager.getAddonByID(ADDON_ID), + null, + "No AOM addon wrapper found as expected" + ); + let url = nonAOMExtension.extension.baseURI.resolve("extpage.html"); + let page = await ExtensionTestUtils.loadContentPage(url); + await assertBackgroundColor( + page, + RGB_BLUE, + "background color (addon installed without uninstall observer)" + ); + await assertImageColor( + page, + RGB_BLUE, + "image (addon uninstalled without clearing cache)" + ); + + // NOTE: unloading a test extension that does not have an AddonManager addon wrapper + // does not trigger the uninstall observer, and this is what this test needs to make + // sure that if the cached resources were not cleared on uninstall, then we will still + // clear it when a newly installed addon is installed even if the two extensions + // are sharing the same addon uuid (and so also the same moz-extension resource urls). + await nonAOMExtension.unload(); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest, + files, + }); + + await extension.startup(); + await page.loadURL(url); + + await assertBackgroundColor( + page, + RGB_RED, + "background color (newly installed addon, same addon id)" + ); + await assertImageColor( + page, + RGB_RED, + "image (newly installed addon, same addon id)" + ); + + await page.close(); + await extension.unload(); + await AddonTestUtils.promiseShutdownManager(); +}); + +// This test verifies that reloading a temporarily installed addon after +// changing a css file cached in a previous run clears the previously +// cached css and uses the new one changed on disk (See Bug 1746841). +add_task( + async function test_cached_resources_cleared_on_temporary_addon_reload() { + await AddonTestUtils.promiseStartupManager(); + + const xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest, + files, + }); + + // 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 = `xpcshelltest_unpacked_addons_${random}`; + let tmpExtPath = FileUtils.getDir("TmpD", [tmpDirName], true); + registerCleanupFunction(() => { + tmpExtPath.remove(true); + }); + + // Unpacking the xpi file into the temporary directory. + const extDir = await AddonTestUtils.manuallyInstall( + xpi, + tmpExtPath, + null, + /* unpacked */ true + ); + + let extension = ExtensionTestUtils.expectExtension(ADDON_ID); + await AddonManager.installTemporaryAddon(extDir); + await extension.awaitStartup(); + + equal( + extension.version, + "1", + "Got the expected version for the initial extension" + ); + + const url = extension.extension.baseURI.resolve("extpage.html"); + let page = await ExtensionTestUtils.loadContentPage(url); + await assertBackgroundColor( + page, + RGB_RED, + "background color (initial extension version)" + ); + await assertImageColor(page, RGB_RED, "image (initial extension version)"); + + info("Verify updated extension page css and image after addon reload"); + + const targetCSSFile = extDir.clone(); + targetCSSFile.append("extpage.css"); + ok( + targetCSSFile.exists(), + `Found the ${targetCSSFile.path} target file on disk` + ); + await IOUtils.writeUTF8(targetCSSFile.path, CSS_GREEN_BG); + + const targetPNGFile = extDir.clone(); + targetPNGFile.append("image.png"); + ok( + targetPNGFile.exists(), + `Found the ${targetPNGFile.path} target file on disk` + ); + await IOUtils.write(targetPNGFile.path, toArrayBuffer(BASE64_G_PIXEL)); + + const addon = await AddonManager.getAddonByID(ADDON_ID); + ok(addon, "Got an AddonWrapper for the test extension"); + await addon.reload(); + + await page.loadURL(url); + + await assertBackgroundColor( + page, + RGB_GREEN, + "background (updated files on disk)" + ); + await assertImageColor(page, RGB_GREEN, "image (updated files on disk)"); + + await page.close(); + await addon.uninstall(); + await AddonTestUtils.promiseShutdownManager(); + } +); + +// This test verifies that cached images are not cleared between +// permanently installed addon reloads. +add_task(async function test_cached_image_kept_on_permanent_addon_restarts() { + await AddonTestUtils.promiseStartupManager(); + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest, + files, + }); + + await extension.startup(); + + equal( + extension.version, + "1", + "Got the expected version for the initial extension" + ); + + const imageUrl = extension.extension.baseURI.resolve("image.png"); + const url = extension.extension.baseURI.resolve("extpage.html"); + + let page = await ExtensionTestUtils.loadContentPage(url); + await assertBackgroundColor( + page, + RGB_RED, + "background color (first startup)" + ); + await assertImageColor(page, RGB_RED, "image (first startup)"); + await assertImageCached(page, imageUrl, "image cached (first startup)"); + + info("Reload the AddonManager to simulate browser restart"); + extension.setRestarting(); + await AddonTestUtils.promiseRestartManager(); + await extension.awaitStartup(); + + await page.loadURL(extension.extension.baseURI.resolve("other_extpage.html")); + await assertImageCached( + page, + imageUrl, + "image still cached after AddonManager restart" + ); + + await page.close(); + await extension.unload(); + await AddonTestUtils.promiseShutdownManager(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentScripts_register.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentScripts_register.js new file mode 100644 index 0000000000..9210d11838 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentScripts_register.js @@ -0,0 +1,809 @@ +"use strict"; + +const { createAppInfo } = AddonTestUtils; + +AddonTestUtils.init(this); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49"); + +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; + +function check_applied_styles() { + const urlElStyle = getComputedStyle( + document.querySelector("#registered-extension-url-style") + ); + const blobElStyle = getComputedStyle( + document.querySelector("#registered-extension-text-style") + ); + + browser.test.sendMessage("registered-styles-results", { + registeredExtensionUrlStyleBG: urlElStyle["background-color"], + registeredExtensionBlobStyleBG: blobElStyle["background-color"], + }); +} + +add_task(async function test_contentscripts_register_css() { + async function background() { + let cssCode = ` + #registered-extension-text-style { + background-color: blue; + } + `; + + const matches = ["http://localhost/*/file_sample_registered_styles.html"]; + + browser.test.assertThrows( + () => { + browser.contentScripts.register({ + matches, + unknownParam: "unexpected property", + }); + }, + /Unexpected property "unknownParam"/, + "contentScripts.register throws on unexpected properties" + ); + + let fileScript = await browser.contentScripts.register({ + css: [{ file: "registered_ext_style.css" }], + matches, + runAt: "document_start", + }); + + let textScript = await browser.contentScripts.register({ + css: [{ code: cssCode }], + matches, + runAt: "document_start", + }); + + browser.test.onMessage.addListener(async msg => { + switch (msg) { + case "unregister-text": + await textScript.unregister().catch(err => { + browser.test.fail( + `Unexpected exception while unregistering text style: ${err}` + ); + }); + + await browser.test.assertRejects( + textScript.unregister(), + /Content script already unregistered/, + "Got the expected rejection on calling script.unregister() multiple times" + ); + + browser.test.sendMessage("unregister-text:done"); + break; + case "unregister-file": + await fileScript.unregister().catch(err => { + browser.test.fail( + `Unexpected exception while unregistering url style: ${err}` + ); + }); + + await browser.test.assertRejects( + fileScript.unregister(), + /Content script already unregistered/, + "Got the expected rejection on calling script.unregister() multiple times" + ); + + browser.test.sendMessage("unregister-file:done"); + break; + default: + browser.test.fail(`Unexpected test message received: ${msg}`); + } + }); + + browser.test.sendMessage("background_ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "http://localhost/*/file_sample_registered_styles.html", + "", + ], + content_scripts: [ + { + matches: ["http://localhost/*/file_sample_registered_styles.html"], + run_at: "document_idle", + js: ["check_applied_styles.js"], + }, + ], + }, + background, + + files: { + "check_applied_styles.js": check_applied_styles, + "registered_ext_style.css": ` + #registered-extension-url-style { + background-color: red; + } + `, + }, + }); + + await extension.startup(); + + await extension.awaitMessage("background_ready"); + + // Ensure that a content page running in a content process and which has been + // started after the content scripts has been registered, it still receives + // and registers the expected content scripts. + let contentPage = await ExtensionTestUtils.loadContentPage(`about:blank`); + + await contentPage.loadURL(`${BASE_URL}/file_sample_registered_styles.html`); + + const registeredStylesResults = await extension.awaitMessage( + "registered-styles-results" + ); + + equal( + registeredStylesResults.registeredExtensionUrlStyleBG, + "rgb(255, 0, 0)", + "The expected style has been applied from the registered extension url style" + ); + equal( + registeredStylesResults.registeredExtensionBlobStyleBG, + "rgb(0, 0, 255)", + "The expected style has been applied from the registered extension blob style" + ); + + extension.sendMessage("unregister-file"); + await extension.awaitMessage("unregister-file:done"); + + await contentPage.loadURL(`${BASE_URL}/file_sample_registered_styles.html`); + + const unregisteredURLStylesResults = await extension.awaitMessage( + "registered-styles-results" + ); + + equal( + unregisteredURLStylesResults.registeredExtensionUrlStyleBG, + "rgba(0, 0, 0, 0)", + "The expected style has been applied once extension url style has been unregistered" + ); + equal( + unregisteredURLStylesResults.registeredExtensionBlobStyleBG, + "rgb(0, 0, 255)", + "The expected style has been applied from the registered extension blob style" + ); + + extension.sendMessage("unregister-text"); + await extension.awaitMessage("unregister-text:done"); + + await contentPage.loadURL(`${BASE_URL}/file_sample_registered_styles.html`); + + const unregisteredBlobStylesResults = await extension.awaitMessage( + "registered-styles-results" + ); + + equal( + unregisteredBlobStylesResults.registeredExtensionUrlStyleBG, + "rgba(0, 0, 0, 0)", + "The expected style has been applied once extension url style has been unregistered" + ); + equal( + unregisteredBlobStylesResults.registeredExtensionBlobStyleBG, + "rgba(0, 0, 0, 0)", + "The expected style has been applied once extension blob style has been unregistered" + ); + + await contentPage.close(); + await extension.unload(); +}); + +add_task(async function test_contentscripts_unregister_on_context_unload() { + async function background() { + const frame = document.createElement("iframe"); + frame.setAttribute("src", "/background-frame.html"); + + document.body.appendChild(frame); + + browser.test.onMessage.addListener(msg => { + switch (msg) { + case "unload-frame": + frame.remove(); + browser.test.sendMessage("unload-frame:done"); + break; + default: + browser.test.fail(`Unexpected test message received: ${msg}`); + } + }); + + browser.test.sendMessage("background_ready"); + } + + async function background_frame() { + await browser.contentScripts.register({ + css: [{ file: "registered_ext_style.css" }], + matches: ["http://localhost/*/file_sample_registered_styles.html"], + runAt: "document_start", + }); + + browser.test.sendMessage("background_frame_ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["http://localhost/*/file_sample_registered_styles.html"], + content_scripts: [ + { + matches: ["http://localhost/*/file_sample_registered_styles.html"], + run_at: "document_idle", + js: ["check_applied_styles.js"], + }, + ], + }, + background, + + files: { + "background-frame.html": ` + + + + + + + + `, + "background-frame.js": background_frame, + "check_applied_styles.js": check_applied_styles, + "registered_ext_style.css": ` + #registered-extension-url-style { + background-color: red; + } + `, + }, + }); + + await extension.startup(); + + await extension.awaitMessage("background_ready"); + + // Wait the background frame to have been loaded and its script + // executed. + await extension.awaitMessage("background_frame_ready"); + + // Ensure that a content page running in a content process and which has been + // started after the content scripts has been registered, it still receives + // and registers the expected content scripts. + let contentPage = await ExtensionTestUtils.loadContentPage(`about:blank`); + + await contentPage.loadURL(`${BASE_URL}/file_sample_registered_styles.html`); + + const registeredStylesResults = await extension.awaitMessage( + "registered-styles-results" + ); + + equal( + registeredStylesResults.registeredExtensionUrlStyleBG, + "rgb(255, 0, 0)", + "The expected style has been applied from the registered extension url style" + ); + + extension.sendMessage("unload-frame"); + await extension.awaitMessage("unload-frame:done"); + + await contentPage.loadURL(`${BASE_URL}/file_sample_registered_styles.html`); + + const unregisteredURLStylesResults = await extension.awaitMessage( + "registered-styles-results" + ); + + equal( + unregisteredURLStylesResults.registeredExtensionUrlStyleBG, + "rgba(0, 0, 0, 0)", + "The expected style has been applied once extension url style has been unregistered" + ); + + await contentPage.close(); + await extension.unload(); +}); + +add_task(async function test_contentscripts_register_js() { + async function background() { + browser.runtime.onMessage.addListener( + ([msg, expectedStates, readyState], sender) => { + if (msg == "chrome-namespace-ok") { + browser.test.sendMessage(msg); + return; + } + + browser.test.assertEq("script-run", msg, "message type is correct"); + browser.test.assertTrue( + expectedStates.includes(readyState), + `readyState "${readyState}" is one of [${expectedStates}]` + ); + browser.test.sendMessage("script-run-" + expectedStates[0]); + } + ); + + // Raise an exception when the content script cannot be registered + // because the extension has no permission to access the specified origin. + + await browser.test.assertRejects( + browser.contentScripts.register({ + matches: ["http://*/*"], + js: [ + { + code: + 'browser.test.fail("content script with wrong matches should not run")', + }, + ], + }), + /Permission denied to register a content script for/, + "The reject contains the expected error message" + ); + + // Register a content script from a JS code string. + + function textScriptCodeStart() { + browser.runtime.sendMessage([ + "script-run", + ["loading"], + document.readyState, + ]); + } + function textScriptCodeEnd() { + browser.runtime.sendMessage([ + "script-run", + ["interactive", "complete"], + document.readyState, + ]); + } + function textScriptCodeIdle() { + browser.runtime.sendMessage([ + "script-run", + ["complete"], + document.readyState, + ]); + } + + // Register content scripts from both extension URLs and plain JS code strings. + + const content_scripts = [ + // Plain JS code strings. + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ code: `(${textScriptCodeStart})()` }], + runAt: "document_start", + }, + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ code: `(${textScriptCodeEnd})()` }], + runAt: "document_end", + }, + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ code: `(${textScriptCodeIdle})()` }], + runAt: "document_idle", + }, + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ code: `(${textScriptCodeIdle})()` }], + runAt: "document_idle", + cookieStoreId: "firefox-container-1", + }, + // Extension URLs. + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ file: "content_script_start.js" }], + runAt: "document_start", + }, + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ file: "content_script_end.js" }], + runAt: "document_end", + }, + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ file: "content_script_idle.js" }], + runAt: "document_idle", + }, + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ file: "content_script.js" }], + // "runAt" is not specified here to ensure that it defaults to document_idle when missing. + }, + { + matches: ["http://localhost/*/file_sample.html"], + js: [{ file: "content_script_idle.js" }], + runAt: "document_idle", + cookieStoreId: "firefox-container-1", + }, + ]; + + const expectedAPIs = ["unregister"]; + + for (const scriptOptions of content_scripts) { + const script = await browser.contentScripts.register(scriptOptions); + const actualAPIs = Object.keys(script); + + browser.test.assertEq( + JSON.stringify(expectedAPIs), + JSON.stringify(actualAPIs), + `Got a script API object for ${scriptOptions.js[0]}` + ); + } + + browser.test.sendMessage("background-ready"); + } + + function contentScriptStart() { + browser.runtime.sendMessage([ + "script-run", + ["loading"], + document.readyState, + ]); + } + function contentScriptEnd() { + browser.runtime.sendMessage([ + "script-run", + ["interactive", "complete"], + document.readyState, + ]); + } + function contentScriptIdle() { + browser.runtime.sendMessage([ + "script-run", + ["complete"], + document.readyState, + ]); + } + + function contentScript() { + let manifest = browser.runtime.getManifest(); + void manifest.permissions; + browser.runtime.sendMessage(["chrome-namespace-ok"]); + } + + let extensionData = { + manifest: { + permissions: ["http://localhost/*/file_sample.html"], + }, + background, + + files: { + "content_script_start.js": contentScriptStart, + "content_script_end.js": contentScriptEnd, + "content_script_idle.js": contentScriptIdle, + "content_script.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let loadingCount = 0; + let interactiveCount = 0; + let completeCount = 0; + extension.onMessage("script-run-loading", () => { + loadingCount++; + }); + extension.onMessage("script-run-interactive", () => { + interactiveCount++; + }); + + let completePromise = new Promise(resolve => { + extension.onMessage("script-run-complete", () => { + completeCount++; + if (completeCount == 2) { + resolve(); + } + }); + }); + + let chromeNamespacePromise = extension.awaitMessage("chrome-namespace-ok"); + + // Ensure that a content page running in a content process and which has been + // already loaded when the content scripts has been registered, it has received + // and registered the expected content scripts. + let contentPage = await ExtensionTestUtils.loadContentPage(`about:blank`); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + await contentPage.loadURL(`${BASE_URL}/file_sample.html`); + + await Promise.all([completePromise, chromeNamespacePromise]); + + await contentPage.close(); + + // Expect two content scripts to run (one registered using an extension URL + // and one registered from plain JS code). + equal(loadingCount, 2, "document_start script ran exactly twice"); + equal(interactiveCount, 2, "document_end script ran exactly twice"); + equal(completeCount, 2, "document_idle script ran exactly twice"); + + await extension.unload(); +}); + +// Test that the contentScripts.register options are correctly translated +// into the expected WebExtensionContentScript properties. +add_task(async function test_contentscripts_register_all_options() { + async function background() { + await browser.contentScripts.register({ + js: [{ file: "content_script.js" }], + css: [{ file: "content_style.css" }], + matches: ["http://localhost/*"], + excludeMatches: ["http://localhost/exclude/*"], + excludeGlobs: ["*_exclude.html"], + includeGlobs: ["*_include.html"], + allFrames: true, + matchAboutBlank: true, + runAt: "document_start", + }); + + browser.test.sendMessage("background-ready", window.location.origin); + } + + const extensionData = { + manifest: { + permissions: ["http://localhost/*"], + }, + background, + + files: { + "content_script.js": "", + "content_style.css": "", + }, + }; + + const extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + const baseExtURL = await extension.awaitMessage("background-ready"); + + const policy = WebExtensionPolicy.getByID(extension.id); + + ok(policy, "Got the WebExtensionPolicy for the test extension"); + equal( + policy.contentScripts.length, + 1, + "Got the expected number of registered content scripts" + ); + + const script = policy.contentScripts[0]; + let { + allFrames, + cssPaths, + jsPaths, + matchAboutBlank, + runAt, + originAttributesPatterns, + } = script; + + deepEqual( + { + allFrames, + cssPaths, + jsPaths, + matchAboutBlank, + runAt, + originAttributesPatterns, + }, + { + allFrames: true, + cssPaths: [`${baseExtURL}/content_style.css`], + jsPaths: [`${baseExtURL}/content_script.js`], + matchAboutBlank: true, + runAt: "document_start", + originAttributesPatterns: null, + }, + "Got the expected content script properties" + ); + + ok( + script.matchesURI(Services.io.newURI("http://localhost/ok_include.html")), + "matched and include globs should match" + ); + ok( + !script.matchesURI( + Services.io.newURI("http://localhost/exclude/ok_include.html") + ), + "exclude matches should not match" + ); + ok( + !script.matchesURI(Services.io.newURI("http://localhost/ok_exclude.html")), + "exclude globs should not match" + ); + + await extension.unload(); +}); + +add_task(async function test_contentscripts_register_cookieStoreId() { + async function background() { + let cookieStoreIdCSSArray = [ + { id: null, color: "rgb(123, 45, 67)" }, + { id: "firefox-private", color: "rgb(255,255,0)" }, + { id: "firefox-default", color: "red" }, + { id: "firefox-container-1", color: "green" }, + { id: "firefox-container-2", color: "blue" }, + { + id: ["firefox-container-3", "firefox-container-4"], + color: "rgb(100,100,0)", + }, + ]; + const matches = ["http://localhost/*/file_sample_registered_styles.html"]; + + for (let { id, color } of cookieStoreIdCSSArray) { + await browser.contentScripts.register({ + css: [ + { + code: `#registered-extension-text-style { + background-color: ${color}}`, + }, + ], + matches, + runAt: "document_start", + cookieStoreId: id, + }); + } + await browser.test.assertRejects( + browser.contentScripts.register({ + css: [{ code: `body {}` }], + matches, + cookieStoreId: "not_a_valid_cookieStoreId", + }), + /Invalid cookieStoreId/, + "contentScripts.register with an invalid cookieStoreId" + ); + + if (!navigator.userAgent.includes("Android")) { + await browser.test.assertRejects( + browser.contentScripts.register({ + css: [{ code: `body {}` }], + matches, + cookieStoreId: "firefox-container-999", + }), + /Invalid cookieStoreId/, + "contentScripts.register with an invalid cookieStoreId" + ); + } else { + // On Android, any firefox-container-... is treated as valid, so it doesn't + // result in an error. + // TODO bug 1743616: Fix implementation and remove this branch. + await browser.contentScripts.register({ + css: [{ code: `body {}` }], + matches, + cookieStoreId: "firefox-container-999", + }); + } + + await browser.test.assertRejects( + browser.contentScripts.register({ + css: [{ code: `body {}` }], + matches, + cookieStoreId: "", + }), + /Invalid cookieStoreId/, + "contentScripts.register with an invalid cookieStoreId" + ); + + browser.test.sendMessage("background_ready"); + } + + const extensionData = { + manifest: { + permissions: [ + "http://localhost/*/file_sample_registered_styles.html", + "", + ], + content_scripts: [ + { + matches: ["http://localhost/*/file_sample_registered_styles.html"], + run_at: "document_idle", + js: ["check_applied_styles.js"], + }, + ], + }, + background, + files: { + "check_applied_styles.js": check_applied_styles, + }, + }; + + const extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + incognitoOverride: "spanning", + }); + + await extension.startup(); + await extension.awaitMessage("background_ready"); + // Index 0 is the one from manifest.json. + let contentScriptMatchTests = [ + { + contentPageOptions: { userContextId: 5 }, + expectedStyles: "rgb(123, 45, 67)", + originAttributesPatternExpected: null, + contentScriptIndex: 1, + }, + { + contentPageOptions: { privateBrowsing: true }, + expectedStyles: "rgb(255, 255, 0)", + originAttributesPatternExpected: [ + { + privateBrowsingId: 1, + userContextId: 0, + }, + ], + contentScriptIndex: 2, + }, + { + contentPageOptions: { userContextId: 0 }, + expectedStyles: "rgb(255, 0, 0)", + originAttributesPatternExpected: [ + { + privateBrowsingId: 0, + userContextId: 0, + }, + ], + contentScriptIndex: 3, + }, + { + contentPageOptions: { userContextId: 1 }, + expectedStyles: "rgb(0, 128, 0)", + originAttributesPatternExpected: [{ userContextId: 1 }], + contentScriptIndex: 4, + }, + { + contentPageOptions: { userContextId: 2 }, + expectedStyles: "rgb(0, 0, 255)", + originAttributesPatternExpected: [{ userContextId: 2 }], + contentScriptIndex: 5, + }, + { + contentPageOptions: { userContextId: 3 }, + expectedStyles: "rgb(100, 100, 0)", + originAttributesPatternExpected: [ + { userContextId: 3 }, + { userContextId: 4 }, + ], + contentScriptIndex: 6, + }, + { + contentPageOptions: { userContextId: 4 }, + expectedStyles: "rgb(100, 100, 0)", + originAttributesPatternExpected: [ + { userContextId: 3 }, + { userContextId: 4 }, + ], + contentScriptIndex: 6, + }, + ]; + + const policy = WebExtensionPolicy.getByID(extension.id); + + for (const testCase of contentScriptMatchTests) { + const { + contentPageOptions, + expectedStyles, + originAttributesPatternExpected, + contentScriptIndex, + } = testCase; + const script = policy.contentScripts[contentScriptIndex]; + + deepEqual(script.originAttributesPatterns, originAttributesPatternExpected); + let contentPage = await ExtensionTestUtils.loadContentPage( + `about:blank`, + contentPageOptions + ); + await contentPage.loadURL(`${BASE_URL}/file_sample_registered_styles.html`); + + let registeredStylesResults = await extension.awaitMessage( + "registered-styles-results" + ); + + equal( + registeredStylesResults.registeredExtensionBlobStyleBG, + expectedStyles, + `Expected styles applied on content page loaded with options + ${JSON.stringify(contentPageOptions)}` + ); + await contentPage.close(); + } + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_content_security_policy.js b/toolkit/components/extensions/test/xpcshell/test_ext_content_security_policy.js new file mode 100644 index 0000000000..35350a1e8e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_content_security_policy.js @@ -0,0 +1,362 @@ +"use strict"; + +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + +const server = createHttpServer({ hosts: ["example.com"] }); + +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write(""); +}); + +server.registerPathHandler("/worker.js", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/javascript", false); + response.write("let x = true;"); +}); + +const baseCSP = []; +// Keep in sync with extensions.webextensions.base-content-security-policy +baseCSP[2] = { + "script-src": [ + "'unsafe-eval'", + "'wasm-unsafe-eval'", + "'unsafe-inline'", + "blob:", + "filesystem:", + "http://localhost:*", + "http://127.0.0.1:*", + "https://*", + "moz-extension:", + "'self'", + ], +}; +// Keep in sync with extensions.webextensions.base-content-security-policy.v3 +baseCSP[3] = { + "script-src": ["'self'", "'wasm-unsafe-eval'"], +}; + +/** + * @typedef TestPolicyExpects + * @type {object} + * @param {boolean} workerEvalAllowed + * @param {boolean} workerImportScriptsAllowed + * @param {boolean} workerWasmAllowed + */ + +/** + * Tests that content security policies for an add-on are actually applied to * + * documents that belong to it. This tests both the base policies and add-on + * specific policies, and ensures that the parsed policies applied to the + * document's principal match what was specified in the policy string. + * + * @param {object} options + * @param {number} [options.manifest_version] + * @param {object} [options.customCSP] + * @param {TestPolicyExpects} options.expects + */ +async function testPolicy({ + manifest_version = 2, + customCSP = null, + expects = {}, +}) { + info( + `Enter tests for extension CSP with ${JSON.stringify({ + manifest_version, + customCSP, + })}` + ); + + let baseURL; + + let addonCSP = { + "script-src": ["'self'"], + }; + + if (manifest_version < 3) { + addonCSP["script-src"].push("'wasm-unsafe-eval'"); + } + + let content_security_policy = null; + + if (customCSP) { + for (let key of Object.keys(customCSP)) { + addonCSP[key] = customCSP[key].split(/\s+/); + } + + content_security_policy = Object.keys(customCSP) + .map(key => `${key} ${customCSP[key]}`) + .join("; "); + } + + function checkSource(name, policy, expected) { + // fallback to script-src when comparing worker-src if policy does not include worker-src + let policySrc = + name != "worker-src" || policy[name] + ? policy[name] + : policy["script-src"]; + equal( + JSON.stringify(policySrc.sort()), + JSON.stringify(expected[name].sort()), + `Expected value for ${name}` + ); + } + + function checkCSP(csp, location) { + let policies = csp["csp-policies"]; + + info(`Base policy for ${location}`); + let base = baseCSP[manifest_version]; + + equal(policies[0]["report-only"], false, "Policy is not report-only"); + for (let key in base) { + checkSource(key, policies[0], base); + } + + info(`Add-on policy for ${location}`); + + equal(policies[1]["report-only"], false, "Policy is not report-only"); + for (let key in addonCSP) { + checkSource(key, policies[1], addonCSP); + } + } + + function background() { + browser.test.sendMessage( + "base-url", + browser.runtime.getURL("").replace(/\/$/, "") + ); + + browser.test.sendMessage("background-csp", window.getCsp()); + } + + function tabScript() { + browser.test.sendMessage("tab-csp", window.getCsp()); + + const worker = new Worker("worker.js"); + worker.onmessage = event => { + browser.test.sendMessage("worker-csp", event.data); + }; + + worker.postMessage({}); + } + + function testWorker(port) { + this.onmessage = () => { + let importScriptsAllowed; + let evalAllowed; + let wasmAllowed; + + try { + eval("let y = true;"); // eslint-disable-line no-eval + evalAllowed = true; + } catch (e) { + evalAllowed = false; + } + + try { + new WebAssembly.Module( + new Uint8Array([0, 0x61, 0x73, 0x6d, 0x1, 0, 0, 0]) + ); + wasmAllowed = true; + } catch (e) { + wasmAllowed = false; + } + + try { + // eslint-disable-next-line no-undef + importScripts(`http://127.0.0.1:${port}/worker.js`); + importScriptsAllowed = true; + } catch (e) { + importScriptsAllowed = false; + } + + postMessage({ evalAllowed, importScriptsAllowed, wasmAllowed }); + }; + } + + let web_accessible_resources = ["content.html", "tab.html"]; + if (manifest_version == 3) { + let extension_pages = content_security_policy; + content_security_policy = { + extension_pages, + }; + let resources = web_accessible_resources; + web_accessible_resources = [ + { resources, matches: ["http://example.com/*"] }, + ]; + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + + files: { + "tab.html": ` + + + `, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let awaitConsole = new Promise(resolve => { + Services.console.registerListener(function listener(message) { + if (/WebExt Privilege Escalation/.test(message.message)) { + Services.console.unregisterListener(listener); + resolve(message); + } + }); + }); + + await extension.startup(); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/data/file_sample.html" + ); + + let message = await awaitConsole; + ok( + message.message.includes( + "WebExt Privilege Escalation: typeof(browser) = undefined" + ), + "Document does not have `browser` APIs." + ); + + await contentPage.close(); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_async_loading.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_async_loading.js new file mode 100644 index 0000000000..cb9a07142d --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_async_loading.js @@ -0,0 +1,79 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); + +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write(""); +}); + +add_task(async function test_async_loading() { + const adder = `(function add(a = 1) { this.count += a; })();\n`; + + const extension = { + manifest: { + content_scripts: [ + { + run_at: "document_start", + matches: ["http://example.com/dummy"], + js: ["first.js", "second.js"], + }, + { + run_at: "document_end", + matches: ["http://example.com/dummy"], + js: ["third.js"], + }, + ], + }, + files: { + "first.js": ` + this.count = 0; + ${adder.repeat(50000)}; // 2Mb + browser.test.assertEq(this.count, 50000, "A 50k line script"); + + this.order = (this.order || 0) + 1; + browser.test.sendMessage("first", this.order); + `, + "second.js": ` + this.order = (this.order || 0) + 1; + browser.test.sendMessage("second", this.order); + `, + "third.js": ` + this.order = (this.order || 0) + 1; + browser.test.sendMessage("third", this.order); + `, + }, + }; + + async function checkOrder(ext) { + const [first, second, third] = await Promise.all([ + ext.awaitMessage("first"), + ext.awaitMessage("second"), + ext.awaitMessage("third"), + ]); + + equal(first, 1, "first.js finished execution first."); + equal(second, 2, "second.js finished execution second."); + equal(third, 3, "third.js finished execution third."); + } + + info("Test pages observed while extension is running"); + const observed = ExtensionTestUtils.loadExtension(extension); + await observed.startup(); + + const contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + await checkOrder(observed); + await observed.unload(); + + info("Test pages already existing on extension startup"); + const existing = ExtensionTestUtils.loadExtension(extension); + + await existing.startup(); + await checkOrder(existing); + await existing.unload(); + + await contentPage.close(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_canvas_tainting.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_canvas_tainting.js new file mode 100644 index 0000000000..4ac22dc700 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_canvas_tainting.js @@ -0,0 +1,128 @@ +"use strict"; + +const server = createHttpServer({ + hosts: ["green.example.com", "red.example.com"], +}); + +server.registerDirectory("/data/", do_get_file("data")); + +server.registerPathHandler("/pixel.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write(` + + `); +}); + +add_task(async function test_contentscript_canvas_tainting() { + async function contentScript() { + let canvas = document.createElement("canvas"); + let ctx = canvas.getContext("2d"); + document.body.appendChild(canvas); + + function draw(url) { + return new Promise(resolve => { + let img = document.createElement("img"); + img.onload = () => { + ctx.drawImage(img, 0, 0, 1, 1); + resolve(); + }; + img.src = url; + }); + } + + function readByExt() { + let { data } = ctx.getImageData(0, 0, 1, 1); + return data.slice(0, 3).join(); + } + + let readByWeb = window.wrappedJSObject.readByWeb; + + // Test reading after drawing an image from the same origin as the web page. + await draw("http://green.example.com/data/pixel_green.gif"); + browser.test.assertEq( + readByWeb(), + "0,255,0", + "Content can read same-origin image" + ); + browser.test.assertEq( + readByExt(), + "0,255,0", + "Extension can read same-origin image" + ); + + // Test reading after drawing a blue pixel data URI from extension content script. + await draw( + "data:image/gif;base64,R0lGODlhAQABAIABAAAA/wAAACwAAAAAAQABAAACAkQBADs=" + ); + browser.test.assertThrows( + readByWeb, + /operation is insecure/, + "Content can't read extension's image" + ); + browser.test.assertEq( + readByExt(), + "0,0,255", + "Extension can read its own image" + ); + + // Test after tainting the canvas with an image from a third party domain. + await draw("http://red.example.com/data/pixel_red.gif"); + browser.test.assertThrows( + readByWeb, + /operation is insecure/, + "Content can't read third party image" + ); + browser.test.assertThrows( + readByExt, + /operation is insecure/, + "Extension can't read fully tainted" + ); + + // Test canvas is still fully tainted after drawing extension's data: image again. + await draw( + "data:image/gif;base64,R0lGODlhAQABAIABAAAA/wAAACwAAAAAAQABAAACAkQBADs=" + ); + browser.test.assertThrows( + readByWeb, + /operation is insecure/, + "Canvas still fully tainted for content" + ); + browser.test.assertThrows( + readByExt, + /operation is insecure/, + "Canvas still fully tainted for extension" + ); + + browser.test.sendMessage("done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://green.example.com/pixel.html"], + js: ["cs.js"], + }, + ], + }, + files: { + "cs.js": contentScript, + }, + }); + + await extension.startup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://green.example.com/pixel.html" + ); + await extension.awaitMessage("done"); + + await contentPage.close(); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_context.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_context.js new file mode 100644 index 0000000000..d3f653f5d7 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_context.js @@ -0,0 +1,359 @@ +"use strict"; + +/* eslint-disable mozilla/balanced-listeners */ + +const server = createHttpServer({ hosts: ["example.com", "example.org"] }); + +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write(""); +}); + +function loadExtension() { + function contentScript() { + browser.test.sendMessage("content-script-ready"); + + window.addEventListener( + "pagehide", + () => { + browser.test.sendMessage("content-script-hide"); + }, + true + ); + window.addEventListener("pageshow", () => { + browser.test.sendMessage("content-script-show"); + }); + } + + return ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/dummy*"], + js: ["content_script.js"], + run_at: "document_start", + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }); +} + +add_task(async function test_contentscript_context() { + let extension = loadExtension(); + await extension.startup(); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + await extension.awaitMessage("content-script-ready"); + await extension.awaitMessage("content-script-show"); + + // Get the content script context and check that it points to the correct window. + await contentPage.spawn(extension.id, async extensionId => { + const { ExtensionContent } = ChromeUtils.import( + "resource://gre/modules/ExtensionContent.jsm" + ); + this.context = ExtensionContent.getContextByExtensionId( + extensionId, + this.content + ); + + Assert.ok(this.context, "Got content script context"); + + Assert.equal( + this.context.contentWindow, + this.content, + "Context's contentWindow property is correct" + ); + + // Navigate so that the content page is hidden in the bfcache. + + this.content.location = "http://example.org/dummy"; + }); + + await extension.awaitMessage("content-script-hide"); + + await contentPage.spawn(null, async () => { + Assert.equal( + this.context.contentWindow, + null, + "Context's contentWindow property is null" + ); + + // Navigate back so the content page is resurrected from the bfcache. + this.content.history.back(); + }); + + await extension.awaitMessage("content-script-show"); + + await contentPage.spawn(null, async () => { + Assert.equal( + this.context.contentWindow, + this.content, + "Context's contentWindow property is correct" + ); + }); + + await contentPage.close(); + await extension.awaitMessage("content-script-hide"); + await extension.unload(); +}); + +add_task(async function test_contentscript_context_incognito_not_allowed() { + async function background() { + await browser.contentScripts.register({ + js: [{ file: "registered_script.js" }], + matches: ["http://example.com/dummy"], + runAt: "document_start", + }); + + browser.test.sendMessage("background-ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/dummy"], + js: ["content_script.js"], + run_at: "document_start", + }, + ], + permissions: ["http://example.com/*"], + }, + background, + files: { + "content_script.js": () => { + browser.test.notifyFail("content_script_loaded"); + }, + "registered_script.js": () => { + browser.test.notifyFail("registered_script_loaded"); + }, + }, + }); + + // Bug 1715801: Re-enable pbm portion on GeckoView + if (AppConstants.platform == "android") { + Services.prefs.setBoolPref("dom.security.https_first_pbm", false); + } + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy", + { privateBrowsing: true } + ); + + await contentPage.spawn(extension.id, async extensionId => { + const { ExtensionContent } = ChromeUtils.import( + "resource://gre/modules/ExtensionContent.jsm" + ); + let context = ExtensionContent.getContextByExtensionId( + extensionId, + this.content + ); + Assert.equal( + context, + null, + "Extension unable to use content_script in private browsing window" + ); + }); + + await contentPage.close(); + await extension.unload(); + + // Bug 1715801: Re-enable pbm portion on GeckoView + if (AppConstants.platform == "android") { + Services.prefs.clearUserPref("dom.security.https_first_pbm"); + } +}); + +add_task(async function test_contentscript_context_unload_while_in_bfcache() { + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy?first" + ); + let extension = loadExtension(); + await extension.startup(); + await extension.awaitMessage("content-script-ready"); + + // Get the content script context and check that it points to the correct window. + await contentPage.spawn(extension.id, async extensionId => { + const { ExtensionContent } = ChromeUtils.import( + "resource://gre/modules/ExtensionContent.jsm" + ); + // Save context so we can verify that contentWindow is nulled after unload. + this.context = ExtensionContent.getContextByExtensionId( + extensionId, + this.content + ); + + Assert.equal( + this.context.contentWindow, + this.content, + "Context's contentWindow property is correct" + ); + + this.contextUnloadedPromise = new Promise(resolve => { + this.context.callOnClose({ close: resolve }); + }); + this.pageshownPromise = new Promise(resolve => { + this.content.addEventListener( + "pageshow", + () => { + // Yield to the event loop once more to ensure that all pageshow event + // handlers have been dispatched before fulfilling the promise. + let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" + ); + setTimeout(resolve, 0); + }, + { once: true, mozSystemGroup: true } + ); + }); + + // Navigate so that the content page is hidden in the bfcache. + this.content.location = "http://example.org/dummy?second"; + }); + + await extension.awaitMessage("content-script-hide"); + + await extension.unload(); + await contentPage.spawn(null, async () => { + await this.contextUnloadedPromise; + Assert.equal(this.context.unloaded, true, "Context has been unloaded"); + + // Normally, when a page is not in the bfcache, context.contentWindow is + // not null when the callOnClose handler is invoked (this is checked by the + // previous subtest). + // Now wait a little bit and check again to ensure that the contentWindow + // property is not somehow restored. + await new Promise(resolve => this.content.setTimeout(resolve, 0)); + Assert.equal( + this.context.contentWindow, + null, + "Context's contentWindow property is null" + ); + + // Navigate back so the content page is resurrected from the bfcache. + this.content.history.back(); + + await this.pageshownPromise; + + Assert.equal( + this.context.contentWindow, + null, + "Context's contentWindow property is null after restore from bfcache" + ); + }); + + await contentPage.close(); +}); + +add_task(async function test_contentscript_context_valid_during_execution() { + // This test does the following: + // - Load page + // - Load extension; inject content script. + // - Navigate page; pagehide triggered. + // - Navigate back; pageshow triggered. + // - Close page; pagehide, unload triggered. + // At each of these last four events, the validity of the context is checked. + + function contentScript() { + browser.test.sendMessage("content-script-ready"); + window.wrappedJSObject.checkContextIsValid("Context is valid on execution"); + + window.addEventListener( + "pagehide", + () => { + window.wrappedJSObject.checkContextIsValid( + "Context is valid on pagehide" + ); + browser.test.sendMessage("content-script-hide"); + }, + true + ); + window.addEventListener("pageshow", () => { + window.wrappedJSObject.checkContextIsValid( + "Context is valid on pageshow" + ); + + // This unload listener is registered after pageshow, to ensure that the + // page can be stored in the bfcache at the previous pagehide. + window.addEventListener("unload", () => { + window.wrappedJSObject.checkContextIsValid( + "Context is valid on unload" + ); + browser.test.sendMessage("content-script-unload"); + }); + + browser.test.sendMessage("content-script-show"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/dummy*"], + js: ["content_script.js"], + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy?first" + ); + await contentPage.spawn(extension.id, async extensionId => { + let context; + let checkContextIsValid = description => { + if (!context) { + const { ExtensionContent } = ChromeUtils.import( + "resource://gre/modules/ExtensionContent.jsm" + ); + context = ExtensionContent.getContextByExtensionId( + extensionId, + this.content + ); + } + Assert.equal( + context.contentWindow, + this.content, + `${description}: contentWindow` + ); + Assert.equal(context.active, true, `${description}: active`); + }; + Cu.exportFunction(checkContextIsValid, this.content, { + defineAs: "checkContextIsValid", + }); + }); + await extension.startup(); + await extension.awaitMessage("content-script-ready"); + + await contentPage.spawn(extension.id, async extensionId => { + // Navigate so that the content page is frozen in the bfcache. + this.content.location = "http://example.org/dummy?second"; + }); + + await extension.awaitMessage("content-script-hide"); + await contentPage.spawn(null, async () => { + // Navigate back so the content page is resurrected from the bfcache. + this.content.history.back(); + }); + + await extension.awaitMessage("content-script-show"); + await contentPage.close(); + await extension.awaitMessage("content-script-hide"); + await extension.awaitMessage("content-script-unload"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_context_isolation.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_context_isolation.js new file mode 100644 index 0000000000..ccc7f1452f --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_context_isolation.js @@ -0,0 +1,168 @@ +"use strict"; + +/* globals exportFunction */ +/* eslint-disable mozilla/balanced-listeners */ + +const server = createHttpServer({ hosts: ["example.com", "example.org"] }); + +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write(""); +}); + +server.registerPathHandler("/bfcachetestpage", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + response.write(` +`); +}); + +add_task(async function test_contentscript_context_isolation() { + function contentScript() { + browser.test.sendMessage("content-script-ready"); + + exportFunction(browser.test.sendMessage, window, { + defineAs: "browserTestSendMessage", + }); + + window.addEventListener("pageshow", () => { + browser.test.fail( + "pageshow should have been suppressed by stopImmediatePropagation" + ); + }); + window.addEventListener( + "pagehide", + () => { + browser.test.fail( + "pagehide should have been suppressed by stopImmediatePropagation" + ); + }, + true + ); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/bfcachetestpage"], + js: ["content_script.js"], + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/bfcachetestpage" + ); + await extension.startup(); + await extension.awaitMessage("content-script-ready"); + + // Get the content script context and check that it points to the correct window. + await contentPage.spawn(extension.id, async extensionId => { + const { ExtensionContent } = ChromeUtils.import( + "resource://gre/modules/ExtensionContent.jsm" + ); + this.context = ExtensionContent.getContextByExtensionId( + extensionId, + this.content + ); + + Assert.ok(this.context, "Got content script context"); + + Assert.equal( + this.context.contentWindow, + this.content, + "Context's contentWindow property is correct" + ); + + // Navigate so that the content page is hidden in the bfcache. + + this.content.location = "http://example.org/dummy?noscripthere1"; + }); + + await extension.awaitMessage("content-script-hide"); + + await contentPage.spawn(null, async () => { + Assert.equal( + this.context.contentWindow, + null, + "Context's contentWindow property is null" + ); + Assert.ok(this.context.sandbox, "Context's sandbox exists"); + + // Navigate back so the content page is resurrected from the bfcache. + this.content.history.back(); + }); + + await extension.awaitMessage("content-script-show"); + + async function testWithoutBfcache() { + return contentPage.spawn(null, async () => { + Assert.equal( + this.context.contentWindow, + this.content, + "Context's contentWindow property is correct" + ); + Assert.ok(this.context.sandbox, "Context's sandbox exists before unload"); + + let contextUnloadedPromise = new Promise(resolve => { + this.context.callOnClose({ close: resolve }); + }); + + // Now add an "unload" event listener, which should prevent a page from entering the bfcache. + await new Promise(resolve => { + this.content.addEventListener("unload", () => { + Assert.equal( + this.context.contentWindow, + this.content, + "Context's contentWindow property should be non-null at unload" + ); + resolve(); + }); + this.content.location = "http://example.org/dummy?noscripthere2"; + }); + + await contextUnloadedPromise; + }); + } + await runWithPrefs( + [["docshell.shistory.bfcache.allow_unload_listeners", false]], + testWithoutBfcache + ); + + await extension.awaitMessage("content-script-unload"); + + await contentPage.spawn(null, async () => { + Assert.equal( + this.context.sandbox, + null, + "Context's sandbox has been destroyed after unload" + ); + }); + + await contentPage.close(); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_create_iframe.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_create_iframe.js new file mode 100644 index 0000000000..c404cdb79a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_create_iframe.js @@ -0,0 +1,177 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerDirectory("/data/", do_get_file("data")); + +add_task(async function test_contentscript_create_iframe() { + function background() { + browser.runtime.onMessage.addListener((msg, sender) => { + let { name, availableAPIs, manifest, testGetManifest } = msg; + let hasExtTabsAPI = availableAPIs.indexOf("tabs") > 0; + let hasExtWindowsAPI = availableAPIs.indexOf("windows") > 0; + + browser.test.assertFalse( + hasExtTabsAPI, + "the created iframe should not be able to use privileged APIs (tabs)" + ); + browser.test.assertFalse( + hasExtWindowsAPI, + "the created iframe should not be able to use privileged APIs (windows)" + ); + + let { + browser_specific_settings: { + gecko: { id: expectedManifestGeckoId }, + }, + } = chrome.runtime.getManifest(); + let { + browser_specific_settings: { + gecko: { id: actualManifestGeckoId }, + }, + } = manifest; + + browser.test.assertEq( + actualManifestGeckoId, + expectedManifestGeckoId, + "the add-on manifest should be accessible from the created iframe" + ); + + let { + browser_specific_settings: { + gecko: { id: testGetManifestGeckoId }, + }, + } = testGetManifest; + + browser.test.assertEq( + testGetManifestGeckoId, + expectedManifestGeckoId, + "GET_MANIFEST() returns manifest data before extension unload" + ); + + browser.test.sendMessage(name); + }); + } + + function contentScriptIframe() { + window.GET_MANIFEST = browser.runtime.getManifest.bind(null); + + window.testGetManifestException = () => { + try { + window.GET_MANIFEST(); + } catch (exception) { + return String(exception); + } + }; + + let testGetManifest = window.GET_MANIFEST(); + + let manifest = browser.runtime.getManifest(); + let availableAPIs = Object.keys(browser).filter(key => browser[key]); + + browser.runtime.sendMessage({ + name: "content-script-iframe-loaded", + availableAPIs, + manifest, + testGetManifest, + }); + } + + const ID = "contentscript@tests.mozilla.org"; + let extensionData = { + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + content_scripts: [ + { + matches: ["http://example.com/data/file_sample.html"], + js: ["content_script.js"], + run_at: "document_idle", + }, + ], + web_accessible_resources: ["content_script_iframe.html"], + }, + + background, + + files: { + "content_script.js"() { + let iframe = document.createElement("iframe"); + iframe.src = browser.runtime.getURL("content_script_iframe.html"); + document.body.appendChild(iframe); + }, + "content_script_iframe.html": ` + + + + + + `, + "content_script_iframe.js": contentScriptIframe, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/data/file_sample.html" + ); + + await extension.awaitMessage("content-script-iframe-loaded"); + + info("testing APIs availability once the extension is unloaded..."); + + await contentPage.spawn(null, () => { + this.iframeWindow = this.content[0]; + + Assert.ok(this.iframeWindow, "content script enabled iframe found"); + Assert.ok( + /content_script_iframe\.html$/.test(this.iframeWindow.location), + "the found iframe has the expected URL" + ); + }); + + await extension.unload(); + + info( + "test content script APIs not accessible from the frame once the extension is unloaded" + ); + + await contentPage.spawn(null, () => { + let win = Cu.waiveXrays(this.iframeWindow); + ok( + !Cu.isDeadWrapper(win.browser), + "the API object should not be a dead object" + ); + + let manifest; + let manifestException; + try { + manifest = win.browser.runtime.getManifest(); + } catch (e) { + manifestException = e; + } + + Assert.ok(!manifest, "manifest should be undefined"); + + Assert.equal( + manifestException.constructor.name, + "TypeError", + "expected exception received" + ); + + Assert.ok( + manifestException.message.endsWith("win.browser.runtime is undefined"), + "expected exception received" + ); + + let getManifestException = win.testGetManifestException(); + + Assert.equal( + getManifestException, + "TypeError: can't access dead object", + "expected exception received" + ); + }); + + await contentPage.close(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js new file mode 100644 index 0000000000..6b03f5b0b0 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js @@ -0,0 +1,433 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + +const server = createHttpServer({ + hosts: ["example.com", "csplog.example.net"], +}); +server.registerDirectory("/data/", do_get_file("data")); + +var gDefaultCSP = `default-src 'self' 'report-sample'; script-src 'self' 'report-sample';`; +var gCSP = gDefaultCSP; +const pageContent = ` + + + + + + + + + `; + +server.registerPathHandler("/plain.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html"); + if (gCSP) { + info(`Content-Security-Policy: ${gCSP}`); + response.setHeader("Content-Security-Policy", gCSP); + } + response.write(pageContent); +}); + +const BASE_URL = `http://example.com`; +const pageURL = `${BASE_URL}/plain.html`; + +const CSP_REPORT_PATH = "/csp-report.sjs"; + +function readUTF8InputStream(stream) { + let buffer = NetUtil.readInputStream(stream, stream.available()); + return new TextDecoder().decode(buffer); +} + +server.registerPathHandler(CSP_REPORT_PATH, (request, response) => { + response.setStatusLine(request.httpVersion, 204, "No Content"); + let data = readUTF8InputStream(request.bodyInputStream); + Services.obs.notifyObservers(null, "extension-test-csp-report", data); +}); + +async function promiseCSPReport(test) { + let res = await TestUtils.topicObserved("extension-test-csp-report", test); + return JSON.parse(res[1]); +} + +// Test functions loaded into extension content script. +function testImage(data = {}) { + return new Promise(resolve => { + let img = window.document.getElementById("testimg"); + img.onload = () => resolve(true); + img.onerror = () => { + browser.test.log(`img error: ${img.src}`); + resolve(false); + }; + img.src = data.image_url; + }); +} + +function testFetch(data = {}) { + let f = data.content ? content.fetch : fetch; + return f(data.url) + .then(() => true) + .catch(e => { + browser.test.assertEq( + e.message, + "NetworkError when attempting to fetch resource.", + "expected fetch failure" + ); + return false; + }); +} + +async function testEval(data = {}) { + try { + // eslint-disable-next-line no-eval + let ev = data.content ? window.eval : eval; + return ev("true"); + } catch (e) { + return false; + } +} + +async function testFunction(data = {}) { + try { + // eslint-disable-next-line no-eval + let fn = data.content ? window.Function : Function; + let sum = new fn("a", "b", "return a + b"); + return sum(1, 1); + } catch (e) { + return 0; + } +} + +function testScriptTag(data) { + return new Promise(resolve => { + let script = document.createElement("script"); + script.src = data.url; + script.onload = () => { + resolve(true); + }; + script.onerror = () => { + resolve(false); + }; + document.body.appendChild(script); + }); +} + +async function testHttpRequestUpgraded(data = {}) { + let f = data.content ? content.fetch : fetch; + return f(data.url) + .then(() => "http:") + .catch(() => "https:"); +} + +async function testWebSocketUpgraded(data = {}) { + let ws = data.content ? content.WebSocket : WebSocket; + new ws(data.url); +} + +function webSocketUpgradeListenerBackground() { + // Catch websocket requests and send the protocol back to be asserted. + browser.webRequest.onBeforeRequest.addListener( + details => { + // Send the protocol back as test result. + // This will either be "wss:", "ws:" + browser.test.sendMessage("result", new URL(details.url).protocol); + return { cancel: true }; + }, + { urls: ["wss://example.com/*", "ws://example.com/*"] }, + ["blocking"] + ); +} + +// If the violation source is the extension the securitypolicyviolation event is not fired. +// If the page is the source, the event is fired and both the content script or page scripts +// will receive the event. If we're expecting a moz-extension report we'll fail in the +// event listener if we receive a report. Otherwise we want to resolve in the listener to +// ensure we've received the event for the test. +function contentScript(report) { + return new Promise(resolve => { + if (!report || report["document-uri"] === "moz-extension") { + resolve(); + } + // eslint-disable-next-line mozilla/balanced-listeners + document.addEventListener("securitypolicyviolation", e => { + browser.test.assertTrue( + e.documentURI !== "moz-extension", + `securitypolicyviolation: ${e.violatedDirective} ${e.documentURI}` + ); + resolve(); + }); + }); +} + +let TESTS = [ + // Image Tests + { + description: + "Image from content script using default extension csp. Image is allowed.", + pageCSP: `${gDefaultCSP} img-src 'none';`, + script: testImage, + data: { image_url: `${BASE_URL}/data/file_image_good.png` }, + expect: true, + }, + // Fetch Tests + { + description: "Fetch url in content script uses default extension csp.", + pageCSP: `${gDefaultCSP} connect-src 'none';`, + script: testFetch, + data: { url: `${BASE_URL}/data/file_image_good.png` }, + expect: true, + }, + { + description: "Fetch full url from content script uses page csp.", + pageCSP: `${gDefaultCSP} connect-src 'none';`, + script: testFetch, + data: { + content: true, + url: `${BASE_URL}/data/file_image_good.png`, + }, + expect: false, + report: { + "blocked-uri": `${BASE_URL}/data/file_image_good.png`, + "document-uri": `${BASE_URL}/plain.html`, + "violated-directive": "connect-src", + }, + }, + + // Eval tests. + { + description: "Eval from content script uses page csp with unsafe-eval.", + pageCSP: `default-src 'none'; script-src 'unsafe-eval';`, + script: testEval, + data: { content: true }, + expect: true, + }, + { + description: "Eval from content script uses page csp.", + pageCSP: `default-src 'self' 'report-sample'; script-src 'self';`, + version: 3, + script: testEval, + data: { content: true }, + expect: false, + report: { + "blocked-uri": "eval", + "document-uri": "http://example.com/plain.html", + "violated-directive": "script-src", + }, + }, + { + description: "Eval in content script allowed by v2 csp.", + pageCSP: `script-src 'self' 'unsafe-eval';`, + script: testEval, + expect: true, + }, + { + description: "Eval in content script disallowed by v3 csp.", + pageCSP: `script-src 'self' 'unsafe-eval';`, + version: 3, + script: testEval, + expect: false, + }, + { + description: "Wrapped Eval in content script uses page csp.", + pageCSP: `script-src 'self' 'unsafe-eval';`, + version: 3, + script: async () => { + return window.wrappedJSObject.eval("true"); + }, + expect: true, + }, + { + description: "Wrapped Eval in content script denied by page csp.", + pageCSP: `script-src 'self';`, + version: 3, + script: async () => { + try { + return window.wrappedJSObject.eval("true"); + } catch (e) { + return false; + } + }, + expect: false, + }, + + { + description: "Function from content script uses page csp.", + pageCSP: `default-src 'self'; script-src 'self' 'unsafe-eval';`, + script: testFunction, + data: { content: true }, + expect: 2, + }, + { + description: "Function from content script uses page csp.", + pageCSP: `default-src 'self' 'report-sample'; script-src 'self';`, + version: 3, + script: testFunction, + data: { content: true }, + expect: 0, + report: { + "blocked-uri": "eval", + "document-uri": "http://example.com/plain.html", + "violated-directive": "script-src", + }, + }, + { + description: "Function in content script uses extension csp.", + pageCSP: `default-src 'self'; script-src 'self' 'unsafe-eval';`, + version: 3, + script: testFunction, + expect: 0, + }, + + // The javascript url tests are not included as we do not execute those, + // aparently even with the urlbar filtering pref flipped. + // (browser.urlbar.filter.javascript) + // https://bugzilla.mozilla.org/show_bug.cgi?id=866522 + + // script tag injection tests + { + description: "remote script in content script passes in v2", + version: 2, + pageCSP: "script-src http://example.com:*;", + script: testScriptTag, + data: { url: `${BASE_URL}/data/file_script_good.js` }, + expect: true, + }, + { + description: "remote script in content script fails in v3", + version: 3, + pageCSP: "script-src http://example.com:*;", + script: testScriptTag, + data: { url: `${BASE_URL}/data/file_script_good.js` }, + expect: false, + }, + { + description: "content.WebSocket in content script is affected by page csp.", + version: 2, + pageCSP: `upgrade-insecure-requests;`, + data: { content: true, url: "ws://example.com/ws_dummy" }, + script: testWebSocketUpgraded, + expect: "wss:", // we expect the websocket to be upgraded. + backgroundScript: webSocketUpgradeListenerBackground, + }, + { + description: "WebSocket in content script is not affected by page csp.", + version: 2, + pageCSP: `upgrade-insecure-requests;`, + data: { url: "ws://example.com/ws_dummy" }, + script: testWebSocketUpgraded, + expect: "ws:", // we expect the websocket to not be upgraded. + backgroundScript: webSocketUpgradeListenerBackground, + }, + { + description: "WebSocket in content script is not affected by page csp. v3", + version: 3, + pageCSP: `upgrade-insecure-requests;`, + data: { url: "ws://example.com/ws_dummy" }, + script: testWebSocketUpgraded, + // TODO bug 1766813: MV3+WebSocket should use content script CSP. + expect: "wss:", // TODO: we expect the websocket to not be upgraded (ws:). + backgroundScript: webSocketUpgradeListenerBackground, + }, + { + description: "Http request in content script is not affected by page csp.", + version: 2, + pageCSP: `upgrade-insecure-requests;`, + data: { url: "http://example.com/plain.html" }, + script: testHttpRequestUpgraded, + expect: "http:", // we expect the request to not be upgraded. + }, + { + description: + "Http request in content script is not affected by page csp. v3", + version: 3, + pageCSP: `upgrade-insecure-requests;`, + data: { url: "http://example.com/plain.html" }, + script: testHttpRequestUpgraded, + // TODO bug 1766813: MV3+fetch should use content script CSP. + expect: "https:", // TODO: we expect the request to not be upgraded (http:). + }, + { + description: "content.fetch in content script is affected by page csp.", + version: 2, + pageCSP: `upgrade-insecure-requests;`, + data: { content: true, url: "http://example.com/plain.html" }, + script: testHttpRequestUpgraded, + expect: "https:", // we expect the request to be upgraded. + }, +]; + +async function runCSPTest(test) { + // Set the CSP for the page loaded into the tab. + gCSP = `${test.pageCSP || gDefaultCSP} report-uri ${CSP_REPORT_PATH}`; + let data = { + manifest: { + manifest_version: test.version || 2, + content_scripts: [ + { + matches: ["http://*/plain.html"], + run_at: "document_idle", + js: ["content_script.js"], + }, + ], + permissions: ["webRequest", "webRequestBlocking"], + host_permissions: [""], + granted_host_permissions: true, + background: { scripts: ["background.js"] }, + }, + temporarilyInstalled: true, + files: { + "content_script.js": ` + (${contentScript})(${JSON.stringify(test.report)}).then(() => { + browser.test.sendMessage("violationEvent"); + }); + (${test.script})(${JSON.stringify(test.data)}).then(result => { + if(result !== undefined) { + browser.test.sendMessage("result", result); + } + }); + `, + "background.js": `(${test.backgroundScript || (() => {})})()`, + ...test.files, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(data); + await extension.startup(); + + let reportPromise = test.report && promiseCSPReport(); + let contentPage = await ExtensionTestUtils.loadContentPage(pageURL); + + info(`running: ${test.description}`); + await extension.awaitMessage("violationEvent"); + + let result = await extension.awaitMessage("result"); + equal(result, test.expect, test.description); + + if (test.report) { + let report = await reportPromise; + for (let key of Object.keys(test.report)) { + equal( + report["csp-report"][key], + test.report[key], + `csp-report ${key} matches` + ); + } + } + + await extension.unload(); + await contentPage.close(); + clearCache(); +} + +add_task(async function test_contentscript_csp() { + for (let test of TESTS) { + await runCSPTest(test); + } +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_css.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_css.js new file mode 100644 index 0000000000..d94023387f --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_css.js @@ -0,0 +1,48 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); + +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write(""); +}); + +add_task(async function test_content_script_css() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/dummy"], + css: ["content.css"], + run_at: "document_start", + }, + ], + }, + + files: { + "content.css": "body { max-width: 42px; }", + }, + }); + + await extension.startup(); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + + function task() { + let style = this.content.getComputedStyle(this.content.document.body); + return style.maxWidth; + } + + let maxWidth = await contentPage.spawn(null, task); + equal(maxWidth, "42px", "Stylesheet correctly applied"); + + await extension.unload(); + + maxWidth = await contentPage.spawn(null, task); + equal(maxWidth, "none", "Stylesheet correctly removed"); + + await contentPage.close(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js new file mode 100644 index 0000000000..3a632e0107 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js @@ -0,0 +1,206 @@ +"use strict"; + +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; + +// ExtensionContent.jsm needs to know when it's running from xpcshell, to use +// the right timeout for content scripts executed at document_idle. +ExtensionTestUtils.mockAppInfo(); + +// Do not use preallocated processes. +Services.prefs.setBoolPref("dom.ipc.processPrelaunch.enabled", false); +// This is needed for Android. +Services.prefs.setIntPref("dom.ipc.keepProcessesAlive.web", 0); + +const makeExtension = ({ background, manifest }) => { + return ExtensionTestUtils.loadExtension({ + manifest: { + ...manifest, + permissions: + manifest.manifest_version === 3 ? ["scripting"] : ["http://*/*/*.html"], + }, + temporarilyInstalled: true, + background, + files: { + "script.js": () => { + browser.test.sendMessage( + `script-ran: ${location.pathname.split("/").pop()}` + ); + }, + "inject_browser.js": () => { + browser.userScripts.onBeforeScript.addListener(script => { + // Inject `browser.test.sendMessage()` so that it can be used in the + // `script.js` defined above when using "user scripts". + script.defineGlobals({ + browser: { + test: { + sendMessage(msg) { + browser.test.sendMessage(msg); + }, + }, + }, + }); + }); + }, + }, + }); +}; + +const verifyRegistrationWithNewProcess = async extension => { + // We override the `broadcast()` method to reliably verify Bug 1756495: when + // a new process is spawned while we register a content script, the script + // should be correctly registered and executed in this new process. Below, + // when we receive the `Extension:RegisterContentScripts`, we open a new tab + // (which is the "new process") and then we invoke the original "broadcast + // logic". The expected result is that the content script registered by the + // extension will run. + const originalBroadcast = Extension.prototype.broadcast; + + let broadcastCalledCount = 0; + let secondContentPage; + + extension.extension.broadcast = async function broadcast(msg, data) { + if (msg !== "Extension:RegisterContentScripts") { + return originalBroadcast.call(this, msg, data); + } + + broadcastCalledCount++; + Assert.equal( + 1, + broadcastCalledCount, + "broadcast override should be called once" + ); + + await originalBroadcast.call(this, msg, data); + + Assert.equal(extension.id, data.id, "got expected extension ID"); + Assert.equal(1, data.scripts.length, "expected 1 script to register"); + Assert.ok( + data.scripts[0].options.jsPaths[0].endsWith("script.js"), + "got expected js file" + ); + + const newPids = []; + const topic = "ipc:content-created"; + + let obs = (subject, topic, data) => { + newPids.push(parseInt(data, 10)); + }; + Services.obs.addObserver(obs, topic); + + secondContentPage = await ExtensionTestUtils.loadContentPage( + `${BASE_URL}/dummy_page.html` + ); + + const { + childID, + } = secondContentPage.browsingContext.currentWindowGlobal.domProcess; + + Services.obs.removeObserver(obs, topic); + + // We expect to have a new process created for `secondContentPage`. + Assert.ok( + newPids.includes(childID), + `expected PID ${childID} to be in [${newPids.join(", ")}])` + ); + }; + + await extension.startup(); + await extension.awaitMessage("background-done"); + + let contentPage = await ExtensionTestUtils.loadContentPage( + `${BASE_URL}/file_sample.html` + ); + + await Promise.all([ + extension.awaitMessage("script-ran: file_sample.html"), + extension.awaitMessage("script-ran: dummy_page.html"), + ]); + + // Unload extension first to avoid an issue on Windows platforms. + await extension.unload(); + await contentPage.close(); + await secondContentPage.close(); +}; + +add_task( + { + pref_set: [["extensions.manifestV3.enabled", true]], + }, + async function test_scripting_registerContentScripts() { + let extension = makeExtension({ + manifest: { + manifest_version: 3, + host_permissions: [""], + granted_host_permissions: true, + }, + async background() { + const script = { + id: "a-script", + js: ["script.js"], + matches: ["http://*/*/*.html"], + persistAcrossSessions: false, + }; + + await browser.scripting.registerContentScripts([script]); + + browser.test.sendMessage("background-done"); + }, + }); + + await verifyRegistrationWithNewProcess(extension); + } +); + +add_task( + { + // We don't have WebIDL bindings for `browser.contentScripts`. + skip_if: () => ExtensionTestUtils.isInBackgroundServiceWorkerTests(), + }, + async function test_contentScripts_register() { + let extension = makeExtension({ + manifest: { + manifest_version: 2, + }, + async background() { + await browser.contentScripts.register({ + js: [{ file: "script.js" }], + matches: ["http://*/*/*.html"], + }); + + browser.test.sendMessage("background-done"); + }, + }); + + await verifyRegistrationWithNewProcess(extension); + } +); + +add_task( + { + // We don't have WebIDL bindings for `browser.userScripts`. + skip_if: () => ExtensionTestUtils.isInBackgroundServiceWorkerTests(), + }, + async function test_userScripts_register() { + let extension = makeExtension({ + manifest: { + manifest_version: 2, + user_scripts: { + api_script: "inject_browser.js", + }, + }, + async background() { + await browser.userScripts.register({ + js: [{ file: "script.js" }], + matches: ["http://*/*/*.html"], + }); + + browser.test.sendMessage("background-done"); + }, + }); + + await verifyRegistrationWithNewProcess(extension); + } +); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js new file mode 100644 index 0000000000..a9daa9d7ab --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js @@ -0,0 +1,127 @@ +"use strict"; + +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; +const TEST_URL_1 = `${BASE_URL}/file_sample.html`; +const TEST_URL_2 = `${BASE_URL}/file_content_script_errors.html`; + +// ExtensionContent.jsm needs to know when it's running from xpcshell, +// to use the right timeout for content scripts executed at document_idle. +ExtensionTestUtils.mockAppInfo(); + +add_task(async function test_cached_contentscript_on_document_start() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + // Use distinct content scripts as some will throw and would prevent executing the next script + { + matches: ["http://*/*/file_content_script_errors.html"], + js: ["script1.js"], + run_at: "document_start", + }, + { + matches: ["http://*/*/file_content_script_errors.html"], + js: ["script2.js"], + run_at: "document_start", + }, + { + matches: ["http://*/*/file_content_script_errors.html"], + js: ["script3.js"], + run_at: "document_start", + }, + { + matches: ["http://*/*/file_content_script_errors.html"], + js: ["script4.js"], + run_at: "document_start", + }, + { + matches: ["http://*/*/file_content_script_errors.html"], + js: ["script5.js"], + run_at: "document_start", + }, + ], + }, + + files: { + "script1.js": ` + throw new Error("Object exception"); + `, + "script2.js": ` + throw "String exception"; + `, + "script3.js": ` + undefinedSymbol(); + `, + "script4.js": ` + ) + `, + "script5.js": ` + Promise.reject("rejected promise"); + + (async () => { + /* make the async, really async */ + await new Promise(r => setTimeout(r, 0)); + throw new Error("async function exception"); + })(); + + setTimeout(() => { + asyncUndefinedSymbol(); + }); + + /* Use a delay in order to resume test execution after these async errors */ + setTimeout(() => { + browser.test.sendMessage("content-script-loaded"); + }, 500); + `, + }, + }); + + await extension.startup(); + + // Load a first page in order to be able to register a console listener in the content process. + // This has to be done in the same domain of the second page to stay in the same process. + let contentPage = await ExtensionTestUtils.loadContentPage(TEST_URL_1); + + // Listen to the errors logged in the content process. + ContentTask.spawn(contentPage.browser, {}, () => { + this.collectedErrors = []; + + this.consoleErrorListener = error => { + error.QueryInterface(Ci.nsIScriptError); + // Ignore errors from ExtensionContent.jsm + if (error.innerWindowID) { + this.collectedErrors.push({ + innerWindowID: error.innerWindowID, + message: error.errorMessage, + }); + } + }; + + Services.console.registerListener(this.consoleErrorListener); + }); + + // Reload the page and check that the cached content script is still able to + // run on document_start. + await contentPage.loadURL(TEST_URL_2); + + await extension.awaitMessage("content-script-loaded"); + + const errors = await ContentTask.spawn(contentPage.browser, {}, () => { + Services.console.unregisterListener(this.consoleErrorListener); + return this.collectedErrors; + }); + equal(errors.length, 7); + for (const { innerWindowID, message } of errors) { + equal( + innerWindowID, + contentPage.browser.innerWindowID, + `Message ${message} has the innerWindowID set` + ); + } + + await extension.unload(); + + await contentPage.close(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_exporthelpers.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_exporthelpers.js new file mode 100644 index 0000000000..f485a012c9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_exporthelpers.js @@ -0,0 +1,98 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerDirectory("/data/", do_get_file("data")); + +add_task(async function test_contentscript_exportHelpers() { + function contentScript() { + browser.test.assertTrue(typeof cloneInto === "function"); + browser.test.assertTrue(typeof createObjectIn === "function"); + browser.test.assertTrue(typeof exportFunction === "function"); + + /* globals exportFunction, precisePi, reportPi */ + let value = 3.14; + exportFunction(() => value, window, { defineAs: "precisePi" }); + + browser.test.assertEq( + "undefined", + typeof precisePi, + "exportFunction should export to the page's scope only" + ); + + browser.test.assertEq( + "undefined", + typeof window.precisePi, + "exportFunction should export to the page's scope only" + ); + + let results = []; + exportFunction(pi => results.push(pi), window, { defineAs: "reportPi" }); + + let s = document.createElement("script"); + s.textContent = `(${function() { + let result1 = "unknown 1"; + let result2 = "unknown 2"; + try { + result1 = precisePi(); + } catch (e) { + result1 = "err:" + e; + } + try { + result2 = window.precisePi(); + } catch (e) { + result2 = "err:" + e; + } + reportPi(result1); + reportPi(result2); + }})();`; + + document.documentElement.appendChild(s); + // Inline script ought to run synchronously. + + browser.test.assertEq( + 3.14, + results[0], + "exportFunction on window should define a global function" + ); + browser.test.assertEq( + 3.14, + results[1], + "exportFunction on window should export a property to window." + ); + + browser.test.assertEq( + 2, + results.length, + "Expecting the number of results to match the number of method calls" + ); + + browser.test.notifyPass("export helper test completed"); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + js: ["contentscript.js"], + matches: ["http://example.com/data/file_sample.html"], + run_at: "document_start", + }, + ], + }, + + files: { + "contentscript.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/data/file_sample.html" + ); + + await extension.awaitFinish("export helper test completed"); + await contentPage.close(); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_importmap.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_importmap.js new file mode 100644 index 0000000000..7e7ca2720d --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_importmap.js @@ -0,0 +1,124 @@ +"use strict"; + +// Currently import maps are not supported for web extensions, neither for +// content scripts nor moz-extension documents. +// For content scripts that's because they use their own sandbox module loaders, +// which is different from the DOM module loader. +// As for moz-extension documents, that's because inline script tags is not +// allowed by CSP. (Currently import maps can be only added through inline +// script tag.) +// +// This test is used to verified import maps are not supported for web +// extensions. +// See Bug 1765275: Enable Import maps for web extension content scripts. +Services.prefs.setBoolPref("dom.importMaps.enabled", true); + +const server = createHttpServer({ hosts: ["example.com"] }); + +const importMapString = ` + `; + +const importMapHtml = ` + + + + Test a simple import map in normal webpage + + ${importMapString} + `; + +// page.html will load page.js, which will call import(); +const pageHtml = ` + + + + Test a simple import map in moz-extension documents + + ${importMapString} + + `; + +const simple2JS = `export let foo = 2;`; + +server.registerPathHandler("/importmap.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write(importMapHtml); +}); + +server.registerPathHandler("/simple.js", (request, response) => { + ok(false, "Unexpected request to /simple.js"); +}); + +server.registerPathHandler("/simple2.js", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/javascript", false); + response.write(simple2JS); +}); + +add_task(async function test_importMaps_not_supported() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/importmap.html"], + js: ["main.js"], + }, + ], + }, + + files: { + "main.js": async function() { + // Content scripts shouldn't be able to use the bare specifier from + // the import map. + await browser.test.assertRejects( + import("simple"), + /The specifier “simple” was a bare specifier/, + `should reject import("simple")` + ); + + browser.test.sendMessage("done"); + }, + "page.html": pageHtml, + "page.js": async function() { + await browser.test.assertRejects( + import("simple"), + /The specifier “simple” was a bare specifier/, + `should reject import("simple")` + ); + browser.test.sendMessage("page-done"); + }, + }, + }); + + await extension.startup(); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/importmap.html" + ); + await extension.awaitMessage("done"); + + await contentPage.spawn(null, async () => { + // Import maps should work for documents. + let promise = content.eval(`import("simple2")`); + let mod = (await promise.wrappedJSObject).wrappedJSObject; + Assert.equal(mod.foo, 2, "mod.foo should be 2"); + }); + + // moz-extension documents doesn't allow inline scripts, so the import map + // script tag won't be processed. + let url = `moz-extension://${extension.uuid}/page.html`; + let page = await ExtensionTestUtils.loadContentPage(url, { extension }); + await extension.awaitMessage("page-done"); + + await page.close(); + await extension.unload(); + await contentPage.close(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_in_background.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_in_background.js new file mode 100644 index 0000000000..e813b46ca0 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_in_background.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"; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler("/dummyFrame", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write(""); +}); + +add_task(async function content_script_in_background_frame() { + async function background() { + const FRAME_URL = "http://example.com:8888/dummyFrame"; + await browser.contentScripts.register({ + matches: ["http://example.com/dummyFrame"], + js: [{ file: "contentscript.js" }], + allFrames: true, + }); + + let f = document.createElement("iframe"); + f.src = FRAME_URL; + document.body.appendChild(f); + } + + function contentScript() { + browser.test.log(`Running content script at ${document.URL}`); + browser.test.sendMessage("done_in_content_script"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["http://example.com/*"], + }, + files: { + "contentscript.js": contentScript, + }, + background, + }); + await extension.startup(); + await extension.awaitMessage("done_in_content_script"); + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_json_api.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_json_api.js new file mode 100644 index 0000000000..ca37e2e951 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_json_api.js @@ -0,0 +1,102 @@ +"use strict"; + +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write( + ` + ` + ); +}); + +async function test_JSON_parse_and_stringify({ manifest_version }) { + let extension = ExtensionTestUtils.loadExtension({ + temporarilyInstalled: true, // Needed for granted_host_permissions + manifest: { + manifest_version, + granted_host_permissions: true, // Test-only: grant permissions in MV3. + host_permissions: ["http://example.com/"], // Work-around for bug 1766752. + content_scripts: [ + { + matches: ["http://example.com/dummy"], + run_at: "document_end", + js: ["contentscript.js"], + }, + ], + }, + files: { + "contentscript.js"() { + let json = `{"a":[123,true,null]}`; + browser.test.assertEq( + JSON.stringify({ a: [123, true, null] }), + json, + "JSON.stringify with basic values" + ); + let parsed = JSON.parse(json); + browser.test.assertTrue( + parsed instanceof Object, + "Parsed JSON is an Object" + ); + browser.test.assertTrue( + parsed.a instanceof Array, + "Parsed JSON has an Array" + ); + browser.test.assertEq( + JSON.stringify(parsed), + json, + "JSON.stringify for parsed JSON returns original input" + ); + browser.test.assertEq( + JSON.stringify({ toJSON: () => "overridden", hideme: true }), + `"overridden"`, + "JSON.parse with toJSON method" + ); + + browser.test.assertEq( + JSON.stringify(window.wrappedJSObject.objFromPage), + `{"serializeMe":"thanks"}`, + "JSON.parse with value from the page" + ); + + browser.test.assertEq( + JSON.stringify(window.wrappedJSObject.objWithToJSON), + `"toJSON ran"`, + "JSON.parse with object with toJSON method from the page" + ); + + browser.test.assertTrue(JSON === globalThis.JSON, "JSON === this.JSON"); + browser.test.assertTrue(JSON === window.JSON, "JSON === window.JSON"); + browser.test.assertEq( + "overridden by page", + window.wrappedJSObject.JSON.toString(), + "page's JSON object is still the original value (overridden by page)" + ); + browser.test.sendMessage("done"); + }, + }, + }); + await extension.startup(); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + await extension.awaitMessage("done"); + await contentPage.close(); + await extension.unload(); +} + +add_task(async function test_JSON_apis_MV2() { + await test_JSON_parse_and_stringify({ manifest_version: 2 }); +}); + +add_task(async function test_JSON_apis_MV3() { + await test_JSON_parse_and_stringify({ manifest_version: 3 }); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_module_import.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_module_import.js new file mode 100644 index 0000000000..3c23eb4dc3 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_module_import.js @@ -0,0 +1,277 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); + +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write(""); +}); + +server.registerPathHandler("/script.js", (request, response) => { + ok(false, "Unexpected request to /script.js"); +}); + +/* eslint-disable no-unsanitized/method, no-eval, no-implied-eval */ + +const MODULE1 = ` + import {foo} from "./module2.js"; + export let bar = foo; + + let count = 0; + + export function counter () { return count++; } +`; + +const MODULE2 = `export let foo = 2;`; + +add_task(async function test_disallowed_import() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/dummy"], + js: ["main.js"], + }, + ], + }, + + files: { + "main.js": async function() { + let disallowedURLs = [ + "data:text/javascript,void 0", + "javascript:void 0", + "http://example.com/script.js", + URL.createObjectURL( + new Blob(["void 0", { type: "text/javascript" }]) + ), + ]; + + for (let url of disallowedURLs) { + await browser.test.assertRejects( + import(url), + /error loading dynamically imported module/, + `should reject import("${url}")` + ); + } + + browser.test.sendMessage("done"); + }, + }, + }); + + await extension.startup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + await extension.awaitMessage("done"); + await extension.unload(); + await contentPage.close(); +}); + +add_task(async function test_normal_import() { + Services.prefs.setBoolPref("extensions.content_web_accessible.enabled", true); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/dummy"], + js: ["main.js"], + }, + ], + }, + + files: { + "main.js": async function() { + /* global exportFunction */ + const url = browser.runtime.getURL("module1.js"); + + await browser.test.assertRejects( + import(url), + /error loading dynamically imported module/, + "Cannot import script that is not web-accessible from page context" + ); + + await browser.test.assertRejects( + window.eval(`import("${url}")`), + /error loading dynamically imported module/, + "Cannot import script that is not web-accessible from page context" + ); + + let promise = new Promise((resolve, reject) => { + exportFunction(resolve, window, { defineAs: "resolve" }); + exportFunction(reject, window, { defineAs: "reject" }); + }); + + window.setTimeout(`import("${url}").then(resolve, reject)`, 0); + + await browser.test.assertRejects( + promise, + /error loading dynamically imported module/, + "Cannot import script that is not web-accessible from page context" + ); + + browser.test.sendMessage("done"); + }, + "module1.js": MODULE1, + "module2.js": MODULE2, + }, + }); + + await extension.startup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + + await extension.awaitMessage("done"); + + // Web page can not import non-web-accessible files. + await contentPage.spawn(extension.uuid, async uuid => { + let files = ["main.js", "module1.js", "module2.js"]; + + for (let file of files) { + let url = `moz-extension://${uuid}/${file}`; + await Assert.rejects( + content.eval(`import("${url}")`), + /error loading dynamically imported module/, + "Cannot import script that is not web-accessible" + ); + } + }); + + await extension.unload(); + await contentPage.close(); +}); + +add_task(async function test_import_web_accessible() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/dummy"], + js: ["main.js"], + }, + ], + web_accessible_resources: ["module1.js", "module2.js"], + }, + + files: { + "main.js": async function() { + let mod = await import(browser.runtime.getURL("module1.js")); + browser.test.assertEq(mod.bar, 2); + browser.test.assertEq(mod.counter(), 0); + browser.test.sendMessage("done"); + }, + "module1.js": MODULE1, + "module2.js": MODULE2, + }, + }); + + await extension.startup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + await extension.awaitMessage("done"); + + // Web page can import web-accessible files, + // even after WebExtension imported the same files. + await contentPage.spawn(extension.uuid, async uuid => { + let base = `moz-extension://${uuid}`; + + await Assert.rejects( + content.eval(`import("${base}/main.js")`), + /error loading dynamically imported module/, + "Cannot import script that is not web-accessible" + ); + + let promise = content.eval(`import("${base}/module1.js")`); + let mod = (await promise.wrappedJSObject).wrappedJSObject; + Assert.equal(mod.bar, 2, "exported value should match"); + Assert.equal(mod.counter(), 0, "Counter should be fresh"); + Assert.equal(mod.counter(), 1, "Counter should be fresh"); + + promise = content.eval(`import("${base}/module2.js")`); + mod = (await promise.wrappedJSObject).wrappedJSObject; + Assert.equal(mod.foo, 2, "exported value should match"); + }); + + await extension.unload(); + await contentPage.close(); +}); + +add_task(async function test_import_web_accessible_after_page() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://example.com/dummy"], + js: ["main.js"], + }, + ], + web_accessible_resources: ["module1.js", "module2.js"], + }, + + files: { + "main.js": async function() { + browser.test.onMessage.addListener(async msg => { + browser.test.assertEq(msg, "import"); + + const url = browser.runtime.getURL("module1.js"); + let mod = await import(url); + browser.test.assertEq(mod.bar, 2); + browser.test.assertEq(mod.counter(), 0, "Counter should be fresh"); + + let promise = window.eval(`import("${url}")`); + let mod2 = (await promise.wrappedJSObject).wrappedJSObject; + browser.test.assertEq( + mod2.counter(), + 2, + "Counter should have been incremented by page" + ); + + browser.test.sendMessage("done"); + }); + browser.test.sendMessage("ready"); + }, + "module1.js": MODULE1, + "module2.js": MODULE2, + }, + }); + + await extension.startup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + await extension.awaitMessage("ready"); + + // The web page imports the web-accessible files first, + // when the WebExtension imports the same file, they should + // not be shared. + await contentPage.spawn(extension.uuid, async uuid => { + let base = `moz-extension://${uuid}`; + + await Assert.rejects( + content.eval(`import("${base}/main.js")`), + /error loading dynamically imported module/, + "Cannot import script that is not web-accessible" + ); + + let promise = content.eval(`import("${base}/module1.js")`); + let mod = (await promise.wrappedJSObject).wrappedJSObject; + Assert.equal(mod.bar, 2, "exported value should match"); + Assert.equal(mod.counter(), 0); + Assert.equal(mod.counter(), 1); + + promise = content.eval(`import("${base}/module2.js")`); + mod = (await promise.wrappedJSObject).wrappedJSObject; + Assert.equal(mod.foo, 2, "exported value should match"); + }); + + extension.sendMessage("import"); + + await extension.awaitMessage("done"); + + await extension.unload(); + await contentPage.close(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_perf_observers.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_perf_observers.js new file mode 100644 index 0000000000..2546b257fb --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_perf_observers.js @@ -0,0 +1,71 @@ +"use strict"; + +const server = createHttpServer({ + hosts: ["a.example.com", "b.example.com", "c.example.com"], +}); +server.registerDirectory("/data/", do_get_file("data")); + +add_task(async function test_perf_observers_cors() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["http://b.example.com/"], + content_scripts: [ + { + matches: ["http://a.example.com/data/file_sample.html"], + js: ["cs.js"], + }, + ], + }, + files: { + "cs.js"() { + let obs = new window.PerformanceObserver(list => { + list.getEntries().forEach(e => { + browser.test.sendMessage("observed", { + url: e.name, + time: e.connectEnd, + size: e.encodedBodySize, + }); + }); + }); + obs.observe({ entryTypes: ["resource"] }); + + let b = document.createElement("link"); + b.rel = "stylesheet"; + + // Simulate page including a cross-origin resource from b.example.com. + b.wrappedJSObject.href = "http://b.example.com/data/file_download.txt"; + document.head.appendChild(b); + + let c = document.createElement("link"); + c.rel = "stylesheet"; + + // Simulate page including a cross-origin resource from c.example.com. + c.wrappedJSObject.href = "http://c.example.com/data/file_download.txt"; + document.head.appendChild(c); + }, + }, + }); + + let page = await ExtensionTestUtils.loadContentPage( + "http://a.example.com/data/file_sample.html" + ); + await extension.startup(); + + let b = await extension.awaitMessage("observed"); + let c = await extension.awaitMessage("observed"); + + if (b.url.startsWith("http://c.")) { + [c, b] = [b, c]; + } + + ok(b.url.startsWith("http://b."), "Observed resource from b.example.com"); + ok(b.time > 0, "connectionEnd available from b.example.com"); + equal(b.size, 46, "encodedBodySize available from b.example.com"); + + ok(c.url.startsWith("http://c."), "Observed resource from c.example.com"); + equal(c.time, 0, "connectionEnd == 0 from c.example.com"); + equal(c.size, 0, "encodedBodySize == 0 from c.example.com"); + + await extension.unload(); + await page.close(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js new file mode 100644 index 0000000000..fbf5cb2906 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js @@ -0,0 +1,104 @@ +"use strict"; + +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + +const server = createHttpServer({ hosts: ["example.com", "example.net"] }); +server.registerDirectory("/data/", do_get_file("data")); + +const HOSTS = ["http://example.com/*", "http://example.net/*"]; + +const { ExtensionPermissions } = ChromeUtils.import( + "resource://gre/modules/ExtensionPermissions.jsm" +); + +function grantOptional({ extension: ext }, origins) { + return ExtensionPermissions.add(ext.id, { origins, permissions: [] }, ext); +} + +function revokeOptional({ extension: ext }, origins) { + return ExtensionPermissions.remove(ext.id, { origins, permissions: [] }, ext); +} + +function makeExtension(id, content_scripts) { + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + + browser_specific_settings: { gecko: { id } }, + content_scripts, + + permissions: ["scripting"], + host_permissions: HOSTS, + }, + files: { + "cs.js"() { + browser.test.log(`${browser.runtime.id} script on ${location.host}`); + browser.test.sendMessage(`${browser.runtime.id}_on_${location.host}`); + }, + }, + background() { + browser.test.onMessage.addListener(async (msg, origins) => { + browser.test.log(`${browser.runtime.id} registering content scripts`); + await browser.scripting.registerContentScripts([ + { + id: "cs1", + persistAcrossSessions: false, + matches: origins, + js: ["cs.js"], + }, + ]); + browser.test.sendMessage("done"); + }); + }, + }); +} + +// Test that content scripts in MV3 enforce origin permissions. +// Test granted optional permissions are available in newly spawned processes. +add_task(async function test_contentscript_mv3_permissions() { + // Alpha lists content scripts in the manifest. + let alpha = makeExtension("alpha@test", [{ matches: HOSTS, js: ["cs.js"] }]); + let beta = makeExtension("beta@test"); + + await grantOptional(alpha, HOSTS); + await grantOptional(beta, ["http://example.net/*"]); + info("Granted initial permissions for both."); + + await alpha.startup(); + await beta.startup(); + + // Beta registers same content scripts using the scripting api. + beta.sendMessage("register", HOSTS); + await beta.awaitMessage("done"); + + // Only Alpha has origin permissions for example.com. + { + let page = await ExtensionTestUtils.loadContentPage( + `http://example.com/data/file_sample.html` + ); + info("Loaded a page from example.com."); + + await alpha.awaitMessage("alpha@test_on_example.com"); + info("Got a message from alpha@test on example.com."); + await page.close(); + } + + await revokeOptional(alpha, ["http://example.net/*"]); + info("Revoked example.net permissions from Alpha."); + + // Now only Beta has origin permissions for example.net. + { + let page = await ExtensionTestUtils.loadContentPage( + `http://example.net/data/file_sample.html` + ); + info("Loaded a page from example.net."); + + await beta.awaitMessage("beta@test_on_example.net"); + info("Got a message from beta@test on example.net."); + await page.close(); + } + + info("Done, unloading Alpha and Beta."); + await beta.unload(); + await alpha.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_fetch.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_fetch.js new file mode 100644 index 0000000000..611ff07c05 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_fetch.js @@ -0,0 +1,87 @@ +"use strict"; + +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + +const server = createHttpServer({ hosts: ["example.com", "example.net"] }); +server.registerDirectory("/data/", do_get_file("data")); + +const { ExtensionPermissions } = ChromeUtils.import( + "resource://gre/modules/ExtensionPermissions.jsm" +); + +function grantOptional({ extension: ext }, origins) { + return ExtensionPermissions.add(ext.id, { origins, permissions: [] }, ext); +} + +function revokeOptional({ extension: ext }, origins) { + return ExtensionPermissions.remove(ext.id, { origins, permissions: [] }, ext); +} + +// Test granted optional permissions work with XHR/fetch in new processes. +add_task( + { + pref_set: [["dom.ipc.keepProcessesAlive.extension", 0]], + }, + async function test_fetch_origin_permissions_change() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + host_permissions: ["http://example.com/*"], + optional_permissions: ["http://example.net/*"], + }, + files: { + "page.js"() { + fetch("http://example.net/data/file_sample.html") + .then(req => req.text()) + .then(text => browser.test.sendMessage("done", { text })) + .catch(e => browser.test.sendMessage("done", { error: e.message })); + }, + "page.html": ``, + }, + }); + + await extension.startup(); + + let osPid; + { + // Grant permissions before extension process exists. + await grantOptional(extension, ["http://example.net/*"]); + + let page = await ExtensionTestUtils.loadContentPage( + extension.extension.baseURI.resolve("page.html") + ); + + let { text } = await extension.awaitMessage("done"); + ok(text.includes("Sample text"), "Can read from granted optional host."); + + osPid = page.browsingContext.currentWindowGlobal.osPid; + await page.close(); + } + + // Release the extension process so that next part starts a new one. + Services.ppmm.releaseCachedProcesses(); + + { + // Revoke permissions and confirm fetch fails. + await revokeOptional(extension, ["http://example.net/*"]); + + let page = await ExtensionTestUtils.loadContentPage( + extension.extension.baseURI.resolve("page.html") + ); + + let { error } = await extension.awaitMessage("done"); + ok(error.includes("NetworkError"), `Expected error: ${error}`); + + if (WebExtensionPolicy.useRemoteWebExtensions) { + notEqual( + osPid, + page.browsingContext.currentWindowGlobal.osPid, + "Second part of the test used a new process." + ); + } + + await page.close(); + } + + await extension.unload(); + } +); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_restrictSchemes.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_restrictSchemes.js new file mode 100644 index 0000000000..d775bb2cfb --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_restrictSchemes.js @@ -0,0 +1,149 @@ +"use strict"; + +function makeExtension({ id, isPrivileged, withScriptingAPI = false }) { + let permissions = []; + let content_scripts = []; + let background = () => { + browser.test.sendMessage("background-ready"); + }; + + if (isPrivileged) { + permissions.push("mozillaAddons"); + } + + if (withScriptingAPI) { + permissions.push("scripting"); + // When we don't use a content script registered via the manifest, we + // should add the origin as a permission. + permissions.push("resource://foo/file_sample.html"); + + // Redefine background script to dynamically register the content script. + if (isPrivileged) { + background = async () => { + await browser.scripting.registerContentScripts([ + { + id: "content_script", + js: ["content_script.js"], + matches: ["resource://foo/file_sample.html"], + persistAcrossSessions: false, + runAt: "document_start", + }, + ]); + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(1, scripts.length, "expected 1 script"); + + browser.test.sendMessage("background-ready"); + }; + } else { + background = async () => { + await browser.test.assertRejects( + browser.scripting.registerContentScripts([ + { + id: "content_script", + js: ["content_script.js"], + matches: ["resource://foo/file_sample.html"], + persistAcrossSessions: false, + runAt: "document_start", + }, + ]), + /Invalid url pattern: resource:/, + "got expected error" + ); + + browser.test.sendMessage("background-ready"); + }; + } + } else { + content_scripts.push({ + js: ["content_script.js"], + matches: ["resource://foo/file_sample.html"], + run_at: "document_start", + }); + } + + return ExtensionTestUtils.loadExtension({ + isPrivileged, + + manifest: { + manifest_version: 2, + browser_specific_settings: { gecko: { id } }, + content_scripts, + permissions, + }, + + background, + + files: { + "content_script.js"() { + browser.test.assertEq( + "resource://foo/file_sample.html", + document.documentURI, + `Loaded content script into the correct document (extension: ${browser.runtime.id})` + ); + browser.test.sendMessage(`content-script-${browser.runtime.id}`); + }, + }, + }); +} + +const verifyRestrictSchemes = async ({ withScriptingAPI }) => { + let resProto = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + resProto.setSubstitutionWithFlags( + "foo", + Services.io.newFileURI(do_get_file("data")), + resProto.ALLOW_CONTENT_ACCESS + ); + + let unprivileged = makeExtension({ + id: "unprivileged@tests.mozilla.org", + isPrivileged: false, + withScriptingAPI, + }); + let privileged = makeExtension({ + id: "privileged@tests.mozilla.org", + isPrivileged: true, + withScriptingAPI, + }); + + await unprivileged.startup(); + await unprivileged.awaitMessage("background-ready"); + + await privileged.startup(); + await privileged.awaitMessage("background-ready"); + + unprivileged.onMessage( + "content-script-unprivileged@tests.mozilla.org", + () => { + ok( + false, + "Unprivileged extension executed content script on resource URL" + ); + } + ); + + let contentPage = await ExtensionTestUtils.loadContentPage( + `resource://foo/file_sample.html` + ); + + await privileged.awaitMessage("content-script-privileged@tests.mozilla.org"); + + await contentPage.close(); + + await privileged.unload(); + await unprivileged.unload(); +}; + +// Bug 1780507: this only works with MV2 currently because MV3's optional +// permission mechanism lacks `restrictSchemes` flags. +add_task(async function test_contentscript_restrictSchemes_mv2() { + await verifyRestrictSchemes({ withScriptingAPI: false }); +}); + +// Bug 1780507: this only works with MV2 currently because MV3's optional +// permission mechanism lacks `restrictSchemes` flags. +add_task(async function test_contentscript_restrictSchemes_scripting_mv2() { + await verifyRestrictSchemes({ withScriptingAPI: true }); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_scriptCreated.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_scriptCreated.js new file mode 100644 index 0000000000..e0ed263065 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_scriptCreated.js @@ -0,0 +1,61 @@ +"use strict"; + +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; + +// ExtensionContent.jsm needs to know when it's running from xpcshell, +// to use the right timeout for content scripts executed at document_idle. +ExtensionTestUtils.mockAppInfo(); + +// Test that document_start content scripts don't block script-created +// parsers. +add_task(async function test_contentscript_scriptCreated() { + let extensionData = { + manifest: { + content_scripts: [ + { + matches: ["http://*/*/file_document_write.html"], + js: ["content_script.js"], + run_at: "document_start", + match_about_blank: true, + all_frames: true, + }, + ], + }, + + files: { + "content_script.js": function() { + if (window === top) { + addEventListener( + "message", + msg => { + browser.test.assertEq( + "ok", + msg.data, + "document.write() succeeded" + ); + browser.test.sendMessage("content-script-done"); + }, + { once: true } + ); + } + }, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let contentPage = await ExtensionTestUtils.loadContentPage( + `${BASE_URL}/file_document_write.html` + ); + + await extension.awaitMessage("content-script-done"); + + await contentPage.close(); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_teardown.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_teardown.js new file mode 100644 index 0000000000..7a0325ae95 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_teardown.js @@ -0,0 +1,101 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerDirectory("/data/", do_get_file("data")); + +add_task(async function test_contentscript_reload_and_unload() { + let extensionData = { + manifest: { + content_scripts: [ + { + matches: ["http://example.com/data/file_sample.html"], + js: ["contentscript.js"], + }, + ], + }, + + files: { + "contentscript.js"() { + browser.test.sendMessage("contentscript-run"); + }, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let events = []; + { + const { Management } = ChromeUtils.import( + "resource://gre/modules/Extension.jsm" + ); + let record = (type, extensionContext) => { + let eventType = type == "proxy-context-load" ? "load" : "unload"; + let url = extensionContext.uri.spec; + let extensionId = extensionContext.extension.id; + events.push({ eventType, url, extensionId }); + }; + + Management.on("proxy-context-load", record); + Management.on("proxy-context-unload", record); + registerCleanupFunction(() => { + Management.off("proxy-context-load", record); + Management.off("proxy-context-unload", record); + }); + } + + const tabUrl = "http://example.com/data/file_sample.html"; + let contentPage = await ExtensionTestUtils.loadContentPage(tabUrl); + + await extension.awaitMessage("contentscript-run"); + + let contextEvents = events.splice(0); + equal( + contextEvents.length, + 1, + "ExtensionContext state change after loading a content script" + ); + equal( + contextEvents[0].eventType, + "load", + "Create ExtensionContext for content script" + ); + equal(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); + + await contentPage.spawn(null, () => { + this.content.location.reload(); + }); + await extension.awaitMessage("contentscript-run"); + + contextEvents = events.splice(0); + equal( + contextEvents.length, + 2, + "ExtensionContext state changes after reloading a content script" + ); + equal(contextEvents[0].eventType, "unload", "Unload old ExtensionContext"); + equal(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); + equal( + contextEvents[1].eventType, + "load", + "Create new ExtensionContext for content script" + ); + equal(contextEvents[1].url, tabUrl, "ExtensionContext URL = page"); + + await contentPage.close(); + + contextEvents = events.splice(0); + equal( + contextEvents.length, + 1, + "ExtensionContext state change after unloading a content script" + ); + equal( + contextEvents[0].eventType, + "unload", + "Unload ExtensionContext after closing the tab with the content script" + ); + equal(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js new file mode 100644 index 0000000000..6f2ee165f5 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js @@ -0,0 +1,1383 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/** + * Tests that various types of inline content elements initiate requests + * with the triggering pringipal of the caller that requested the load, + * and that the correct security policies are applied to the resulting + * loads. + */ + +// Make sure media pre-loading is enabled on Android so that our