summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/mochitest
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/mochitest')
-rw-r--r--toolkit/components/extensions/test/mochitest/.eslintrc.js12
-rw-r--r--toolkit/components/extensions/test/mochitest/chrome.ini38
-rw-r--r--toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js65
-rw-r--r--toolkit/components/extensions/test/mochitest/chrome_head.js1
-rw-r--r--toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html7
-rw-r--r--toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html9
-rw-r--r--toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html10
-rw-r--r--toolkit/components/extensions/test/mochitest/file_contains_iframe.html13
-rw-r--r--toolkit/components/extensions/test/mochitest/file_contains_img.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html11
-rw-r--r--toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html10
-rw-r--r--toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html10
-rw-r--r--toolkit/components/extensions/test/mochitest/file_green.html3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_green_blue.html16
-rw-r--r--toolkit/components/extensions/test/mochitest/file_image_bad.pngbin0 -> 5401 bytes
-rw-r--r--toolkit/components/extensions/test/mochitest/file_image_good.pngbin0 -> 580 bytes
-rw-r--r--toolkit/components/extensions/test/mochitest/file_image_great.pngbin0 -> 580 bytes
-rw-r--r--toolkit/components/extensions/test/mochitest/file_image_redirect.pngbin0 -> 5401 bytes
-rw-r--r--toolkit/components/extensions/test/mochitest/file_indexedDB.html28
-rw-r--r--toolkit/components/extensions/test/mochitest/file_mixed.html13
-rw-r--r--toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html30
-rw-r--r--toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html9
-rw-r--r--toolkit/components/extensions/test/mochitest/file_remote_frame.html20
-rw-r--r--toolkit/components/extensions/test/mochitest/file_sample.html13
-rw-r--r--toolkit/components/extensions/test/mochitest/file_sample.txt1
-rw-r--r--toolkit/components/extensions/test/mochitest/file_sample.txt^headers^1
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_bad.js3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_good.js12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_redirect.js3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_xhr.js9
-rw-r--r--toolkit/components/extensions/test/mochitest/file_serviceWorker.html16
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_iframe_worker.html26
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html23
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html10
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_sharedworker.js11
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_webrequest_worker.html28
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_worker.js8
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_xhr.html19
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html19
-rw-r--r--toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html23
-rw-r--r--toolkit/components/extensions/test/mochitest/file_slowed_document.sjs49
-rw-r--r--toolkit/components/extensions/test/mochitest/file_streamfilter.txt1
-rw-r--r--toolkit/components/extensions/test/mochitest/file_style_bad.css3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_style_good.css3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_style_redirect.css3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html10
-rw-r--r--toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html11
-rw-r--r--toolkit/components/extensions/test/mochitest/file_third_party.html21
-rw-r--r--toolkit/components/extensions/test/mochitest/file_to_drawWindow.html9
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html9
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html8
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^1
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html8
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html7
-rw-r--r--toolkit/components/extensions/test/mochitest/file_with_about_blank.html10
-rw-r--r--toolkit/components/extensions/test/mochitest/file_with_images.html10
-rw-r--r--toolkit/components/extensions/test/mochitest/file_with_subframes_and_embed.html21
-rw-r--r--toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html6
-rw-r--r--toolkit/components/extensions/test/mochitest/head.js117
-rw-r--r--toolkit/components/extensions/test/mochitest/head_cookies.js287
-rw-r--r--toolkit/components/extensions/test/mochitest/head_notifications.js171
-rw-r--r--toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js45
-rw-r--r--toolkit/components/extensions/test/mochitest/head_webrequest.js481
-rw-r--r--toolkit/components/extensions/test/mochitest/hsts.sjs10
-rw-r--r--toolkit/components/extensions/test/mochitest/mochitest-common.ini350
-rw-r--r--toolkit/components/extensions/test/mochitest/mochitest-remote.ini11
-rw-r--r--toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini28
-rw-r--r--toolkit/components/extensions/test/mochitest/mochitest.ini13
-rw-r--r--toolkit/components/extensions/test/mochitest/mochitest_console.js54
-rw-r--r--toolkit/components/extensions/test/mochitest/oauth.html26
-rw-r--r--toolkit/components/extensions/test/mochitest/redirect_auto.sjs24
-rw-r--r--toolkit/components/extensions/test/mochitest/redirection.sjs6
-rw-r--r--toolkit/components/extensions/test/mochitest/return_headers.sjs19
-rw-r--r--toolkit/components/extensions/test/mochitest/serviceWorker.js0
-rw-r--r--toolkit/components/extensions/test/mochitest/slow_response.sjs60
-rw-r--r--toolkit/components/extensions/test/mochitest/test_check_startupcache.html61
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html104
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html68
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html80
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html114
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html257
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html116
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html172
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_svg_context_fill.html204
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html98
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html81
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html94
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html89
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html193
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html58
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_action.html51
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_activityLog.html390
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_all_apis.js245
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html401
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html42
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_page.html84
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_page_dpi.html46
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup.html183
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_incognito_window.html151
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_windowId.html162
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_without_pref.html58
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html159
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html323
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html69
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html141
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html65
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html64
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_clipboard.html210
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html262
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html116
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html711
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html117
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html134
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html77
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html109
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_getFrameId.html189
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html101
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html59
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies.html367
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html98
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html72
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html316
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html107
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html115
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html89
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_dnr_other_extensions.html113
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_dnr_tabIds.html137
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html137
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html94
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html94
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html91
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_extension_iframe_messaging.html124
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html110
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_generate.html48
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_geolocation.html86
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_identity.html390
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_idle.html68
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html49
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html62
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html168
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_notifications.html340
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_optional_permissions.html98
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html580
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html92
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html130
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html83
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html102
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html136
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html126
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html77
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_script_filenames.html62
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html1532
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript.html1479
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html144
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_injectImmediately.html215
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_scripting_insertCSS.html395
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_scripting_permissions.html149
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_scripting_removeCSS.html135
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html100
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html45
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html115
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html78
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html202
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html277
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html130
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html108
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html91
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html76
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html340
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html324
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_tabs_create_cookieStoreId.html210
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_tabs_executeScript_good.html162
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html752
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html102
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html152
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_test.html341
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html138
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html170
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html567
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html610
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html313
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html105
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html131
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html181
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html120
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html445
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html59
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html226
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html213
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_getSecurityInfo.html98
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html252
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html75
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html83
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html139
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html265
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_worker.html192
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html104
-rw-r--r--toolkit/components/extensions/test/mochitest/test_startup_canary.html76
-rw-r--r--toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html32
-rw-r--r--toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html22
-rw-r--r--toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html24
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js9
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_test.sys.mjs16
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_worker.js3
208 files changed, 26073 insertions, 0 deletions
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..30781a8759
--- /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.sys.mjs
+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 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="file_WebNavigation_page2.html" width="200" height="200"></iframe>
+
+<form>
+</form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<a id="elt" href="file_WebNavigation_page3.html#ref">click me</a>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+"use strict";
+</script>
+</head>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>file contains iframe</title>
+</head>
+<body>
+
+<iframe src="//example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_img.html">
+</iframe>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>file contains img</title>
+</head>
+<body>
+
+<img src="file_image_good.png"/>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <iframe id="emptyframe"></iframe>
+ <iframe id="regularframe" src="http://test1.example.com/"></iframe>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <iframe srcdoc="<iframe src='http://test1.example.com/'&gt;</iframe&gt;"></iframe>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <iframe id="frame" src="https://test2.example.com/"></iframe>
+</body>
+</html>
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 @@
+<meta charset=utf-8>
+<title>Super green test page</title>
+<body style="background: #0f0">
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 @@
+<meta charset=utf-8>
+<title>Upper square green, rest blue</title>
+<style>
+ div {
+ position: absolute;
+ width: 50vw;
+ height: 50vh;
+ top: 0;
+ left: 0;
+ background-color: lime;
+ }
+ :root {
+ background-color: blue;
+ }
+</style>
+<div></div>
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
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_image_bad.png
Binary files 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
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_image_good.png
Binary files 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
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_image_great.png
Binary files 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
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_image_redirect.png
Binary files 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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <script>
+"use strict";
+
+const objectStoreName = "Objects";
+
+let test = {key: 0, value: "test"};
+
+let request = indexedDB.open("WebExtensionTest", 1);
+request.onupgradeneeded = event => {
+ let db = event.target.result;
+ let objectStore = db.createObjectStore(objectStoreName,
+ {autoIncrement: 0});
+ request = objectStore.add(test.value, test.key);
+ request.onsuccess = event => {
+ db.close();
+ window.postMessage("indexedDBCreated", "*");
+ };
+};
+ </script>
+ </head>
+ <body>
+ This is a test page.
+ </body>
+<html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+<img id="bad-image" src="http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" />
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>1450965 Skip Cors Check for Early WebExtention Redirects</title>
+</head>
+<body>
+ <pre id="c">
+ Fetching ...
+ </pre>
+ <script>
+ "use strict";
+ let c = document.querySelector("#c");
+ const channel = new BroadcastChannel("test_bus");
+ function l(t) { c.innerText += `${t}\n`; }
+
+ fetch("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_cors_blocked.txt")
+ .then(r => r.text())
+ .then(t => {
+ // This Request should have been redirected to /file_sample.txt in
+ // onBeforeRequest. So the text should be 'Sample'
+ l(`Loaded: ${t}`);
+ channel.postMessage(t);
+ }).catch(e => {
+ // The Redirect Failed, most likly due to a CORS Error
+ l(`e`);
+ channel.postMessage(e.toString());
+ });
+ </script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1434357: Allow Web Request API to redirect to data: URI</title>
+</head>
+<body>
+ <div id="testdiv">foo</div>
+</body>
+</html>
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 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script>
+ "use strict";
+ var response = {
+ tabs: false,
+ cookie: document.cookie,
+ };
+ try {
+ browser.tabs.create({url: "file_sample.html"});
+ response.tabs = true;
+ } catch (e) {
+ // ok
+ }
+ window.parent.postMessage(response, "*");
+ </script>
+ </head>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+<title>file sample</title>
+</head>
+<body>
+
+<div id="test">Sample text</div>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <script>
+ "use strict";
+
+ navigator.serviceWorker.register("serviceWorker.js").then(() => {
+ window.postMessage("serviceWorkerRegistered", "*");
+ });
+ </script>
+ </head>
+ <body>
+ This is a test page.
+ </body>
+<html>
diff --git a/toolkit/components/extensions/test/mochitest/file_simple_iframe_worker.html b/toolkit/components/extensions/test/mochitest/file_simple_iframe_worker.html
new file mode 100644
index 0000000000..2ecc24e648
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_simple_iframe_worker.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script type="application/javascript">
+"use strict";
+
+fetch("file_simple_iframe.txt");
+const worker = new Worker("file_simple_worker.js?iniframe=true");
+worker.onmessage = (msg) => {
+ worker.postMessage("file_simple_iframe_worker.txt");
+}
+
+const sharedworker = new SharedWorker("file_simple_sharedworker.js?iniframe=true");
+sharedworker.port.onmessage = (msg) => {
+ sharedworker.port.postMessage("file_simple_iframe_sharedworker.txt");
+}
+sharedworker.port.start();
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script>
+"use strict";
+
+let req = new XMLHttpRequest();
+req.open("GET", "/xhr_sandboxed");
+req.send();
+
+let sandbox = document.createElement("iframe");
+sandbox.setAttribute("sandbox", "allow-scripts");
+sandbox.setAttribute("src", "file_simple_sandboxed_subframe.html");
+document.documentElement.appendChild(sandbox);
+</script>
+<img src="file_image_great.png"/>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_simple_sharedworker.js b/toolkit/components/extensions/test/mochitest/file_simple_sharedworker.js
new file mode 100644
index 0000000000..e8776216f1
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_simple_sharedworker.js
@@ -0,0 +1,11 @@
+"use strict";
+
+self.onconnect = async evt => {
+ const port = evt.ports[0];
+ port.onmessage = async message => {
+ await fetch(message.data);
+ self.close();
+ };
+ port.start();
+ port.postMessage("loaded");
+};
diff --git a/toolkit/components/extensions/test/mochitest/file_simple_webrequest_worker.html b/toolkit/components/extensions/test/mochitest/file_simple_webrequest_worker.html
new file mode 100644
index 0000000000..a90c4509be
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_simple_webrequest_worker.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script type="application/javascript">
+"use strict";
+
+fetch("file_simple_toplevel.txt");
+const worker = new Worker("file_simple_worker.js");
+worker.onmessage = (msg) => {
+ worker.postMessage("file_simple_worker.txt");
+}
+
+const sharedworker = new SharedWorker("file_simple_sharedworker.js");
+sharedworker.port.onmessage = (msg) => {
+ dump(`postMessage to sharedworker\n`);
+ sharedworker.port.postMessage("file_simple_sharedworker.txt");
+}
+sharedworker.port.start();
+
+</script>
+<iframe src="file_simple_iframe_worker.html"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_simple_worker.js b/toolkit/components/extensions/test/mochitest/file_simple_worker.js
new file mode 100644
index 0000000000..9638a8e9c2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_simple_worker.js
@@ -0,0 +1,8 @@
+"use strict";
+
+self.onmessage = async message => {
+ await fetch(message.data);
+ self.close();
+};
+
+self.postMessage("loaded");
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..1b43f804d9
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script>
+"use strict";
+
+let req = new XMLHttpRequest();
+req.open("GET", "https://example.org/example.txt");
+req.send();
+</script>
+<img src="file_image_good.png"/>
+<iframe src="file_simple_xhr_frame.html"/>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html
new file mode 100644
index 0000000000..7f38247ac0
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script>
+"use strict";
+
+let req = new XMLHttpRequest();
+req.open("GET", "/xhr_resource");
+req.send();
+</script>
+<img src="file_image_bad.png"/>
+<iframe src="file_simple_xhr_frame2.html"/>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html
new file mode 100644
index 0000000000..6174a0b402
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script>
+"use strict";
+
+let req = new XMLHttpRequest();
+req.open("GET", "/xhr_resource_2");
+req.send();
+
+let sandbox = document.createElement("iframe");
+sandbox.setAttribute("sandbox", "allow-scripts");
+sandbox.setAttribute("src", "file_simple_sandboxed_frame.html");
+document.documentElement.appendChild(sandbox);
+</script>
+<img src="file_image_redirect.png"/>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_slowed_document.sjs b/toolkit/components/extensions/test/mochitest/file_slowed_document.sjs
new file mode 100644
index 0000000000..8c42fcc966
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_slowed_document.sjs
@@ -0,0 +1,49 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */
+"use strict";
+
+// This script slows the load of an HTML document so that we can reliably test
+// all phases of the load cycle supported by the extension API.
+
+/* eslint-disable no-unused-vars */
+
+const URL = "file_slowed_document.sjs";
+
+const DELAY = 2 * 1000; // Delay two seconds before completing the request.
+
+let nsTimer = Components.Constructor(
+ "@mozilla.org/timer;1",
+ "nsITimer",
+ "initWithCallback"
+);
+
+let timer;
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(`<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title></title>
+ </head>
+ <body>
+ `);
+
+ // Note: We need to store a reference to the timer to prevent it from being
+ // canceled when it's GCed.
+ timer = new nsTimer(
+ () => {
+ if (request.queryString.includes("with-iframe")) {
+ response.write(`<iframe src="${URL}?r=${Math.random()}"></iframe>`);
+ }
+ response.write(`</body></html>`);
+ 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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+ <title>The Title</title>
+</head>
+<body>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+ <title>Another Title</title>
+ <link href="file_image_great.png" rel="icon" type="image/png" />
+</head>
+<body>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script>
+
+"use strict"
+
+let url = new URL(location);
+let img = new Image();
+img.src = `http://${url.searchParams.get("domain")}/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png`;
+document.body.appendChild(img);
+
+</script>
+
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body style="background: #ff9">
+ &nbsp;
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+ <head>
+ <meta http-equiv="refresh" content="1;dummy_page.html">
+ </head>
+ <body>
+ </body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+ <head>
+ </head>
+ <body>
+ </body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="file_webNavigation_clientRedirect.html" width="200" height="200"></iframe>
+
+<form>
+</form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="redirection.sjs" width="200" height="200"></iframe>
+
+<form>
+</form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="file_webNavigation_manualSubframe_page1.html" width="200" height="200"></iframe>
+
+<form>
+</form>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+
+<html>
+ <body>
+ <h1>page1</h1>
+ <a href="file_webNavigation_manualSubframe_page2.html">page2</a>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+
+<html>
+ <body>
+ <h1>page2</h1>
+ </body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <iframe id="a_b" src="about:blank"></iframe>
+ <iframe srcdoc="galactica actual" src="adama"></iframe>
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <img src="https://example.com/chrome/toolkit/components/extensions/test/mochitest/file_image_good.png">
+ <img src="http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_image_great.png">
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+
+Load a bunch of iframes with subframes.
+<p>
+<iframe src="file_contains_iframe.html"></iframe>
+<iframe src="file_WebNavigation_page1.html"></iframe>
+<iframe src="file_with_xorigin_frame.html"></iframe>
+
+<p>
+Load an embed frame.
+<p>
+<embed type="text/html" src="file_sample.html"></embed>
+
+<p>
+And an object.
+<p>
+<object type="text/html" data="file_contains_img.html"></embed>
+
+<p>
+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 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+
+<img src="file_image_great.png"/>
+Load a cross-origin iframe from example.net <p>
+<iframe src="https://example.net/tests/toolkit/components/extensions/test/mochitest/file_sample.html"></iframe>
diff --git a/toolkit/components/extensions/test/mochitest/head.js b/toolkit/components/extensions/test/mochitest/head.js
new file mode 100644
index 0000000000..ad8fb2052f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head.js
@@ -0,0 +1,117 @@
+"use strict";
+
+/* exported AppConstants, Assert, AppTestDelegate */
+
+var { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { AppTestDelegate } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://specialpowers/AppTestDelegate.sys.mjs"
+);
+
+{
+ 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..bba3f59d49
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_notifications.js
@@ -0,0 +1,171 @@
+"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..9e6b5cc910
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js
@@ -0,0 +1,481 @@
+"use strict";
+
+let commonEvents = {
+ onBeforeRequest: [{ urls: ["<all_urls>"] }, ["blocking"]],
+ onBeforeSendHeaders: [
+ { urls: ["<all_urls>"] },
+ ["blocking", "requestHeaders"],
+ ],
+ onSendHeaders: [{ urls: ["<all_urls>"] }, ["requestHeaders"]],
+ onBeforeRedirect: [{ urls: ["<all_urls>"] }],
+ onHeadersReceived: [
+ { urls: ["<all_urls>"] },
+ ["blocking", "responseHeaders"],
+ ],
+ // Auth tests will need to set their own events object
+ // "onAuthRequired": [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]],
+ onResponseStarted: [{ urls: ["<all_urls>"] }],
+ onCompleted: [{ urls: ["<all_urls>"] }, ["responseHeaders"]],
+ onErrorOccurred: [{ urls: ["<all_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", "<all_urls>"],
+ },
+ 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 = "<!DOCTYPE html><html><body><p>HSTS page</p></body></html>";
+ 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..83d4bdc41b
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -0,0 +1,350 @@
+[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_iframe_worker.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_simple_sharedworker.js
+ file_simple_webrequest_worker.html
+ file_simple_worker.js
+ 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
+ http3
+[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]
+skip-if =
+ http3
+[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]
+skip-if =
+ http3
+[test_ext_browsingData_localStorage.html]
+skip-if =
+ http3
+[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."
+ http3
+[test_ext_browsingData_settings.html]
+[test_ext_canvas_resistFingerprinting.html]
+skip-if =
+ http3
+[test_ext_clipboard.html]
+skip-if =
+ os == 'android'
+ http3
+[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"
+ http3
+[test_ext_contentscript_activeTab.html]
+skip-if =
+ http3
+[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]
+skip-if =
+ http3
+[test_ext_contentscript_fission_frame.html]
+skip-if =
+ http3
+[test_ext_contentscript_getFrameId.html]
+[test_ext_contentscript_incognito.html]
+skip-if =
+ os == 'android' # Android does not support multiple windows.
+ http3
+[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"
+ http3
+[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_other_extensions.html]
+[test_ext_dnr_tabIds.html]
+[test_ext_dnr_upgradeScheme.html]
+skip-if =
+ http3
+[test_ext_downloads_download.html]
+[test_ext_embeddedimg_iframe_frameAncestors.html]
+skip-if =
+ http3
+[test_ext_exclude_include_globs.html]
+skip-if =
+ http3
+[test_ext_extension_iframe_messaging.html]
+skip-if =
+ http3
+[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 =
+ win11_2009 && !debug && socketprocess_networking # Bug 1777016
+ 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/"
+ http3
+[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
+ http3
+[test_ext_runtime_connect.html]
+skip-if =
+ http3
+[test_ext_runtime_connect_iframe.html]
+[test_ext_runtime_connect_twoway.html]
+skip-if =
+ http3
+[test_ext_runtime_connect2.html]
+skip-if =
+ http3
+[test_ext_runtime_disconnect.html]
+skip-if =
+ http3
+[test_ext_script_filenames.html]
+[test_ext_scripting_contentScripts.html]
+skip-if =
+ http3
+[test_ext_scripting_executeScript.html]
+skip-if =
+ http3
+[test_ext_scripting_executeScript_activeTab.html]
+skip-if =
+ http3
+[test_ext_scripting_executeScript_injectImmediately.html]
+skip-if =
+ http3
+[test_ext_scripting_insertCSS.html]
+skip-if =
+ http3
+[test_ext_scripting_permissions.html]
+skip-if =
+ http3
+[test_ext_scripting_removeCSS.html]
+skip-if =
+ http3
+[test_ext_sendmessage_doublereply.html]
+skip-if =
+ http3
+[test_ext_sendmessage_frameId.html]
+[test_ext_sendmessage_no_receiver.html]
+skip-if =
+ http3
+[test_ext_sendmessage_reply.html]
+skip-if =
+ http3
+[test_ext_sendmessage_reply2.html]
+skip-if =
+ os == 'android'
+ http3
+[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}
+ http3
+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]
+skip-if =
+ http3
+[test_ext_subframes_privileges.html]
+skip-if =
+ os == 'android' || verify # bug 1489771
+ http3
+[test_ext_tabs_captureTab.html]
+skip-if =
+ http3
+[test_ext_tabs_executeScript_good.html]
+skip-if =
+ http3
+[test_ext_tabs_create_cookieStoreId.html]
+[test_ext_tabs_query_popup.html]
+[test_ext_tabs_permissions.html]
+skip-if =
+ http3
+[test_ext_tabs_sendMessage.html]
+skip-if =
+ http3
+[test_ext_test.html]
+skip-if =
+ http3
+[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
+ http3
+[test_ext_webnavigation_filters.html]
+skip-if =
+ (os == 'android' && debug) || (verify && (os == 'linux' || os == 'mac')) # bug 1397615
+ http3
+[test_ext_webnavigation_incognito.html]
+skip-if =
+ os == 'android' # bug 1513544
+ http3
+[test_ext_webrequest_and_proxy_filter.html]
+skip-if =
+ http3
+[test_ext_webrequest_auth.html]
+skip-if =
+ os == 'android'
+ http3
+[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
+ http3
+[test_ext_webrequest_errors.html]
+skip-if =
+ tsan
+ http3
+[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]
+[test_ext_webrequest_getSecurityInfo.html]
+skip-if =
+ http3
+[test_ext_webrequest_hsts.html]
+https_first_disabled = true
+skip-if =
+ http3
+[test_ext_webrequest_upgrade.html]
+https_first_disabled = true
+[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_webrequest_worker.html]
+[test_ext_window_postMessage.html]
+skip-if =
+ http3
+# 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..0a66b11755
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/mochitest-remote.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = webextensions remote-webextensions
+skip-if = os == 'android' # Bug 1620091: disable on android until extension process is done
+prefs =
+ extensions.webextensions.remote=true
+ # We don't want to reset this at the end of the test, so that we don't have
+ # to spawn a new extension child process for each test unit.
+ dom.ipc.keepProcessesAlive.extension=1
+
+[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..4468349d84
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini
@@ -0,0 +1,28 @@
+[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
+ http3
+
+prefs =
+ extensions.webextensions.remote=true
+ # We don't want to reset this at the end of the test, so that we don't have
+ # to spawn a new extension child process for each test unit.
+ dom.ipc.keepProcessesAlive.extension=1
+ 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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ "use strict";
+
+ onload = () => {
+ let url = new URL(location);
+ if (url.searchParams.get("post")) {
+ let server_redirect = `${url.searchParams.get("server_uri")}?redirect_uri=${encodeURIComponent(url.searchParams.get("redirect_uri"))}`;
+ let form = document.forms.testform;
+ form.setAttribute("action", server_redirect);
+ form.submit();
+ } else {
+ let end = new URL(url.searchParams.get("redirect_uri"));
+ end.searchParams.set("access_token", "here ya go");
+ location.href = end.href;
+ }
+ };
+ </script>
+</head>
+<body>
+ <form name="testform" action="" method="POST">
+ </form>
+</body>
+</html>
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
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/serviceWorker.js
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 = [
+ `<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title></title>
+ </head>
+ <body>`,
+ "Lorem ipsum dolor sit amet, <br>",
+ "consectetur adipiscing elit, <br>",
+ "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br>",
+ "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <br>",
+ "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br>",
+ "Excepteur sint occaecat cupidatat non proident, <br>",
+ "sunt in culpa qui officia deserunt mollit anim id est laborum.<br>",
+ `
+ </body>
+ </html>`,
+];
+
+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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Check StartupCache</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function check_ExtensionParent_StartupCache_is_non_empty() {
+ // This test aims to verify that the StartupCache of extensions is populated.
+ // Ideally, we would load an extension, restart the browser and confirm the
+ // existence of the StartupCache. That is not possible in a mochitest.
+ // So we will just read the contents of the StartupCache and verify that it
+ // populated and assume that it carries over to the next startup.
+ // The latter is checked in test_startup_canary.html
+
+ const { WebExtensionPolicy } = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services);
+ // The Mochikit extension is part of the mochitests framework, so the fact
+ // that this test runs implies that the extension should have been started.
+ ok(
+ WebExtensionPolicy.getByID("mochikit@mozilla.org"),
+ "This test expects the Mochikit extension to be running"
+ );
+
+ let chromeScript = loadChromeScript(() => {
+ const {
+ ExtensionParent,
+ } = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
+ const { StartupCache } = ExtensionParent;
+ this.sendAsyncMessage("StartupCache_data", StartupCache._data);
+ });
+
+ let map = await chromeScript.promiseOneMessage("StartupCache_data");
+ chromeScript.destroy();
+
+ // "manifests" is populated by Extension's parseManifest in Extension.jsm.
+ const keys = ["manifests", "mochikit@mozilla.org", "2.0", "en-US"];
+ for (let key of keys) {
+ map = map.get(key);
+ ok(map, `StartupCache data map contains ${key}`);
+ }
+
+ // At this point `map` is expected to be the return value of
+ // ExtensionData's parseManifest.
+
+ is(
+ map?.manifest?.applications?.gecko?.id,
+ "mochikit@mozilla.org",
+ "StartupCache.manifests contains a parsed manifest"
+ );
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test content script matching a data: URI</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(async function test_contentscript_data_uri() {
+ const target = ExtensionTestUtils.loadExtension({
+ files: {
+ "page.html": `<!DOCTYPE html>
+ <meta charset="utf-8">
+ <iframe id="inherited" src="data:text/html;charset=utf-8,inherited"></iframe>
+ `,
+ },
+ background() {
+ browser.test.sendMessage("page", browser.runtime.getURL("page.html"));
+ },
+ });
+
+ const scripts = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation"],
+ content_scripts: [{
+ all_frames: true,
+ matches: ["<all_urls>"],
+ run_at: "document_start",
+ css: ["all_urls.css"],
+ js: ["all_urls.js"],
+ }],
+ },
+ files: {
+ "all_urls.css": `
+ body { background: yellow; }
+ `,
+ "all_urls.js": function() {
+ document.body.style.color = "red";
+ browser.test.assertTrue(location.protocol !== "data:",
+ `Matched document not a data URI: ${location.href}`);
+ },
+ },
+ background() {
+ browser.webNavigation.onCompleted.addListener(({url, frameId}) => {
+ browser.test.log(`Document loading complete: ${url}`);
+ if (frameId === 0) {
+ browser.test.sendMessage("tab-ready", url);
+ }
+ });
+ },
+ });
+
+ await target.startup();
+ await scripts.startup();
+
+ // Test extension page with a data: iframe.
+ const page = await target.awaitMessage("page");
+
+ // Hold on to the tab by the browser, as extension loads are COOP loads, and
+ // will break WindowProxy references.
+ let win = window.open();
+ const browserFrame = win.browsingContext.embedderElement;
+ win.location.href = page;
+
+ await scripts.awaitMessage("tab-ready");
+ win = browserFrame.contentWindow;
+ is(win.location.href, page, "Extension page loaded into a tab");
+ is(win.document.readyState, "complete", "Page finished loading");
+
+ const iframe = win.document.getElementById("inherited").contentWindow;
+ is(iframe.document.readyState, "complete", "iframe finished loading");
+
+ const style1 = iframe.getComputedStyle(iframe.document.body);
+ is(style1.color, "rgb(0, 0, 0)", "iframe text color is unmodified");
+ is(style1.backgroundColor, "rgba(0, 0, 0, 0)", "iframe background unmodified");
+
+ // Test extension tab navigated to a data: URI.
+ const data = "data:text/html;charset=utf-8,also-inherits";
+ win.location.href = data;
+
+ await scripts.awaitMessage("tab-ready");
+ win = browserFrame.contentWindow;
+ is(win.location.href, data, "Extension tab navigated to a data: URI");
+ is(win.document.readyState, "complete", "Tab finished loading");
+
+ const style2 = win.getComputedStyle(win.document.body);
+ is(style2.color, "rgb(0, 0, 0)", "Tab text color is unmodified");
+ is(style2.backgroundColor, "rgba(0, 0, 0, 0)", "Tab background unmodified");
+
+ win.close();
+ await target.unload();
+ await scripts.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for telemetry for content script injection</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script>
+"use strict";
+
+const HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS";
+
+add_task(async function test_contentscript_telemetry() {
+ // Turn on telemetry and reset it to the previous state once the test is completed.
+ const telemetryCanRecordBase = SpecialPowers.Services.telemetry.canRecordBase;
+ SpecialPowers.Services.telemetry.canRecordBase = true;
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.Services.telemetry.canRecordBase = telemetryCanRecordBase;
+ });
+
+ function background() {
+ browser.test.onMessage.addListener(() => {
+ browser.tabs.executeScript({code: 'browser.test.sendMessage("content-script-run");'});
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["<all_urls>"],
+ },
+ background,
+ };
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://example.com",
+ true
+ );
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let histogram = SpecialPowers.Services.telemetry.getHistogramById(HISTOGRAM);
+ histogram.clear();
+ is(histogram.snapshot().sum, 0,
+ `No data recorded for histogram: ${HISTOGRAM}.`);
+
+ await extension.startup();
+ is(histogram.snapshot().sum, 0,
+ `No data recorded for histogram after startup: ${HISTOGRAM}.`);
+
+ extension.sendMessage();
+ await extension.awaitMessage("content-script-run");
+
+ let histogramSum = histogram.snapshot().sum;
+ ok(histogramSum > 0,
+ `Data recorded for first extension for histogram: ${HISTOGRAM}.`);
+
+ await AppTestDelegate.removeTab(window, tab);
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script unrecognized property on manifest</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
+
+add_task(async function test_contentscript() {
+ function background() {
+ browser.runtime.onMessage.addListener(async (msg) => {
+ if (msg == "loaded") {
+ // NOTE: we're removing the tab from here because doing a win.close()
+ // from the chrome test code is raising a "TypeError: can't access
+ // dead object" exception.
+ let tabs = await browser.tabs.query({active: true, currentWindow: true});
+ await browser.tabs.remove(tabs[0].id);
+
+ browser.test.notifyPass("content-script-loaded");
+ }
+ });
+ }
+
+ function contentScript() {
+ chrome.runtime.sendMessage("loaded");
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ "unrecognized_property": "with-a-random-value",
+ },
+ ],
+ },
+ background,
+
+ files: {
+ "content_script.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{
+ message: /Reading manifest: Warning processing content_scripts.*.unrecognized_property: An unexpected property was found/,
+ }]);
+ });
+
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+
+ window.open(`${BASE}/file_sample.html`);
+
+ await Promise.all([extension.awaitFinish("content-script-loaded")]);
+ info("test page loaded");
+
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for permissions</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_downloads_open_permission() {
+ function backgroundScript() {
+ browser.test.assertEq(browser.downloads.open, undefined,
+ "`downloads.open` permission is required.");
+ browser.test.notifyPass("downloads tests");
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ await extension.awaitFinish("downloads tests");
+ await extension.unload();
+});
+
+add_task(async function test_downloads_open_requires_user_interaction() {
+ async function backgroundScript() {
+ await browser.test.assertRejects(
+ browser.downloads.open(10),
+ "downloads.open may only be called from a user input handler",
+ "The error is informative.");
+
+ browser.test.notifyPass("downloads tests");
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads", "downloads.open"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ await extension.awaitFinish("downloads tests");
+ await extension.unload();
+});
+
+add_task(async function downloads_open_invalid_id() {
+ async function pageScript() {
+ window.addEventListener("keypress", async function handler() {
+ try {
+ await browser.downloads.open(10);
+ browser.test.sendMessage("download-open.result", {success: true});
+ } catch (e) {
+ browser.test.sendMessage("download-open.result", {
+ success: false,
+ error: e.message,
+ });
+ }
+ window.removeEventListener("keypress", handler);
+ });
+
+ browser.test.sendMessage("page-ready");
+ }
+
+ let extensionData = {
+ background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("page.html"));
+ },
+ files: {
+ "foo.txt": "It's the file called foo.txt.",
+ "page.html": `<html><head>
+ <script src="page.js"><\/script>
+ </head></html>`,
+ "page.js": pageScript,
+ },
+ manifest: {
+ permissions: ["downloads", "downloads.open"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let url = await extension.awaitMessage("ready");
+ let win = window.open();
+ let browserFrame = win.browsingContext.embedderElement;
+ win.location.href = url;
+ await extension.awaitMessage("page-ready");
+
+ synthesizeKey("a", {}, browserFrame.contentWindow);
+ let result = await extension.awaitMessage("download-open.result");
+
+ is(result.success, false, "Opening download fails.");
+ is(result.error, "Invalid download id 10", "The error is informative.");
+
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test downloads.download() saveAs option</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+
+const PROMPTLESS_DOWNLOAD_PREF = "browser.download.useDownloadDir";
+
+const DOWNLOAD_FILENAME = "file_download.nonext.txt";
+const DEFAULT_SUBDIR = "subdir";
+
+// We need to be able to distinguish files downloaded by the file picker from
+// files downloaded without it.
+let pickerDir;
+let pbPickerDir; // for incognito downloads
+let defaultDir;
+
+add_task(async function setup() {
+ // Reset DownloadLastDir preferences in case other tests set them.
+ SpecialPowers.Services.obs.notifyObservers(
+ null,
+ "browser:purge-session-history"
+ );
+
+ // Set up temporary directories.
+ let downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ pickerDir = downloadDir.clone();
+ pickerDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ info(`Using file picker download directory ${pickerDir.path}`);
+ pbPickerDir = downloadDir.clone();
+ pbPickerDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ info(`Using private browsing file picker download directory ${pbPickerDir.path}`);
+ defaultDir = downloadDir.clone();
+ defaultDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ info(`Using default download directory ${defaultDir.path}`);
+ let subDir = defaultDir.clone();
+ subDir.append(DEFAULT_SUBDIR);
+ subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ isnot(pickerDir.path, defaultDir.path,
+ "Should be able to distinguish between files saved with or without the file picker");
+ isnot(pickerDir.path, pbPickerDir.path,
+ "Should be able to distinguish between files saved in and out of private browsing mode");
+
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["browser.download.folderList", 2],
+ ["browser.download.dir", defaultDir.path],
+ ]});
+
+ SimpleTest.registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ pickerDir.remove(true);
+ pbPickerDir.remove(true);
+ defaultDir.remove(true); // This also removes DEFAULT_SUBDIR.
+ });
+});
+
+add_task(async function test_downloads_saveAs() {
+ const pickerFile = pickerDir.clone();
+ pickerFile.append(DOWNLOAD_FILENAME);
+
+ const pbPickerFile = pbPickerDir.clone();
+ pbPickerFile.append(DOWNLOAD_FILENAME);
+
+ const defaultFile = defaultDir.clone();
+ defaultFile.append(DOWNLOAD_FILENAME);
+
+ const {MockFilePicker} = SpecialPowers;
+ MockFilePicker.init(window);
+
+ function mockFilePickerCallback(expectedStartingDir, pickedFile) {
+ return fp => {
+ // Assert that the downloads API correctly sets the starting directory.
+ ok(fp.displayDirectory.equals(expectedStartingDir), "Got the expected FilePicker displayDirectory");
+
+ // Assert that the downloads API configures both default properties.
+ is(fp.defaultString, DOWNLOAD_FILENAME, "Got the expected FilePicker defaultString");
+ is(fp.defaultExtension, "txt", "Got the expected FilePicker defaultExtension");
+
+ MockFilePicker.setFiles([pickedFile]);
+ };
+ }
+
+ function background() {
+ const url = URL.createObjectURL(new Blob(["file content"]));
+ browser.test.onMessage.addListener(async (filename, saveAs, isPrivate) => {
+ try {
+ let options = {
+ url,
+ filename,
+ incognito: isPrivate,
+ };
+ // Only define the saveAs option if the argument was actually set
+ if (saveAs !== undefined) {
+ options.saveAs = saveAs;
+ }
+ let id = await browser.downloads.download(options);
+ browser.downloads.onChanged.addListener(delta => {
+ if (delta.id == id && delta.state.current === "complete") {
+ browser.test.sendMessage("done", {ok: true, id});
+ }
+ });
+ } catch ({message}) {
+ browser.test.sendMessage("done", {ok: false, message});
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ const manifest = {
+ background,
+ incognitoOverride: "spanning",
+ manifest: {permissions: ["downloads"]},
+ };
+ const extension = ExtensionTestUtils.loadExtension(manifest);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // options should have the following properties:
+ // saveAs (Boolean or undefined)
+ // isPrivate (Boolean)
+ // fileName (string)
+ // expectedStartingDir (nsIFile)
+ // destinationFile (nsIFile)
+ async function testExpectFilePicker(options) {
+ ok(!options.destinationFile.exists(), "the file should have been cleaned up properly previously");
+
+ MockFilePicker.showCallback = mockFilePickerCallback(
+ options.expectedStartingDir,
+ options.destinationFile
+ );
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ extension.sendMessage(options.fileName, options.saveAs, options.isPrivate);
+ let result = await extension.awaitMessage("done");
+ ok(result.ok, `downloads.download() works with saveAs=${options.saveAs}`);
+
+ ok(options.destinationFile.exists(), "the file exists.");
+ is(options.destinationFile.fileSize, 12, "downloaded file is the correct size");
+ options.destinationFile.remove(false);
+ MockFilePicker.reset();
+
+ // Test the user canceling the save dialog.
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+
+ extension.sendMessage(options.fileName, options.saveAs, options.isPrivate);
+ result = await extension.awaitMessage("done");
+
+ ok(!result.ok, "download rejected if the user cancels the dialog");
+ is(result.message, "Download canceled by the user", "with the correct message");
+ ok(!options.destinationFile.exists(), "file was not downloaded");
+ MockFilePicker.reset();
+ }
+
+ async function testNoFilePicker(saveAs) {
+ ok(!defaultFile.exists(), "the file should have been cleaned up properly previously");
+
+ extension.sendMessage(DOWNLOAD_FILENAME, saveAs, false);
+ let result = await extension.awaitMessage("done");
+ ok(result.ok, `downloads.download() works with saveAs=${saveAs}`);
+
+ ok(defaultFile.exists(), "the file exists.");
+ is(defaultFile.fileSize, 12, "downloaded file is the correct size");
+ defaultFile.remove(false);
+ }
+
+ info("Testing that saveAs=true uses the file picker as expected");
+ let expectedStartingDir = defaultDir;
+ let fpOptions = {
+ saveAs: true,
+ isPrivate: false,
+ fileName: DOWNLOAD_FILENAME,
+ expectedStartingDir: expectedStartingDir,
+ destinationFile: pickerFile,
+ };
+ await testExpectFilePicker(fpOptions);
+
+ info("Testing that saveas=true reuses last file picker directory");
+ fpOptions.expectedStartingDir = pickerDir;
+ await testExpectFilePicker(fpOptions);
+
+ info("Testing that saveAs=true in PB reuses last directory");
+ let nonPBStartingDir = fpOptions.expectedStartingDir;
+ fpOptions.isPrivate = true;
+ fpOptions.destinationFile = pbPickerFile;
+ await testExpectFilePicker(fpOptions);
+
+ info("Testing that saveAs=true in PB uses a separate last directory");
+ fpOptions.expectedStartingDir = pbPickerDir;
+ await testExpectFilePicker(fpOptions);
+
+ info("Testing that saveAs=true in Permanent PB mode ignores the incognito option");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+ fpOptions.isPrivate = false;
+ fpOptions.expectedStartingDir = pbPickerDir;
+ await testExpectFilePicker(fpOptions);
+
+ info("Testing that saveas=true reuses the non-PB last directory after private download");
+ await SpecialPowers.popPrefEnv();
+ fpOptions.isPrivate = false;
+ fpOptions.expectedStartingDir = nonPBStartingDir;
+ fpOptions.destinationFile = pickerFile;
+ await testExpectFilePicker(fpOptions);
+
+ info("Testing that saveAs=true does not reuse last directory when filename contains a path separator");
+ fpOptions.fileName = DEFAULT_SUBDIR + "/" + DOWNLOAD_FILENAME;
+ let destinationFile = defaultDir.clone();
+ destinationFile.append(DEFAULT_SUBDIR);
+ fpOptions.expectedStartingDir = destinationFile.clone();
+ destinationFile.append(DOWNLOAD_FILENAME);
+ fpOptions.destinationFile = destinationFile;
+ await testExpectFilePicker(fpOptions);
+
+ info("Testing that saveAs=false does not use the file picker");
+ fpOptions.saveAs = false;
+ await testNoFilePicker(fpOptions.saveAs);
+
+ // When saveAs is not set, the behavior should be determined by the Firefox
+ // pref that normally determines whether the "Save As" prompt should be
+ // displayed.
+ info(`Testing that the file picker is used when saveAs is not specified ` +
+ `but ${PROMPTLESS_DOWNLOAD_PREF} is disabled`);
+ fpOptions.saveAs = undefined;
+ await SpecialPowers.pushPrefEnv({"set": [
+ [PROMPTLESS_DOWNLOAD_PREF, false],
+ ]});
+ await testExpectFilePicker(fpOptions);
+
+ info(`Testing that the file picker is NOT used when saveAs is not ` +
+ `specified but ${PROMPTLESS_DOWNLOAD_PREF} is enabled`);
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.pushPrefEnv({"set": [
+ [PROMPTLESS_DOWNLOAD_PREF, true],
+ ]});
+ await testNoFilePicker(fpOptions.saveAs);
+
+ await extension.unload();
+ MockFilePicker.cleanup();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test downloads.download() uniquify option</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+
+let directory;
+
+add_task(async function setup() {
+ directory = FileUtils.getDir("TmpD", ["downloads"]);
+ directory.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ info(`Using download directory ${directory.path}`);
+
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["browser.download.folderList", 2],
+ ["browser.download.dir", directory.path],
+ ]});
+
+ SimpleTest.registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ directory.remove(true);
+ });
+});
+
+add_task(async function test_downloads_uniquify() {
+ const file = directory.clone();
+ file.append("file_download.txt");
+
+ const unique = directory.clone();
+ unique.append("file_download(1).txt");
+
+ const {MockFilePicker} = SpecialPowers;
+ MockFilePicker.init(window);
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ MockFilePicker.showCallback = fp => {
+ let file = directory.clone();
+ file.append(fp.defaultString);
+ MockFilePicker.setFiles([file]);
+ };
+
+ function background() {
+ const url = URL.createObjectURL(new Blob(["file content"]));
+ browser.test.onMessage.addListener(async (filename, saveAs) => {
+ try {
+ let id = await browser.downloads.download({
+ url,
+ filename,
+ saveAs,
+ conflictAction: "uniquify",
+ });
+ browser.downloads.onChanged.addListener(delta => {
+ if (delta.id == id && delta.state.current === "complete") {
+ browser.test.sendMessage("done", {ok: true, id});
+ }
+ });
+ } catch ({message}) {
+ browser.test.sendMessage("done", {ok: false, message});
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ const manifest = {background, manifest: {permissions: ["downloads"]}};
+ const extension = ExtensionTestUtils.loadExtension(manifest);
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ async function testUniquify(saveAs) {
+ info(`Testing conflictAction:"uniquify" with saveAs=${saveAs}`);
+
+ ok(!file.exists(), "downloaded file should have been cleaned up before test ran");
+ ok(!unique.exists(), "uniquified file should have been cleaned up before test ran");
+
+ // Test download without uniquify and create a conflicting file so we can
+ // test with uniquify.
+ extension.sendMessage("file_download.txt", saveAs);
+ let result = await extension.awaitMessage("done");
+ ok(result.ok, "downloads.download() works with saveAs");
+
+ ok(file.exists(), "the file exists.");
+ is(file.fileSize, 12, "downloaded file is the correct size");
+
+ // Now that a conflicting file exists, test the uniquify behavior
+ extension.sendMessage("file_download.txt", saveAs);
+ result = await extension.awaitMessage("done");
+ ok(result.ok, "downloads.download() works with saveAs and uniquify");
+
+ ok(unique.exists(), "the file exists.");
+ is(unique.fileSize, 12, "downloaded file is the correct size");
+
+ file.remove(false);
+ unique.remove(false);
+ }
+ await testUniquify(true);
+ await testUniquify(false);
+
+ await extension.unload();
+ MockFilePicker.cleanup();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for permissions</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function makeTest(manifestPermissions, optionalPermissions, checkFetch = true) {
+ return async function() {
+ function pageScript() {
+ /* global PERMISSIONS */
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "set-cookie") {
+ try {
+ await browser.cookies.set({
+ url: "http://example.com/",
+ name: "COOKIE",
+ value: "NOM NOM",
+ });
+ browser.test.sendMessage("set-cookie.result", {success: true});
+ } catch (err) {
+ dump(`set cookie failed with ${err.message}\n`);
+ browser.test.sendMessage("set-cookie.result",
+ {success: false, message: err.message});
+ }
+ } else if (msg == "remove") {
+ browser.permissions.remove(PERMISSIONS).then(result => {
+ browser.test.sendMessage("remove.result", result);
+ });
+ } else if (msg == "request") {
+ browser.test.withHandlingUserInput(() => {
+ browser.permissions.request(PERMISSIONS).then(result => {
+ browser.test.sendMessage("request.result", result);
+ });
+ });
+ }
+ });
+
+ browser.test.sendMessage("page-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("page.html"));
+ },
+
+ manifest: {
+ permissions: manifestPermissions,
+ optional_permissions: [...(optionalPermissions.permissions || []),
+ ...(optionalPermissions.origins || [])],
+
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html"],
+ js: ["content_script.js"],
+ }],
+ },
+
+ files: {
+ "content_script.js": async () => {
+ let url = new URL(window.location.pathname, "http://example.com/");
+ fetch(url, {}).then(response => {
+ browser.test.sendMessage("fetch.result", response.ok);
+ }).catch(err => {
+ browser.test.sendMessage("fetch.result", false);
+ });
+ },
+
+ "page.html": `<html><head>
+ <script src="page.js"><\/script>
+ </head></html>`,
+
+ "page.js": `const PERMISSIONS = ${JSON.stringify(optionalPermissions)}; (${pageScript})();`,
+ },
+ });
+
+ await extension.startup();
+
+ function call(method) {
+ extension.sendMessage(method);
+ return extension.awaitMessage(`${method}.result`);
+ }
+
+ let base = window.location.href.replace(/^chrome:\/\/mochitests\/content/,
+ "http://mochi.test:8888");
+ let file = new URL("file_sample.html", base);
+
+ async function testContentScript() {
+ let win = window.open(file);
+ let result = await extension.awaitMessage("fetch.result");
+ win.close();
+ return result;
+ }
+
+ let url = await extension.awaitMessage("ready");
+ let win = window.open();
+ win.location.href = url;
+ await extension.awaitMessage("page-ready");
+
+ // Using the cookies API from an extension page should fail
+ let result = await call("set-cookie");
+ is(result.success, false, "setting cookie failed");
+ if (manifestPermissions.includes("cookies")) {
+ ok(/^Permission denied/.test(result.message),
+ "setting cookie failed with an appropriate error due to missing host permission");
+ } else {
+ ok(/browser\.cookies is undefined/.test(result.message),
+ "setting cookie failed since cookies API is not present");
+ }
+
+ // Making a cross-origin request from a content script should fail
+ if (checkFetch) {
+ result = await testContentScript();
+ is(result, false, "fetch() failed from content script due to lack of host permission");
+ }
+
+ result = await call("request");
+ is(result, true, "permissions.request() succeeded");
+
+ // Using the cookies API from an extension page should succeed
+ result = await call("set-cookie");
+ is(result.success, true, "setting cookie succeeded");
+
+ // Making a cross-origin request from a content script should succeed
+ if (checkFetch) {
+ result = await testContentScript();
+ is(result, true, "fetch() succeeded from content script due to lack of host permission");
+ }
+
+ // Now revoke our permissions
+ result = await call("remove");
+
+ // The cookies API should once again fail
+ result = await call("set-cookie");
+ is(result.success, false, "setting cookie failed");
+
+ // As should the cross-origin request from a content script
+ if (checkFetch) {
+ result = await testContentScript();
+ is(result, false, "fetch() failed from content script due to lack of host permission");
+ }
+
+ await extension.unload();
+ };
+}
+
+add_task(function setup() {
+ // Don't bother with prompts in this test.
+ return SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextOptionalPermissionPrompts", false]],
+ });
+});
+
+const ORIGIN = "*://example.com/";
+add_task(makeTest([], {
+ permissions: ["cookies"],
+ origins: [ORIGIN],
+}));
+
+add_task(makeTest(["cookies"], {origins: [ORIGIN]}));
+add_task(makeTest([ORIGIN], {permissions: ["cookies"]}, false));
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for permissions</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <style>
+ img {
+ -moz-context-properties: fill;
+ fill: green;
+ }
+
+ img, div.ref {
+ width: 100px;
+ height: 100px;
+ }
+
+ div#green {
+ background: green;
+ }
+
+ div#red {
+ background: red;
+ }
+ </style>
+ <h3>Testing on: <span id="test-params"></span></h3>
+ <table>
+ <thead>
+ <tr>
+ <th>webext image</th>
+ <th>allowed ref</th>
+ <th>disallowed ref</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <img id="actual">
+ </td>
+ <td>
+ <div id="green" class="ref"></div>
+ </td>
+ <td>
+ <div id="red" class="ref"></div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+<script type="text/javascript">
+"use strict";
+
+const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+function screenshotPage(win, elementSelector) {
+ const el = win.document.querySelector(elementSelector);
+ return TestUtils.screenshotArea(el, win);
+}
+
+async function test_moz_extension_svg_context_fill({
+ addonId,
+ isPrivileged,
+ expectAllowed,
+}) {
+ // Include current test params in the rendered html page (to be included in failure
+ // screenshots).
+ document.querySelector("#test-params").textContent = JSON.stringify({
+ addonId,
+ isPrivileged,
+ expectAllowed,
+ });
+
+ let extDefinition = {
+ manifest: {
+ browser_specific_settings: { gecko: { id: addonId } },
+ },
+ background() {
+ browser.test.sendMessage("svg-url", browser.runtime.getURL("context-fill-fallback-red.svg"));
+ },
+ files: {
+ "context-fill-fallback-red.svg": `
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect height="100%" width="100%" fill="context-fill red" />
+ </svg>
+ `,
+ },
+ }
+
+ if (isPrivileged) {
+ // isPrivileged is unused when useAddonManager is set (see ExtensionTestCommon.generate),
+ // the internal permission being tested is only added when the extension has a startupReason
+ // related to new installations and upgrades/downgrades and so the `startupReason` is set here
+ // to be able to mock the startupReason expected when useAddonManager can't be used.
+ extDefinition = {
+ ...extDefinition,
+ isPrivileged,
+ startupReason: "ADDON_INSTALL",
+ };
+ } else {
+ // useAddonManager temporary is instead used to explicitly test the other cases when the extension
+ // is not expected to be privileged.
+ extDefinition = {
+ ...extDefinition,
+ useAddonManager: "temporary",
+ };
+ }
+
+ const extension = ExtensionTestUtils.loadExtension(extDefinition);
+
+ await extension.startup();
+
+ // Set the extension url on the img element part of the
+ // comparison table defined in the html part of this test file.
+ const svgURL = await extension.awaitMessage("svg-url");
+ document.querySelector("#actual").src = svgURL;
+
+ let screenshots;
+
+ // Wait until the svg context fill has been applied
+ // (unfortunately waiting for a document reflow does
+ // not seem to be enough).
+ const expectedColor = expectAllowed ? "green" : "red";
+ await TestUtils.waitForCondition(
+ async () => {
+ const result = await screenshotPage(window, "#actual");
+ const reference = await screenshotPage(window, `#${expectedColor}`);
+ screenshots = {result, reference};
+ return result == reference;
+ },
+ `Context-fill should be ${
+ expectAllowed ? "allowed" : "disallowed"
+ } (resulting in ${expectedColor}) on "${addonId}" extension`
+ );
+
+ // At least an assertion is required to prevent the test from
+ // failing.
+ is(
+ screenshots.result,
+ screenshots.reference,
+ "svg context-fill test completed, result does match reference"
+ );
+
+ await extension.unload();
+}
+
+// This test file verify that the non-standard svg context-fill feature is allowed
+// on extensions svg files coming from Mozilla-owned extensions.
+//
+// NOTE: line extension permission to use context fill is tested in test_recommendations.js
+
+add_task(async function test_allowed_on_privileged_ext() {
+ await test_moz_extension_svg_context_fill({
+ addonId: "privileged-addon@mochi.test",
+ isPrivileged: true,
+ expectAllowed: true,
+ });
+});
+
+add_task(async function test_disallowed_on_non_privileged_ext() {
+ await test_moz_extension_svg_context_fill({
+ addonId: "non-privileged-arbitrary-addon-id@mochi.test",
+ isPrivileged: false,
+ expectAllowed: false,
+ });
+});
+
+add_task(async function test_allowed_on_privileged_ext_with_mozilla_id() {
+ await test_moz_extension_svg_context_fill({
+ addonId: "privileged-addon@mozilla.org",
+ isPrivileged: true,
+ expectAllowed: true,
+ });
+
+ await test_moz_extension_svg_context_fill({
+ addonId: "privileged-addon@mozilla.com",
+ isPrivileged: true,
+ expectAllowed: true,
+ });
+});
+
+add_task(async function test_allowed_on_non_privileged_ext_with_mozilla_id() {
+ await test_moz_extension_svg_context_fill({
+ addonId: "non-privileged-addon@mozilla.org",
+ isPrivileged: false,
+ expectAllowed: true,
+ });
+
+ await test_moz_extension_svg_context_fill({
+ addonId: "non-privileged-addon@mozilla.com",
+ isPrivileged: false,
+ expectAllowed: true,
+ });
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+var {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+
+function tp_background(expectFail = true) {
+ fetch("https://tracking.example.com/example.txt").then(() => {
+ browser.test.assertTrue(!expectFail, "fetch received");
+ browser.test.sendMessage("done");
+ }, () => {
+ browser.test.assertTrue(expectFail, "fetch failure");
+ browser.test.sendMessage("done");
+ });
+}
+
+async function test_permission(permissions, expectFail) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions,
+ },
+ background: `(${tp_background})(${expectFail})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+}
+
+add_task(async function setup() {
+ await UrlClassifierTestUtils.addTestTrackers();
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.trackingprotection.enabled", true]],
+ });
+});
+
+// Fetch would be blocked with these tests
+add_task(async function() { await test_permission([], true); });
+add_task(async function() { await test_permission(["http://*/"], true); });
+add_task(async function() { await test_permission(["http://*.example.com/"], true); });
+add_task(async function() { await test_permission(["http://localhost/*"], true); });
+// Fetch will not be blocked if the extension has host permissions.
+add_task(async function() { await test_permission(["<all_urls>"], false); });
+add_task(async function() { await test_permission(["*://tracking.example.com/*"], false); });
+
+add_task(async function test_contentscript() {
+ function contentScript() {
+ fetch("https://tracking.example.com/example.txt").then(() => {
+ browser.test.notifyPass("fetch received");
+ }, () => {
+ browser.test.notifyFail("fetch failure");
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["*://tracking.example.com/*"],
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ },
+ ],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+ };
+ const url = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_sample.html";
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+ let win = window.open(url);
+ await extension.awaitFinish();
+ win.close();
+ await extension.unload();
+});
+
+add_task(async function teardown() {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function webnav_unresolved_uri_on_expected_URI_scheme() {
+ function background() {
+ let checkURLs;
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ if (checkURLs.length) {
+ let expectedURL = checkURLs.shift();
+ browser.test.assertEq(expectedURL, msg.url, "Got the expected URL");
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("next");
+ }
+ });
+
+ browser.test.onMessage.addListener((name, urls) => {
+ if (name == "checkURLs") {
+ checkURLs = urls;
+ }
+ });
+
+ browser.test.sendMessage("ready", browser.runtime.getURL("/tab.html"));
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background,
+ files: {
+ "tab.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ </html>
+ `,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+
+ let checkURLs = [
+ "resource://gre/modules/XPCOMUtils.sys.mjs",
+ "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js",
+ "about:mozilla",
+ ];
+
+ let tabURL = await extension.awaitMessage("ready");
+ checkURLs.push(tabURL);
+
+ extension.sendMessage("checkURLs", checkURLs);
+
+ for (let url of checkURLs) {
+ window.open(url);
+ await extension.awaitMessage("next");
+ }
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {webrequest_test} = ChromeUtils.import(SimpleTest.getTestFileURL("webrequest_test.jsm"));
+let {testFetch, testXHR} = webrequest_test;
+
+// Here we test that any requests originating from a system principal are not
+// accessible through WebRequest. text_ext_webrequest_background_events tests
+// non-system principal requests.
+
+let testExtension = {
+ manifest: {
+ permissions: [
+ "webRequest",
+ "<all_urls>",
+ ],
+ },
+ background() {
+ let eventNames = [
+ "onBeforeRequest",
+ "onBeforeSendHeaders",
+ "onSendHeaders",
+ "onHeadersReceived",
+ "onResponseStarted",
+ "onCompleted",
+ ];
+
+ function listener(name, details) {
+ // If we get anything, we failed. Removing the system principal check
+ // in ext-webrequest triggers this failure.
+ browser.test.fail(`received ${name}`);
+ }
+
+ for (let name of eventNames) {
+ browser.webRequest[name].addListener(
+ listener.bind(null, name),
+ {urls: ["https://example.com/*"]}
+ );
+ }
+ },
+};
+
+add_task(async function test_webRequest_chromeworker_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ await extension.startup();
+ await new Promise(resolve => {
+ let worker = new ChromeWorker("webrequest_chromeworker.js");
+ worker.onmessage = event => {
+ ok("chrome worker fetch finished");
+ resolve();
+ };
+ worker.postMessage("go");
+ });
+ await extension.unload();
+});
+
+add_task(async function test_webRequest_chromepage_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ await extension.startup();
+ await new Promise(resolve => {
+ fetch("https://example.com/example.txt").then(() => {
+ ok("test page loaded");
+ resolve();
+ });
+ });
+ await extension.unload();
+});
+
+add_task(async function test_webRequest_jsm_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ await extension.startup();
+ await testFetch("https://example.com/example.txt").then(() => {
+ ok("fetch page loaded");
+ });
+ await testXHR("https://example.com/example.txt").then(() => {
+ ok("xhr page loaded");
+ });
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test webRequest checks host permissions</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_webRequest_host_permissions() {
+ function background() {
+ function png(details) {
+ browser.test.sendMessage("png", details.url);
+ }
+ browser.webRequest.onBeforeRequest.addListener(png, {urls: ["*://*/*.png"]});
+ browser.test.sendMessage("ready");
+ }
+
+ const all = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "<all_urls>"]}});
+ const example = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "https://example.com/"]}});
+ const mochi_test = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "http://mochi.test/"]}});
+
+ await all.startup();
+ await example.startup();
+ await mochi_test.startup();
+
+ await all.awaitMessage("ready");
+ await example.awaitMessage("ready");
+ await mochi_test.awaitMessage("ready");
+
+ const win1 = window.open("https://example.com/chrome/toolkit/components/extensions/test/mochitest/file_with_images.html");
+ let urls = [await all.awaitMessage("png"),
+ await all.awaitMessage("png")];
+ ok(urls.some(url => url.endsWith("good.png")), "<all_urls> permission gets to see good.png");
+ ok((await example.awaitMessage("png")).endsWith("good.png"), "example permission sees same-origin example.com image");
+ ok(urls.some(url => url.endsWith("great.png")), "<all_urls> permission also sees great.png");
+
+ // Clear the in-memory image cache, it can prevent listeners from receiving events.
+ const imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
+ imgTools.getImgCacheForDocument(win1.document).clearCache(false);
+ win1.close();
+
+ const win2 = window.open("http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_with_images.html");
+ urls = [await all.awaitMessage("png"),
+ await all.awaitMessage("png")];
+ ok(urls.some(url => url.endsWith("good.png")), "<all_urls> permission gets to see good.png");
+ ok((await mochi_test.awaitMessage("png")).endsWith("great.png"), "mochi.test permission sees same-origin mochi.test image");
+ ok(urls.some(url => url.endsWith("great.png")), "<all_urls> permission also sees great.png");
+ win2.close();
+
+ await all.unload();
+ await example.unload();
+ await mochi_test.unload();
+});
+
+add_task(async function test_webRequest_filter_permissions_warning() {
+ const manifest = {
+ permissions: ["webRequest", "http://example.com/"],
+ };
+
+ async function background() {
+ await browser.webRequest.onBeforeRequest.addListener(() => {}, {urls: ["http://example.org/"]});
+ browser.test.notifyPass();
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({manifest, background});
+
+ const warning = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{message: /filter doesn't overlap with host permissions/}]);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+
+ SimpleTest.endMonitorConsole();
+ await warning;
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test moz-extension protocol use</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let peakAchu;
+add_task(async function setup() {
+ peakAchu = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "<all_urls>",
+ ],
+ },
+ background() {
+ // ID for the extension in the tests. Try to observe it to ensure we cannot.
+ browser.webRequest.onBeforeRequest.addListener(details => {
+ browser.test.notifyFail(`PeakAchu onBeforeRequest ${details.url}`);
+ }, {urls: ["<all_urls>", "moz-extension://*/*"]});
+
+ browser.test.onMessage.addListener((msg, extensionUrl) => {
+ browser.test.log(`spying for ${extensionUrl}`);
+ browser.webRequest.onBeforeRequest.addListener(details => {
+ browser.test.notifyFail(`PeakAchu onBeforeRequest ${details.url}`);
+ }, {urls: [extensionUrl]});
+ });
+ },
+ });
+ await peakAchu.startup();
+});
+
+add_task(async function test_webRequest_no_mozextension_permission() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "tabs",
+ "moz-extension://c9e007e0-e518-ed4c-8202-83849981dd21/*",
+ "moz-extension://*/*",
+ ],
+ },
+ background() {
+ browser.test.notifyPass("loaded");
+ },
+ });
+
+ let messages = [
+ {message: /processing permissions\.2: Value "moz-extension:\/\/c9e007e0-e518-ed4c-8202-83849981dd21\/\*"/},
+ {message: /processing permissions\.3: Value "moz-extension:\/\/\*\/\*"/},
+ ];
+
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, messages);
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("loaded");
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
+
+add_task(async function test_webRequest_mozextension_fetch() {
+ function background() {
+ let page = browser.runtime.getURL("fetched.html");
+ browser.webRequest.onBeforeRequest.addListener(details => {
+ browser.test.assertEq(details.url, page, "got correct url in onBeforeRequest");
+ browser.test.sendMessage("request-started");
+ }, {urls: [browser.runtime.getURL("*")]}, ["blocking"]);
+ browser.webRequest.onCompleted.addListener(details => {
+ browser.test.assertEq(details.url, page, "got correct url in onCompleted");
+ browser.test.sendMessage("request-complete");
+ }, {urls: [browser.runtime.getURL("*")]});
+
+ browser.test.onMessage.addListener((msg, data) => {
+ fetch(page).then(() => {
+ browser.test.notifyPass("fetch success");
+ browser.test.sendMessage("done");
+ }, () => {
+ browser.test.fail("fetch failed");
+ browser.test.sendMessage("done");
+ });
+ });
+ browser.test.sendMessage("extensionUrl", browser.runtime.getURL("*"));
+ }
+
+ // Use webrequest to monitor moz-extension:// requests
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "tabs",
+ "<all_urls>",
+ ],
+ },
+ files: {
+ "fetched.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <h1>moz-extension file</h1>
+ </body>
+ </html>
+ `.trim(),
+ },
+ background,
+ });
+
+ await extension.startup();
+ // send the url for this extension to the monitoring extension
+ peakAchu.sendMessage("extensionUrl", await extension.awaitMessage("extensionUrl"));
+
+ extension.sendMessage("testFetch");
+ await extension.awaitMessage("request-started");
+ await extension.awaitMessage("request-complete");
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
+
+add_task(async function test_webRequest_mozextension_tab_query() {
+ function background() {
+ browser.test.sendMessage("extensionUrl", browser.runtime.getURL("*"));
+ let page = browser.runtime.getURL("tab.html");
+
+ async function onUpdated(tabId, tabInfo, tab) {
+ if (tabInfo.status !== "complete") {
+ return;
+ }
+ browser.test.log(`tab created ${tabId} ${JSON.stringify(tabInfo)} ${tab.url}`);
+ let tabs = await browser.tabs.query({url: browser.runtime.getURL("*")});
+ browser.test.assertEq(1, tabs.length, "got one tab");
+ browser.test.assertEq(tabs.length && tabs[0].id, tab.id, "got the correct tab");
+ browser.test.assertEq(tabs.length && tabs[0].url, page, "got correct url in tab");
+ browser.tabs.remove(tabId);
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ browser.test.sendMessage("tabs-done");
+ }
+ browser.tabs.onUpdated.addListener(onUpdated);
+ browser.tabs.create({url: page});
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "tabs",
+ "<all_urls>",
+ ],
+ },
+ files: {
+ "tab.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <h1>moz-extension file</h1>
+ </body>
+ </html>
+ `.trim(),
+ },
+ background,
+ });
+
+ await extension.startup();
+ peakAchu.sendMessage("extensionUrl", await extension.awaitMessage("extensionUrl"));
+ await extension.awaitMessage("tabs-done");
+ await extension.unload();
+});
+
+add_task(async function teardown() {
+ await peakAchu.unload();
+});
+</script>
+
+</body>
+</html>
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..c29b6286d9
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// Test that the default paths searched for native host manifests
+// are the ones we expect.
+add_task(async function test_default_paths() {
+ let expectUser, expectGlobal;
+ switch (AppConstants.platform) {
+ case "macosx": {
+ expectUser = PathUtils.joinRelative(
+ Services.dirsvc.get("Home", Ci.nsIFile).path,
+ "Library/Application Support/Mozilla"
+ );
+ expectGlobal = "/Library/Application Support/Mozilla";
+
+ break;
+ }
+
+ case "linux": {
+ expectUser = PathUtils.join(
+ Services.dirsvc.get("Home", Ci.nsIFile).path,
+ ".mozilla"
+ );
+
+ const libdir = AppConstants.HAVE_USR_LIB64_DIR ? "lib64" : "lib";
+ expectGlobal = PathUtils.join("/usr", libdir, "mozilla");
+ break;
+ }
+
+ default:
+ // Fixed filesystem paths are only defined for MacOS and Linux,
+ // there's nothing to test on other platforms.
+ ok(false, `This test does not apply on ${AppConstants.platform}`);
+ break;
+ }
+
+ let userDir = Services.dirsvc.get("XREUserNativeManifests", Ci.nsIFile).path;
+ is(userDir, expectUser, "user-specific native messaging directory is correct");
+
+ let globalDir = Services.dirsvc.get("XRESysNativeManifests", Ci.nsIFile).path;
+ is(globalDir, expectGlobal, "system-wide native messaing directory is correct");
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Action with MV3</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_action_onClicked() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ background() {
+ browser.action.onClicked.addListener(async () => {
+ browser.test.notifyPass("action-clicked");
+ });
+
+ browser.test.sendMessage("background-ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ await AppTestDelegate.clickBrowserAction(window, extension);
+ await extension.awaitFinish("action-clicked");
+ await AppTestDelegate.closeBrowserAction(window, extension);
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension activityLog test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_api() {
+ let URL =
+ "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_sample.html";
+
+ // Test that an unspecified extension is not logged by the watcher extension.
+ let unlogged = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ browser_specific_settings: { gecko: { id: "unlogged@tests.mozilla.org" } },
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+ background() {
+ // This privileged test extension should not affect the webRequest
+ // data received by non-privileged extensions (See Bug 1576272).
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ return { cancel: false };
+ },
+ { urls: ["http://mochi.test/*/file_sample.html"] },
+ ["blocking"]
+ );
+ },
+ });
+ await unlogged.startup();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "watched@tests.mozilla.org" } },
+ permissions: [
+ "tabs",
+ "tabHide",
+ "storage",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ ],
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/*/file_sample.html"],
+ js: ["content_script.js"],
+ run_at: "document_idle",
+ },
+ ],
+ },
+ files: {
+ "content_script.js": () => {
+ browser.test.sendMessage("content_script");
+ },
+ "registered_script.js": () => {
+ browser.test.sendMessage("registered_script");
+ },
+ },
+ async background() {
+ let listen = () => {};
+ async function runTest() {
+ // Test activity for a child function call.
+ browser.test.assertEq(
+ undefined,
+ browser.activityLog,
+ "activityLog requires permission"
+ );
+
+ // Test a child event manager.
+ browser.storage.onChanged.addListener(listen);
+ browser.storage.onChanged.removeListener(listen);
+
+ // Test a parent event manager.
+ let webRequestListener = details => {
+ browser.webRequest.onBeforeRequest.removeListener(webRequestListener);
+ return { cancel: false };
+ };
+ browser.webRequest.onBeforeRequest.addListener(
+ webRequestListener,
+ { urls: ["http://mochi.test/*/file_sample.html"] },
+ ["blocking"]
+ );
+
+ // A manifest based content script is already
+ // registered, we do a dynamic registration here.
+ await browser.contentScripts.register({
+ js: [{ file: "registered_script.js" }],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ runAt: "document_start",
+ });
+ browser.test.sendMessage("ready");
+ }
+ browser.test.onMessage.addListener((msg, data) => {
+ // Logging has started here so this listener is logged, but the
+ // call adding it was not. We do an additional onMessage.addListener
+ // call in the test function to validate child based event managers.
+ if (msg == "runtest") {
+ browser.test.assertTrue(true, msg);
+ runTest();
+ }
+ if (msg == "hideTab") {
+ browser.tabs.hide(data);
+ }
+ });
+ browser.test.sendMessage("url", browser.runtime.getURL(""));
+ },
+ });
+
+ async function backgroundScript(expectedUrl, extensionUrl) {
+ let expecting = [
+ // Test child-only api_call.
+ {
+ type: "api_call",
+ name: "test.assertTrue",
+ data: { args: [true, "runtest"] },
+ },
+
+ // Test child-only api_call.
+ {
+ type: "api_call",
+ name: "test.assertEq",
+ data: {
+ args: [undefined, undefined, "activityLog requires permission"],
+ },
+ },
+ // Test child addListener calls.
+ {
+ type: "api_call",
+ name: "storage.onChanged.addListener",
+ data: {
+ args: [],
+ },
+ },
+ {
+ type: "api_call",
+ name: "storage.onChanged.removeListener",
+ data: {
+ args: [],
+ },
+ },
+ // Test parent addListener calls.
+ {
+ type: "api_call",
+ name: "webRequest.onBeforeRequest.addListener",
+ data: {
+ args: [
+ {
+ incognito: null,
+ tabId: null,
+ types: null,
+ urls: ["http://mochi.test/*/file_sample.html"],
+ windowId: null,
+ },
+ ["blocking"],
+ ],
+ },
+ },
+ // Test an api that makes use of callParentAsyncFunction.
+ {
+ type: "api_call",
+ name: "contentScripts.register",
+ data: {
+ args: [
+ {
+ allFrames: null,
+ css: null,
+ excludeGlobs: null,
+ excludeMatches: null,
+ includeGlobs: null,
+ js: [
+ {
+ file: `${extensionUrl}registered_script.js`,
+ },
+ ],
+ matchAboutBlank: null,
+ matches: ["http://mochi.test/*/file_sample.html"],
+ runAt: "document_start",
+ },
+ ],
+ },
+ },
+ // Test child api_event calls.
+ {
+ type: "api_event",
+ name: "test.onMessage",
+ data: { args: ["runtest"] },
+ },
+ {
+ type: "api_call",
+ name: "test.sendMessage",
+ data: { args: ["ready"] },
+ },
+ // Test parent api_event calls.
+ {
+ type: "api_call",
+ name: "webRequest.onBeforeRequest.removeListener",
+ data: {
+ args: [],
+ },
+ },
+ {
+ type: "api_event",
+ name: "webRequest.onBeforeRequest",
+ data: {
+ args: [
+ {
+ url: expectedUrl,
+ method: "GET",
+ type: "main_frame",
+ frameId: 0,
+ parentFrameId: -1,
+ incognito: false,
+ thirdParty: false,
+ ip: null,
+ frameAncestors: [],
+ urlClassification: { firstParty: [], thirdParty: [] },
+ requestSize: 0,
+ responseSize: 0,
+ },
+ ],
+ result: {
+ cancel: false,
+ },
+ },
+ },
+ // Test manifest based content script.
+ {
+ type: "content_script",
+ name: "content_script.js",
+ data: { url: expectedUrl, tabId: 1 },
+ },
+ // registered script test
+ {
+ type: "content_script",
+ name: `${extensionUrl}registered_script.js`,
+ data: { url: expectedUrl, tabId: 1 },
+ },
+ {
+ type: "api_call",
+ name: "test.sendMessage",
+ data: { args: ["registered_script"], tabId: 1 },
+ },
+ {
+ type: "api_call",
+ name: "test.sendMessage",
+ data: { args: ["content_script"], tabId: 1 },
+ },
+ // Child api call
+ {
+ type: "api_call",
+ name: "tabs.hide",
+ data: { args: ["__TAB_ID"] },
+ },
+ {
+ type: "api_event",
+ name: "test.onMessage",
+ data: { args: ["hideTab", "__TAB_ID"] },
+ },
+ ];
+ browser.test.assertTrue(browser.activityLog, "activityLog is privileged");
+
+ // Slightly less than a normal deep equal, we want to know that the values
+ // in our expected data are the same in the actual data, but we don't care
+ // if actual data has additional data or if data is in the same order in objects.
+ // This allows us to ignore keys that may be variable, or that are set in
+ // the api with an undefined value.
+ function deepEquivalent(a, b) {
+ if (a === b) {
+ return true;
+ }
+ if (
+ typeof a != "object" ||
+ typeof b != "object" ||
+ a === null ||
+ b === null
+ ) {
+ return false;
+ }
+ for (let k in a) {
+ if (!deepEquivalent(a[k], b[k])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ let tab;
+ let handler = async details => {
+ browser.test.log(`onExtensionActivity ${JSON.stringify(details)}`);
+ let test = expecting.shift();
+ if (!test) {
+ browser.test.notifyFail(`no test for ${details.name}`);
+ }
+
+ // On multiple runs, tabId will be different. Set the current
+ // tabId where we need it.
+ if (test.data.tabId !== undefined) {
+ test.data.tabId = tab.id;
+ }
+ if (test.data.args !== undefined) {
+ test.data.args = test.data.args.map(value =>
+ value === "__TAB_ID" ? tab.id : value
+ );
+ }
+
+ browser.test.assertEq(test.type, details.type, "type matches");
+ if (test.type == "content_script") {
+ browser.test.assertTrue(
+ details.name.includes(test.name),
+ "content script name matches"
+ );
+ } else {
+ browser.test.assertEq(test.name, details.name, "name matches");
+ }
+
+ browser.test.assertTrue(
+ deepEquivalent(test.data, details.data),
+ `expected ${JSON.stringify(
+ test.data
+ )} included in actual ${JSON.stringify(details.data)}`
+ );
+ if (!expecting.length) {
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("activity");
+ }
+ };
+ browser.activityLog.onExtensionActivity.addListener(
+ handler,
+ "watched@tests.mozilla.org"
+ );
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "opentab") {
+ tab = await browser.tabs.create({ url: expectedUrl });
+ browser.test.sendMessage("tabid", tab.id);
+ }
+ if (msg === "done") {
+ browser.activityLog.onExtensionActivity.removeListener(
+ handler,
+ "watched@tests.mozilla.org"
+ );
+ }
+ });
+ }
+
+ await extension.startup();
+ let extensionUrl = await extension.awaitMessage("url");
+
+ let logger = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ browser_specific_settings: { gecko: { id: "watcher@tests.mozilla.org" } },
+ permissions: ["activityLog"],
+ },
+ background: `(${backgroundScript})("${URL}", "${extensionUrl}")`,
+ });
+ await logger.startup();
+ extension.sendMessage("runtest");
+ await extension.awaitMessage("ready");
+ logger.sendMessage("opentab");
+ let id = await logger.awaitMessage("tabid");
+
+ await Promise.all([
+ extension.awaitMessage("content_script"),
+ extension.awaitMessage("registered_script"),
+ ]);
+
+ extension.sendMessage("hideTab", id);
+ await logger.awaitFinish("activity");
+
+ // Stop watching because we get extra calls on extension shutdown
+ // such as listener removal.
+ logger.sendMessage("done");
+
+ await extension.unload();
+ await unlogged.unload();
+ await logger.unload();
+});
+</script>
+
+</body>
+</html>
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..458dc65d99
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html
@@ -0,0 +1,401 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Async Clipboard permissions tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+// Bug 1479956 - On android-debug verify this test times out
+SimpleTest.requestLongerTimeout(2);
+
+/* globals ClipboardItem, clipboardWriteText, clipboardWrite, clipboardReadText, clipboardRead */
+function shared() {
+ this.clipboardWriteText = function(txt) {
+ return navigator.clipboard.writeText(txt);
+ };
+
+ this.clipboardWrite = function(items) {
+ return navigator.clipboard.write(items);
+ };
+
+ this.clipboardReadText = function() {
+ return navigator.clipboard.readText();
+ };
+
+ this.clipboardRead = function() {
+ return navigator.clipboard.read();
+ };
+}
+
+/**
+ * Clear the clipboard.
+ *
+ * This is needed because Services.clipboard.emptyClipboard() does not clear the actual system clipboard.
+ */
+function clearClipboard() {
+ if (AppConstants.platform == "android") {
+ // On android, this clears the actual system clipboard
+ SpecialPowers.Services.clipboard.emptyClipboard(SpecialPowers.Services.clipboard.kGlobalClipboard);
+ return;
+ }
+ // Need to do this hack on other platforms to clear the actual system clipboard
+ let transf = SpecialPowers.Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(SpecialPowers.Ci.nsITransferable);
+ transf.init(null);
+ // Empty transferables may cause crashes, so just add an unknown type.
+ const TYPE = "text/x-moz-place-empty";
+ transf.addDataFlavor(TYPE);
+ transf.setTransferData(TYPE, {});
+ SpecialPowers.Services.clipboard.setData(transf, null, SpecialPowers.Services.clipboard.kGlobalClipboard);
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.events.asyncClipboard.clipboardItem", true],
+ ]});
+});
+
+// Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in background script
+add_task(async function test_background_async_clipboard_no_permissions() {
+ function backgroundScript() {
+ const item = new ClipboardItem({
+ "text/plain": new Blob(["HI"], {type: "text/plain"})
+ });
+ browser.test.assertRejects(
+ clipboardRead(),
+ (err) => err === undefined,
+ "Read should be denied without permission"
+ );
+ browser.test.assertRejects(
+ clipboardWrite([item]),
+ "Clipboard write was blocked due to lack of user activation.",
+ "Write should be denied without permission"
+ );
+ browser.test.assertRejects(
+ clipboardWriteText("blabla"),
+ "Clipboard write was blocked due to lack of user activation.",
+ "WriteText should be denied without permission"
+ );
+ browser.test.assertRejects(
+ clipboardReadText(),
+ (err) => err === undefined,
+ "ReadText should be denied without permission"
+ );
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ background: [shared, backgroundScript],
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await extension.unload();
+});
+
+// Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in content script
+add_task(async function test_contentscript_async_clipboard_no_permission() {
+ function contentScript() {
+ const item = new ClipboardItem({
+ "text/plain": new Blob(["HI"], {type: "text/plain"})
+ });
+ browser.test.assertRejects(
+ clipboardRead(),
+ (err) => err === undefined,
+ "Read should be denied without permission"
+ );
+ browser.test.assertRejects(
+ clipboardWrite([item]),
+ "Clipboard write was blocked due to lack of user activation.",
+ "Write should be denied without permission"
+ );
+ browser.test.assertRejects(
+ clipboardWriteText("blabla"),
+ "Clipboard write was blocked due to lack of user activation.",
+ "WriteText should be denied without permission"
+ );
+ browser.test.assertRejects(
+ clipboardReadText(),
+ (err) => err === undefined,
+ "ReadText should be denied without permission"
+ );
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["https://example.com/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+ await extension.awaitMessage("ready");
+ win.close();
+ await extension.unload();
+});
+
+// Test that with enough permissions, we are allowed to use writeText in content script
+add_task(async function test_contentscript_clipboard_permission_writetext() {
+ function contentScript() {
+ let str = "HI";
+ clipboardWriteText(str).then(function() {
+ // nothing here
+ browser.test.sendMessage("ready");
+ }, function(err) {
+ browser.test.fail("WriteText promise rejected");
+ browser.test.sendMessage("ready");
+ }); // clipboardWriteText
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["https://example.com/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardWrite",
+ "clipboardRead",
+ ],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+ await extension.awaitMessage("ready");
+ const actual = SpecialPowers.getClipboardData("text/plain");
+ is(actual, "HI", "right string copied by write");
+ win.close();
+ await extension.unload();
+});
+
+// Test that with enough permissions, we are allowed to use readText in content script
+add_task(async function test_contentscript_clipboard_permission_readtext() {
+ function contentScript() {
+ let str = "HI";
+ clipboardReadText().then(function(strData) {
+ if (strData == str) {
+ browser.test.succeed("Successfully read from clipboard");
+ } else {
+ browser.test.fail("ReadText read the wrong thing from clipboard:" + strData);
+ }
+ browser.test.sendMessage("ready");
+ }, function(err) {
+ browser.test.fail("ReadText promise rejected");
+ browser.test.sendMessage("ready");
+ }); // clipboardReadText
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["https://example.com/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardWrite",
+ "clipboardRead",
+ ],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+ await SimpleTest.promiseClipboardChange("HI", () => {
+ SpecialPowers.clipboardCopyString("HI");
+ }, "text/plain");
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+ await extension.awaitMessage("ready");
+ win.close();
+ await extension.unload();
+});
+
+// Test that with enough permissions, we are allowed to use write in content script
+add_task(async function test_contentscript_clipboard_permission_write() {
+ function contentScript() {
+ const item = new ClipboardItem({
+ "text/plain": new Blob(["HI"], {type: "text/plain"})
+ });
+ clipboardWrite([item]).then(function() {
+ // nothing here
+ browser.test.sendMessage("ready");
+ }, function(err) { // clipboardWrite promise error function
+ browser.test.fail("Write promise rejected");
+ browser.test.sendMessage("ready");
+ }); // clipboard write
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["https://example.com/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardWrite",
+ "clipboardRead",
+ ],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+ await extension.awaitMessage("ready");
+ const actual = SpecialPowers.getClipboardData("text/plain");
+ is(actual, "HI", "right string copied by write");
+ win.close();
+ await extension.unload();
+});
+
+// Test that with enough permissions, we are allowed to use read in content script
+add_task(async function test_contentscript_clipboard_permission_read() {
+ function contentScript() {
+ clipboardRead().then(async function(items) {
+ let blob = await items[0].getType("text/plain");
+ let s = await blob.text();
+ if (s == "HELLO") {
+ browser.test.succeed("Read promise successfully read the right thing");
+ } else {
+ browser.test.fail("Read read the wrong string from clipboard:" + s);
+ }
+ browser.test.sendMessage("ready");
+ }, function(err) { // clipboardRead promise error function
+ browser.test.fail("Read promise rejected");
+ browser.test.sendMessage("ready");
+ }); // clipboard read
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["https://example.com/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardWrite",
+ "clipboardRead",
+ ],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+ await SimpleTest.promiseClipboardChange("HELLO", () => {
+ SpecialPowers.clipboardCopyString("HELLO");
+ }, "text/plain");
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+ await extension.awaitMessage("ready");
+ win.close();
+ await extension.unload();
+});
+
+// Test that performing readText(...) when the clipboard is empty returns an empty string
+add_task(async function test_contentscript_clipboard_nocontents_readtext() {
+ function contentScript() {
+ clipboardReadText().then(function(strData) {
+ if (strData == "") {
+ browser.test.succeed("ReadText successfully read correct thing from an empty clipboard");
+ } else {
+ browser.test.fail("ReadText should have read an empty string, but read:" + strData);
+ }
+ browser.test.sendMessage("ready");
+ }, function(err) {
+ browser.test.fail("ReadText promise rejected: " + err);
+ browser.test.sendMessage("ready");
+ });
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["https://example.com/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardRead",
+ ],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+
+ await SimpleTest.promiseClipboardChange("", () => {
+ clearClipboard();
+ }, "text/x-moz-place-empty");
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+ await extension.awaitMessage("ready");
+ win.close();
+ await extension.unload();
+});
+
+// Test that performing read(...) when the clipboard is empty returns an empty ClipboardItem
+add_task(async function test_contentscript_clipboard_nocontents_read() {
+ function contentScript() {
+ clipboardRead().then(function(items) {
+ if (items[0].types.length) {
+ browser.test.fail("Read read the wrong thing from clipboard, " +
+ "ClipboardItem has this many entries: " + items[0].types.length);
+ } else {
+ browser.test.succeed("Read promise successfully resolved");
+ }
+ browser.test.sendMessage("ready");
+ }, function(err) {
+ browser.test.fail("Read promise rejected: " + err);
+ browser.test.sendMessage("ready");
+ });
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["https://example.com/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardRead",
+ ],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+
+ await SimpleTest.promiseClipboardChange("", () => {
+ clearClipboard();
+ }, "text/x-moz-place-empty");
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+ await extension.awaitMessage("ready");
+ win.close();
+ await extension.unload();
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for background page canvas rendering</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_background_canvas() {
+ function background() {
+ try {
+ let canvas = document.createElement("canvas");
+
+ let context = canvas.getContext("2d");
+
+ // This ensures that we have a working PresShell, and can successfully
+ // calculate font metrics.
+ context.font = "8pt fixed";
+
+ browser.test.notifyPass("background-canvas");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("background-canvas");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({ background });
+
+ await extension.startup();
+ await extension.awaitFinish("background-canvas");
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js" type="text/javascript"></script>
+ <link href="/tests/SimpleTest/test.css" rel="stylesheet"/>
+ </head>
+ <body>
+
+ <script type="text/javascript">
+ "use strict";
+
+ /* eslint-disable mozilla/balanced-listeners */
+
+ add_task(async function testAlertNotShownInBackgroundWindow() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function () {
+ alert("I am an alert in the background.");
+
+ browser.test.notifyPass("alertCalled");
+ }
+ });
+
+ let consoleOpened = loadChromeScript(() => {
+ const {sendAsyncMessage, assert} = this;
+ assert.ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), "Alerts should not be present at the start of the test.");
+
+ Services.obs.addObserver(function observer() {
+ sendAsyncMessage("web-console-created");
+ Services.obs.removeObserver(observer, "web-console-created");
+ }, "web-console-created");
+ });
+ let opened = consoleOpened.promiseOneMessage("web-console-created");
+
+ consoleMonitor.start([
+ {
+ message: /alert\(\) is not supported in background windows/
+ }, {
+ message: /I am an alert in the background/
+ }
+ ]);
+
+ await extension.startup();
+ await extension.awaitFinish("alertCalled");
+
+ let chromeScript = loadChromeScript(async () => {
+ const {assert} = this;
+ assert.ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), "Alerts should not be present after calling alert().");
+ });
+ chromeScript.destroy();
+
+ await consoleMonitor.finished();
+
+ await opened;
+ consoleOpened.destroy();
+
+ chromeScript = loadChromeScript(async () => {
+ const {sendAsyncMessage} = this;
+ let {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ require("devtools/client/framework/devtools-browser");
+ let {BrowserConsoleManager} = require("devtools/client/webconsole/browser-console-manager");
+
+ // And then double check that we have an actual browser console.
+ let haveConsole = !!BrowserConsoleManager.getBrowserConsole();
+
+ if (haveConsole) {
+ await BrowserConsoleManager.toggleBrowserConsole();
+ }
+ sendAsyncMessage("done", haveConsole);
+ });
+
+ let consoleShown = await chromeScript.promiseOneMessage("done");
+ ok(consoleShown, "console was shown");
+ chromeScript.destroy();
+
+ await extension.unload();
+ });
+ </script>
+
+ </body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<title>DPI of background page</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+<script src="head.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script>
+"use strict";
+
+async function testDPIMatches(description) {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ browser.test.sendMessage("dpi", window.devicePixelRatio);
+ },
+ });
+ await extension.startup();
+ let dpi = await extension.awaitMessage("dpi");
+ await extension.unload();
+
+ // This assumes that the window is loaded in a device DPI.
+ is(
+ dpi,
+ window.devicePixelRatio,
+ `DPI in a background page should match DPI in primary chrome page ${description}`
+ );
+}
+
+add_task(async function test_dpi_simple() {
+ await testDPIMatches("by default");
+});
+
+add_task(async function test_dpi_devPixelsPerPx() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.css.devPixelsPerPx", 1.5]],
+ });
+ await testDPIMatches("with devPixelsPerPx");
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_dpi_os_zoom() {
+ await SpecialPowers.pushPrefEnv({ set: [["ui.textScaleFactor", 200]] });
+ await testDPIMatches("with OS zoom");
+ await SpecialPowers.popPrefEnv();
+});
+</script>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>action.openPopup Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let extensionData = {
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "open-popup@tests.mozilla.org",
+ }
+ },
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ permissions: ["activeTab"]
+ },
+
+ useAddonManager: "android-only",
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["extensions.openPopupWithoutUserGesture.enabled", true],
+ ],
+ });
+});
+
+async function testActiveTabPermissions(withHandlingUserInput) {
+ const background = async function(withHandlingUserInput) {
+ let tabPromise;
+ let tabLoadedPromise = new Promise(resolve => {
+ // Wait for the tab to actually finish loading (bug 1589734)
+ browser.tabs.onUpdated.addListener(async (id, { status }) => {
+ if (id === (await tabPromise).id && status === "complete") {
+ resolve();
+ }
+ });
+ });
+ tabPromise = browser.tabs.create({ url: "https://www.example.com" });
+ tabLoadedPromise.then(() => {
+ // Once the popup opens, check if we have activeTab permission
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg === "popup-open") {
+ let tabs = await browser.tabs.query({});
+
+ browser.test.assertEq(
+ withHandlingUserInput ? 1 : 0,
+ tabs.filter((t) => typeof t.url !== "undefined").length,
+ "active tab permission only granted with user input"
+ );
+
+ await browser.tabs.remove((await tabPromise).id);
+ browser.test.sendMessage("activeTabsChecked");
+ }
+ });
+
+ if (withHandlingUserInput) {
+ browser.test.withHandlingUserInput(() => {
+ browser.browserAction.openPopup();
+ });
+ } else {
+ browser.browserAction.openPopup();
+ }
+ })
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+
+ background: `(${background})(${withHandlingUserInput})`,
+
+ files: {
+ "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`,
+ async "popup.js"() {
+ browser.runtime.sendMessage("popup-open");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("activeTabsChecked");
+ await extension.unload();
+}
+
+add_task(async function test_browserAction_openPopup_activeTab() {
+ await testActiveTabPermissions(true);
+});
+
+add_task(async function test_browserAction_openPopup_non_activeTab() {
+ await testActiveTabPermissions(false);
+});
+
+add_task(async function test_browserAction_openPopup_invalid_states() {
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+
+ background: async function() {
+ await browser.browserAction.setPopup({ popup: "" })
+ await browser.test.assertRejects(
+ browser.browserAction.openPopup(),
+ "No popup URL is set",
+ "Should throw when no URL is set"
+ );
+
+ await browser.browserAction.disable()
+ await browser.test.assertRejects(
+ browser.browserAction.openPopup(),
+ "Popup is disabled",
+ "Should throw when disabled"
+ );
+
+ browser.test.notifyPass("invalidStates");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("invalidStates");
+ await extension.unload();
+});
+
+add_task(async function test_browserAction_openPopup_no_click_event() {
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+
+ background: async function() {
+ let clicks = 0;
+
+ browser.browserAction.onClicked.addListener(() => {
+ clicks++;
+ });
+
+ // Test with popup set
+ await browser.browserAction.openPopup();
+ browser.test.sendMessage("close-popup");
+
+ browser.test.onMessage.addListener(async (msg) => {
+ if (msg === "popup-closed") {
+ // Test without popup
+ await browser.browserAction.setPopup({ popup: "" });
+
+ await browser.test.assertRejects(
+ browser.browserAction.openPopup(),
+ "No popup URL is set",
+ "Should throw when no URL is set"
+ );
+
+ // We expect the last call to be a no-op, so there isn't really anything
+ // to wait on. Instead, check that no clicks are registered after waiting
+ // for a sufficient amount of time.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ browser.test.assertEq(0, clicks, "onClicked should not be called");
+ browser.test.notifyPass("noClick");
+ }, 1000);
+ }
+ });
+ },
+ });
+
+ extension.onMessage("close-popup", async () => {
+ await AppTestDelegate.closeBrowserAction(window, extension);
+ extension.sendMessage("popup-closed");
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("noClick");
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>action.openPopup Incognito Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let extensionData = {
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "open-popup@tests.mozilla.org",
+ }
+ },
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ permissions: ["activeTab"]
+ },
+
+ useAddonManager: "android-only",
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["extensions.openPopupWithoutUserGesture.enabled", true],
+ ],
+ });
+});
+
+async function getIncognitoWindow() {
+ // Since events will be limited based on incognito, we need a
+ // spanning extension to get the tab id so we can test access failure.
+
+ let windowWatcher = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background: function() {
+ browser.windows.create({ incognito: true }).then(({ id: windowId }) => {
+ browser.test.onMessage.addListener(async data => {
+ if (data === "close") {
+ await browser.windows.remove(windowId);
+ browser.test.sendMessage("window-closed");
+ }
+ });
+
+ browser.test.sendMessage("window-id", windowId);
+ });
+ },
+ incognitoOverride: "spanning",
+ });
+
+ await windowWatcher.startup();
+ let windowId = await windowWatcher.awaitMessage("window-id");
+
+ return {
+ windowId,
+ close: async () => {
+ windowWatcher.sendMessage("close");
+ await windowWatcher.awaitMessage("window-closed");
+ await windowWatcher.unload();
+ },
+ };
+}
+
+async function testWithIncognitoOverride(incognitoOverride) {
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+
+ incognitoOverride,
+
+ background: async function() {
+ browser.test.onMessage.addListener(async ({ windowId, incognitoOverride }) => {
+ const openPromise = browser.browserAction.openPopup({ windowId });
+
+ if (incognitoOverride === "not_allowed") {
+ await browser.test.assertRejects(
+ openPromise,
+ /Invalid window ID/,
+ "Should prevent open popup call for incognito window"
+ );
+ } else {
+ try {
+ browser.test.assertEq(await openPromise, undefined, "openPopup resolved");
+ } catch (e) {
+ browser.test.fail(`Unexpected error: ${e}`);
+ }
+ }
+
+ browser.test.sendMessage("incognitoWindow");
+ });
+ },
+
+ files: {
+ "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`,
+ "popup.js"() {
+ browser.test.sendMessage("popup");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let incognitoWindow = await getIncognitoWindow();
+ await extension.sendMessage({ windowId: incognitoWindow.windowId, incognitoOverride });
+
+ await extension.awaitMessage("incognitoWindow");
+
+ // Wait for the popup to open - bug 1800100
+ if (incognitoOverride === "spanning") {
+ await extension.awaitMessage("popup");
+ }
+
+ await extension.unload();
+
+ await incognitoWindow.close();
+}
+
+add_task(async function test_browserAction_openPopup_incognito_window_spanning() {
+ if (AppConstants.platform == "android") {
+ // TODO bug 1372178: Cannot open private windows from an extension.
+ todo(false, "Cannot open private windows on Android");
+ return;
+ }
+
+ await testWithIncognitoOverride("spanning");
+});
+
+add_task(async function test_browserAction_openPopup_incognito_window_not_allowed() {
+ if (AppConstants.platform == "android") {
+ // TODO bug 1372178: Cannot open private windows from an extension.
+ todo(false, "Cannot open private windows on Android");
+ return;
+ }
+
+
+ await testWithIncognitoOverride("not_allowed");
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>action.openPopup Window ID Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let extensionData = {
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "open-popup@tests.mozilla.org",
+ }
+ },
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ permissions: ["activeTab"]
+ },
+
+ useAddonManager: "android-only",
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["extensions.openPopupWithoutUserGesture.enabled", true],
+ ],
+ });
+});
+
+async function testWithWindowState(state) {
+ const background = async function(state) {
+ const originalWindow = await browser.windows.getCurrent();
+
+ let newWindowPromise;
+ const tabLoadedPromise = new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(async (id, { status }, tab) => {
+ if (tab.windowId === (await newWindowPromise).id && status === "complete") {
+ resolve();
+ }
+ });
+ });
+
+ newWindowPromise = browser.windows.create({ url: "tab.html" });
+
+ browser.test.onMessage.addListener(async (msg) => {
+ if (msg === "close-window") {
+ await browser.windows.remove((await newWindowPromise).id);
+ browser.test.sendMessage("window-closed");
+ }
+ });
+
+ tabLoadedPromise.then(async () => {
+ const windowId = (await newWindowPromise).id;
+
+ switch (state) {
+ case "inactive":
+ const focusChangePromise = new Promise(resolve => {
+ browser.windows.onFocusChanged.addListener((focusedWindowId) => {
+ if (focusedWindowId === originalWindow.id) {
+ resolve();
+ }
+ })
+ });
+ await browser.windows.update(originalWindow.id, { focused: true });
+ await focusChangePromise;
+ break;
+ case "minimized":
+ await browser.windows.update(windowId, { state: "minimized" });
+ break;
+ default:
+ throw new Error(`Invalid state: ${state}`);
+ }
+
+ await browser.browserAction.openPopup({ windowId });
+ });
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+
+ background: `(${background})(${JSON.stringify(state)})`,
+
+ files: {
+ "tab.html": "<!DOCTYPE html>",
+ "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`,
+ "popup.js"() {
+ // Small timeout to ensure the popup doesn't immediately close, which can
+ // happen when focus moves between windows
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(async () => {
+ let windows = await browser.windows.getAll();
+ let highestWindowIdIsFocused = Math.max(...windows.map((w) => w.id))
+ === windows.find((w) => w.focused).id;
+
+ browser.test.assertEq(true, highestWindowIdIsFocused, "new window is focused");
+
+ await browser.test.sendMessage("popup-open");
+
+ // Bug 1800100: Window leaks if not explicitly closed
+ window.close();
+ }, 1000);
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("popup-open");
+ await extension.sendMessage("close-window");
+ await extension.awaitMessage("window-closed");
+ await extension.unload();
+}
+
+add_task(async function test_browserAction_openPopup_window_inactive() {
+ if (AppConstants.platform == "linux") {
+ // TODO bug 1798334: Currently unreliable on linux
+ todo(false, "Unreliable on linux");
+ return;
+ }
+ await testWithWindowState("inactive");
+});
+
+add_task(async function test_browserAction_openPopup_window_minimized() {
+ if (AppConstants.platform == "linux") {
+ // TODO bug 1798334: Currently unreliable on linux
+ todo(false, "Unreliable on linux");
+ return;
+ }
+ await testWithWindowState("minimized");
+});
+
+add_task(async function test_browserAction_openPopup_invalid_window() {
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+
+ background: async function() {
+ await browser.test.assertRejects(
+ browser.browserAction.openPopup({ windowId: Number.MAX_SAFE_INTEGER }),
+ /Invalid window ID/,
+ "Should throw for invalid window ID"
+ );
+ browser.test.notifyPass("invalidWindow");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("invalidWindow");
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>action.openPopup Preference Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let extensionData = {
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "open-popup@tests.mozilla.org",
+ }
+ },
+ browser_action: {
+ default_popup: "popup.html",
+ }
+ },
+
+ useAddonManager: "android-only",
+};
+
+add_task(async function test_browserAction_openPopup_without_pref() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["extensions.openPopupWithoutUserGesture.enabled", false],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+
+ background: async function() {
+ await browser.test.assertRejects(
+ browser.browserAction.openPopup(),
+ "openPopup requires a user gesture",
+ "Should throw when preference is unset"
+ );
+
+ browser.test.notifyPass("withoutPref");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("withoutPref");
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test browsingData.remove indexedDB</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function testIndexedDB() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ async function background() {
+ const PAGE =
+ "/tests/toolkit/components/extensions/test/mochitest/file_indexedDB.html";
+
+ let tabs = [];
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "cleanup") {
+ await Promise.all(tabs.map(tabId => browser.tabs.remove(tabId)));
+ browser.test.sendMessage("done");
+ return;
+ }
+
+ await browser.browsingData.remove(msg, { indexedDB: true });
+ browser.test.sendMessage("indexedDBRemoved");
+ });
+
+ // Create two tabs.
+ let tab = await browser.tabs.create({ url: `https://example.org${PAGE}` });
+ tabs.push(tab.id);
+
+ tab = await browser.tabs.create({ url: `https://example.com${PAGE}` });
+ tabs.push(tab.id);
+
+ // Create tab with cookieStoreId "firefox-container-1"
+ tab = await browser.tabs.create({ url: `https://example.net${PAGE}`, cookieStoreId: 'firefox-container-1' });
+ tabs.push(tab.id);
+ }
+
+ function contentScript() {
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener(
+ "message",
+ msg => {
+ browser.test.sendMessage("indexedDBCreated");
+ },
+ true
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData", "tabs", "cookies"],
+ content_scripts: [
+ {
+ matches: [
+ "https://example.org/*/file_indexedDB.html",
+ "https://example.com/*/file_indexedDB.html",
+ "https://example.net/*/file_indexedDB.html",
+ ],
+ js: ["script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+ files: {
+ "script.js": contentScript,
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("indexedDBCreated");
+ await extension.awaitMessage("indexedDBCreated");
+ await extension.awaitMessage("indexedDBCreated");
+
+ function getUsage() {
+ return new Promise(resolve => {
+ let qms = SpecialPowers.Services.qms;
+ let cb = SpecialPowers.wrapCallback(request => resolve(request.result));
+ qms.getUsage(cb);
+ });
+ }
+
+ async function getOrigins() {
+ let origins = [];
+ let result = await getUsage();
+ for (let i = 0; i < result.length; ++i) {
+ if (result[i].usage === 0) {
+ continue;
+ }
+ if (
+ result[i].origin.startsWith("https://example.org") ||
+ result[i].origin.startsWith("https://example.com") ||
+ result[i].origin.startsWith("https://example.net")
+ ) {
+ origins.push(result[i].origin);
+ }
+ }
+ return origins.sort();
+ }
+
+ let origins = await getOrigins();
+ is(origins.length, 3, "IndexedDB databases have been populated.");
+
+ // Deleting private browsing mode data is silently ignored.
+ extension.sendMessage({ cookieStoreId: "firefox-private" });
+ await extension.awaitMessage("indexedDBRemoved");
+
+ origins = await getOrigins();
+ is(origins.length, 3, "All indexedDB remains after clearing firefox-private");
+
+ // Delete by hostname
+ extension.sendMessage({ hostnames: ["example.com"] });
+ await extension.awaitMessage("indexedDBRemoved");
+
+ origins = await getOrigins();
+ is(origins.length, 2, "IndexedDB data only for only two domains left");
+ ok(origins[0].startsWith("https://example.net"), "example.net not deleted");
+ ok(origins[1].startsWith("https://example.org"), "example.org not deleted");
+
+ // TODO: Bug 1643740
+ if (AppConstants.platform != "android") {
+ // Delete by cookieStoreId
+ extension.sendMessage({ cookieStoreId: "firefox-container-1" });
+ await extension.awaitMessage("indexedDBRemoved");
+
+ origins = await getOrigins();
+ is(origins.length, 1, "IndexedDB data only for only one domain");
+ ok(origins[0].startsWith("https://example.org"), "example.org not deleted");
+ }
+
+ // Delete all
+ extension.sendMessage({});
+ await extension.awaitMessage("indexedDBRemoved");
+
+ origins = await getOrigins();
+ is(origins.length, 0, "All IndexedDB data has been removed.");
+
+ await extension.sendMessage("cleanup");
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
+</script>
+</body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test browsingData.remove indexedDB</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ // make sure userContext is enabled.
+ return SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+});
+
+add_task(async function testLocalStorage() {
+ async function background() {
+ function waitForTabs() {
+ return new Promise(resolve => {
+ let tabs = {};
+
+ let listener = async (msg, { tab }) => {
+ if (msg !== "content-script-ready") {
+ return;
+ }
+
+ tabs[tab.url] = tab;
+ if (Object.keys(tabs).length == 3) {
+ browser.runtime.onMessage.removeListener(listener);
+ resolve(tabs);
+ }
+ };
+ browser.runtime.onMessage.addListener(listener);
+ });
+ }
+
+ function sendMessageToTabs(tabs, message) {
+ return Promise.all(
+ Object.values(tabs).map(tab => {
+ return browser.tabs.sendMessage(tab.id, message);
+ })
+ );
+ }
+
+ let tabs = await waitForTabs();
+
+ browser.test.assertRejects(
+ browser.browsingData.removeLocalStorage({ since: Date.now() }),
+ "Firefox does not support clearing localStorage with 'since'.",
+ "Expected error received when using unimplemented parameter 'since'."
+ );
+
+ await sendMessageToTabs(tabs, "resetLocalStorage");
+ await browser.browsingData.removeLocalStorage({
+ hostnames: ["example.com"],
+ });
+ await browser.tabs.sendMessage(tabs["https://example.com/"].id, "checkLocalStorageCleared");
+ await browser.tabs.sendMessage(tabs["https://example.net/"].id, "checkLocalStorageSet");
+
+ if (
+ SpecialPowers.Services.domStorageManager.nextGenLocalStorageEnabled ===
+ false
+ ) {
+ // This assertion fails when localStorage is using the legacy
+ // implementation (See Bug 1595431).
+ browser.test.log("Skipped assertion on nextGenLocalStorageEnabled=false");
+ } else {
+ await browser.tabs.sendMessage(tabs["https://test1.example.com/"].id, "checkLocalStorageSet");
+ }
+
+ await sendMessageToTabs(tabs, "resetLocalStorage");
+ await sendMessageToTabs(tabs, "checkLocalStorageSet");
+ await browser.browsingData.removeLocalStorage({});
+ await sendMessageToTabs(tabs, "checkLocalStorageCleared");
+
+ await sendMessageToTabs(tabs, "resetLocalStorage");
+ await sendMessageToTabs(tabs, "checkLocalStorageSet");
+ await browser.browsingData.remove({}, { localStorage: true });
+ await sendMessageToTabs(tabs, "checkLocalStorageCleared");
+
+ // Can only delete cookieStoreId with LSNG enabled.
+ if (SpecialPowers.Services.domStorageManager.nextGenLocalStorageEnabled) {
+ await sendMessageToTabs(tabs, "resetLocalStorage");
+ await sendMessageToTabs(tabs, "checkLocalStorageSet");
+ await browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-container-1",
+ });
+ await browser.tabs.sendMessage(tabs["https://example.com/"].id, "checkLocalStorageSet");
+ await browser.tabs.sendMessage(tabs["https://example.net/"].id, "checkLocalStorageSet");
+
+ // TODO: containers support is lacking on GeckoView (Bug 1643740)
+ if (!navigator.userAgent.includes("Android")) {
+ await browser.tabs.sendMessage(tabs["https://test1.example.com/"].id, "checkLocalStorageCleared");
+ }
+
+ await sendMessageToTabs(tabs, "resetLocalStorage");
+ await sendMessageToTabs(tabs, "checkLocalStorageSet");
+ // Hostname doesn't match, so nothing cleared.
+ await browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-container-1",
+ hostnames: ["example.net"],
+ });
+ await sendMessageToTabs(tabs, "checkLocalStorageSet");
+
+ await sendMessageToTabs(tabs, "resetLocalStorage");
+ await sendMessageToTabs(tabs, "checkLocalStorageSet");
+ // Deleting private browsing mode data is silently ignored.
+ await browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-private",
+ });
+ await sendMessageToTabs(tabs, "checkLocalStorageSet");
+ } else {
+ await browser.test.assertRejects(
+ browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-container-1",
+ }),
+ "Firefox does not support clearing localStorage with 'cookieStoreId'.",
+ "removeLocalStorage with cookieStoreId requires LSNG"
+ );
+ }
+
+ // Cleanup (checkLocalStorageCleared creates empty LS databases).
+ await browser.browsingData.removeLocalStorage({});
+
+ browser.test.notifyPass("done");
+ }
+
+ function contentScript() {
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg === "resetLocalStorage") {
+ localStorage.clear();
+ localStorage.setItem("test", "test");
+ } else if (msg === "checkLocalStorageSet") {
+ browser.test.assertEq(
+ "test",
+ localStorage.getItem("test"),
+ `checkLocalStorageSet: ${location.href}`
+ );
+ } else if (msg === "checkLocalStorageCleared") {
+ browser.test.assertEq(
+ null,
+ localStorage.getItem("test"),
+ `checkLocalStorageCleared: ${location.href}`
+ );
+ }
+ });
+ browser.runtime.sendMessage("content-script-ready");
+ }
+
+ // This extension is responsible for opening tabs with a specified
+ // cookieStoreId, we use a separate extension to make sure that browsingData
+ // works without the cookies permission.
+ let openTabsExtension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ name: "Open tabs",
+ browser_specific_settings: { gecko: { id: "open-tabs@tests.mozilla.org" }, },
+ permissions: ["cookies"],
+ },
+ async background() {
+ const TABS = [
+ { url: "https://example.com" },
+ { url: "https://example.net" },
+ {
+ url: "https://test1.example.com",
+ cookieStoreId: 'firefox-container-1',
+ },
+ ];
+
+ function awaitLoad(tabId) {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) {
+ if (tabId == tabId_ && changed.status == "complete") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ }
+
+ let tabs = [];
+ let loaded = [];
+ for (let options of TABS) {
+ let tab = await browser.tabs.create(options);
+ loaded.push(awaitLoad(tab.id));
+ tabs.push(tab);
+ }
+
+ await Promise.all(loaded);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "cleanup") {
+ const tabIds = tabs.map(tab => tab.id);
+ let removedTabs = 0;
+ browser.tabs.onRemoved.addListener(tabId => {
+ browser.test.log(`Removing tab ${tabId}.`);
+ if (tabIds.includes(tabId)) {
+ removedTabs++;
+ if (removedTabs == tabIds.length) {
+ browser.test.sendMessage("done");
+ }
+ }
+ });
+ await browser.tabs.remove(tabIds);
+ }
+ });
+ }
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ background,
+ manifest: {
+ name: "Test Extension",
+ browser_specific_settings: { gecko: { id: "localStorage@tests.mozilla.org" } },
+ permissions: ["browsingData", "tabs"],
+ content_scripts: [
+ {
+ matches: [
+ "https://example.com/",
+ "https://example.net/",
+ "https://test1.example.com/",
+ ],
+ js: ["content-script.js"],
+ run_at: "document_end",
+ },
+ ],
+ },
+ files: {
+ "content-script.js": contentScript,
+ },
+ });
+
+ await openTabsExtension.startup();
+
+ await extension.startup();
+ await extension.awaitFinish("done");
+ await extension.unload();
+
+ await openTabsExtension.sendMessage("cleanup");
+ await openTabsExtension.awaitMessage("done");
+ await openTabsExtension.unload();
+});
+
+// Verify that browsingData.removeLocalStorage doesn't break on data stored
+// in about:newtab or file principals.
+add_task(async function test_browserData_on_aboutnewtab_and_file_data() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ async background() {
+ await browser.browsingData.removeLocalStorage({}).catch(err => {
+ browser.test.fail(`${err} :: ${err.stack}`);
+ });
+ browser.test.sendMessage("done");
+ },
+ manifest: {
+ browser_specific_settings: { gecko: { id: "indexed-db-file@test.mozilla.org" } },
+ permissions: ["browsingData"],
+ },
+ });
+
+ await new Promise(resolve => {
+ const chromeScript = SpecialPowers.loadChromeScript(async () => {
+ /* eslint-env mozilla/chrome-script */
+ const { SiteDataTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SiteDataTestUtils.jsm"
+ );
+ await SiteDataTestUtils.addToIndexedDB("about:newtab");
+ await SiteDataTestUtils.addToIndexedDB("file:///fake/file");
+ sendAsyncMessage("done");
+ });
+
+ chromeScript.addMessageListener("done", () => {
+ chromeScript.destroy();
+ resolve();
+ });
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_browserData_should_not_remove_extension_data() {
+ if (SpecialPowers.getBoolPref("dom.storage.enable_unsupported_legacy_implementation")) {
+ // When LSNG isn't enabled, the browsingData API does still clear
+ // all the extensions localStorage if called without a list of specific
+ // origins to clear.
+ info("Test skipped because LSNG is currently disabled");
+ return;
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ async background() {
+ window.localStorage.setItem("key", "value");
+ await browser.browsingData.removeLocalStorage({}).catch(err => {
+ browser.test.fail(`${err} :: ${err.stack}`);
+ });
+ browser.test.sendMessage("done", window.localStorage.getItem("key"));
+ },
+ manifest: {
+ browser_specific_settings: { gecko: { id: "extension-data@tests.mozilla.org" } },
+ permissions: ["browsingData"],
+ },
+ });
+
+ await extension.startup();
+ const lsValue = await extension.awaitMessage("done");
+ is(lsValue, "value", "Got the expected localStorage data");
+ await extension.unload();
+});
+</script>
+</body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test browsingData.remove indexedDB</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// NB: Since plugins are disabled, there is never any data to clear.
+// We are really testing that these operations are no-ops.
+
+add_task(async function testPluginData() {
+ async function background() {
+ const REFERENCE_DATE = Date.now();
+ const TEST_CASES = [
+ // Clear plugin data with no since value.
+ {},
+ // Clear pluginData with recent since value.
+ { since: REFERENCE_DATE - 20000 },
+ // Clear pluginData with old since value.
+ { since: REFERENCE_DATE - 1000000 },
+ // Clear pluginData for specific hosts.
+ { hostnames: ["bar.com", "baz.com"] },
+ // Clear pluginData for no hosts.
+ { hostnames: [] },
+ ];
+
+ for (let method of ["removePluginData", "remove"]) {
+ for (let options of TEST_CASES) {
+ browser.test.log(`Testing ${method} with ${JSON.stringify(options)}`);
+ if (method == "removePluginData") {
+ await browser.browsingData.removePluginData(options);
+ } else {
+ await browser.browsingData.remove(options, { pluginData: true });
+ }
+ }
+ }
+
+ browser.test.sendMessage("done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["tabs", "browsingData"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+
+ // This test has no assertions because it's only meant to check that we don't
+ // throw when calling removePluginData and remove with pluginData: true.
+ ok(true, "dummy check");
+
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test browsingData.remove indexedDB</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+});
+
+add_task(async function testServiceWorkers() {
+ async function background() {
+ const PAGE =
+ "/tests/toolkit/components/extensions/test/mochitest/file_serviceWorker.html";
+
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.sendMessage("serviceWorkerRegistered");
+ });
+
+ let tabs = [];
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "cleanup") {
+ await browser.tabs.remove(tabs.map(tab => tab.id));
+ browser.test.sendMessage("done");
+ return;
+ }
+
+ await browser.browsingData.remove(
+ { hostnames: msg.hostnames },
+ { serviceWorkers: true }
+ );
+ browser.test.sendMessage("serviceWorkersRemoved");
+ });
+
+ // Create two serviceWorkers.
+ let tab = await browser.tabs.create({ url: `http://mochi.test:8888${PAGE}` });
+ tabs.push(tab);
+
+ tab = await browser.tabs.create({ url: `https://example.com${PAGE}` });
+ tabs.push(tab);
+ }
+
+ function contentScript() {
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener(
+ "message",
+ msg => {
+ if (msg.data == "serviceWorkerRegistered") {
+ browser.runtime.sendMessage("serviceWorkerRegistered");
+ }
+ },
+ true
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData", "tabs"],
+ content_scripts: [
+ {
+ matches: [
+ "http://mochi.test/*/file_serviceWorker.html",
+ "https://example.com/*/file_serviceWorker.html",
+ ],
+ js: ["script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+ files: {
+ "script.js": contentScript,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("serviceWorkerRegistered");
+ await extension.awaitMessage("serviceWorkerRegistered");
+
+ // Even though we await the registrations by waiting for the messages,
+ // sometimes the serviceWorkers are still not registered at this point.
+ async function getRegistrations(count) {
+ await TestUtils.waitForCondition(
+ async () => (await SpecialPowers.registeredServiceWorkers()).length === count,
+ `Wait for ${count} service workers to be registered`
+ );
+ return SpecialPowers.registeredServiceWorkers();
+ }
+
+ let serviceWorkers = await getRegistrations(2);
+ is(serviceWorkers.length, 2, "ServiceWorkers have been registered.");
+
+ extension.sendMessage({ hostnames: ["example.com"] });
+ await extension.awaitMessage("serviceWorkersRemoved");
+
+ serviceWorkers = await getRegistrations(1);
+ is(
+ serviceWorkers.length,
+ 1,
+ "ServiceWorkers for example.com have been removed."
+ );
+
+ let { scriptSpec } = serviceWorkers[0];
+ dump(`Service worker spec: ${scriptSpec}`);
+ ok(scriptSpec.startsWith("http://mochi.test:8888/"),
+ "ServiceWorkers for example.com have been removed.");
+
+ extension.sendMessage({});
+ await extension.awaitMessage("serviceWorkersRemoved");
+
+ serviceWorkers = await getRegistrations(0);
+ is(serviceWorkers.length, 0, "All ServiceWorkers have been removed.");
+
+ extension.sendMessage("cleanup");
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+</script>
+</body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test browsingData.settings</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const SETTINGS_LIST = [
+ "cache",
+ "cookies",
+ "history",
+ "formData",
+ "downloads",
+].sort();
+
+add_task(async function testSettings() {
+ async function background() {
+ browser.browsingData.settings().then(settings => {
+ browser.test.sendMessage("settings", settings);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ await extension.startup();
+ let settings = await extension.awaitMessage("settings");
+
+ // Verify that we get the keys back we expect.
+ isDeeply(
+ Object.entries(settings.dataToRemove)
+ .filter(([key, value]) => value)
+ .map(([key, value]) => key)
+ .sort(),
+ SETTINGS_LIST,
+ "dataToRemove contains expected properties."
+ );
+ isDeeply(
+ Object.entries(settings.dataRemovalPermitted)
+ .filter(([key, value]) => value)
+ .map(([key, value]) => key)
+ .sort(),
+ SETTINGS_LIST,
+ "dataToRemove contains expected properties."
+ );
+ is("since" in settings.options, true, "options contains |since|");
+
+ await extension.unload();
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_cookies.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting", true]],
+ });
+});
+
+add_task(async function test_contentscript() {
+ function contentScript() {
+ let canvas = document.createElement("canvas");
+ canvas.width = canvas.height = "100";
+
+ let ctx = canvas.getContext("2d");
+ ctx.fillStyle = "green";
+ ctx.fillRect(0, 0, 100, 100);
+ let data = ctx.getImageData(0, 0, 100, 100);
+
+ browser.test.sendMessage("data-color", data.data[1]);
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ },
+ ],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+ };
+ const url = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_sample.html";
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+ let win = window.open(url);
+ let color = await extension.awaitMessage("data-color");
+ is(color, 128, "Got correct pixel data for green");
+ win.close();
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Clipboard permissions tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+/* globals doCopy, doPaste */
+function shared() {
+ let field = document.createElement("textarea");
+ document.body.appendChild(field);
+ field.contentEditable = true;
+
+ this.doCopy = function(txt) {
+ field.value = txt;
+ field.select();
+ return document.execCommand("copy");
+ };
+
+ this.doPaste = function() {
+ field.select();
+ return document.execCommand("paste") && field.value;
+ };
+}
+
+add_task(async function test_background_clipboard_permissions() {
+ function backgroundScript() {
+ browser.test.assertEq(false, doCopy("whatever"),
+ "copy should be denied without permission");
+ browser.test.assertEq(false, doPaste(),
+ "paste should be denied without permission");
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ background: [shared, backgroundScript],
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ await extension.awaitMessage("ready");
+
+ await extension.unload();
+});
+
+add_task(async function test_background_clipboard_copy() {
+ function backgroundScript() {
+ browser.test.onMessage.addListener(txt => {
+ browser.test.assertEq(true, doCopy(txt),
+ "copy should be allowed with permission");
+ });
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ background: `(${shared})();(${backgroundScript})();`,
+ manifest: {
+ permissions: [
+ "clipboardWrite",
+ ],
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ const DUMMY_STR = "dummy string to copy";
+ await new Promise(resolve => {
+ SimpleTest.waitForClipboard(DUMMY_STR, () => {
+ extension.sendMessage(DUMMY_STR);
+ }, resolve, resolve);
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_contentscript_clipboard_permissions() {
+ function contentScript() {
+ browser.test.assertEq(false, doCopy("whatever"),
+ "copy should be denied without permission");
+ browser.test.assertEq(false, doPaste(),
+ "paste should be denied without permission");
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+ await extension.awaitMessage("ready");
+ win.close();
+
+ await extension.unload();
+});
+
+add_task(async function test_contentscript_clipboard_copy() {
+ function contentScript() {
+ browser.test.onMessage.addListener(txt => {
+ browser.test.assertEq(true, doCopy(txt),
+ "copy should be allowed with permission");
+ });
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["shared.js", "contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardWrite",
+ ],
+ },
+ files: {
+ "shared.js": shared,
+ "contentscript.js": contentScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+ await extension.awaitMessage("ready");
+
+ const DUMMY_STR = "dummy string to copy in content script";
+ await new Promise(resolve => {
+ SimpleTest.waitForClipboard(DUMMY_STR, () => {
+ extension.sendMessage(DUMMY_STR);
+ }, resolve, resolve);
+ });
+
+ win.close();
+
+ await extension.unload();
+});
+
+add_task(async function test_contentscript_clipboard_paste() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "clipboardRead",
+ ],
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html"],
+ js: ["shared.js", "content_script.js"],
+ }],
+ },
+ files: {
+ "shared.js": shared,
+ "content_script.js": () => {
+ browser.test.sendMessage("paste", doPaste());
+ },
+ },
+ });
+
+ const STRANGE = "A Strange Thing";
+ SpecialPowers.clipboardCopyString(STRANGE);
+
+ await extension.startup();
+ const win = window.open("file_sample.html");
+
+ const paste = await extension.awaitMessage("paste");
+ is(paste, STRANGE, "the correct string was pasted");
+
+ win.close();
+ await extension.unload();
+});
+
+add_task(async function test_background_clipboard_paste() {
+ function background() {
+ browser.test.sendMessage("paste", doPaste());
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["clipboardRead"],
+ },
+ background: [shared, background],
+ });
+
+ const STRANGE = "Stranger Things";
+ SpecialPowers.clipboardCopyString(STRANGE);
+
+ await extension.startup();
+
+ const paste = await extension.awaitMessage("paste");
+ is(paste, STRANGE, "the correct string was pasted");
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Clipboard permissions tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+/**
+ * This cannot be a xpcshell test, because:
+ * - On Android, copyString of nsIClipboardHelper segfaults because
+ * widget/android/nsClipboard.cpp calls java::Clipboard::SetText, which is
+ * unavailable in xpcshell.
+ * - On Windows, the clipboard is unavailable to xpcshell.
+ */
+
+function resetClipboard() {
+ SpecialPowers.clipboardCopyString(
+ "This is the default value of the clipboard in the test.");
+}
+
+async function checkClipboardHasTestImage(imageType) {
+ async function backgroundScript(imageType) {
+ async function verifyImage(img) {
+ // Checks whether the image is a 1x1 red image.
+ browser.test.assertEq(1, img.naturalWidth, "image width should match");
+ browser.test.assertEq(1, img.naturalHeight, "image height should match");
+
+ let canvas = document.createElement("canvas");
+ canvas.width = 1;
+ canvas.height = 1;
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0); // Draw without scaling.
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+ let expectedColor;
+ if (imageType === "png") {
+ expectedColor = [255, 0, 0];
+ } else if (imageType === "jpeg") {
+ expectedColor = [254, 0, 0];
+ }
+ let {os} = await browser.runtime.getPlatformInfo();
+ if (os === "mac") {
+ // Due to https://bugzil.la/1396587, the pasted image differs from the
+ // original/expected image.
+ // Once that bug is fixed, this whole macOS-only branch can be removed.
+ if (imageType === "png") {
+ expectedColor = [255, 38, 0];
+ } else if (imageType === "jpeg") {
+ expectedColor = [255, 38, 0];
+ }
+ }
+ browser.test.assertEq(expectedColor[0], r, "pixel should be red");
+ browser.test.assertEq(expectedColor[1], g, "pixel should not contain green");
+ browser.test.assertEq(expectedColor[2], b, "pixel should not contain blue");
+ browser.test.assertEq(255, a, "pixel should be opaque");
+ }
+
+ let editable = document.body;
+ editable.contentEditable = true;
+ let file;
+ await new Promise(resolve => {
+ document.addEventListener("paste", function(event) {
+ browser.test.assertEq(1, event.clipboardData.types.length, "expected one type");
+ browser.test.assertEq("Files", event.clipboardData.types[0], "expected type");
+ browser.test.assertEq(1, event.clipboardData.files.length, "expected one file");
+
+ // After returning from the paste event, event.clipboardData is cleaned, so we
+ // have to store the file in a separate variable.
+ file = event.clipboardData.files[0];
+ resolve();
+ }, {once: true});
+
+ document.execCommand("paste"); // requires clipboardWrite permission.
+ });
+
+ // When image data is copied, its first frame is decoded and exported to the
+ // clipboard. The pasted result is always an unanimated PNG file, regardless
+ // of the input.
+ browser.test.assertEq("image/png", file.type, "expected file.type");
+
+ // event.files[0] should be an accurate representation of the input image.
+ {
+ let img = new Image();
+ await new Promise((resolve, reject) => {
+ img.onload = resolve;
+ img.onerror = () => reject(new Error(`Failed to load image ${img.src} of size ${file.size}`));
+ img.src = URL.createObjectURL(file);
+ });
+
+ await verifyImage(img);
+ }
+
+ // This confirms that an image was put on the clipboard.
+ // In contrast, when document.execCommand('copy') + clipboardData.setData
+ // is used, then the 'paste' event will also have the image data (as tested
+ // above), but the contentEditable area will be empty.
+ {
+ let imgs = editable.querySelectorAll("img");
+ browser.test.assertEq(1, imgs.length, "should have pasted one image");
+ await verifyImage(imgs[0]);
+ }
+ browser.test.sendMessage("tested image on clipboard");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})("${imageType}");`,
+ manifest: {
+ permissions: ["clipboardRead"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("tested image on clipboard");
+ await extension.unload();
+}
+
+add_task(async function test_without_clipboard_permission() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.assertEq(undefined, browser.clipboard,
+ "clipboard API requires the clipboardWrite permission.");
+ browser.test.notifyPass();
+ },
+ manifest: {
+ permissions: ["clipboardRead"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function test_copy_png() {
+ if (AppConstants.platform === "android") {
+ return; // Android does not support images on the clipboard.
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ // A 1x1 red PNG image.
+ let b64data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEX/AAD///9BHTQRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg==";
+ let imageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer;
+ await browser.clipboard.setImageData(imageData, "png");
+ browser.test.sendMessage("Called setImageData with PNG");
+ },
+ manifest: {
+ permissions: ["clipboardWrite"],
+ },
+ });
+
+ resetClipboard();
+
+ await extension.startup();
+ await extension.awaitMessage("Called setImageData with PNG");
+ await extension.unload();
+
+ await checkClipboardHasTestImage("png");
+});
+
+add_task(async function test_copy_jpeg() {
+ if (AppConstants.platform === "android") {
+ return; // Android does not support images on the clipboard.
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ // A 1x1 red JPEG image, created using: convert xc:red red.jpg.
+ // JPEG is lossy, and the red pixel value is actually #FE0000 instead of
+ // #FF0000 (also seen using: convert red.jpg text:-).
+ let b64data = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACP/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAVAQEBAAAAAAAAAAAAAAAAAAAHCf/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/ADoDFU3/2Q==";
+ let imageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer;
+ await browser.clipboard.setImageData(imageData, "jpeg");
+ browser.test.sendMessage("Called setImageData with JPEG");
+ },
+ manifest: {
+ permissions: ["clipboardWrite"],
+ },
+ });
+
+ resetClipboard();
+
+ await extension.startup();
+ await extension.awaitMessage("Called setImageData with JPEG");
+ await extension.unload();
+
+ await checkClipboardHasTestImage("jpeg");
+});
+
+add_task(async function test_copy_invalid_image() {
+ if (AppConstants.platform === "android") {
+ // Android does not support images on the clipboard.
+ return;
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ // This is a PNG image.
+ let b64data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEX/AAD///9BHTQRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg==";
+ let pngImageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer;
+ await browser.test.assertRejects(
+ browser.clipboard.setImageData(pngImageData, "jpeg"),
+ "Data is not a valid jpeg image",
+ "Image data that is not valid for the given type should be rejected.");
+ browser.test.sendMessage("finished invalid image");
+ },
+ manifest: {
+ permissions: ["clipboardWrite"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished invalid image");
+ await extension.unload();
+});
+
+add_task(async function test_copy_invalid_image_type() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ // setImageData expects "png" or "jpeg", but we pass "image/png" here.
+ browser.test.assertThrows(
+ () => { browser.clipboard.setImageData(new ArrayBuffer(0), "image/png"); },
+ "Type error for parameter imageType (Invalid enumeration value \"image/png\") for clipboard.setImageData.",
+ "An invalid type for setImageData should be rejected.");
+ browser.test.sendMessage("finished invalid type");
+ },
+ manifest: {
+ permissions: ["clipboardWrite"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished invalid type");
+ await extension.unload();
+});
+
+if (AppConstants.platform === "android") {
+ add_task(async function test_setImageData_unsupported_on_android() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ // Android does not support images on the clipboard,
+ // so it should not try to decode an image but fail immediately.
+ await browser.test.assertRejects(
+ browser.clipboard.setImageData(new ArrayBuffer(0), "png"),
+ "Writing images to the clipboard is not supported on Android",
+ "Should get an error when setImageData is called on Android.");
+ browser.test.sendMessage("finished unsupported setImageData");
+ },
+ manifest: {
+ permissions: ["clipboardWrite"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished unsupported setImageData");
+ await extension.unload();
+ });
+}
+
+</script>
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test content script match_about_blank option</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_contentscript_about_blank() {
+ const manifest = {
+ content_scripts: [
+ {
+ match_about_blank: true,
+ matches: ["http://mochi.test/*/file_with_about_blank.html", "https://example.com/*"],
+ all_frames: true,
+ css: ["all.css"],
+ js: ["all.js"],
+ }, {
+ matches: ["http://mochi.test/*/file_with_about_blank.html"],
+ css: ["mochi_without.css"],
+ js: ["mochi_without.js"],
+ all_frames: true,
+ }, {
+ match_about_blank: true,
+ matches: ["http://mochi.test/*/file_with_about_blank.html"],
+ css: ["mochi_with.css"],
+ js: ["mochi_with.js"],
+ all_frames: true,
+ },
+ ],
+ };
+
+ const files = {
+ "all.js": function() {
+ browser.runtime.sendMessage("all");
+ },
+ "all.css": `
+ body { color: red; }
+ `,
+ "mochi_without.js": function() {
+ browser.runtime.sendMessage("mochi_without");
+ },
+ "mochi_without.css": `
+ body { background: yellow; }
+ `,
+ "mochi_with.js": function() {
+ browser.runtime.sendMessage("mochi_with");
+ },
+ "mochi_with.css": `
+ body { text-align: right; }
+ `,
+ };
+
+ function background() {
+ browser.runtime.onMessage.addListener((script, {url}) => {
+ const kind = url.startsWith("about:") ? url : "top";
+ browser.test.sendMessage("script", [script, kind, url]);
+ browser.test.sendMessage(`${script}:${kind}`);
+ });
+ }
+
+ const PATH = "tests/toolkit/components/extensions/test/mochitest/file_with_about_blank.html";
+ const extension = ExtensionTestUtils.loadExtension({manifest, files, background});
+ await extension.startup();
+
+ let count = 0;
+ extension.onMessage("script", script => {
+ info(`script ran: ${script}`);
+ count++;
+ });
+
+ let win = window.open("https://example.com/" + PATH);
+ await Promise.all([
+ extension.awaitMessage("all:top"),
+ extension.awaitMessage("all:about:blank"),
+ extension.awaitMessage("all:about:srcdoc"),
+ ]);
+ is(count, 3, "exactly 3 scripts ran");
+ win.close();
+
+ win = window.open("http://mochi.test:8888/" + PATH);
+ await Promise.all([
+ extension.awaitMessage("all:top"),
+ extension.awaitMessage("all:about:blank"),
+ extension.awaitMessage("all:about:srcdoc"),
+ extension.awaitMessage("mochi_without:top"),
+ extension.awaitMessage("mochi_with:top"),
+ extension.awaitMessage("mochi_with:about:blank"),
+ extension.awaitMessage("mochi_with:about:srcdoc"),
+ ]);
+
+ let style = win.getComputedStyle(win.document.body);
+ is(style.color, "rgb(255, 0, 0)", "top window text color is red");
+ is(style.backgroundColor, "rgb(255, 255, 0)", "top window background is yellow");
+ is(style.textAlign, "right", "top window text is right-aligned");
+
+ let a_b = win.document.getElementById("a_b");
+ style = a_b.contentWindow.getComputedStyle(a_b.contentDocument.body);
+ is(style.color, "rgb(255, 0, 0)", "about:blank iframe text color is red");
+ is(style.backgroundColor, "rgba(0, 0, 0, 0)", "about:blank iframe background is transparent");
+ is(style.textAlign, "right", "about:blank text is right-aligned");
+
+ is(count, 10, "exactly 7 more scripts ran");
+ win.close();
+
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+// Create a test extension with the provided function as the background
+// script. The background script will have a few helpful functions
+// available.
+/* global awaitLoad, gatherFrameSources */
+function makeExtension({
+ background,
+ useScriptingAPI = false,
+ manifest_version = 2,
+ host_permissions,
+}) {
+ // Wait for a webNavigation.onCompleted event where the details for the
+ // loaded page match the attributes of `filter`.
+ function awaitLoad(filter) {
+ return new Promise(resolve => {
+ const listener = details => {
+ if (Object.keys(filter).every(key => details[key] === filter[key])) {
+ browser.webNavigation.onCompleted.removeListener(listener);
+ resolve();
+ }
+ };
+ browser.webNavigation.onCompleted.addListener(listener);
+ });
+ }
+
+ // Return a string with a (sorted) list of the source of all frames
+ // in the given tab into which this extension can inject scripts
+ // (ie all frames for which it has the activeTab permission).
+ // Source is the hostname for frames in http sources, or the full
+ // location href in other documents (eg about: pages)
+ const gatherFrameSources = useScriptingAPI ?
+ async function gatherFrameSources(tabid) {
+ let results = await browser.scripting.executeScript({
+ target: { tabId: tabid, allFrames: true },
+ func: () => window.location.hostname || window.location.href,
+ });
+ // Adjust `result` so that it looks like the one returned by
+ // `tabs.executeScript()`.
+ let result = results.map(res => res.result);
+
+ return String(result.sort());
+ } : async function gatherFrameSources(tabid) {
+ let result = await browser.tabs.executeScript(tabid, {
+ allFrames: true,
+ matchAboutBlank: true,
+ code: "window.location.hostname || window.location.href;",
+ });
+
+ return String(result.sort());
+ };
+
+ const permissions = ["webNavigation"];
+ if (useScriptingAPI) {
+ permissions.push("scripting");
+ }
+
+ // When host_permissions is passed, test "automatic activeTab" for ungranted
+ // host_permissions in mv3, else test with the normal activeTab permission.
+ if (!host_permissions) {
+ permissions.push("activeTab");
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ permissions,
+ host_permissions,
+ },
+ background: [
+ `const useScriptingAPI = ${useScriptingAPI};`,
+ `const manifest_version = ${manifest_version};`,
+ `${awaitLoad}`,
+ `${gatherFrameSources}`,
+ `${ExtensionTestCommon.serializeScript(background)}`,
+ ].join("\n")
+ });
+}
+
+// Helper function to verify that executeScript() fails without the activeTab
+// permission (or any specific origin permissions).
+const verifyNoActiveTab = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
+ let extension = makeExtension({
+ async background() {
+ const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html";
+
+ let [tab] = await Promise.all([
+ browser.tabs.create({url: URL}),
+ awaitLoad({frameId: 0}),
+ ]);
+
+ await browser.test.assertRejects(
+ gatherFrameSources(tab.id),
+ /^Missing host permission/,
+ "executeScript should fail without activeTab permission"
+ );
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("no-active-tab");
+ },
+ useScriptingAPI,
+ manifest_version,
+ host_permissions,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("no-active-tab");
+ await extension.unload();
+};
+
+add_task(async function test_no_activeTab_tabs() {
+ await verifyNoActiveTab({ useScriptingAPI: false });
+});
+
+add_task(async function test_no_activeTab_scripting() {
+ await verifyNoActiveTab({ useScriptingAPI: true });
+});
+
+add_task(async function test_no_activeTab_scripting_mv3() {
+ await verifyNoActiveTab({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: null,
+ });
+});
+
+add_task(async function test_no_activeTab_scripting_mv3_autoActiveTab() {
+ await verifyNoActiveTab({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: ["http://mochi.test/"],
+ });
+});
+
+// Test helper to verify that dynamically created iframes do not get the
+// activeTab permission.
+const verifyDynamicFrames = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
+ let extension = makeExtension({
+ async background() {
+ const BASE_HOST = "www.example.com";
+
+ let [tab] = await Promise.all([
+ browser.tabs.create({url: `https://${BASE_HOST}/`}),
+ awaitLoad({frameId: 0}),
+ ]);
+
+ function inject() {
+ let nframes = 4;
+ function frameLoaded() {
+ nframes--;
+ if (nframes == 0) {
+ browser.runtime.sendMessage("frames-loaded");
+ }
+ }
+
+ let frame = document.createElement("iframe");
+ frame.addEventListener("load", frameLoaded, {once: true});
+ document.body.appendChild(frame);
+
+ let div = document.createElement("div");
+ div.innerHTML = "<iframe src='https://test1.example.com/'></iframe>";
+ let framelist = div.getElementsByTagName("iframe");
+ browser.test.assertEq(1, framelist.length, "Found 1 frame inside div");
+ framelist[0].addEventListener("load", frameLoaded, {once: true});
+ document.body.appendChild(div);
+
+ let div2 = document.createElement("div");
+ div2.innerHTML = "<iframe srcdoc=\"<iframe src='https://test2.example.com/'&gt;</iframe&gt;\"></iframe>";
+ framelist = div2.getElementsByTagName("iframe");
+ browser.test.assertEq(1, framelist.length, "Found 1 frame inside div");
+ framelist[0].addEventListener("load", frameLoaded, {once: true});
+ document.body.appendChild(div2);
+
+ const URL = "https://www.example.com/tests/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html";
+
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", URL);
+ xhr.responseType = "document";
+ xhr.overrideMimeType("text/html");
+
+ xhr.addEventListener("load", () => {
+ if (xhr.readyState != 4) {
+ return;
+ }
+ if (xhr.status != 200) {
+ browser.runtime.sendMessage("error");
+ }
+
+ let frame = xhr.response.getElementById("frame");
+ browser.test.assertTrue(frame, "Found frame in response document");
+ frame.addEventListener("load", frameLoaded, {once: true});
+ document.body.appendChild(frame);
+ }, {once: true});
+ xhr.addEventListener("error", () => {
+ browser.runtime.sendMessage("error");
+ }, {once: true});
+ xhr.send();
+ }
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "go") {
+ browser.test.fail(`unexpected message received: ${msg}`);
+ return;
+ }
+
+ let loadedPromise = new Promise((resolve, reject) => {
+ let listener = msg => {
+ let unlisten = () => browser.runtime.onMessage.removeListener(listener);
+ if (msg == "frames-loaded") {
+ unlisten();
+ resolve();
+ } else if (msg == "error") {
+ unlisten();
+ reject();
+ }
+ };
+ browser.runtime.onMessage.addListener(listener);
+ });
+
+ if (useScriptingAPI) {
+ await browser.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: inject,
+ });
+ } else {
+ await browser.tabs.executeScript(tab.id, {
+ code: `(${inject})();`,
+ });
+ }
+
+ await loadedPromise;
+
+ let result = await gatherFrameSources(tab.id);
+
+ if (manifest_version < 3) {
+ browser.test.assertEq(
+ String([BASE_HOST]),
+ result,
+ "Script is not injected into dynamically created frames"
+ );
+ } else {
+ browser.test.assertEq(
+ String(["about:blank", "about:srcdoc", BASE_HOST]),
+ result,
+ `Script injected only into (same origin) about:blank-ish dynamically created frames`
+ );
+ }
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("dynamic-frames");
+ });
+
+ browser.test.sendMessage("ready", tab.id);
+ },
+ useScriptingAPI,
+ manifest_version,
+ host_permissions,
+ });
+
+ await extension.startup();
+
+ let tabId = await extension.awaitMessage("ready");
+ extension.grantActiveTab(tabId);
+
+ extension.sendMessage("go");
+ await extension.awaitFinish("dynamic-frames");
+
+ await extension.unload();
+};
+
+add_task(async function test_dynamic_frames_tabs() {
+ await verifyDynamicFrames({ useScriptingAPI: false });
+});
+
+add_task(async function test_dynamic_frames_scripting() {
+ await verifyDynamicFrames({ useScriptingAPI: true });
+});
+
+add_task(async function test_dynamic_frames_scripting_mv3() {
+ await verifyDynamicFrames({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: null,
+ });
+});
+
+add_task(async function test_dynamic_frames_scripting_mv3_autoActiveTab() {
+ await verifyDynamicFrames({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: ["https://www.example.com/"],
+ });
+});
+
+// Test helper to verify that an iframe created from an <iframe srcdoc> gets
+// the activeTab permission.
+const verifySrcdoc = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
+ let extension = makeExtension({
+ async background() {
+ const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html";
+ const OUTER_SOURCE = "about:srcdoc";
+ const PAGE_SOURCE = "mochi.test";
+ const FRAME_SOURCE = "test1.example.com";
+
+ let [tab] = await Promise.all([
+ browser.tabs.create({url: URL}),
+ awaitLoad({frameId: 0}),
+ ]);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "go") {
+ browser.test.fail(`unexpected message received: ${msg}`);
+ return;
+ }
+
+ let result = await gatherFrameSources(tab.id);
+
+ if (manifest_version < 3) {
+ browser.test.assertEq(
+ String([OUTER_SOURCE, PAGE_SOURCE, FRAME_SOURCE]),
+ result,
+ "Script is injected into frame created from <iframe srcdoc>"
+ );
+ } else {
+ browser.test.assertEq(
+ String([OUTER_SOURCE, PAGE_SOURCE]),
+ result,
+ "Script is not injected into cross-origin frame created from <iframe srcdoc>"
+ );
+ }
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("srcdoc");
+ });
+
+ browser.test.sendMessage("ready", tab.id);
+ },
+ useScriptingAPI,
+ manifest_version,
+ host_permissions,
+ });
+
+ await extension.startup();
+
+ let tabId = await extension.awaitMessage("ready");
+ extension.grantActiveTab(tabId);
+
+ extension.sendMessage("go");
+ await extension.awaitFinish("srcdoc");
+
+ await extension.unload();
+};
+
+add_task(async function test_srcdoc_tabs() {
+ await verifySrcdoc({ useScriptingAPI: false });
+});
+
+add_task(async function test_srcdoc_scripting() {
+ await verifySrcdoc({ useScriptingAPI: true });
+});
+
+add_task(async function test_srcdoc_scripting_mv3() {
+ await verifySrcdoc({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: null,
+ });
+});
+
+add_task(async function test_srcdoc_scripting_mv3_autoActiveTab() {
+ await verifySrcdoc({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: ["http://mochi.test/"],
+ });
+});
+
+// Test helper to verify that navigating frames by setting the src attribute
+// from the parent page revokes the activeTab permission.
+const verifyNavigateBySrc = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
+ let extension = makeExtension({
+ async background() {
+ const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html";
+ const PAGE_SOURCE = "mochi.test";
+ const EMPTY_SOURCE = "about:blank";
+ const FRAME_SOURCE = "test1.example.com";
+
+ let [tab] = await Promise.all([
+ browser.tabs.create({url: URL}),
+ awaitLoad({frameId: 0}),
+ ]);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "go") {
+ browser.test.fail(`unexpected message received: ${msg}`);
+ return;
+ }
+
+ let result = await gatherFrameSources(tab.id);
+ if (manifest_version < 3) {
+ browser.test.assertEq(
+ String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]),
+ result,
+ "In original page, script is injected into base page and original frames"
+ );
+ } else {
+ browser.test.assertEq(
+ String([EMPTY_SOURCE, PAGE_SOURCE]),
+ result,
+ "In original page, script is injected into same-origin frames"
+ );
+ }
+
+ let loadedPromise = awaitLoad({tabId: tab.id});
+
+ let func = () => {
+ document.getElementById('emptyframe').src = 'http://test2.example.com/';
+ };
+
+ if (useScriptingAPI) {
+ await browser.scripting.executeScript({
+ target: { tabId: tab.id },
+ func,
+ });
+ } else {
+ await browser.tabs.executeScript(tab.id, { code: `(${func})();` });
+ }
+
+ await loadedPromise;
+
+
+ result = await gatherFrameSources(tab.id);
+ if (manifest_version < 3) {
+ browser.test.assertEq(
+ String([PAGE_SOURCE, FRAME_SOURCE]),
+ result,
+ "Script is not injected into initially empty frame after navigation"
+ );
+ } else {
+ browser.test.assertEq(
+ String([PAGE_SOURCE]),
+ result,
+ "Script is not injected into initially empty frame after navigation"
+ );
+ }
+
+ loadedPromise = awaitLoad({tabId: tab.id});
+
+ func = () => {
+ document.getElementById('regularframe').src = 'http://mochi.test:8888/';
+ };
+
+ if (useScriptingAPI) {
+ await browser.scripting.executeScript({
+ target: { tabId: tab.id },
+ func,
+ });
+ } else {
+ await browser.tabs.executeScript(tab.id, { code: `(${func})();` });
+ }
+
+ await loadedPromise;
+
+ result = await gatherFrameSources(tab.id);
+
+ if (manifest_version < 3) {
+ browser.test.assertEq(
+ String([PAGE_SOURCE]),
+ result,
+ "Script is not injected into regular frame after navigation"
+ );
+ } else {
+ browser.test.assertEq(
+ String([PAGE_SOURCE, PAGE_SOURCE]),
+ result,
+ "Script injected into frame after navigating to same-origin"
+ );
+ }
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("test-scripts");
+ });
+
+ browser.test.sendMessage("ready", tab.id);
+ },
+ useScriptingAPI,
+ manifest_version,
+ host_permissions,
+ });
+
+ await extension.startup();
+
+ let tabId = await extension.awaitMessage("ready");
+ extension.grantActiveTab(tabId);
+
+ extension.sendMessage("go");
+ await extension.awaitFinish("test-scripts");
+
+ await extension.unload();
+};
+
+add_task(async function test_navigate_by_src_tabs() {
+ await verifyNavigateBySrc({ useScriptingAPI: false });
+});
+
+add_task(async function test_navigate_by_src_scripting() {
+ await verifyNavigateBySrc({ useScriptingAPI: true });
+});
+
+add_task(async function test_navigate_by_src_scripting_mv3() {
+ await verifyNavigateBySrc({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: null,
+ });
+});
+
+add_task(async function test_navigate_by_src_scripting_mv3_autoActiveTab() {
+ await verifyNavigateBySrc({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: ["http://mochi.test/"],
+ });
+});
+
+// Test helper to verify that navigating frames by setting window.location from
+// inside the frame revokes the activeTab permission.
+const verifyNavigateByWindowLocation = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
+ let extension = makeExtension({
+ async background() {
+ const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html";
+ const PAGE_SOURCE = "mochi.test";
+ const EMPTY_SOURCE = "about:blank";
+ const FRAME_SOURCE = "test1.example.com";
+
+ let [tab] = await Promise.all([
+ browser.tabs.create({url: URL}),
+ awaitLoad({frameId: 0}),
+ ]);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "go") {
+ browser.test.fail(`unexpected message received: ${msg}`);
+ return;
+ }
+
+ let result = await gatherFrameSources(tab.id);
+
+ if (manifest_version < 3) {
+ browser.test.assertEq(
+ String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]),
+ result,
+ "Script initially injected into all frames"
+ );
+ } else {
+ browser.test.assertEq(
+ String([EMPTY_SOURCE, PAGE_SOURCE]),
+ result,
+ "Script initially injected into all same-origin frames"
+ );
+ }
+
+ let nframes = 0;
+ let frames = await browser.webNavigation.getAllFrames({tabId: tab.id});
+ for (let frame of frames) {
+ if (frame.parentFrameId == -1) {
+ continue;
+ }
+
+ if (manifest_version >= 3 && frame.url.includes(FRAME_SOURCE)) {
+ // In MV3, can't access cross-origin iframes from the start.
+
+ let invalidPromise = browser.scripting.executeScript({
+ target: { tabId: tab.id, frameIds: [frame.frameId] },
+ func: () => window.location.hostname,
+ });
+ await browser.test.assertRejects(
+ invalidPromise,
+ /^Missing host permission for the tab or frames/,
+ "executeScript should fail on cross-origin frame"
+ );
+
+ continue;
+ }
+
+ let loadPromise = awaitLoad({
+ tabId: tab.id,
+ frameId: frame.frameId,
+ });
+
+ let func = () => {
+ window.location.href = 'https://test2.example.com/';
+ };
+
+ if (useScriptingAPI) {
+ await browser.scripting.executeScript({
+ target: { tabId: tab.id, frameIds: [frame.frameId] },
+ func,
+ });
+ } else {
+ await browser.tabs.executeScript(tab.id, {
+ frameId: frame.frameId,
+ matchAboutBlank: true,
+ code: `(${func})();`,
+ });
+ }
+
+ await loadPromise;
+
+ let executePromise;
+ func = () => window.location.hostname;
+
+ if (useScriptingAPI) {
+ executePromise = browser.scripting.executeScript({
+ target: { tabId: tab.id, frameIds: [frame.frameId] },
+ func,
+ });
+ } else {
+ executePromise = browser.tabs.executeScript(tab.id, {
+ frameId: frame.frameId,
+ matchAboutBlank: true,
+ code: `(${func})();`,
+ });
+ }
+
+ await browser.test.assertRejects(
+ executePromise,
+ /^Missing host permission for the tab or frames/,
+ "executeScript should have failed on navigated frame"
+ );
+
+ nframes++;
+ }
+
+ if (manifest_version < 3) {
+ browser.test.assertEq(2, nframes, "Found 2 frames");
+ } else {
+ browser.test.assertEq(1, nframes, "Found 1 frame");
+ }
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("scripted-navigation");
+ });
+
+ browser.test.sendMessage("ready", tab.id);
+ },
+ useScriptingAPI,
+ manifest_version,
+ host_permissions,
+ });
+
+ await extension.startup();
+
+ let tabId = await extension.awaitMessage("ready");
+ extension.grantActiveTab(tabId);
+
+ extension.sendMessage("go");
+ await extension.awaitFinish("scripted-navigation");
+
+ await extension.unload();
+};
+
+add_task(async function test_navigate_by_window_location_tabs() {
+ await verifyNavigateByWindowLocation({ useScriptingAPI: false });
+});
+
+add_task(async function test_navigate_by_window_location_scripting() {
+ await verifyNavigateByWindowLocation({ useScriptingAPI: true });
+});
+
+add_task(async function test_navigate_by_window_location_scripting_mv3() {
+ await verifyNavigateByWindowLocation({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: null,
+ });
+});
+
+add_task(async function test_navigate_by_window_location_scripting_mv3_autoActiveTab() {
+ await verifyNavigateByWindowLocation({
+ useScriptingAPI: true,
+ manifest_version: 3,
+ host_permissions: ["http://mochi.test/"],
+ });
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script caching</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// This file defines content scripts.
+
+const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+
+add_task(async function test_contentscript_cache() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+
+ permissions: ["<all_urls>", "tabs"],
+ },
+
+ async background() {
+ // Force our extension instance to be initialized for the current content process.
+ await browser.tabs.insertCSS({code: ""});
+
+ browser.test.sendMessage("origin", location.origin);
+ },
+
+ files: {
+ "content_script.js": function() {
+ browser.test.sendMessage("content-script-loaded");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let origin = await extension.awaitMessage("origin");
+ let scriptUrl = `${origin}/content_script.js`;
+
+ const { ExtensionProcessScript } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/ExtensionProcessScript.jsm"
+ );
+ let ext = ExtensionProcessScript.getExtensionChild(extension.id);
+
+ ext.staticScripts.expiryTimeout = 3000;
+ is(ext.staticScripts.size, 0, "Should have no cached scripts");
+
+ let win = window.open(`${BASE}/file_sample.html`);
+ await extension.awaitMessage("content-script-loaded");
+
+ if (AppConstants.platform !== "android") {
+ is(ext.staticScripts.size, 1, "Should have one cached script");
+ ok(ext.staticScripts.has(scriptUrl), "Script cache should contain script URL");
+ }
+
+ let chromeScript, chromeScriptDone;
+ let { appinfo } = SpecialPowers.Services;
+ if (appinfo.processType === appinfo.PROCESS_TYPE_CONTENT) {
+ /* globals addMessageListener, assert */
+ chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("check-script-cache", extensionId => {
+ const { ExtensionProcessScript } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionProcessScript.jsm"
+ );
+ let ext = ExtensionProcessScript.getExtensionChild(extensionId);
+
+ if (ext && ext.staticScripts) {
+ assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process");
+ }
+
+ sendAsyncMessage("done");
+ });
+ });
+ chromeScript.sendAsyncMessage("check-script-cache", extension.id);
+ chromeScriptDone = chromeScript.promiseOneMessage("done");
+ }
+
+ SimpleTest.requestFlakyTimeout("Required to test expiry timeout");
+ await new Promise(resolve => setTimeout(resolve, 3000));
+ is(ext.staticScripts.size, 0, "Should have no cached scripts");
+
+ if (chromeScript) {
+ await chromeScriptDone;
+ chromeScript.destroy();
+ }
+
+ win.close();
+
+ win = window.open(`${BASE}/file_sample.html`);
+ await extension.awaitMessage("content-script-loaded");
+
+ is(ext.staticScripts.size, 1, "Should have one cached script");
+ ok(ext.staticScripts.has(scriptUrl));
+
+ SpecialPowers.Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
+
+ is(ext.staticScripts.size, 0, "Should have no cached scripts after heap-minimize");
+
+ win.close();
+
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test content script access to canvas drawWindow()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+"use strict";
+
+add_task(async function test_drawWindow() {
+ const permissions = [
+ "<all_urls>",
+ ];
+
+ const content_scripts = [{
+ matches: ["https://example.org/*"],
+ js: ["content_script.js"],
+ }];
+
+ const files = {
+ "content_script.js": () => {
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ try {
+ ctx.drawWindow(window, 0, 0, 10, 10, "red");
+ const {data} = ctx.getImageData(0, 0, 10, 10);
+ browser.test.sendMessage("success", data.slice(0, 3).join());
+ } catch (e) {
+ browser.test.sendMessage("error", e.message);
+ }
+ },
+ };
+
+ const first = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions,
+ content_scripts
+ },
+ files
+ });
+ const second = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts
+ },
+ files
+ });
+
+ consoleMonitor.start([{ message: /Use of drawWindow [\w\s]+ is deprecated. Use tabs.captureTab/ }]);
+
+ await first.startup();
+ await second.startup();
+
+ const win = window.open("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html");
+
+ const colour = await first.awaitMessage("success");
+ is(colour, "255,255,153", "drawWindow() call was successful: #ff9 == rgb(255,255,153)");
+
+ const error = await second.awaitMessage("error");
+ is(error, "ctx.drawWindow is not a function", "drawWindow() method not awailable without permission");
+
+ win.close();
+ await first.unload();
+ await second.unload();
+ await consoleMonitor.finished();
+});
+
+add_task(async function test_tainted_canvas() {
+ const permissions = [
+ "<all_urls>",
+ ];
+
+ const content_scripts = [{
+ matches: ["https://example.org/*"],
+ js: ["content_script.js"],
+ }];
+
+ const files = {
+ "content_script.js": () => {
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ const img = new Image();
+
+ img.onload = function() {
+ ctx.drawImage(img, 0, 0);
+ try {
+ const png = canvas.toDataURL();
+ const {data} = ctx.getImageData(0, 0, 10, 10);
+ browser.test.sendMessage("success", {png, colour: data.slice(0, 4).join()});
+ } catch (e) {
+ browser.test.log(`Exception: ${e.message}`);
+ browser.test.sendMessage("error", e.message);
+ }
+ };
+
+ // Cross-origin image from example.com.
+ img.src = "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_good.png";
+ },
+ };
+
+ const first = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions,
+ content_scripts
+ },
+ files
+ });
+ const second = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts
+ },
+ files
+ });
+
+ await first.startup();
+ await second.startup();
+
+ const win = window.open("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html");
+
+ const {png, colour} = await first.awaitMessage("success");
+ ok(png.startsWith("data:image/png;base64,"), "toDataURL() call was successful.");
+ is(colour, "0,0,0,0", "getImageData() returned the correct colour (transparent).");
+
+ const error = await second.awaitMessage("error");
+ is(error, "The operation is insecure.", "toDataURL() throws without permission.");
+
+ win.close();
+ await first.unload();
+ await second.unload();
+});
+
+</script>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Sandbox metadata on WebExtensions ContentScripts</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_contentscript_devtools_sandbox_metadata() {
+ function contentScript() {
+ browser.runtime.sendMessage("contentScript.executed");
+ }
+
+ function background() {
+ browser.runtime.onMessage.addListener((msg) => {
+ if (msg == "contentScript.executed") {
+ browser.test.notifyPass("contentScript.executed");
+ }
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ },
+
+ background,
+ files: {
+ "content_script.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ let innerWindowID = SpecialPowers.wrap(win).windowGlobalChild.innerWindowId;
+
+ await extension.awaitFinish("contentScript.executed");
+
+ const { ExtensionContent } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/ExtensionContent.jsm"
+ );
+
+ let res = ExtensionContent.getContentScriptGlobals(win);
+ is(res.length, 1, "Got the expected array of globals");
+ let metadata = SpecialPowers.Cu.getSandboxMetadata(res[0]) || {};
+
+ is(metadata.addonId, extension.id, "Got the expected addonId");
+ is(metadata["inner-window-id"], innerWindowID, "Got the expected inner-window-id");
+
+ await extension.unload();
+ info("extension unloaded");
+
+ res = ExtensionContent.getContentScriptGlobals(win);
+ is(res.length, 0, "No content scripts globals found once the extension is unloaded");
+
+ win.close();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!doctype html>
+<head>
+ <title>Test content script in cross-origin frame</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+"use strict";
+
+add_task(async function test_content_script_cross_origin_frame() {
+
+ const extensionData = {
+ manifest: {
+ content_scripts: [{
+ matches: ["https://example.net/*/file_sample.html"],
+ all_frames: true,
+ js: ["cs.js"],
+ }],
+ permissions: ["https://example.net/"],
+ },
+
+ background() {
+ browser.runtime.onConnect.addListener(port => {
+ port.onMessage.addListener(async num => {
+ let { tab, url, frameId } = port.sender;
+
+ browser.test.assertTrue(frameId > 0, "sender frameId is ok");
+ browser.test.assertTrue(url.endsWith("file_sample.html"), "url is ok");
+
+ let shared = await browser.tabs.executeScript(tab.id, {
+ allFrames: true,
+ code: `window.sharedVal`,
+ });
+ browser.test.assertEq(shared[0], 357, "CS runs in a shared Sandbox");
+
+ let code = "does.not.exist";
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, { allFrames: true, code }),
+ /does is not defined/,
+ "Got the expected rejection from tabs.executeScript"
+ );
+
+ code = "() => {}";
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(tab.id, { allFrames: true, code }),
+ /Script .* result is non-structured-clonable data/,
+ "Got the expected rejection from tabs.executeScript"
+ );
+
+ let result = await browser.tabs.sendMessage(tab.id, num);
+ port.postMessage(result);
+ port.disconnect();
+ });
+ });
+ },
+
+ files: {
+ "cs.js"() {
+ let text = document.getElementById("test").textContent;
+ browser.test.assertEq(text, "Sample text", "CS can access page DOM");
+
+ let manifest = browser.runtime.getManifest();
+ browser.test.assertEq(manifest.version, "1.0");
+ browser.test.assertEq(manifest.name, "Generated extension");
+
+ browser.runtime.onMessage.addListener(async num => {
+ browser.test.log("content script received tabs.sendMessage");
+ return num * 3;
+ })
+
+ let response;
+ window.sharedVal = 357;
+
+ let port = browser.runtime.connect();
+ port.onMessage.addListener(num => {
+ response = num;
+ });
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(response, 21, "Got correct response");
+ browser.test.notifyPass();
+ });
+ port.postMessage(7);
+ },
+ },
+ };
+
+ info("Load first extension");
+ let ext1 = ExtensionTestUtils.loadExtension(extensionData);
+ await ext1.startup();
+
+ info("Load a page, test content scripts in new frame with extension loaded");
+ let base = "https://example.org/tests/toolkit/components/extensions/test";
+ let win = window.open(`${base}/mochitest/file_with_xorigin_frame.html`);
+
+ await ext1.awaitFinish();
+ await ext1.unload();
+
+ info("Load second extension, test content scripts in existing frame");
+ let ext2 = ExtensionTestUtils.loadExtension(extensionData);
+ await ext2.startup();
+ await ext2.awaitFinish();
+
+ win.close();
+ await ext2.unload();
+});
+
+</script>
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 @@
+<!doctype html>
+<head>
+ <title>Test content script runtime.getFrameId</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+"use strict";
+
+add_task(async function test_runtime_getFrameId_invalid() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let proxy = new Proxy(window, {});
+ let proto = Object.create(window);
+
+ class FakeFrame extends HTMLIFrameElement {
+ constructor() {
+ super();
+ console.log("FakeFrame ctor"); // eslint-disable-line
+ }
+ }
+ customElements.define('fake-frame', FakeFrame, { extends: 'iframe' });
+ let custom = document.createElement("fake-frame");
+
+ let invalid = [null, 13, "blah", document.body, proxy, proto, custom];
+
+ for (let value of invalid) {
+ browser.test.assertThrows(
+ () => browser.runtime.getFrameId(value),
+ /Invalid argument/,
+ "Correct exception thrown."
+ );
+ }
+
+ let detached = document.createElement("iframe");
+ let id = browser.runtime.getFrameId(detached);
+ browser.test.assertEq(id, -1, "Detached iframe has no frameId.");
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_contentscript_runtime_getFrameId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation", "tabs"],
+ host_permissions: ["https://example.org/"],
+ },
+
+ files: {
+ "cs.js"() {
+ browser.test.log(`Content script loaded on: ${location.href}`);
+ let parents = {};
+
+ // Recursivelly walk descendant frames and get parent frameIds.
+ function visit(win) {
+ let frameId = browser.runtime.getFrameId(win);
+ let parentId = browser.runtime.getFrameId(win.parent);
+ parents[frameId] = (win.parent != win) ? parentId : -1;
+
+ try {
+ let frameEl = browser.runtime.getFrameId(win.frameElement);
+ browser.test.assertEq(frameId, frameEl, "frameElement id correct");
+ } catch (e) {
+ // Can't access a cross-origin .frameElement.
+ }
+
+ for (let i = 0; i < win.frames.length; i++) {
+ visit(win.frames[i]);
+ }
+ }
+ visit(window);
+
+ // Add the <embed> frame if it exists.
+ let embed = document.querySelector("embed");
+ if (embed) {
+ let id = browser.runtime.getFrameId(embed);
+ parents[id] = 0;
+ }
+
+ // Add the <object> frame if it exists.
+ let object = document.querySelector("object");
+ if (object) {
+ let id = browser.runtime.getFrameId(object);
+ parents[id] = 0;
+ }
+
+ browser.test.log(`Parents tree: ${JSON.stringify(parents)}`);
+ return parents;
+ },
+
+ async "closedPopup.js"() {
+ let popup = window.open("https://example.org/?popup");
+ popup.close();
+ for (let i = 0; i < 100; i++) {
+ await new Promise(r => setTimeout(r, 50));
+ try {
+ popup.blur();
+ } catch(e) {
+ if (e.message === "can't access dead object") {
+ browser.test.assertThrows(
+ () => browser.runtime.getFrameId(popup),
+ /An exception was thrown/,
+ "Passing a dead object throws."
+ );
+ browser.test.sendMessage("done");
+ return;
+ }
+ }
+ }
+ browser.test.fail("Timed out while waiting for popup to close.");
+ },
+ "closedPopup.html": `
+ <!doctype html>
+ <meta charset="utf-8">
+ <script src="closedPopup.js"><\/script>
+ `,
+ },
+
+ async background() {
+ let base = "https://example.org/tests/toolkit/components/extensions/test/mochitest";
+ let files = {
+ "file_contains_iframe.html": 2,
+ "file_WebNavigation_page1.html": 2,
+ "file_with_xorigin_frame.html": 2,
+ // Contains all of the above.
+ "file_with_subframes_and_embed.html": 9,
+ };
+
+ for (let [file, count] of Object.entries(files)) {
+ let tab;
+ let completed = new Promise(resolve => {
+ browser.webNavigation.onCompleted.addListener(function cb(details) {
+ browser.test.log(`onCompleted: ${JSON.stringify(details)}`);
+
+ if (details.tabId === tab?.id && details.frameId === 0) {
+ browser.webNavigation.onCompleted.removeListener(cb);
+ resolve();
+ }
+ });
+ });
+
+ browser.test.log(`Load a test page: ${file}`);
+ tab = await browser.tabs.create({ url: `${base}/${file}` });
+ await completed;
+
+ let [parents] = await browser.tabs.executeScript(tab.id, {
+ file: "cs.js"
+ });
+
+ let all = await browser.webNavigation.getAllFrames({ tabId: tab.id });
+ browser.test.log(`getAllFrames: ${JSON.stringify(all)}`);
+
+ browser.test.assertEq(all.length, count, "All frames accounted for.");
+
+ browser.test.assertEq(
+ Object.keys(parents).length,
+ count,
+ "All frames accounted for from content script."
+ );
+
+ for (let frame of all) {
+ browser.test.assertEq(
+ frame.parentFrameId,
+ parents[frame.frameId],
+ "Correct frame ancestor info."
+ );
+ }
+
+ await browser.tabs.remove(tab.id);
+ }
+
+ browser.tabs.create({ url: browser.runtime.getURL("closedPopup.html" )});
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+</script>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script private browsing ID</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ChromeTask.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+async function test_contentscript_incognito() {
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ },
+ ],
+ },
+
+ background() {
+ let windowId;
+
+ browser.test.onMessage.addListener(([msg, url]) => {
+ if (msg === "open-window") {
+ browser.windows.create({url, incognito: true}).then(window => {
+ windowId = window.id;
+ });
+ } else if (msg === "close-window") {
+ browser.windows.remove(windowId).then(() => {
+ browser.test.sendMessage("done");
+ });
+ }
+ });
+ },
+
+ files: {
+ "content_script.js": async () => {
+ const COOKIE = "foo=florgheralzps";
+ document.cookie = COOKIE;
+
+ let url = new URL("return_headers.sjs", location.href);
+
+ let responses = [
+ new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.onload = () => resolve(JSON.parse(xhr.responseText));
+ xhr.send();
+ }),
+
+ fetch(url, {credentials: "include"}).then(body => body.json()),
+ ];
+
+ try {
+ for (let response of await Promise.all(responses)) {
+ browser.test.assertEq(COOKIE, response.cookie, "Got expected cookie header");
+ }
+ browser.test.notifyPass("cookies");
+ } catch (e) {
+ browser.test.fail(`Error: ${e}`);
+ browser.test.notifyFail("cookies");
+ }
+ },
+ },
+ });
+
+ await extension.startup();
+
+ extension.sendMessage(["open-window", SimpleTest.getTestFileURL("file_sample.html")]);
+
+ await extension.awaitFinish("cookies");
+
+ extension.sendMessage(["close-window"]);
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+}
+
+add_task(async function() {
+ await test_contentscript_incognito();
+});
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({set: [
+ ["network.cookie.cookieBehavior", 3],
+ ]});
+ await test_contentscript_incognito();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_contentscript() {
+ function background() {
+ browser.test.onMessage.addListener(async url => {
+ let tab = await browser.tabs.create({url});
+
+ let executed = true;
+ try {
+ await browser.tabs.executeScript(tab.id, {code: "true;"});
+ } catch (e) {
+ executed = false;
+ }
+
+ await browser.tabs.remove([tab.id]);
+ browser.test.sendMessage("executed", executed);
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["<all_urls>"],
+ },
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ extension.sendMessage("https://example.com");
+ let result = await extension.awaitMessage("executed");
+ is(result, true, "Content script can be run in a page without mozAddonManager");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+
+ extension.sendMessage("https://example.com");
+ result = await extension.awaitMessage("executed");
+ is(result, false, "Content script cannot be run in a page with mozAddonManager");
+
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_cookies() {
+ await SpecialPowers.pushPrefEnv({set: [
+ ["dom.security.https_first_pbm", false],
+ ["dom.security.https_first", false],
+ ]});
+
+ async function background() {
+ function assertExpected(expected, cookie) {
+ for (let key of Object.keys(cookie)) {
+ browser.test.assertTrue(key in expected, `found property ${key}`);
+ browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`);
+ }
+ browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found");
+ }
+
+ async function getDocumentCookie(tabId) {
+ let results = await browser.tabs.executeScript(tabId, {
+ code: "document.cookie",
+ });
+ browser.test.assertEq(1, results.length, "executeScript returns one result");
+ return results[0];
+ }
+
+ async function testIpCookie(ipAddress, setHostOnly) {
+ const IP_TEST_HOST = ipAddress;
+ const IP_TEST_URL = `http://${IP_TEST_HOST}/`;
+ const IP_THE_FUTURE = Date.now() + 5 * 60;
+ const IP_STORE_ID = "firefox-default";
+
+ let expectedCookie = {
+ name: "name1",
+ value: "value1",
+ domain: IP_TEST_HOST,
+ hostOnly: true,
+ path: "/",
+ secure: false,
+ httpOnly: false,
+ sameSite: "no_restriction",
+ session: false,
+ expirationDate: IP_THE_FUTURE,
+ storeId: IP_STORE_ID,
+ firstPartyDomain: "",
+ partitionKey: null,
+ };
+
+ await browser.browsingData.removeCookies({});
+ let ip_cookie = await browser.cookies.set({
+ url: IP_TEST_URL,
+ domain: setHostOnly ? ipAddress : undefined,
+ name: "name1",
+ value: "value1",
+ expirationDate: IP_THE_FUTURE,
+ });
+ assertExpected(expectedCookie, ip_cookie);
+
+ let ip_cookies = await browser.cookies.getAll({name: "name1"});
+ browser.test.assertEq(1, ip_cookies.length, "ip cookie can be added");
+ assertExpected(expectedCookie, ip_cookies[0]);
+
+ ip_cookies = await browser.cookies.getAll({domain: IP_TEST_HOST, name: "name1"});
+ browser.test.assertEq(1, ip_cookies.length, "can get ip cookie by host");
+ assertExpected(expectedCookie, ip_cookies[0]);
+
+ let ip_details = await browser.cookies.remove({url: IP_TEST_URL, name: "name1"});
+ assertExpected({url: IP_TEST_URL, name: "name1", storeId: IP_STORE_ID, firstPartyDomain: "", partitionKey: null}, ip_details);
+
+ ip_cookies = await browser.cookies.getAll({name: "name1"});
+ browser.test.assertEq(0, ip_cookies.length, "ip cookie can be removed");
+ }
+
+ async function openPrivateWindowAndTab(TEST_URL) {
+ // Add some random suffix to make sure that we select the right tab.
+ const PRIVATE_TEST_URL = TEST_URL + "?random" + Math.random();
+
+ let tabReadyPromise = new Promise((resolve) => {
+ browser.webNavigation.onDOMContentLoaded.addListener(function listener({tabId}) {
+ browser.webNavigation.onDOMContentLoaded.removeListener(listener);
+ resolve(tabId);
+ }, {
+ url: [{
+ urlPrefix: PRIVATE_TEST_URL,
+ }],
+ });
+ });
+ // This tab is opened for two purposes:
+ // 1. To allow tests to run content scripts in the context of a tab,
+ // for fetching the value of document.cookie.
+ // 2. TODO Bug 1309637 To work around cookies in incognito windows,
+ // based on the analysis in comment 8.
+ let {id: windowId} = await browser.windows.create({
+ incognito: true,
+ url: PRIVATE_TEST_URL,
+ });
+ let tabId = await tabReadyPromise;
+ return {windowId, tabId};
+ }
+
+ function changePort(href, port) {
+ let url = new URL(href);
+ url.port = port;
+ return url.href;
+ }
+
+ await testIpCookie("[2a03:4000:6:310e:216:3eff:fe53:99b]", false);
+ await testIpCookie("[2a03:4000:6:310e:216:3eff:fe53:99b]", true);
+ await testIpCookie("192.168.1.1", false);
+ await testIpCookie("192.168.1.1", true);
+
+ const TEST_URL = "http://example.org/";
+ const TEST_SECURE_URL = "https://example.org/";
+ const THE_FUTURE = Date.now() + 5 * 60;
+ const TEST_PATH = "set_path";
+ const TEST_URL_WITH_PATH = TEST_URL + TEST_PATH;
+ const TEST_COOKIE_PATH = `/${TEST_PATH}`;
+ const STORE_ID = "firefox-default";
+ const PRIVATE_STORE_ID = "firefox-private";
+
+ let expected = {
+ name: "name1",
+ value: "value1",
+ domain: "example.org",
+ hostOnly: true,
+ path: "/",
+ secure: false,
+ httpOnly: false,
+ sameSite: "no_restriction",
+ session: false,
+ expirationDate: THE_FUTURE,
+ storeId: STORE_ID,
+ firstPartyDomain: "",
+ partitionKey: null,
+ };
+
+ // Remove all cookies before starting the test.
+ await browser.browsingData.removeCookies({});
+
+ let cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", expirationDate: THE_FUTURE});
+ assertExpected(expected, cookie);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ assertExpected(expected, cookie);
+
+ let cookies = await browser.cookies.getAll({name: "name1"});
+ browser.test.assertEq(1, cookies.length, "one cookie found for matching name");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({domain: "example.org"});
+ browser.test.assertEq(1, cookies.length, "one cookie found for matching domain");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({domain: "example.net"});
+ browser.test.assertEq(0, cookies.length, "no cookies found for non-matching domain");
+
+ cookies = await browser.cookies.getAll({secure: false});
+ browser.test.assertEq(1, cookies.length, "one non-secure cookie found");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({secure: true});
+ browser.test.assertEq(0, cookies.length, "no secure cookies found");
+
+ cookies = await browser.cookies.getAll({storeId: STORE_ID});
+ browser.test.assertEq(1, cookies.length, "one cookie found for valid storeId");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({storeId: "invalid_id"});
+ browser.test.assertEq(0, cookies.length, "no cookies found for invalid storeId");
+
+ let details = await browser.cookies.remove({url: TEST_URL, name: "name1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ // Ports in cookie URLs should be ignored. Every API call uses a different port number for better coverage.
+ cookie = await browser.cookies.set({url: changePort(TEST_URL, 1234), name: "name1", value: "value1", expirationDate: THE_FUTURE});
+ assertExpected(expected, cookie);
+
+ cookie = await browser.cookies.get({url: changePort(TEST_URL, 65535), name: "name1"});
+ assertExpected(expected, cookie);
+
+ cookies = await browser.cookies.getAll({url: TEST_URL});
+ browser.test.assertEq(cookies.length, 1, "Found cookie using getAll without port");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({url: changePort(TEST_URL, 1)});
+ browser.test.assertEq(cookies.length, 1, "Found cookie using getAll with port");
+ assertExpected(expected, cookies[0]);
+
+ // .remove should return the URL of the API call, so the port is included in the return value.
+ const TEST_URL_TO_REMOVE = changePort(TEST_URL, 1023);
+ details = await browser.cookies.remove({url: TEST_URL_TO_REMOVE, name: "name1"});
+ assertExpected({url: TEST_URL_TO_REMOVE, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ let stores = await browser.cookies.getAllCookieStores();
+ browser.test.assertEq(1, stores.length, "expected number of stores returned");
+ browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned");
+ browser.test.assertEq(1, stores[0].tabIds.length, "one tabId returned for store");
+ browser.test.assertEq("number", typeof stores[0].tabIds[0], "tabId is a number");
+
+ // TODO bug 1372178: Opening private windows/tabs is not supported on Android
+ if (browser.windows) {
+ let {windowId} = await openPrivateWindowAndTab(TEST_URL);
+ let stores = await browser.cookies.getAllCookieStores();
+
+ browser.test.assertEq(2, stores.length, "expected number of stores returned");
+ browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned");
+ browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store");
+ browser.test.assertEq(PRIVATE_STORE_ID, stores[1].id, "expected private store id returned");
+ browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for private store");
+
+ await browser.windows.remove(windowId);
+ }
+
+ cookie = await browser.cookies.set({url: TEST_URL, name: "name2", domain: ".example.org", expirationDate: THE_FUTURE});
+ browser.test.assertEq(false, cookie.hostOnly, "cookie is not a hostOnly cookie");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "name2"});
+ assertExpected({url: TEST_URL, name: "name2", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ // Create a session cookie.
+ cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1"});
+ browser.test.assertEq(true, cookie.session, "session cookie set");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(true, cookie.session, "got session cookie");
+
+ cookies = await browser.cookies.getAll({session: true});
+ browser.test.assertEq(1, cookies.length, "one session cookie found");
+ browser.test.assertEq(true, cookies[0].session, "found session cookie");
+
+ cookies = await browser.cookies.getAll({session: false});
+ browser.test.assertEq(0, cookies.length, "no non-session cookies found");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "name1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ cookie = await browser.cookies.set({url: TEST_SECURE_URL, name: "name1", value: "value1", secure: true});
+ browser.test.assertEq(true, cookie.secure, "secure cookie set");
+
+ cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"});
+ browser.test.assertEq(true, cookie.session, "got secure cookie");
+
+ cookies = await browser.cookies.getAll({secure: true});
+ browser.test.assertEq(1, cookies.length, "one secure cookie found");
+ browser.test.assertEq(true, cookies[0].secure, "found secure cookie");
+
+ cookies = await browser.cookies.getAll({secure: false});
+ browser.test.assertEq(0, cookies.length, "no non-secure cookies found");
+
+ details = await browser.cookies.remove({url: TEST_SECURE_URL, name: "name1"});
+ assertExpected({url: TEST_SECURE_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ cookie = await browser.cookies.set({url: TEST_URL_WITH_PATH, path: TEST_COOKIE_PATH, name: "name1", value: "value1", expirationDate: THE_FUTURE});
+ browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "created cookie with path");
+
+ cookie = await browser.cookies.get({url: TEST_URL_WITH_PATH, name: "name1"});
+ browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "got cookie with path");
+
+ cookies = await browser.cookies.getAll({path: TEST_COOKIE_PATH});
+ browser.test.assertEq(1, cookies.length, "one cookie with path found");
+ browser.test.assertEq(TEST_COOKIE_PATH, cookies[0].path, "found cookie with path");
+
+ cookie = await browser.cookies.get({url: TEST_URL + "invalid_path", name: "name1"});
+ browser.test.assertEq(null, cookie, "get with invalid path returns null");
+
+ cookies = await browser.cookies.getAll({path: "/invalid_path"});
+ browser.test.assertEq(0, cookies.length, "getAll with invalid path returns 0 cookies");
+
+ details = await browser.cookies.remove({url: TEST_URL_WITH_PATH, name: "name1"});
+ assertExpected({url: TEST_URL_WITH_PATH, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: true});
+ browser.test.assertEq(true, cookie.httpOnly, "httpOnly cookie set");
+
+ cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: false});
+ browser.test.assertEq(false, cookie.httpOnly, "non-httpOnly cookie set");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "name1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.set({url: TEST_URL});
+ browser.test.assertEq("", cookie.name, "default name set");
+ browser.test.assertEq("", cookie.value, "default value set");
+ browser.test.assertEq(true, cookie.session, "no expiry date created session cookie");
+
+ // TODO bug 1372178: Opening private windows/tabs is not supported on Android
+ if (browser.windows) {
+ let {tabId, windowId} = await openPrivateWindowAndTab(TEST_URL);
+
+ browser.test.assertEq("", await getDocumentCookie(tabId), "initially no cookie");
+
+ let cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "private", expirationDate: THE_FUTURE, storeId: PRIVATE_STORE_ID});
+ browser.test.assertEq("private", cookie.value, "set the private cookie");
+
+ cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "default", expirationDate: THE_FUTURE, storeId: STORE_ID});
+ browser.test.assertEq("default", cookie.value, "set the default cookie");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+ browser.test.assertEq("private", cookie.value, "get the private cookie");
+ browser.test.assertEq(PRIVATE_STORE_ID, cookie.storeId, "get the private cookie storeId");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID});
+ browser.test.assertEq("default", cookie.value, "get the default cookie");
+ browser.test.assertEq(STORE_ID, cookie.storeId, "get the default cookie storeId");
+
+ browser.test.assertEq("store=private", await getDocumentCookie(tabId), "private document.cookie should be set");
+
+ let details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: STORE_ID});
+ assertExpected({url: TEST_URL, name: "store", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID});
+ browser.test.assertEq(null, cookie, "deleted the default cookie");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+ assertExpected({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID, firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+ browser.test.assertEq(null, cookie, "deleted the private cookie");
+
+ browser.test.assertEq("", await getDocumentCookie(tabId), "private document.cookie should be removed");
+
+ await browser.windows.remove(windowId);
+ }
+
+ browser.test.notifyPass("cookies");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ background,
+ manifest: {
+ permissions: ["cookies", "*://example.org/", "*://[2a03:4000:6:310e:216:3eff:fe53:99b]/", "*://192.168.1.1/", "webNavigation", "browsingData"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("cookies");
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ // make sure userContext is enabled.
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["privacy.userContext.enabled", true],
+ ]});
+});
+
+add_task(async function test_cookie_containers() {
+ async function background() {
+ // Sometimes there is a cookie without name/value when running tests.
+ let cookiesAtStart = await browser.cookies.getAll({storeId: "firefox-default"});
+
+ function assertExpected(expected, cookie) {
+ for (let key of Object.keys(cookie)) {
+ browser.test.assertTrue(key in expected, `found property ${key}`);
+ browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`);
+ }
+ browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found");
+ }
+
+ const TEST_URL = "http://example.org/";
+ const THE_FUTURE = Date.now() + 5 * 60;
+
+ let expected = {
+ name: "name1",
+ value: "value1",
+ domain: "example.org",
+ hostOnly: true,
+ path: "/",
+ secure: false,
+ httpOnly: false,
+ sameSite: "no_restriction",
+ session: false,
+ expirationDate: THE_FUTURE,
+ storeId: "firefox-container-1",
+ firstPartyDomain: "",
+ partitionKey: null,
+ };
+
+ let cookie = await browser.cookies.set({
+ url: TEST_URL, name: "name1", value: "value1",
+ expirationDate: THE_FUTURE, storeId: "firefox-container-1",
+ });
+ browser.test.assertEq("firefox-container-1", cookie.storeId, "the cookie has the correct storeId");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "get() without storeId returns null");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"});
+ assertExpected(expected, cookie);
+
+ let cookies = await browser.cookies.getAll({storeId: "firefox-default"});
+ browser.test.assertEq(0, cookiesAtStart.length - cookies.length, "getAll() with default storeId hasn't added cookies");
+
+ cookies = await browser.cookies.getAll({storeId: "firefox-container-1"});
+ browser.test.assertEq(1, cookies.length, "one cookie found for matching domain");
+ assertExpected(expected, cookies[0]);
+
+ let details = await browser.cookies.remove({url: TEST_URL, name: "name1", storeId: "firefox-container-1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: "firefox-container-1", firstPartyDomain: "", partitionKey: null}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ browser.test.notifyPass("cookies");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["cookies", "*://example.org/"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("cookies");
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension cookies test</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_cookies_expiry() {
+ function background() {
+ let expectedEvents = [];
+
+ browser.cookies.onChanged.addListener(event => {
+ expectedEvents.push(`${event.removed}:${event.cause}`);
+ if (expectedEvents.length === 1) {
+ browser.test.assertEq("true:expired", expectedEvents[0], "expired cookie removed");
+ browser.test.assertEq("first", event.cookie.name, "expired cookie has the expected name");
+ browser.test.assertEq("one", event.cookie.value, "expired cookie has the expected value");
+ } else {
+ browser.test.assertEq("false:explicit", expectedEvents[1], "new cookie added");
+ browser.test.assertEq("first", event.cookie.name, "new cookie has the expected name");
+ browser.test.assertEq("one-again", event.cookie.value, "new cookie has the expected value");
+ browser.test.notifyPass("cookie-expiry");
+ }
+ });
+
+ setTimeout(() => {
+ browser.test.sendMessage("change-cookies");
+ }, 1000);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["http://example.com/", "cookies"],
+ },
+ background,
+ });
+
+ let chromeScript = loadChromeScript(() => {
+ const {sendAsyncMessage} = this;
+ Services.cookies.add(".example.com", "/", "first", "one", false, false, false, Date.now() / 1000 + 1, {}, Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_HTTP);
+ sendAsyncMessage("done");
+ });
+ await chromeScript.promiseOneMessage("done");
+ chromeScript.destroy();
+
+ await extension.startup();
+ await extension.awaitMessage("change-cookies");
+
+ chromeScript = loadChromeScript(() => {
+ const {sendAsyncMessage} = this;
+ Services.cookies.add(".example.com", "/", "first", "one-again", false, false, false, Date.now() / 1000 + 10, {}, Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_HTTP);
+ sendAsyncMessage("done");
+ });
+ await chromeScript.promiseOneMessage("done");
+ chromeScript.destroy();
+
+ await extension.awaitFinish("cookie-expiry");
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+async function background() {
+ const url = "http://ext-cookie-first-party.mochi.test/";
+ const firstPartyDomain = "ext-cookie-first-party.mochi.test";
+ // A first party domain with invalid characters for the file system, which just happens to be a IPv6 address.
+ const firstPartyDomainInvalidChars = "[2606:4700:4700::1111]";
+ const expectedError = "First-Party Isolation is enabled, but the required 'firstPartyDomain' attribute was not set.";
+
+ const assertExpectedCookies = (expected, cookies, message) => {
+ let matches = (cookie, expected) => {
+ if (!cookie || !expected) {
+ return cookie === expected; // true if both are null.
+ }
+ for (let key of Object.keys(expected)) {
+ if (cookie[key] !== expected[key]) {
+ return false;
+ }
+ }
+ return true;
+ };
+ browser.test.assertEq(expected.length, cookies.length, `Got expected number of cookies - ${message}`);
+ if (cookies.length !== expected.length) {
+ return;
+ }
+ for (let expect of expected) {
+ let foundCookies = cookies.filter(cookie => matches(cookie, expect));
+ browser.test.assertEq(1, foundCookies.length,
+ `Expected cookie ${JSON.stringify(expect)} found - ${message}`);
+ }
+ };
+
+ // Test when FPI is disabled.
+ const test_fpi_disabled = async () => {
+ let cookie, cookies;
+
+ // set
+ cookie = await browser.cookies.set({url, name: "foo1", value: "bar1"});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], [cookie], "set: FPI off, w/ empty firstPartyDomain, non-FP cookie");
+ cookie = await browser.cookies.set({url, name: "foo2", value: "bar2", firstPartyDomain});
+ assertExpectedCookies([
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ ], [cookie], "set: FPI off, w/ firstPartyDomain, FP cookie");
+
+ // get
+ // When FPI is disabled, missing key/null/undefined is equivalent to "".
+ cookie = await browser.cookies.get({url, name: "foo1"});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], [cookie], "get: FPI off, w/o firstPartyDomain, non-FP cookie");
+ cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: ""});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], [cookie], "get: FPI off, w/ empty firstPartyDomain, non-FP cookie");
+ cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: null});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], [cookie], "get: FPI off, w/ null firstPartyDomain, non-FP cookie");
+ cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: undefined});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], [cookie], "get: FPI off, w/ undefined firstPartyDomain, non-FP cookie");
+
+ cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain});
+ assertExpectedCookies([
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ ], [cookie], "get: FPI off, w/ firstPartyDomain, FP cookie");
+ // There is no match for non-FP cookies with name "foo2".
+ cookie = await browser.cookies.get({url, name: "foo2"});
+ assertExpectedCookies([null], [cookie], "get: FPI off, w/o firstPartyDomain, no cookie");
+ cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: ""});
+ assertExpectedCookies([null], [cookie], "get: FPI off, w/ empty firstPartyDomain, no cookie");
+ cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: null});
+ assertExpectedCookies([null], [cookie], "get: FPI off, w/ null firstPartyDomain, no cookie");
+ cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: undefined});
+ assertExpectedCookies([null], [cookie], "get: FPI off, w/ undefined firstPartyDomain, no cookie");
+
+ // getAll
+ for (let extra of [{}, {url}, {domain: firstPartyDomain}]) {
+ const prefix = `getAll(${JSON.stringify(extra)})`;
+ cookies = await browser.cookies.getAll({...extra});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], cookies, `${prefix}: FPI off, w/o firstPartyDomain, non-FP cookies`);
+ cookies = await browser.cookies.getAll({...extra, firstPartyDomain: ""});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], cookies, `${prefix}: FPI off, w/ empty firstPartyDomain, non-FP cookies`);
+ cookies = await browser.cookies.getAll({...extra, firstPartyDomain: null});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ ], cookies, `${prefix}: FPI off, w/ null firstPartyDomain, all cookies`);
+ cookies = await browser.cookies.getAll({...extra, firstPartyDomain: undefined});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ ], cookies, `${prefix}: FPI off, w/ undefined firstPartyDomain, all cookies`);
+ cookies = await browser.cookies.getAll({...extra, firstPartyDomain});
+ assertExpectedCookies([
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ ], cookies, `${prefix}: FPI off, w/ firstPartyDomain, FP cookies`);
+ }
+
+ // remove
+ cookie = await browser.cookies.remove({url, name: "foo1"});
+ assertExpectedCookies([
+ {url, name: "foo1", firstPartyDomain: ""},
+ ], [cookie], "remove: FPI off, w/ empty firstPartyDomain, non-FP cookie");
+ cookie = await browser.cookies.remove({url, name: "foo2", firstPartyDomain});
+ assertExpectedCookies([
+ {url, name: "foo2", firstPartyDomain},
+ ], [cookie], "remove: FPI off, w/ firstPartyDomain, FP cookie");
+
+ // Test if FP cookies set when FPI off can be accessed when FPI on.
+ await browser.cookies.set({url, name: "foo1", value: "bar1"});
+ await browser.cookies.set({url, name: "foo2", value: "bar2", firstPartyDomain});
+
+ browser.test.sendMessage("test_fpi_disabled");
+ };
+
+ // Test when FPI is enabled.
+ const test_fpi_enabled = async () => {
+ let cookie, cookies;
+
+ // set
+ await browser.test.assertRejects(
+ browser.cookies.set({url, name: "foo3", value: "bar3"}),
+ expectedError,
+ "set: FPI on, w/o firstPartyDomain, rejection");
+ cookie = await browser.cookies.set({url, name: "foo4", value: "bar4", firstPartyDomain});
+ assertExpectedCookies([
+ {name: "foo4", value: "bar4", firstPartyDomain},
+ ], [cookie], "set: FPI on, w/ firstPartyDomain, FP cookie");
+
+ // get
+ await browser.test.assertRejects(
+ browser.cookies.get({url, name: "foo3"}),
+ expectedError,
+ "get: FPI on, w/o firstPartyDomain, rejection");
+ await browser.test.assertRejects(
+ browser.cookies.get({url, name: "foo3", firstPartyDomain: null}),
+ expectedError,
+ "get: FPI on, w/ null firstPartyDomain, rejection");
+ await browser.test.assertRejects(
+ browser.cookies.get({url, name: "foo3", firstPartyDomain: undefined}),
+ expectedError,
+ "get: FPI on, w/ undefined firstPartyDomain, rejection");
+ cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: ""});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], [cookie], "get: FPI on, w/ empty firstPartyDomain, non-FP cookie");
+ cookie = await browser.cookies.get({url, name: "foo4", firstPartyDomain});
+ assertExpectedCookies([
+ {name: "foo4", value: "bar4", firstPartyDomain},
+ ], [cookie], "get: FPI on, w/ firstPartyDomain, FP cookie");
+ cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain});
+ assertExpectedCookies([
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ ], [cookie], "get: FPI on, w/ firstPartyDomain, FP cookie (set when FPI off)");
+
+ // getAll
+ for (let extra of [{}, {url}, {domain: firstPartyDomain}]) {
+ const prefix = `getAll(${JSON.stringify(extra)})`;
+ await browser.test.assertRejects(
+ browser.cookies.getAll({...extra}),
+ expectedError,
+ `${prefix}: FPI on, w/o firstPartyDomain, rejection`);
+ cookies = await browser.cookies.getAll({...extra, firstPartyDomain: ""});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ ], cookies, `${prefix}: FPI on, w/ empty firstPartyDomain, non-FP cookies`);
+ cookies = await browser.cookies.getAll({...extra, firstPartyDomain: null});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ {name: "foo4", value: "bar4", firstPartyDomain},
+ ], cookies, `${prefix}: FPI on, w/ null firstPartyDomain, all cookies`);
+ cookies = await browser.cookies.getAll({...extra, firstPartyDomain: undefined});
+ assertExpectedCookies([
+ {name: "foo1", value: "bar1", firstPartyDomain: ""},
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ {name: "foo4", value: "bar4", firstPartyDomain},
+ ], cookies, `${prefix}: FPI on, w/ undefined firstPartyDomain, all cookies`);
+ cookies = await browser.cookies.getAll({...extra, firstPartyDomain});
+ assertExpectedCookies([
+ {name: "foo2", value: "bar2", firstPartyDomain},
+ {name: "foo4", value: "bar4", firstPartyDomain},
+ ], cookies, `${prefix}: FPI on, w/ firstPartyDomain, FP cookies`);
+ }
+
+ // remove
+ await browser.test.assertRejects(
+ browser.cookies.remove({url, name: "foo3"}),
+ expectedError,
+ "remove: FPI on, w/o firstPartyDomain, rejection");
+ cookie = await browser.cookies.remove({url, name: "foo4", firstPartyDomain});
+ assertExpectedCookies([
+ {url, name: "foo4", firstPartyDomain},
+ ], [cookie], "remove: FPI on, w/ firstPartyDomain, FP cookie");
+ cookie = await browser.cookies.remove({url, name: "foo2", firstPartyDomain});
+ assertExpectedCookies([
+ {url, name: "foo2", firstPartyDomain},
+ ], [cookie], "remove: FPI on, w/ firstPartyDomain, FP cookie (set when FPI off)");
+
+ // Test if FP cookies set when FPI on can be accessed when FPI off.
+ await browser.cookies.set({url, name: "foo4", value: "bar4", firstPartyDomain});
+
+ browser.test.sendMessage("test_fpi_enabled");
+ };
+
+ // Test FPI with a first party domain with invalid characters for
+ // the file system.
+ const test_fpi_with_invalid_characters = async () => {
+ let cookie;
+
+ // Test setting a cookie with a first party domain with invalid characters
+ // for the file system.
+ cookie = await browser.cookies.set({url, name: "foo5", value: "bar5",
+ firstPartyDomain: firstPartyDomainInvalidChars});
+ assertExpectedCookies([
+ {name: "foo5", value: "bar5", firstPartyDomain: firstPartyDomainInvalidChars},
+ ], [cookie], "set: FPI on, w/ firstPartyDomain with invalid characters, FP cookie");
+
+ // Test getting a cookie with a first party domain with invalid characters
+ // for the file system.
+ cookie = await browser.cookies.get({url, name: "foo5",
+ firstPartyDomain: firstPartyDomainInvalidChars});
+ assertExpectedCookies([
+ {name: "foo5", value: "bar5", firstPartyDomain: firstPartyDomainInvalidChars},
+ ], [cookie], "get: FPI on, w/ firstPartyDomain with invalid characters, FP cookie");
+
+ // Test removing a cookie with a first party domain with invalid characters
+ // for the file system.
+ cookie = await browser.cookies.remove({url, name: "foo5",
+ firstPartyDomain: firstPartyDomainInvalidChars});
+ assertExpectedCookies([
+ {url, name: "foo5", firstPartyDomain: firstPartyDomainInvalidChars},
+ ], [cookie], "remove: FPI on, w/ firstPartyDomain with invalid characters, FP cookie");
+
+ browser.test.sendMessage("test_fpi_with_invalid_characters");
+ };
+
+ // Test when FPI is disabled again, accessing FP cookies set when FPI is enabled.
+ const test_fpd_cookies_on_fpi_disabled = async () => {
+ let cookie, cookies;
+ cookie = await browser.cookies.get({url, name: "foo4", firstPartyDomain});
+ assertExpectedCookies([
+ {name: "foo4", value: "bar4", firstPartyDomain},
+ ], [cookie], "get: FPI off, w/ firstPartyDomain, FP cookie (set when FPI on)");
+ cookie = await browser.cookies.remove({url, name: "foo4", firstPartyDomain});
+ assertExpectedCookies([
+ {url, name: "foo4", firstPartyDomain},
+ ], [cookie], "remove: FPI off, w/ firstPartyDomain, FP cookie (set when FPI on)");
+
+ // Clean up.
+ await browser.cookies.remove({url, name: "foo1"});
+
+ cookies = await browser.cookies.getAll({firstPartyDomain: null});
+ assertExpectedCookies([], cookies, "Test is finishing, all cookies removed");
+
+ browser.test.sendMessage("test_fpd_cookies_on_fpi_disabled");
+ };
+
+ browser.test.onMessage.addListener((message) => {
+ switch (message) {
+ case "test_fpi_disabled": return test_fpi_disabled();
+ case "test_fpi_enabled": return test_fpi_enabled();
+ case "test_fpi_with_invalid_characters": return test_fpi_with_invalid_characters();
+ case "test_fpd_cookies_on_fpi_disabled": return test_fpd_cookies_on_fpi_disabled();
+ default: return browser.test.notifyFail("unknown-message");
+ }
+ });
+}
+
+function enableFirstPartyIsolation() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.firstparty.isolate", true],
+ ],
+ });
+}
+
+function disableFirstPartyIsolation() {
+ return SpecialPowers.popPrefEnv();
+}
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["cookies", "*://ext-cookie-first-party.mochi.test/"],
+ },
+ });
+ await extension.startup();
+ extension.sendMessage("test_fpi_disabled");
+ await extension.awaitMessage("test_fpi_disabled");
+ await enableFirstPartyIsolation();
+ extension.sendMessage("test_fpi_enabled");
+ await extension.awaitMessage("test_fpi_enabled");
+ extension.sendMessage("test_fpi_with_invalid_characters");
+ await extension.awaitMessage("test_fpi_with_invalid_characters");
+ await disableFirstPartyIsolation();
+ extension.sendMessage("test_fpd_cookies_on_fpi_disabled");
+ await extension.awaitMessage("test_fpd_cookies_on_fpi_disabled");
+ await extension.unload();
+});
+</script>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_cookies_incognito_not_allowed() {
+ let privateExtension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ async background() {
+ let window = await browser.windows.create({incognito: true});
+ browser.test.onMessage.addListener(async () => {
+ await browser.windows.remove(window.id);
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("ready");
+ },
+ manifest: {
+ permissions: ["cookies", "*://example.org/"],
+ },
+ });
+ await privateExtension.startup();
+ await privateExtension.awaitMessage("ready");
+
+ async function background() {
+ const storeId = "firefox-private";
+ const url = "http://example.org/";
+
+ // Getting the wrong storeId will fail, otherwise we should finish the test fine.
+ browser.cookies.onChanged.addListener(changeInfo => {
+ let {cookie} = changeInfo;
+ browser.test.assertTrue(cookie.storeId != storeId, "cookie store is correct");
+ });
+
+ browser.test.onMessage.addListener(async () => {
+ let stores = await browser.cookies.getAllCookieStores();
+ let store = stores.find(s => s.incognito);
+ browser.test.assertTrue(!store, "incognito cookie store should not be available");
+ browser.test.notifyPass("cookies");
+ });
+
+ await browser.test.assertRejects(
+ browser.cookies.set({url, name: "test", storeId}),
+ /Extension disallowed access/,
+ "API should reject setting cookie");
+ await browser.test.assertRejects(
+ browser.cookies.get({url, name: "test", storeId}),
+ /Extension disallowed access/,
+ "API should reject getting cookie");
+ await browser.test.assertRejects(
+ browser.cookies.getAll({url, storeId}),
+ /Extension disallowed access/,
+ "API should reject getting cookie");
+ await browser.test.assertRejects(
+ browser.cookies.remove({url, name: "test", storeId}),
+ /Extension disallowed access/,
+ "API should reject getting cookie");
+ await browser.test.assertRejects(
+ browser.cookies.getAll({url, storeId}),
+ /Extension disallowed access/,
+ "API should reject getting cookie");
+
+ browser.test.sendMessage("set-cookies");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["cookies", "*://example.org/"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("set-cookies");
+
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ Services.cookies.add("example.org", "/", "public", `foo${Math.random()}`,
+ false, false, false, Number.MAX_SAFE_INTEGER, {},
+ Ci.nsICookie.SAMESITE_NONE);
+ Services.cookies.add("example.org", "/", "private", `foo${Math.random()}`,
+ false, false, false, Number.MAX_SAFE_INTEGER, {privateBrowsingId: 1},
+ Ci.nsICookie.SAMESITE_NONE);
+ });
+ extension.sendMessage("test-cookie-store");
+ await extension.awaitFinish("cookies");
+
+ await extension.unload();
+ privateExtension.sendMessage("close");
+ await privateExtension.awaitMessage("done");
+ await privateExtension.unload();
+ chromeScript.destroy();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_cookies.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function init() {
+ // We need to trigger a cookie eviction in order to test our batch delete
+ // observer.
+
+ // Set quotaPerHost to maxPerHost - 1, so there is only one cookie
+ // will be evicted everytime.
+ SpecialPowers.setIntPref("network.cookie.quotaPerHost", 2);
+ SpecialPowers.setIntPref("network.cookie.maxPerHost", 3);
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("network.cookie.quotaPerHost");
+ SpecialPowers.clearUserPref("network.cookie.maxPerHost");
+ });
+});
+
+add_task(async function test_bad_cookie_permissions() {
+ info("Test non-matching, non-secure domain with non-secure cookie");
+ await testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.net/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test non-matching, secure domain with non-secure cookie");
+ await testCookies({
+ permissions: ["https://example.com/", "cookies"],
+ url: "https://example.net/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test non-matching, secure domain with secure cookie");
+ await testCookies({
+ permissions: ["https://example.com/", "cookies"],
+ url: "https://example.net/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test matching subdomain with superdomain privileges, secure cookie (http)");
+ await testCookies({
+ permissions: ["http://foo.bar.example.com/", "cookies"],
+ url: "http://foo.bar.example.com/",
+ domain: ".example.com",
+ secure: true,
+ shouldPass: false,
+ shouldWrite: true,
+ });
+
+ info("Test matching, non-secure domain with secure cookie");
+ await testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.com/",
+ domain: "example.com",
+ secure: true,
+ shouldPass: false,
+ shouldWrite: true,
+ });
+
+ info("Test matching, non-secure host, secure URL");
+ await testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "https://example.com/",
+ domain: "example.com",
+ secure: true,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test non-matching domain");
+ await testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.com/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test invalid scheme");
+ await testCookies({
+ permissions: ["ftp://example.com/", "cookies"],
+ url: "ftp://example.com/",
+ domain: "example.com",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_cookies.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function init() {
+ // We need to trigger a cookie eviction in order to test our batch delete
+ // observer.
+
+ // Set quotaPerHost to maxPerHost - 1, so there is only one cookie
+ // will be evicted everytime.
+ SpecialPowers.setIntPref("network.cookie.quotaPerHost", 2);
+ SpecialPowers.setIntPref("network.cookie.maxPerHost", 3);
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("network.cookie.quotaPerHost");
+ SpecialPowers.clearUserPref("network.cookie.maxPerHost");
+ });
+});
+
+add_task(async function test_good_cookie_permissions() {
+ info("Test matching, non-secure domain with non-secure cookie");
+ await testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.com/",
+ domain: "example.com",
+ secure: false,
+ shouldPass: true,
+ });
+
+ info("Test matching, secure domain with non-secure cookie");
+ await testCookies({
+ permissions: ["https://example.com/", "cookies"],
+ url: "https://example.com/",
+ domain: "example.com",
+ secure: false,
+ shouldPass: true,
+ });
+
+ info("Test matching, secure domain with secure cookie");
+ await testCookies({
+ permissions: ["https://example.com/", "cookies"],
+ url: "https://example.com/",
+ domain: "example.com",
+ secure: true,
+ shouldPass: true,
+ });
+
+ info("Test matching subdomain with superdomain privileges, secure cookie (https)");
+ await testCookies({
+ permissions: ["https://foo.bar.example.com/", "cookies"],
+ url: "https://foo.bar.example.com/",
+ domain: ".example.com",
+ secure: true,
+ shouldPass: true,
+ });
+
+ info("Test matching subdomain with superdomain privileges, non-secure cookie (https)");
+ await testCookies({
+ permissions: ["https://foo.bar.example.com/", "cookies"],
+ url: "https://foo.bar.example.com/",
+ domain: ".example.com",
+ secure: false,
+ shouldPass: true,
+ });
+
+ info("Test matching subdomain with superdomain privileges, non-secure cookie (http)");
+ await testCookies({
+ permissions: ["http://foo.bar.example.com/", "cookies"],
+ url: "http://foo.bar.example.com/",
+ domain: ".example.com",
+ secure: false,
+ shouldPass: true,
+ });
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_dnr_other_extensions.html b/toolkit/components/extensions/test/mochitest/test_ext_dnr_other_extensions.html
new file mode 100644
index 0000000000..d3074b3dec
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_dnr_other_extensions.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>DNR and tabs.create from other extension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+
+
+// While most DNR tests are xpcshell tests, this one is a mochitest because the
+// tabs.create API does not work in a xpcshell test.
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.manifestV3.enabled", true],
+ ["extensions.dnr.enabled", true],
+ ],
+ });
+});
+
+
+add_task(async function tabs_create_can_be_redirected_by_other_dnr_extension() {
+ let dnrExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["declarativeNetRequestWithHostAccess"],
+ // redirect action requires host permissions:
+ host_permissions: ["*://example.com/*"],
+ },
+ async background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ {
+ id: 1,
+ condition: {
+ resourceTypes: ["main_frame"],
+ urlFilter: "?dnr_redir_me_pls",
+ },
+ action: {
+ type: "redirect",
+ redirect: {
+ transform: {
+ query: "?dnr_redir_target"
+ },
+ },
+ },
+ },
+ ],
+ });
+ browser.test.sendMessage("dnr_registered");
+ },
+ });
+ await dnrExtension.startup();
+ await dnrExtension.awaitMessage("dnr_registered");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ async background() {
+ async function createTabAndGetFinalUrl(url) {
+ let navigationDonePromise = new Promise(resolve => {
+ browser.webNavigation.onDOMContentLoaded.addListener(
+ function listener(details) {
+ browser.webNavigation.onDOMContentLoaded.removeListener(listener);
+ resolve(details);
+ },
+ // All input URLs and redirection targets match this URL filter:
+ { url: [{ queryPrefix: "dnr_redir_" }] }
+ );
+ });
+ const tab = await browser.tabs.create({ url });
+ browser.test.log(`Waiting for navigation done, starting from ${url}`);
+ const result = await navigationDonePromise;
+ browser.test.assertEq(
+ tab.id,
+ result.tabId,
+ `Observed load completion for navigation tab with initial URL ${url}`
+ );
+ await browser.tabs.remove(tab.id);
+ return result.url;
+ }
+
+ browser.test.assertEq(
+ "https://example.com/?dnr_redir_target",
+ await createTabAndGetFinalUrl("https://example.com/?dnr_redir_me_pls"),
+ "DNR rule from other extension should have redirected the navigation"
+ );
+
+ browser.test.assertEq(
+ "https://example.org/?dnr_redir_me_pls",
+ await createTabAndGetFinalUrl("https://example.org/?dnr_redir_me_pls"),
+ "DNR redirect ignored for URLs without host permission"
+ );
+ browser.test.sendMessage("done");
+ }
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+
+ await dnrExtension.unload();
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>DNR with tabIds condition</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+
+// While most DNR tests are xpcshell tests, this one is a mochitest because it
+// is not possible to create a tab and get a tabId in a xpcshell test.
+
+// toolkit/components/extensions/test/xpcshell/test_ext_dnr_tabIds.js does
+// exist, as an isolated xpcshell is needed to verify that the internals are
+// working as expected. A mochitest is not a good fit for that because it has
+// built-in add-ons that may affect the observed behavior.
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.manifestV3.enabled", true],
+ ["extensions.dnr.enabled", true],
+ ],
+ });
+});
+
+add_task(async function match_by_tabIds() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ async function createTabAndPort() {
+ let portPromise = new Promise(resolve => {
+ browser.runtime.onConnect.addListener(function listener(port) {
+ browser.runtime.onConnect.removeListener(listener);
+ browser.test.assertEq("port_from_tab", port.name, "Got port");
+ resolve(port);
+ });
+ });
+ const tab = await browser.tabs.create({ url: "tab.html" });
+ const port = await portPromise;
+ browser.test.assertEq(tab.id, port.sender.tab.id, "Got port from tab");
+ browser.test.assertTrue(tab.id > 0, `tabId must be valid: ${tab.id}`);
+ tab.port = port;
+ return tab;
+ }
+ async function getFinalUrlForFetchInTab(tabWithPort, url) {
+ const port = tabWithPort.port; // from createTabAndPort.
+ return new Promise(resolve => {
+ port.onMessage.addListener(function listener(responseUrl) {
+ port.onMessage.removeListener(listener);
+ resolve(responseUrl);
+ });
+ port.postMessage(url);
+ });
+ }
+ let tab1 = await createTabAndPort();
+ let tab2 = await createTabAndPort();
+
+ const URL_PREFIX = "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.txt";
+
+ function makeRedirect(id, condition, url) {
+ return {
+ id,
+ // The test sends a request to example.net and expects a redirect to
+ // URL_PREFIX (example.com).
+ condition: { requestDomains: ["example.net"], ...condition },
+ action: { type: "redirect", redirect: { url }},
+ };
+ }
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ makeRedirect(1, { tabIds: [-1] }, `${URL_PREFIX}?tabId/-1`),
+ makeRedirect(2, { tabIds: [tab1.id] }, `${URL_PREFIX}?tabId/tab1`),
+ makeRedirect(
+ 3,
+ { excludedTabIds: [-1, tab1.id] },
+ `${URL_PREFIX}?tabId/not-1,not-tab1`
+ ),
+ ],
+ });
+
+ browser.test.assertEq(
+ `${URL_PREFIX}?tabId/-1`,
+ (await fetch("https://example.net/?pre-redirect-bg")).url,
+ "Request from background should match tabIds: [-1]"
+ );
+ browser.test.assertEq(
+ `${URL_PREFIX}?tabId/tab1`,
+ await getFinalUrlForFetchInTab(tab1, "https://example.net/?pre-tab1"),
+ "Request from tab1 should match tabIds: [tab1]"
+ );
+ browser.test.assertEq(
+ `${URL_PREFIX}?tabId/not-1,not-tab1`,
+ await getFinalUrlForFetchInTab(tab2, "https://example.net/?pre-tab2"),
+ "Request from tab2 should match excludedTabIds: [-1, tab1]"
+ );
+
+ await browser.tabs.remove(tab1.id);
+ await browser.tabs.remove(tab2.id);
+
+ browser.test.sendMessage("done");
+ },
+ temporarilyInstalled: true, // Needed for granted_host_permissions
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*", "*://example.net/*"],
+ permissions: ["declarativeNetRequest"],
+ granted_host_permissions: true,
+ },
+ files: {
+ "tab.html": `<!DOCTYPE html><script src="tab.js"><\/script>`,
+ "tab.js": () => {
+ let port = browser.runtime.connect({ name: "port_from_tab" });
+ port.onMessage.addListener(async url => {
+ try {
+ let res = await fetch(url);
+ port.postMessage(res.url);
+ } catch (e) {
+ port.postMessage(e.message);
+ }
+ });
+ },
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
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..43bc8a5a00
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>DNR with upgradeScheme action</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+
+// This test is not a xpcshell test, because we want to test upgrades to https,
+// and HttpServer helper does not support https (bug 1742061).
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.manifestV3.enabled", true],
+ ["extensions.dnr.enabled", true],
+ ["extensions.dnr.match_requests_from_other_extensions", true],
+ ],
+ });
+});
+
+// Tests that the upgradeScheme action works as expected:
+// - http should be upgraded to https
+// - after the https upgrade the request should happen instead of being stuck
+// in a upgrade redirect loop.
+add_task(async function upgradeScheme_with_dnr() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [{ id: 1, condition: { requestDomains: ["example.com"] }, action: { type: "upgradeScheme" } }],
+ });
+
+ let sanityCheckResponse = await fetch(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_sample.txt"
+ );
+ browser.test.assertEq(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_sample.txt",
+ sanityCheckResponse.url,
+ "non-matching request should not be upgraded"
+ );
+
+ let res = await fetch(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.txt"
+ );
+ browser.test.assertEq(
+ "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.txt",
+ res.url,
+ "upgradeScheme should have upgraded to https"
+ );
+ // Server adds "Access-Control-Allow-Origin: *" to file_sample.txt, so
+ // we should be able to read the response despite no host_permissions.
+ browser.test.assertEq("Sample", await res.text(), "read body with CORS");
+
+ browser.test.sendMessage("dnr_registered");
+ },
+ manifest: {
+ manifest_version: 3,
+ // Note: host_permissions missing. upgradeScheme should not need it.
+ permissions: ["declarativeNetRequest"],
+ },
+ allowInsecureRequests: true,
+ });
+ await extension.startup();
+ await extension.awaitMessage("dnr_registered");
+
+ // The request made by otherExtension is affected by the DNR rule from the
+ // extension because extensions.dnr.match_requests_from_other_extensions was
+ // set to true. A realistic alternative would have been to trigger the fetch
+ // requests from a content page instead of the extension.
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let firstRequestPromise = new Promise(resolve => {
+ let count = 0;
+ browser.webRequest.onBeforeRequest.addListener(
+ ({ url }) => {
+ ++count;
+ browser.test.assertTrue(
+ count <= 2,
+ `Expected at most two requests; got ${count} to ${url}`
+ );
+ resolve(url);
+ },
+ { urls: ["*://example.com/?test_dnr_upgradeScheme"] }
+ );
+ });
+ // Round-trip through ext-webRequest.js implementation to ensure that the
+ // listener has been registered (workaround for bug 1300234).
+ await browser.webRequest.handlerBehaviorChanged();
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const insecureInitialUrl = "http://example.com/?test_dnr_upgradeScheme";
+ browser.test.log(`Requesting insecure URL: ${insecureInitialUrl}`);
+
+ let req = await fetch(insecureInitialUrl);
+ browser.test.assertEq(
+ "https://example.com/?test_dnr_upgradeScheme",
+ req.url,
+ "upgradeScheme action upgraded http to https"
+ );
+ browser.test.assertEq(200, req.status, "Correct HTTP status");
+
+ await req.text(); // Verify that the body can be read, just in case.
+
+ // Sanity check that the test did not pass trivially due to an automatic
+ // https upgrade of the extension / test environment.
+ browser.test.assertEq(
+ insecureInitialUrl,
+ await firstRequestPromise,
+ "Initial URL should be http"
+ );
+
+ browser.test.sendMessage("tested_dnr_upgradeScheme");
+ },
+ manifest: {
+ host_permissions: ["*://example.com/*"],
+ permissions: ["webRequest"],
+ },
+ });
+ await otherExtension.startup();
+ await otherExtension.awaitMessage("tested_dnr_upgradeScheme");
+ await otherExtension.unload();
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
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..23058c35ec
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Downloads Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+async function background() {
+ const url = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html";
+
+ browser.test.assertThrows(
+ () => browser.downloads.download(),
+ /Incorrect argument types for downloads.download/,
+ "Should fail without options"
+ );
+
+ browser.test.assertThrows(
+ () => browser.downloads.download({url: "invalid url"}),
+ /invalid url is not a valid URL/,
+ "Should fail on invalid URL"
+ );
+
+ browser.test.assertThrows(
+ () => browser.downloads.download({}),
+ /Property "url" is required/,
+ "Should fail with no URL"
+ );
+
+ browser.test.assertThrows(
+ () => browser.downloads.download({url, method: "DELETE"}),
+ /Invalid enumeration value "DELETE"/,
+ "Should fail with invalid method"
+ );
+
+ await browser.test.assertRejects(
+ browser.downloads.download({url, headers: [{name: "Host", value: "Banana"}]}),
+ /Forbidden request header name/,
+ "Should fail with a forbidden header"
+ );
+
+ const absoluteFilename = SpecialPowers.Services.appinfo.OS === "WINNT"
+ ? "C:\\tmp\\file.gif"
+ : "/tmp/file.gif";
+
+ await browser.test.assertRejects(
+ browser.downloads.download({url, filename: absoluteFilename}),
+ /filename must not be an absolute path/,
+ "Should fail with an absolute file path"
+ );
+
+ await browser.test.assertRejects(
+ browser.downloads.download({url, filename: ""}),
+ /filename must not be empty/,
+ "Should fail with an empty file path"
+ );
+
+ await browser.test.assertRejects(
+ browser.downloads.download({url, filename: "file."}),
+ /filename must not contain illegal characters/,
+ "Should fail with a dot in the filename"
+ );
+
+ await browser.test.assertRejects(
+ browser.downloads.download({url, filename: "../file.gif"}),
+ /filename must not contain back-references/,
+ "Should fail with a file path that contains back-references"
+ );
+
+ browser.test.notifyPass("download.done");
+}
+
+add_task(async function test_invalid_download_parameters() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {permissions: ["downloads"]},
+ background,
+ });
+ await extension.startup();
+
+ await extension.awaitFinish("download.done");
+
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test checking webRequest.onBeforeRequest details object</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let expected = {
+ "file_contains_iframe.html": {
+ type: "main_frame",
+ frameAncestor_length: 0,
+ },
+ "file_contains_img.html": {
+ type: "sub_frame",
+ frameAncestor_length: 1,
+ },
+ "file_image_good.png": {
+ type: "image",
+ frameAncestor_length: 1,
+ }
+};
+
+function checkDetails(details) {
+ let url = new URL(details.url);
+ let filename = url.pathname.split("/").pop();
+ ok(expected.hasOwnProperty(filename), `Should be expecting a request for ${filename}`);
+ let expect = expected[filename];
+ is(expect.type, details.type, `${details.type} type matches`);
+ is(expect.frameAncestor_length, details.frameAncestors.length, "incorrect frameAncestors length");
+ if (filename == "file_contains_img.html") {
+ is(details.frameAncestors[0].frameId, details.parentFrameId,
+ "frameAncestors[0] should match parentFrameId");
+ expected["file_image_good.png"].frameId = details.frameId;
+ } else if (filename == "file_image_good.png") {
+ is(details.frameAncestors[0].frameId, details.parentFrameId,
+ "frameAncestors[0] should match parentFrameId");
+ is(details.frameId, expect.frameId,
+ "frameId for image and iframe should match");
+ }
+}
+
+add_task(async () => {
+ // Clear the image cache, since it gets in the way otherwise.
+ let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
+ let cache = imgTools.getImgCacheForDocument(document);
+ cache.clearCache(false);
+ await SpecialPowers.spawnChrome([], async () => {
+ Services.cache2.clear();
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "<all_urls>"],
+ },
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ browser.test.sendMessage("onBeforeRequest", details);
+ },
+ {
+ urls: [
+ "http://example.org/*/file_contains_img.html",
+ "http://mochi.test/*/file_contains_iframe.html",
+ "*://*/*.png",
+ ],
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ const FILE_URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html";
+ let win = window.open(FILE_URL);
+ await new Promise(resolve => win.addEventListener("load", () => resolve(), {once: true}));
+
+ for (let i = 0; i < Object.keys(expected).length; i++) {
+ checkDetails(await extension.awaitMessage("onBeforeRequest"));
+ }
+
+ win.close();
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_contentscript() {
+ function background() {
+ browser.runtime.onMessage.addListener(([script], sender) => {
+ browser.test.sendMessage("run", {script});
+ browser.test.sendMessage("run-" + script);
+ });
+ browser.test.sendMessage("running");
+ }
+
+ function contentScriptAll() {
+ browser.runtime.sendMessage(["all"]);
+ }
+ function contentScriptIncludesTest1() {
+ browser.runtime.sendMessage(["includes-test1"]);
+ }
+ function contentScriptExcludesTest1() {
+ browser.runtime.sendMessage(["excludes-test1"]);
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["https://example.org/", "https://*.example.org/"],
+ "exclude_globs": [],
+ "include_globs": ["*"],
+ "js": ["content_script_all.js"],
+ },
+ {
+ "matches": ["https://example.org/", "https://*.example.org/"],
+ "include_globs": ["*test1*"],
+ "js": ["content_script_includes_test1.js"],
+ },
+ {
+ "matches": ["https://example.org/", "https://*.example.org/"],
+ "exclude_globs": ["*test1*"],
+ "js": ["content_script_excludes_test1.js"],
+ },
+ ],
+ },
+ background,
+
+ files: {
+ "content_script_all.js": contentScriptAll,
+ "content_script_includes_test1.js": contentScriptIncludesTest1,
+ "content_script_excludes_test1.js": contentScriptExcludesTest1,
+ },
+
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let ran = 0;
+ extension.onMessage("run", ({script}) => {
+ ran++;
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ info("extension loaded");
+
+ let win = window.open("https://example.org/");
+ await Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-excludes-test1")]);
+ win.close();
+ is(ran, 2);
+
+ win = window.open("https://test1.example.org/");
+ await Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-includes-test1")]);
+ win.close();
+ is(ran, 4);
+
+ await extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test moz-extension iframe messaging</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+
+add_task(async function test_moz_extension_iframe_messaging() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.content_web_accessible.enabled", true],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ js: ["cs.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ web_accessible_resources: ["iframe.html"],
+ permissions: ["tabs"],
+ },
+ files: {
+ "cs.js"() {
+ let iframe = document.createElement("iframe");
+ iframe.src = browser.runtime.getURL("iframe.html");
+ document.body.append(iframe);
+ },
+
+ "iframe.html": `<!doctype html><script src=iframe.js><\/script>`,
+ async "iframe.js"() {
+ browser.runtime.onMessage.addListener(async msg => {
+ browser.test.assertEq(msg, "from-background", "Correct message.");
+ return "iframe-response";
+ });
+
+ browser.runtime.onConnect.addListener(async port => {
+ port.postMessage("port-message");
+ });
+
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage("from-iframe"),
+ "Could not establish connection. Receiving end does not exist.",
+ "No onMessage listener in the background."
+ );
+
+ await new Promise(resolve => {
+ let port = browser.runtime.connect();
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ port.error.message,
+ "Could not establish connection. Receiving end does not exist.",
+ "No onConnect listener in the background."
+ );
+ resolve();
+ })
+ });
+
+ // TODO: If/when the tabs API is available from extension iframes, test
+ // that it won't send a message to itself via browser.tabs.sendMessage()
+ browser.test.assertEq(browser.tabs, undefined, "No tabs API");
+
+ browser.test.sendMessage("iframe-done");
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(async msg => {
+
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage("from-background"),
+ "Could not establish connection. Receiving end does not exist.",
+ "No onMessage listener in another extension page."
+ );
+
+ await new Promise(resolve => {
+ let port = browser.runtime.connect();
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ port.error.message,
+ "Could not establish connection. Receiving end does not exist.",
+ "No onConnect listener in another extension page."
+ );
+ resolve();
+ })
+ });
+
+ let [tab] = await browser.tabs.query({
+ url: "http://mochi.test/*/file_sample.html",
+ });
+ let res = await browser.tabs.sendMessage(tab.id, "from-background");
+ browser.test.assertEq(res, "iframe-response", "Correct response.");
+
+ let port = browser.tabs.connect(tab.id);
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "port-message", "Correct port message.");
+ browser.test.notifyPass("done");
+ });
+ })
+ }
+ });
+
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+ await extension.awaitMessage("iframe-done");
+
+ extension.sendMessage("run-background");
+ await extension.awaitFinish("done");
+ win.close();
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension external messaging</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function backgroundScript(id, otherId) {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.fail(`Got unexpected message: ${uneval(msg)} ${uneval(sender)}`);
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.fail(`Got unexpected connection: ${uneval(port.sender)}`);
+ });
+
+ browser.runtime.onMessageExternal.addListener((msg, sender) => {
+ browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`);
+ browser.test.assertEq(`helo-${id}`, msg, "Got expected message");
+
+ browser.test.sendMessage("onMessage-done");
+
+ return Promise.resolve(`ehlo-${otherId}`);
+ });
+
+ browser.runtime.onConnectExternal.addListener(port => {
+ browser.test.assertEq(otherId, port.sender.id, `${id}: Got expected external connecter ID`);
+
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(`helo-${id}`, msg, "Got expected port message");
+
+ port.postMessage(`ehlo-${otherId}`);
+
+ browser.test.sendMessage("onConnect-done");
+ });
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "go") {
+ browser.runtime.sendMessage(otherId, `helo-${otherId}`).then(result => {
+ browser.test.assertEq(`ehlo-${id}`, result, "Got expected reply");
+ browser.test.sendMessage("sendMessage-done");
+ });
+
+ let port = browser.runtime.connect(otherId);
+ port.postMessage(`helo-${otherId}`);
+
+ port.onMessage.addListener(msg => {
+ port.disconnect();
+
+ browser.test.assertEq(msg, `ehlo-${id}`, "Got expected port reply");
+ browser.test.sendMessage("connect-done");
+ });
+ }
+ });
+}
+
+function makeExtension(id, otherId) {
+ let args = `${JSON.stringify(id)}, ${JSON.stringify(otherId)}`;
+
+ let extensionData = {
+ background: `(${backgroundScript})(${args})`,
+ manifest: {
+ browser_specific_settings: {gecko: {id}},
+ },
+ };
+
+ return ExtensionTestUtils.loadExtension(extensionData);
+}
+
+add_task(async function test_contentscript() {
+ const ID1 = "foo-message@mochitest.mozilla.org";
+ const ID2 = "bar-message@mochitest.mozilla.org";
+
+ let extension1 = makeExtension(ID1, ID2);
+ let extension2 = makeExtension(ID2, ID1);
+
+ await Promise.all([extension1.startup(), extension2.startup()]);
+
+ extension1.sendMessage("go");
+ extension2.sendMessage("go");
+
+ await Promise.all([
+ extension1.awaitMessage("sendMessage-done"),
+ extension2.awaitMessage("sendMessage-done"),
+
+ extension1.awaitMessage("onMessage-done"),
+ extension2.awaitMessage("onMessage-done"),
+
+ extension1.awaitMessage("connect-done"),
+ extension2.awaitMessage("connect-done"),
+
+ extension1.awaitMessage("onConnect-done"),
+ extension2.awaitMessage("onConnect-done"),
+ ]);
+
+ await extension1.unload();
+ await extension2.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for generating WebExtensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.test.log("running background script");
+
+ browser.test.onMessage.addListener((x, y) => {
+ browser.test.assertEq(x, 10, "x is 10");
+ browser.test.assertEq(y, 20, "y is 20");
+
+ browser.test.notifyPass("background test passed");
+ });
+
+ browser.test.sendMessage("running", 1);
+}
+
+let extensionData = {
+ background,
+};
+
+add_task(async function test_background() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ info("load complete");
+ let [, x] = await Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ is(x, 1, "got correct value from extension");
+ info("startup complete");
+ extension.sendMessage(10, 20);
+ await extension.awaitFinish();
+ info("test complete");
+ await extension.unload();
+ info("extension unloaded successfully");
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+add_task(async function test_geolocation_nopermission() {
+ let GEO_URL = "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs";
+ await SpecialPowers.pushPrefEnv({"set": [["geo.provider.network.url", GEO_URL]]});
+});
+
+add_task(async function test_geolocation() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "geolocation",
+ ],
+ },
+ background() {
+ navigator.geolocation.getCurrentPosition(() => {
+ browser.test.notifyPass("success geolocation call");
+ }, (error) => {
+ browser.test.notifyFail(`geolocation call ${error}`);
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function test_geolocation_nopermission() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ navigator.geolocation.getCurrentPosition(() => {
+ browser.test.notifyFail("success geolocation call");
+ }, (error) => {
+ browser.test.notifyPass(`geolocation call ${error}`);
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function test_geolocation_prompt() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.tabs.create({url: "tab.html"});
+ },
+ files: {
+ "tab.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="tab.js"><\/script>
+ </head></html>`,
+ "tab.js": () => {
+ navigator.geolocation.getCurrentPosition(() => {
+ browser.test.notifyPass("success geolocation call");
+ }, (error) => {
+ browser.test.notifyFail(`geolocation call ${error}`);
+ });
+ },
+ },
+ });
+
+ // Bypass the actual prompt, but the prompt result is to allow access.
+ await SpecialPowers.pushPrefEnv({"set": [["geo.prompt.testing", true], ["geo.prompt.testing.allow", true]]});
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+</script>
+</head>
+<body>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebExtension Identity</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webextensions.identity.redirectDomain", "example.com"],
+ // Disable the network cache first-party partition during this
+ // test (TODO: look more closely to how that is affecting the intermittency
+ // of this test on MacOS, see Bug 1626482).
+ ["privacy.partition.network_state", false],
+ ],
+ });
+});
+
+add_task(async function test_noPermission() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.assertEq(
+ undefined,
+ browser.identity,
+ "No identity api without permission"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_getRedirectURL() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "identity@mozilla.org",
+ },
+ },
+ permissions: ["identity", "https://example.com/"],
+ },
+ async background() {
+ let redirect_base =
+ "https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/";
+ await browser.test.assertEq(
+ redirect_base,
+ browser.identity.getRedirectURL(),
+ "redirect url ok"
+ );
+ await browser.test.assertEq(
+ redirect_base,
+ browser.identity.getRedirectURL(""),
+ "redirect url ok"
+ );
+ await browser.test.assertEq(
+ redirect_base + "foobar",
+ browser.identity.getRedirectURL("foobar"),
+ "redirect url ok"
+ );
+ await browser.test.assertEq(
+ redirect_base + "callback",
+ browser.identity.getRedirectURL("/callback"),
+ "redirect url ok"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_badAuthURI() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["identity", "https://example.com/"],
+ },
+ async background() {
+ for (let url of [
+ "foobar",
+ "about:addons",
+ "about:blank",
+ "ftp://example.com/test",
+ ]) {
+ await browser.test.assertThrows(
+ () => {
+ browser.identity.launchWebAuthFlow({ interactive: true, url });
+ },
+ /Type error for parameter details/,
+ "details.url is invalid"
+ );
+ }
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_badRequestURI() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["identity", "https://example.com/"],
+ },
+ async background() {
+ let base_uri =
+ "https://example.com/tests/toolkit/components/extensions/test/mochitest/";
+ let url = `${base_uri}?redirect_uri=badrobot}`;
+ await browser.test.assertRejects(
+ browser.identity.launchWebAuthFlow({ interactive: true, url }),
+ "redirect_uri is invalid",
+ "invalid redirect url"
+ );
+ url = `${base_uri}?redirect_uri=https://somesite.com`;
+ await browser.test.assertRejects(
+ browser.identity.launchWebAuthFlow({ interactive: true, url }),
+ "redirect_uri not allowed",
+ "invalid redirect url"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function background_launchWebAuthFlow_requires_interaction() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["identity", "https://example.com/"],
+ },
+ async background() {
+ let base_uri =
+ "https://example.com/tests/toolkit/components/extensions/test/mochitest/";
+ let url = `${base_uri}?redirect_uri=${browser.identity.getRedirectURL(
+ "redirect"
+ )}`;
+ await browser.test.assertRejects(
+ browser.identity.launchWebAuthFlow({ interactive: false, url }),
+ "Requires user interaction",
+ "Rejects on required user interaction"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+function background_launchWebAuthFlow({
+ interactive = false,
+ path = "redirect_auto.sjs",
+ params = {},
+ redirect = true,
+ useRedirectUri = true,
+} = {}) {
+ let uri_path = useRedirectUri ? "identity_cb" : "";
+ let expected_redirect = `https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/${uri_path}`;
+ let base_uri =
+ "https://example.com/tests/toolkit/components/extensions/test/mochitest/";
+ let redirect_uri = browser.identity.getRedirectURL(
+ useRedirectUri ? uri_path : undefined
+ );
+ browser.test.assertEq(
+ expected_redirect,
+ redirect_uri,
+ "expected redirect uri matches hash"
+ );
+ let url = `${base_uri}${path}`;
+ if (useRedirectUri) {
+ params.redirect_uri = redirect_uri;
+ } else {
+ // We kind of fake it with the redirect url that would normally be configured
+ // in the oauth service. This does still test that the identity service falls back
+ // to the extensions redirect url.
+ params.default_redirect = expected_redirect;
+ }
+ if (!redirect) {
+ params.no_redirect = 1;
+ }
+ let query = [];
+ for (let [param, value] of Object.entries(params)) {
+ query.push(`${param}=${encodeURIComponent(value)}`);
+ }
+ url = `${url}?${query.join("&")}`;
+
+ // Ensure we do not start the actual request for the redirect url. In the case
+ // of a 303 POST redirect we are getting a request started.
+ let watchRedirectRequest = () => {};
+ if (params.post !== 303) {
+ watchRedirectRequest = details => {
+ if (details.url.startsWith(expected_redirect)) {
+ browser.test.fail(`onBeforeRequest called for redirect url: ${JSON.stringify(details)}`);
+ }
+ };
+
+ browser.webRequest.onBeforeRequest.addListener(
+ watchRedirectRequest,
+ {
+ urls: [
+ "https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/*",
+ ],
+ }
+ );
+ }
+
+ browser.identity
+ .launchWebAuthFlow({ interactive, url })
+ .then(redirectURL => {
+ browser.test.assertTrue(
+ redirectURL.startsWith(redirect_uri),
+ `correct redirect url ${redirectURL}`
+ );
+ if (redirect) {
+ let url = new URL(redirectURL);
+ browser.test.assertEq(
+ "here ya go",
+ url.searchParams.get("access_token"),
+ "Handled auto redirection"
+ );
+ }
+ })
+ .catch(error => {
+ if (redirect) {
+ browser.test.fail(error.message);
+ } else {
+ browser.test.assertEq(
+ "Requires user interaction",
+ error.message,
+ "Auth page loaded, interaction required."
+ );
+ }
+ }).then(() => {
+ browser.webRequest.onBeforeRequest.removeListener(watchRedirectRequest);
+ browser.test.sendMessage("done");
+ });
+}
+
+// Tests the situation where the oauth provider has already granted access and
+// simply redirects the oauth client to provide the access key or code.
+add_task(async function test_autoRedirect() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "identity@mozilla.org",
+ },
+ },
+ permissions: ["webRequest", "identity", "https://*.example.com/*"],
+ },
+ background: `(${background_launchWebAuthFlow})()`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_autoRedirect_noRedirectURI() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "identity@mozilla.org",
+ },
+ },
+ permissions: ["webRequest", "identity", "https://*.example.com/*"],
+ },
+ background: `(${background_launchWebAuthFlow})({useRedirectUri: false})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+// Tests the situation where the oauth provider has not granted access and interactive=false
+add_task(async function test_noRedirect() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "identity@mozilla.org",
+ },
+ },
+ permissions: ["webRequest", "identity", "https://*.example.com/*"],
+ },
+ background: `(${background_launchWebAuthFlow})({redirect: false})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+// Tests the situation where the oauth provider must show a window where
+// presumably the user interacts, then the redirect occurs and access key or
+// code is provided. We bypass any real interaction, but want the window to
+// open and result in a redirect.
+add_task(async function test_interaction() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "identity@mozilla.org",
+ },
+ },
+ permissions: ["webRequest", "identity", "https://*.example.com/*"],
+ },
+ background: `(${background_launchWebAuthFlow})({interactive: true, path: "oauth.html"})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+// Tests the situation where the oauth provider redirects with a 303.
+add_task(async function test_auto303Redirect() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "identity@mozilla.org",
+ },
+ },
+ permissions: ["webRequest", "identity", "https://*.example.com/*"],
+ },
+ background: `(${background_launchWebAuthFlow})({interactive: true, path: "oauth.html", params: {post: 303, server_uri: "redirect_auto.sjs"}})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_loopbackRedirectURI() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "identity@mozilla.org",
+ },
+ },
+ permissions: ["identity"],
+ },
+ async background() {
+ let redirectURL = "http://127.0.0.1/mozoauth2/35b64b676900f491c00e7f618d43f7040e88422e";
+ let actualRedirect = await browser.identity.launchWebAuthFlow({
+ interactive: true,
+ url: `https://example.com/tests/toolkit/components/extensions/test/mochitest/oauth.html?redirect_uri=${encodeURIComponent(redirectURL)}`
+ }).catch(error => {
+ browser.test.fail(error.message)
+ });
+ browser.test.assertTrue(
+ actualRedirect.startsWith(redirectURL),
+ "Expected redirect url to be loopback address"
+ )
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function testWithRealIdleService() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ let detectionInterval = args[0];
+ if (msg == "addListener") {
+ let status = await browser.idle.queryState(detectionInterval);
+ browser.test.assertEq("active", status, "Idle status is active");
+ browser.idle.setDetectionInterval(detectionInterval);
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.test.sendMessage("listenerAdded");
+ } else if (msg == "checkState") {
+ let status = await browser.idle.queryState(detectionInterval);
+ browser.test.assertEq("idle", status, "Idle status is idle");
+ browser.test.notifyPass("idle");
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ await extension.startup();
+
+ let chromeScript = loadChromeScript(() => {
+ const {sendAsyncMessage} = this;
+ const idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService(Ci.nsIUserIdleService);
+ let idleTime = idleService.idleTime;
+ sendAsyncMessage("detectionInterval", Math.max(Math.ceil(idleTime / 1000) + 10, 15));
+ });
+ let detectionInterval = await chromeScript.promiseOneMessage("detectionInterval");
+ chromeScript.destroy();
+
+ info(`Setting interval to ${detectionInterval}`);
+ extension.sendMessage("addListener", detectionInterval);
+ await extension.awaitMessage("listenerAdded");
+ info("Listener added");
+ await extension.awaitMessage("listenerFired");
+ info("Listener fired");
+ extension.sendMessage("checkState", detectionInterval);
+ await extension.awaitFinish("idle");
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_in_incognito_context_true() {
+ function background() {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(true, msg, "inIncognitoContext is true");
+ browser.test.notifyPass("inIncognitoContext");
+ });
+
+ browser.windows.create({url: browser.runtime.getURL("/tab.html"), incognito: true});
+ }
+
+ function tabScript() {
+ browser.runtime.sendMessage(browser.extension.inIncognitoContext);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ files: {
+ "tab.js": tabScript,
+ "tab.html": `<!DOCTYPE html><html><head>
+ <meta charset="utf-8">
+ <script src="tab.js"><\/script>
+ </head></html>`,
+ },
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("inIncognitoContext");
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_listener_proxies() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ "permissions": ["storage"],
+ },
+
+ async background() {
+ // Test that adding multiple listeners for the same event works as
+ // expected.
+
+ let awaitChanged = () => new Promise(resolve => {
+ browser.storage.onChanged.addListener(function listener() {
+ browser.storage.onChanged.removeListener(listener);
+ resolve();
+ });
+ });
+
+ let promises = [
+ awaitChanged(),
+ awaitChanged(),
+ ];
+
+ function removedListener() {}
+ browser.storage.onChanged.addListener(removedListener);
+ browser.storage.onChanged.removeListener(removedListener);
+
+ promises.push(awaitChanged(), awaitChanged());
+
+ browser.storage.local.set({foo: "bar"});
+
+ await Promise.all(promises);
+
+ browser.test.notifyPass("onchanged-listeners");
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("onchanged-listeners");
+
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for opening links in new tabs from extension frames</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function promiseObserved(topic, check) {
+ return new Promise(resolve => {
+ let obs = SpecialPowers.Services.obs;
+
+ function observer(subject, topic, data) {
+ subject = SpecialPowers.wrap(subject);
+ if (check(subject, data)) {
+ obs.removeObserver(observer, topic);
+ resolve({subject, data});
+ }
+ }
+ obs.addObserver(observer, topic);
+ });
+}
+
+add_task(async function test_target_blank_link_no_opener_from_privileged() {
+ const linkURL = "https://example.com/";
+
+ function extension_tab() {
+ document.getElementById("link").click();
+ }
+
+ function content_script() {
+ browser.runtime.sendMessage("content_page_loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ js: ["content_script.js"],
+ matches: ["https://example.com/*"],
+ run_at: "document_idle",
+ }],
+ permissions: ["tabs"],
+ },
+ files: {
+ "page.html": `<!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></html>
+ <body>
+ <a href="${linkURL}" target="_blank" id="link">link</a>
+ <script src="extension_tab.js"><\/script>
+ </body>
+ </html>`,
+ "extension_tab.js": extension_tab,
+ "content_script.js": content_script,
+ },
+ async background() {
+ let pageTab;
+ browser.test.onMessage.addListener(async (msg) => {
+ if (msg !== "close_tab") {
+ browser.test.fail("Unexpected test message: " + msg);
+ return;
+ }
+ if (!pageTab) {
+ browser.test.fail("Unexpected close-tab test message received when there is no pageTab");
+ return;
+ }
+ await browser.tabs.remove(pageTab.id);
+ browser.test.sendMessage("close_tab_done");
+ });
+ browser.runtime.onMessage.addListener(async (msg, sender) => {
+ if (sender.tab) {
+ await browser.tabs.remove(sender.tab.id);
+ browser.test.sendMessage(msg, sender.tab.url);
+ }
+ });
+ pageTab = await browser.tabs.create({ url: browser.runtime.getURL("page.html") });
+ browser.test.sendMessage("tab_created");
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("tab_created");
+
+ // Make sure page is loaded correctly
+ const url = await extension.awaitMessage("content_page_loaded");
+ is(url, linkURL, "Page URL should match");
+
+ // Clean up opened tab.
+ extension.sendMessage("close_tab");
+ await extension.awaitMessage("close_tab_done");
+
+ await extension.unload();
+});
+
+add_task(async function test_target_blank_link() {
+ const linkURL = "http://mochi.test:8888/tests/toolkit/";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_security_policy: "script-src 'self' 'unsafe-eval'; object-src 'self';",
+
+ web_accessible_resources: ["iframe.html"],
+ },
+ files: {
+ "iframe.html": `<!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></html>
+ <body>
+ <a href="${linkURL}" target="_blank" id="link" rel="opener">link</a>
+ </body>
+ </html>`,
+ },
+ background() {
+ browser.test.sendMessage("frame_url", browser.runtime.getURL("iframe.html"));
+ },
+ });
+
+ await extension.startup();
+
+ let url = await extension.awaitMessage("frame_url");
+
+ let iframe = document.createElement("iframe");
+ iframe.src = url;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => iframe.addEventListener("load", () => setTimeout(resolve, 0), {once: true}));
+
+ let win = SpecialPowers.wrap(iframe).contentWindow;
+
+ {
+ // Flush layout so that synthesizeMouseAtCenter on a cross-origin iframe
+ // works as expected.
+ document.body.getBoundingClientRect();
+
+ let promise = promiseObserved("document-element-inserted", doc => doc.documentURI === linkURL);
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ this.content.document.getElementById("link").click();
+ });
+
+ let {subject: doc} = await promise;
+ info("Link opened");
+ doc.defaultView.close();
+ info("Window closed");
+ }
+
+ {
+ let promise = promiseObserved("document-element-inserted", doc => doc.documentURI === linkURL);
+
+ let res = win.eval(`window.open("${linkURL}")`);
+ let {subject: doc} = await promise;
+ is(SpecialPowers.unwrap(res), SpecialPowers.unwrap(doc.defaultView), "window.open worked as expected");
+
+ doc.defaultView.close();
+ }
+
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for notifications</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head_notifications.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// A 1x1 PNG image.
+// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+add_task(async function setup_mock_alert_service() {
+ await MockAlertsService.register();
+});
+
+add_task(async function test_notification() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ let id = await browser.notifications.create(opts);
+
+ browser.test.sendMessage("running", id);
+ browser.test.notifyPass("background test passed");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ await extension.startup();
+ let x = await extension.awaitMessage("running");
+ is(x, "0", "got correct id from notifications.create");
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function test_notification_events() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ let createdId = "98";
+
+ // Test an ignored listener.
+ browser.notifications.onButtonClicked.addListener(function() {});
+
+ // We cannot test onClicked listener without a mock
+ // but we can attempt to add a listener.
+ browser.notifications.onClicked.addListener(async function(id) {
+ browser.test.assertEq(createdId, id, "onClicked has the expected ID");
+ browser.test.sendMessage("notification-event", "clicked");
+ });
+
+ browser.notifications.onShown.addListener(async function listener(id) {
+ browser.test.assertEq(createdId, id, "onShown has the expected ID");
+ browser.test.sendMessage("notification-event", "shown");
+ });
+
+ browser.test.onMessage.addListener(async function(msg, expectedCount) {
+ if (msg === "create-again") {
+ let newId = await browser.notifications.create(createdId, opts);
+ browser.test.assertEq(createdId, newId, "create returned the expected id.");
+ browser.test.sendMessage("notification-created-twice");
+ } else if (msg === "check-count") {
+ let notifications = await browser.notifications.getAll();
+ let ids = Object.keys(notifications);
+ browser.test.assertEq(expectedCount, ids.length, `getAll() = ${ids}`);
+ browser.test.sendMessage("check-count-result");
+ }
+ });
+
+ // Test onClosed listener.
+ browser.notifications.onClosed.addListener(function listener(id) {
+ browser.test.assertEq(createdId, id, "onClosed received the expected id.");
+ browser.test.sendMessage("notification-event", "closed");
+ });
+
+ await browser.notifications.create(createdId, opts);
+
+ browser.test.sendMessage("notification-created-once");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ async function waitForNotificationEvent(name) {
+ info(`Waiting for notification event: ${name}`);
+ is(name, await extension.awaitMessage("notification-event"),
+ "Expected notification event");
+ }
+ async function checkNotificationCount(expectedCount) {
+ extension.sendMessage("check-count", expectedCount);
+ await extension.awaitMessage("check-count-result");
+ }
+
+ await extension.awaitMessage("notification-created-once");
+ await waitForNotificationEvent("shown");
+ await checkNotificationCount(1);
+
+ // On most platforms, clicking the notification closes it.
+ // But on macOS, the notification can repeatedly be clicked without closing.
+ await MockAlertsService.clickNotificationsWithoutClose();
+ await waitForNotificationEvent("clicked");
+ await checkNotificationCount(1);
+ await MockAlertsService.clickNotificationsWithoutClose();
+ await waitForNotificationEvent("clicked");
+ await checkNotificationCount(1);
+ await MockAlertsService.clickNotifications();
+ await waitForNotificationEvent("clicked");
+ await waitForNotificationEvent("closed");
+ await checkNotificationCount(0);
+
+ extension.sendMessage("create-again");
+ await extension.awaitMessage("notification-created-twice");
+ await waitForNotificationEvent("shown");
+ await checkNotificationCount(1);
+
+ await MockAlertsService.closeNotifications();
+ await waitForNotificationEvent("closed");
+ await checkNotificationCount(0);
+
+ await extension.unload();
+});
+
+add_task(async function test_notification_clear() {
+ function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ let createdId = "99";
+
+ browser.notifications.onShown.addListener(async id => {
+ browser.test.assertEq(createdId, id, "onShown received the expected id.");
+ let wasCleared = await browser.notifications.clear(id);
+ browser.test.assertTrue(wasCleared, "notifications.clear returned true.");
+ });
+
+ browser.notifications.onClosed.addListener(id => {
+ browser.test.assertEq(createdId, id, "onClosed received the expected id.");
+ browser.test.notifyPass("background test passed");
+ });
+
+ browser.notifications.create(createdId, opts);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(async function test_notifications_empty_getAll() {
+ async function background() {
+ let notifications = await browser.notifications.getAll();
+
+ browser.test.assertEq("object", typeof notifications, "getAll() returned an object");
+ browser.test.assertEq(0, Object.keys(notifications).length, "the object has no properties");
+ browser.test.notifyPass("getAll empty");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ await extension.startup();
+ await extension.awaitFinish("getAll empty");
+ await extension.unload();
+});
+
+add_task(async function test_notifications_populated_getAll() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ iconUrl: "a.png",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ await browser.notifications.create("p1", opts);
+ await browser.notifications.create("p2", opts);
+ let notifications = await browser.notifications.getAll();
+
+ browser.test.assertEq("object", typeof notifications, "getAll() returned an object");
+ browser.test.assertEq(2, Object.keys(notifications).length, "the object has 2 properties");
+
+ for (let notificationId of ["p1", "p2"]) {
+ for (let key of Object.keys(opts)) {
+ browser.test.assertEq(
+ opts[key],
+ notifications[notificationId][key],
+ `the notification has the expected value for option: ${key}`
+ );
+ }
+ }
+
+ browser.test.notifyPass("getAll populated");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ files: {
+ "a.png": IMAGE_ARRAYBUFFER,
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("getAll populated");
+ await extension.unload();
+});
+
+add_task(async function test_buttons_unsupported() {
+ function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ buttons: [{title: "Button title"}],
+ };
+
+ let exception = {};
+ try {
+ browser.notifications.create(opts);
+ } catch (e) {
+ exception = e;
+ }
+
+ browser.test.assertTrue(
+ String(exception).includes('Property "buttons" is unsupported by Firefox'),
+ "notifications.create with buttons option threw an expected exception"
+ );
+ browser.test.notifyPass("buttons-unsupported");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ await extension.startup();
+ await extension.awaitFinish("buttons-unsupported");
+ await extension.unload();
+});
+
+add_task(async function test_notifications_different_contexts() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ let id = await browser.notifications.create(opts);
+
+ browser.runtime.onMessage.addListener(async (message, sender) => {
+ await browser.tabs.remove(sender.tab.id);
+
+ // We should be able to clear the notification after creating and
+ // destroying the tab.html page.
+ let wasCleared = await browser.notifications.clear(id);
+ browser.test.assertTrue(wasCleared, "The notification was cleared.");
+ browser.test.notifyPass("notifications");
+ });
+
+ browser.tabs.create({url: browser.runtime.getURL("/tab.html")});
+ }
+
+ async function tabScript() {
+ // We should be able to see the notification created in the background page
+ // in this page.
+ let notifications = await browser.notifications.getAll();
+ browser.test.assertEq(1, Object.keys(notifications).length,
+ "One notification found.");
+ browser.runtime.sendMessage("continue-test");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ files: {
+ "tab.js": tabScript,
+ "tab.html": `<!DOCTYPE html><html><head>
+ <meta charset="utf-8">
+ <script src="tab.js"><\/script>
+ </head></html>`,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("notifications");
+ await extension.unload();
+});
+
+add_task(async function teardown_mock_alert_service() {
+ await MockAlertsService.unregister();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>optional permissions and preloaded processes</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextOptionalPermissionPrompts", false]],
+ });
+});
+
+// This test case verifies that newly granted optional permissions are
+// propagated to all processes, especially preloaded processes.
+add_task(async function test_optional_permissions_should_be_propagated() {
+ let anOptionalPermission = "*://example.org/*";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "scripting",
+ "*://example.com/*",
+ ],
+ optional_permissions: [anOptionalPermission],
+ },
+ async background() {
+ browser.test.onMessage.addListener(async (msg, value) => {
+ browser.test.assertEq("grant-permission", msg, "expected message");
+
+ let granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(browser.permissions.request(value));
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+ browser.test.sendMessage("permission-granted");
+ });
+
+ await browser.scripting.registerContentScripts([
+ {
+ id: "script",
+ js: ["script.js"],
+ matches: ["*://example.com/*", "*://example.org/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.sendMessage("background-ready");
+ },
+ files: {
+ "script.js": () => {
+ browser.test.sendMessage("script-ran", window.location.host);
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "http://example.com/",
+ true
+ );
+ let host = await extension.awaitMessage("script-ran");
+ is(host, "example.com", "expected host: example.com");
+ await AppTestDelegate.removeTab(window, tab);
+
+ extension.sendMessage("grant-permission", {
+ origins: ["*://example.org/*"],
+ });
+ await extension.awaitMessage("permission-granted");
+
+ tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://example.org/",
+ true
+ );
+ host = await extension.awaitMessage("script-ran");
+ is(host, "example.org", "expected host: example.org");
+ await AppTestDelegate.removeTab(window, tab);
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for protocol handlers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+function protocolChromeScript() {
+ const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
+ const PERMISSION_KEY_DELIMITER = "^";
+
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("setup", ({ protocol, principalOrigins }) => {
+ let data = {};
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo(protocol);
+ data.preferredAction = protoInfo.preferredAction == protoInfo.useHelperApp;
+
+ let handlers = protoInfo.possibleApplicationHandlers;
+ data.handlers = handlers.length;
+
+ let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp);
+ data.isWebHandler = handler instanceof Ci.nsIWebHandlerApp;
+ data.uriTemplate = handler.uriTemplate;
+
+ // ext+ protocols should be set as default when there is only one
+ data.preferredApplicationHandler =
+ protoInfo.preferredApplicationHandler == handler;
+ data.alwaysAskBeforeHandling = protoInfo.alwaysAskBeforeHandling;
+ const handlerSvc = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+ ].getService(Ci.nsIHandlerService);
+ handlerSvc.store(protoInfo);
+
+ for (let origin of principalOrigins) {
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(origin),
+ {}
+ );
+ let pbPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(origin),
+ {
+ privateBrowsingId: 1,
+ }
+ );
+ let permKey =
+ PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER + protocol;
+ Services.perms.addFromPrincipal(
+ principal,
+ permKey,
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+ Services.perms.addFromPrincipal(
+ pbPrincipal,
+ permKey,
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+ }
+
+ sendAsyncMessage("handlerData", data);
+ });
+ addMessageListener("setPreferredAction", data => {
+ let { protocol, template } = data;
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo(protocol);
+
+ for (let handler of protoInfo.possibleApplicationHandlers.enumerate()) {
+ if (handler.uriTemplate.startsWith(template)) {
+ protoInfo.preferredApplicationHandler = handler;
+ protoInfo.preferredAction = protoInfo.useHelperApp;
+ protoInfo.alwaysAskBeforeHandling = false;
+ }
+ }
+ const handlerSvc = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+ ].getService(Ci.nsIHandlerService);
+ handlerSvc.store(protoInfo);
+ sendAsyncMessage("set");
+ });
+}
+
+add_task(async function test_protocolHandler() {
+ let extensionData = {
+ manifest: {
+ protocol_handlers: [
+ {
+ protocol: "ext+foo",
+ name: "a foo protocol handler",
+ uriTemplate: "foo.html?val=%s",
+ },
+ ],
+ },
+
+ background() {
+ browser.test.onMessage.addListener(async (msg, arg) => {
+ if (msg == "open") {
+ let tab = await browser.tabs.create({ url: arg });
+ browser.test.sendMessage("opened", tab.id);
+ } else if (msg == "close") {
+ await browser.tabs.remove(arg);
+ browser.test.sendMessage("closed");
+ }
+ });
+ browser.test.sendMessage("test-url", browser.runtime.getURL("foo.html"));
+ },
+
+ files: {
+ "foo.js": function() {
+ browser.test.sendMessage("test-query", location.search);
+ browser.tabs.getCurrent().then(tab => browser.test.sendMessage("test-tab", tab.id));
+ },
+ "foo.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="foo.js"><\/script>
+ </head>
+ </html>`,
+ },
+ };
+
+ let pb_extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.onMessage.addListener(async (msg, arg) => {
+ if (msg == "open") {
+ let win = await browser.windows.create({ url: arg, incognito: true });
+ browser.test.sendMessage("opened", {
+ windowId: win.id,
+ tabId: win.tabs[0].id,
+ });
+ } else if (msg == "nav") {
+ await browser.tabs.update(arg.tabId, { url: arg.url });
+ browser.test.sendMessage("navigated");
+ } else if (msg == "close") {
+ await browser.windows.remove(arg);
+ browser.test.sendMessage("closed");
+ }
+ });
+ },
+ incognitoOverride: "spanning",
+ });
+ await pb_extension.startup();
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ let handlerUrl = await extension.awaitMessage("test-url");
+
+ // Ensure that the protocol handler is configured, and set it as default to
+ // bypass the dialog.
+ let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript);
+
+ let msg = chromeScript.promiseOneMessage("handlerData");
+ chromeScript.sendAsyncMessage("setup", {
+ protocol: "ext+foo",
+ principalOrigins: [
+ `moz-extension://${extension.uuid}/`,
+ `moz-extension://${pb_extension.uuid}/`,
+ ],
+ });
+ let data = await msg;
+ ok(
+ data.preferredAction,
+ "using a helper application is the preferred action"
+ );
+ ok(data.preferredApplicationHandler, "handler was set as default handler");
+ is(data.handlers, 1, "one handler is set");
+ ok(!data.alwaysAskBeforeHandling, "will not show dialog");
+ ok(data.isWebHandler, "the handler is a web handler");
+ is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template");
+ chromeScript.destroy();
+
+ extension.sendMessage("open", "ext+foo:test");
+ let id = await extension.awaitMessage("opened");
+
+ let query = await extension.awaitMessage("test-query");
+ is(query, "?val=ext%2Bfoo%3Atest", "test query ok");
+ is(id, await extension.awaitMessage("test-tab"), "id should match opened tab");
+
+ extension.sendMessage("close", id);
+ await extension.awaitMessage("closed");
+
+ // Test that handling a URL from the commandline works.
+ chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ );
+ let fakeCmdLine = Cu.createCommandLine(
+ ["-url", "ext+foo:cmdline"],
+ null,
+ Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
+ );
+ cmdLineHandler.handle(fakeCmdLine);
+ });
+ query = await extension.awaitMessage("test-query");
+ is(query, "?val=ext%2Bfoo%3Acmdline", "cmdline query ok");
+ id = await extension.awaitMessage("test-tab");
+ extension.sendMessage("close", id);
+ await extension.awaitMessage("closed");
+ chromeScript.destroy();
+
+ // Test the protocol in a private window, watch for the
+ // console error.
+ consoleMonitor.start([{ message: /NS_ERROR_FILE_NOT_FOUND/ }]);
+
+ // Expect the chooser window to be open, close it.
+ chromeScript = SpecialPowers.loadChromeScript(async () => {
+ /* eslint-env mozilla/chrome-script */
+ const CONTENT_HANDLING_URL =
+ "chrome://mozapps/content/handling/appChooser.xhtml";
+ const { BrowserTestUtils } = ChromeUtils.import(
+ "resource://testing-common/BrowserTestUtils.jsm"
+ );
+
+ let windowOpen = BrowserTestUtils.domWindowOpenedAndLoaded();
+
+ sendAsyncMessage("listenWindow");
+
+ let window = await windowOpen;
+ let gBrowser = window.gBrowser;
+ let tabDialogBox = gBrowser.getTabDialogBox(gBrowser.selectedBrowser);
+ let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack;
+
+ let checkFn = dialogEvent =>
+ dialogEvent.detail.dialog?._openedURL == CONTENT_HANDLING_URL;
+
+ let eventPromise = BrowserTestUtils.waitForEvent(
+ dialogStack,
+ "dialogopen",
+ true,
+ checkFn
+ );
+
+ sendAsyncMessage("listenDialog");
+
+ let event = await eventPromise;
+
+ let { dialog } = event.detail;
+
+ let entry = dialog._frame.contentDocument.getElementById("items")
+ .firstChild;
+ sendAsyncMessage("handling", {
+ name: entry.getAttribute("name"),
+ disabled: entry.disabled,
+ });
+
+ dialog.close();
+ });
+
+ // Wait for the chrome script to attach window listener
+ await chromeScript.promiseOneMessage("listenWindow");
+
+ let listenDialog = chromeScript.promiseOneMessage("listenDialog");
+ let windowOpen = pb_extension.awaitMessage("opened");
+
+ pb_extension.sendMessage("open", "ext+foo:test");
+
+ // Wait for chrome script to attach dialog listener
+ await listenDialog;
+ let { tabId, windowId } = await windowOpen;
+
+ let testData = chromeScript.promiseOneMessage("handling");
+ let navPromise = pb_extension.awaitMessage("navigated");
+ pb_extension.sendMessage("nav", { url: "ext+foo:test", tabId });
+ await navPromise;
+ await consoleMonitor.finished();
+ let entry = await testData;
+
+ is(entry.name, "a foo protocol handler", "entry is correct");
+ ok(entry.disabled, "handler is disabled");
+
+ let promiseClosed = pb_extension.awaitMessage("closed");
+ pb_extension.sendMessage("close", windowId);
+ await promiseClosed;
+ await pb_extension.unload();
+
+ // Shutdown the addon, then ensure the protocol was removed.
+ await extension.unload();
+ chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("setup", () => {
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo");
+ sendAsyncMessage(
+ "preferredApplicationHandler",
+ !protoInfo.preferredApplicationHandler
+ );
+ let handlers = protoInfo.possibleApplicationHandlers;
+
+ sendAsyncMessage("handlerData", {
+ preferredApplicationHandler: !protoInfo.preferredApplicationHandler,
+ handlers: handlers.length,
+ });
+ });
+ });
+
+ msg = chromeScript.promiseOneMessage("handlerData");
+ chromeScript.sendAsyncMessage("setup");
+ data = await msg;
+ ok(data.preferredApplicationHandler, "no preferred handler is set");
+ is(data.handlers, 0, "no handler is set");
+ chromeScript.destroy();
+});
+
+add_task(async function test_protocolHandler_two() {
+ let extensionData = {
+ manifest: {
+ protocol_handlers: [
+ {
+ protocol: "ext+foo",
+ name: "a foo protocol handler",
+ uriTemplate: "foo.html?val=%s",
+ },
+ {
+ protocol: "ext+foo",
+ name: "another foo protocol handler",
+ uriTemplate: "foo2.html?val=%s",
+ },
+ ],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ // Ensure that the protocol handler is configured, and set it as default,
+ // but because there are two handlers, the dialog is not bypassed. We
+ // don't test the actual dialog ui, it's been here forever and works based
+ // on the alwaysAskBeforeHandling value.
+ let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript);
+
+ let msg = chromeScript.promiseOneMessage("handlerData");
+ chromeScript.sendAsyncMessage("setup", {
+ protocol: "ext+foo",
+ principalOrigins: [],
+ });
+ let data = await msg;
+ ok(
+ data.preferredAction,
+ "using a helper application is the preferred action"
+ );
+ ok(data.preferredApplicationHandler, "preferred handler is set");
+ is(data.handlers, 2, "two handlers are set");
+ ok(data.alwaysAskBeforeHandling, "will show dialog");
+ ok(data.isWebHandler, "the handler is a web handler");
+ chromeScript.destroy();
+ await extension.unload();
+});
+
+add_task(async function test_protocolHandler_https_target() {
+ let extensionData = {
+ manifest: {
+ protocol_handlers: [
+ {
+ protocol: "ext+foo",
+ name: "http target",
+ uriTemplate: "https://example.com/foo.html?val=%s",
+ },
+ ],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ ok(true, "https uriTemplate target works");
+ await extension.unload();
+});
+
+add_task(async function test_protocolHandler_http_target() {
+ let extensionData = {
+ manifest: {
+ protocol_handlers: [
+ {
+ protocol: "ext+foo",
+ name: "http target",
+ uriTemplate: "http://example.com/foo.html?val=%s",
+ },
+ ],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ ok(true, "http uriTemplate target works");
+ await extension.unload();
+});
+
+add_task(async function test_protocolHandler_restricted_protocol() {
+ let extensionData = {
+ manifest: {
+ protocol_handlers: [
+ {
+ protocol: "http",
+ name: "take over the http protocol",
+ uriTemplate: "http.html?val=%s",
+ },
+ ],
+ },
+ };
+
+ consoleMonitor.start([
+ { message: /processing protocol_handlers\.0\.protocol/ },
+ ]);
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await Assert.rejects(
+ extension.startup(),
+ /startup failed/,
+ "unable to register restricted handler protocol"
+ );
+
+ await consoleMonitor.finished();
+});
+
+add_task(async function test_protocolHandler_restricted_uriTemplate() {
+ let extensionData = {
+ manifest: {
+ protocol_handlers: [
+ {
+ protocol: "ext+foo",
+ name: "take over the http protocol",
+ uriTemplate: "ftp://example.com/file.txt",
+ },
+ ],
+ },
+ };
+
+ consoleMonitor.start([
+ { message: /processing protocol_handlers\.0\.uriTemplate/ },
+ ]);
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await Assert.rejects(
+ extension.startup(),
+ /startup failed/,
+ "unable to register restricted handler uriTemplate"
+ );
+
+ await consoleMonitor.finished();
+});
+
+add_task(async function test_protocolHandler_duplicate() {
+ let extensionData = {
+ manifest: {
+ protocol_handlers: [
+ {
+ protocol: "ext+foo",
+ name: "foo protocol",
+ uriTemplate: "foo.html?val=%s",
+ },
+ {
+ protocol: "ext+foo",
+ name: "foo protocol",
+ uriTemplate: "foo.html?val=%s",
+ },
+ ],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ // Get the count of handlers installed.
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("setup", () => {
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo");
+ let handlers = protoInfo.possibleApplicationHandlers;
+ sendAsyncMessage("handlerData", handlers.length);
+ });
+ });
+
+ let msg = chromeScript.promiseOneMessage("handlerData");
+ chromeScript.sendAsyncMessage("setup");
+ let data = await msg;
+ is(data, 1, "cannot re-register the same handler config");
+ chromeScript.destroy();
+ await extension.unload();
+});
+
+// Test that a protocol handler will work if ftp is enabled
+add_task(async function test_ftp_protocolHandler() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disabling the external protocol permission prompt. We don't need it
+ // for this test.
+ ["security.external_protocol_requires_permission", false],
+ ],
+ });
+ let extensionData = {
+ manifest: {
+ protocol_handlers: [
+ {
+ protocol: "ftp",
+ name: "an ftp protocol handler",
+ uriTemplate: "ftp.html?val=%s",
+ },
+ ],
+ },
+
+ async background() {
+ let url = "ftp://example.com/file.txt";
+ browser.test.onMessage.addListener(async () => {
+ await browser.tabs.create({ url });
+ });
+ },
+
+ files: {
+ "ftp.js": function() {
+ browser.test.sendMessage("test-query", location.search);
+ },
+ "ftp.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="ftp.js"><\/script>
+ </head>
+ </html>`,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ const handlerUrl = `moz-extension://${extension.uuid}/ftp.html`;
+
+ let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript);
+
+ // Set the preferredAction to this extension as ftp will default to system. If
+ // we didn't bypass the dialog for this test, the user would get asked in this case.
+ let msg = chromeScript.promiseOneMessage("set");
+ chromeScript.sendAsyncMessage("setPreferredAction", {
+ protocol: "ftp",
+ template: handlerUrl,
+ });
+ await msg;
+
+ msg = chromeScript.promiseOneMessage("handlerData");
+ chromeScript.sendAsyncMessage("setup", { protocol: "ftp", principalOrigins: [] });
+ let data = await msg;
+ ok(
+ data.preferredAction,
+ "using a helper application is the preferred action"
+ );
+ ok(data.preferredApplicationHandler, "handler was set as default handler");
+ is(data.handlers, 1, "one handler is set");
+ ok(!data.alwaysAskBeforeHandling, "will not show dialog");
+ ok(data.isWebHandler, "the handler is a web handler");
+ is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template");
+
+ chromeScript.destroy();
+
+ extension.sendMessage("run");
+ let query = await extension.awaitMessage("test-query");
+ is(query, "?val=ftp%3A%2F%2Fexample.com%2Ffile.txt", "test query ok");
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script>
+"use strict";
+
+function getExtension() {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "redirect-to-jar@mochi.test",
+ },
+ },
+ "permissions": [
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ ],
+ "web_accessible_resources": [
+ "finished.html",
+ ],
+ },
+ useAddonManager: "temporary",
+ files: {
+ "finished.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <h1>redirected!</h1>
+ </body>
+ </html>
+ `,
+ },
+ background: async () => {
+ let redirectUrl = browser.runtime.getURL("finished.html");
+ browser.webRequest.onBeforeRequest.addListener(details => {
+ return {redirectUrl};
+ }, {urls: ["*://*/intercept*"]}, ["blocking"]);
+
+ let code = `new Promise(resolve => {
+ var s = document.createElement('iframe');
+ s.src = "/intercept?r=" + Math.random();
+ s.onload = async () => {
+ let url = await window.wrappedJSObject.SpecialPowers.spawn(s, [], () => content.location.href );
+ resolve(['loaded', url]);
+ }
+ s.onerror = () => resolve(['error']);
+ document.documentElement.appendChild(s);
+ });`;
+
+ async function testSubFrameResource(tabId, code) {
+ let [result] = await browser.tabs.executeScript(tabId, { code });
+ return result;
+ }
+
+ let tab = await browser.tabs.create({url: "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_sample.html"});
+ let result = await testSubFrameResource(tab.id, code);
+ browser.test.assertEq("loaded", result[0], "frame 1 loaded");
+ browser.test.assertEq(redirectUrl, result[1], "frame 1 redirected");
+ // If jar caching breaks redirects, this next test will fail (See Bug 1390346).
+ result = await testSubFrameResource(tab.id, code);
+ browser.test.assertEq("loaded", result[0], "frame 2 loaded");
+ browser.test.assertEq(redirectUrl, result[1], "frame 2 redirected");
+ await browser.tabs.remove(tab.id);
+ browser.test.sendMessage("requestsCompleted");
+ },
+ });
+}
+
+add_task(async function test_redirect_to_jar() {
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("requestsCompleted");
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebRequest urlClassification</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.trackingprotection.enabled", true]],
+ });
+
+ let chromeScript = SpecialPowers.loadChromeScript(async _ => {
+ /* eslint-env mozilla/chrome-script */
+ const {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+ await UrlClassifierTestUtils.addTestTrackers();
+ sendAsyncMessage("trackersLoaded");
+ });
+ await chromeScript.promiseOneMessage("trackersLoaded");
+ chromeScript.destroy();
+});
+
+add_task(async function test_urlClassification() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "proxy", "<all_urls>"],
+ },
+ background() {
+ let expected = {
+ "http://tracking.example.org/": {first: "tracking", thirdParty: false, },
+ "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=tracking.example.org": { thirdParty: false, },
+ "http://tracking.example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png": {third: "tracking", thirdParty: true, },
+ "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=example.net": { thirdParty: false, },
+ "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png": { thirdParty: true, },
+ };
+ function testRequest(details) {
+ let expect = expected[details.url];
+ if (expect) {
+ if (expect.first) {
+ browser.test.assertTrue(details.urlClassification.firstParty.includes("tracking"), "tracking firstParty");
+ } else {
+ browser.test.assertEq(details.urlClassification.firstParty.length, 0, "not tracking firstParty");
+ }
+ if (expect.third) {
+ browser.test.assertTrue(details.urlClassification.thirdParty.includes("tracking"), "tracking thirdParty");
+ } else {
+ browser.test.assertEq(details.urlClassification.thirdParty.length, 0, "not tracking thirdParty");
+ }
+
+ browser.test.assertEq(details.thirdParty, expect.thirdParty, "3rd party flag matches");
+ return true;
+ }
+ return false;
+ }
+
+ browser.proxy.onRequest.addListener(details => {
+ browser.test.log(`proxy.onRequest ${JSON.stringify(details)}`);
+ testRequest(details);
+ }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]});
+ browser.webRequest.onBeforeRequest.addListener(async (details) => {
+ browser.test.log(`webRequest.onBeforeRequest ${JSON.stringify(details)}`);
+ testRequest(details);
+ }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]}, ["blocking"]);
+ browser.webRequest.onCompleted.addListener(async (details) => {
+ browser.test.log(`webRequest.onCompleted ${JSON.stringify(details)}`);
+ if (testRequest(details)) {
+ browser.test.sendMessage("classification", details.url);
+ }
+ }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]});
+ },
+ });
+ await extension.startup();
+
+ // Test first party tracking classification.
+ let url = "http://tracking.example.org/";
+ let win = window.open(url);
+ is(await extension.awaitMessage("classification"), url, "request completed");
+ win.close();
+
+ // Test third party tracking classification, expecting two results.
+ url = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=tracking.example.org";
+ win = window.open(url);
+ is(await extension.awaitMessage("classification"), url);
+ is(await extension.awaitMessage("classification"),
+ "http://tracking.example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png",
+ "request completed");
+ win.close();
+
+ // Test third party tracking classification, expecting two results.
+ url = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=example.net";
+ win = window.open(url);
+ is(await extension.awaitMessage("classification"), url);
+ is(await extension.awaitMessage("classification"),
+ "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png",
+ "request completed");
+ win.close();
+
+ await extension.unload();
+});
+
+add_task(async function teardown() {
+ let chromeScript = SpecialPowers.loadChromeScript(async _ => {
+ /* eslint-env mozilla/chrome-script */
+ // Cleanup cache
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+ });
+
+ const {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+ await UrlClassifierTestUtils.cleanupTestTrackers();
+ sendAsyncMessage("trackersUnloaded");
+ });
+ await chromeScript.promiseOneMessage("trackersUnloaded");
+ chromeScript.destroy();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq(port.name, "ernie", "port name correct");
+ browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "URL correct");
+ browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "tab URL correct");
+ browser.test.assertEq(port.sender.frameId, 0, "frameId of top frame");
+
+ let expected = "message 1";
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, expected, "message is expected");
+ if (expected == "message 1") {
+ port.postMessage("message 2");
+ expected = "message 3";
+ } else if (expected == "message 3") {
+ expected = "disconnect";
+ browser.test.notifyPass("runtime.connect");
+ }
+ });
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(null, port.error, "No error because port is closed by disconnect() at other end");
+ browser.test.assertEq(expected, "disconnect", "got disconnection at right time");
+ });
+ });
+}
+
+function contentScript() {
+ let port = browser.runtime.connect({name: "ernie"});
+ port.postMessage("message 1");
+ port.onMessage.addListener(msg => {
+ if (msg == "message 2") {
+ port.postMessage("message 3");
+ port.disconnect();
+ }
+ });
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(async function test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ await Promise.all([waitForLoad(win), extension.awaitFinish("runtime.connect")]);
+
+ win.close();
+
+ await extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function backgroundScript(token) {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "done");
+ browser.test.notifyPass("sendmessage_reply");
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "sender url correct");
+ browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ let tabId = port.sender.tab.id;
+ browser.tabs.connect(tabId, {name: token});
+
+ browser.test.assertEq(port.name, token, "token matches");
+ port.postMessage(token + "-done");
+ });
+
+ browser.test.sendMessage("background-ready");
+}
+
+function contentScript(token) {
+ let gotTabMessage = false;
+ let badTabMessage = false;
+ browser.runtime.onConnect.addListener(port => {
+ if (port.name == token) {
+ gotTabMessage = true;
+ } else {
+ badTabMessage = true;
+ }
+ port.disconnect();
+ });
+
+ let port = browser.runtime.connect(null, {name: token});
+ port.onMessage.addListener(function(msg) {
+ if (msg != token + "-done" || !gotTabMessage || badTabMessage) {
+ return; // test failed
+ }
+
+ // FIXME: Removing this line causes the test to fail:
+ // resource://gre/modules/ExtensionUtils.jsm, line 651: NS_ERROR_NOT_INITIALIZED
+ port.disconnect();
+ browser.runtime.sendMessage("done");
+ });
+}
+
+function makeExtension() {
+ let token = Math.random();
+ let extensionData = {
+ background: `(${backgroundScript})("${token}")`,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": `(${contentScript})("${token}")`,
+ },
+ };
+ return extensionData;
+}
+
+add_task(async function test_contentscript() {
+ let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
+ let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+ await Promise.all([extension1.startup(), extension2.startup()]);
+
+ await extension1.awaitMessage("background-ready");
+ await extension2.awaitMessage("background-ready");
+
+ let win = window.open("file_sample.html");
+
+ await Promise.all([waitForLoad(win),
+ extension1.awaitFinish("sendmessage_reply"),
+ extension2.awaitFinish("sendmessage_reply")]);
+
+ win.close();
+
+ await extension1.unload();
+ await extension2.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// The purpose of this test is to verify that the port.sender properties are
+// not set for messages from iframes in background scripts. This is the toolkit
+// version of the browser_ext_contentscript_nontab_connect.js test, and exists
+// to provide test coverage for non-toolkit builds (e.g. Android).
+//
+// This used to be a xpcshell test (from bug 1488105), but became a mochitest
+// because port.sender.tab and port.sender.frameId do not represent the real
+// values in xpcshell tests.
+// Specifically, ProxyMessenger.prototype.getSender uses the tabTracker, which
+// expects real tabs instead of browsers from the ContentPage API in xpcshell
+// tests.
+add_task(async function connect_from_background_frame() {
+ if (!SpecialPowers.getBoolPref("extensions.webextensions.remote", true)) {
+ info("Cannot load remote content in parent process; skipping test task");
+ return;
+ }
+ async function background() {
+ const FRAME_URL = "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html";
+ browser.runtime.onConnect.addListener(port => {
+ // The next two assertions are the reason for this being a mochitest
+ // instead of a xpcshell test.
+ browser.test.assertEq(port.sender.tab, undefined, "Sender is not a tab");
+ browser.test.assertEq(port.sender.frameId, undefined, "frameId unset");
+ browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL");
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq("pong", msg, "Reply from content script");
+ port.disconnect();
+ });
+ port.postMessage("ping");
+ });
+
+ await browser.contentScripts.register({
+ matches: [FRAME_URL],
+ 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}`);
+
+ let port = browser.runtime.connect();
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq("ping", msg, "Expected message to content script");
+ port.postMessage("pong");
+ });
+ port.onDisconnect.addListener(() => {
+ browser.test.sendMessage("disconnected_in_content_script");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["https://example.com/*"],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ },
+ background,
+ });
+ await extension.startup();
+ await extension.awaitMessage("disconnected_in_content_script");
+ await extension.unload();
+});
+
+// The test_ext_contentscript_fission_frame.html test already checks the
+// behavior of onConnect in cross-origin frames, so here we just limit the test
+// to checking that the port.sender properties are sensible.
+add_task(async function connect_from_content_script_in_frame() {
+ async function background() {
+ const TAB_URL = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html";
+ const FRAME_URL = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_img.html";
+ let createdTab;
+ browser.runtime.onConnect.addListener(port => {
+ // The next two assertions are the reason for this being a mochitest
+ // instead of a xpcshell test.
+ browser.test.assertEq(port.sender.tab.url, TAB_URL, "Sender is the tab");
+ browser.test.assertTrue(port.sender.frameId > 0, "frameId is set");
+ browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL");
+
+ browser.test.assertEq(createdTab.id, port.sender.tab.id, "Tab to close");
+ browser.tabs.remove(port.sender.tab.id).then(() => {
+ browser.test.sendMessage("tab_port_checked_and_tab_closed");
+ });
+ });
+
+ await browser.contentScripts.register({
+ matches: [FRAME_URL],
+ js: [{ file: "contentscript.js" }],
+ allFrames: true,
+ });
+
+ createdTab = await browser.tabs.create({ url: TAB_URL });
+ }
+
+ function contentScript() {
+ browser.test.log(`Running content script at ${document.URL}`);
+
+ browser.runtime.connect();
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["https://example.org/*"],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ },
+ background,
+ });
+ await extension.startup();
+ await extension.awaitMessage("tab_port_checked_and_tab_closed");
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(async function test_connect_bidirectionally_and_postMessage() {
+ function background() {
+ let onConnectCount = 0;
+ browser.runtime.onConnect.addListener(port => {
+ // 3. onConnect by connect() from CS.
+ browser.test.assertEq("from-cs", port.name);
+ browser.test.assertEq(1, ++onConnectCount,
+ "BG onConnect should be called once");
+
+ let tabId = port.sender.tab.id;
+ browser.test.assertTrue(tabId, "content script must have a tab ID");
+
+ let port2;
+ let postMessageCount1 = 0;
+ port.onMessage.addListener(msg => {
+ // 11. port.onMessage by port.postMessage in CS.
+ browser.test.assertEq("from CS to port", msg);
+ browser.test.assertEq(1, ++postMessageCount1,
+ "BG port.onMessage should be called once");
+
+ // 12. should trigger port2.onMessage in CS.
+ port2.postMessage("from BG to port2");
+ });
+
+ // 4. Should trigger onConnect in CS.
+ port2 = browser.tabs.connect(tabId, {name: "from-bg"});
+ let postMessageCount2 = 0;
+ port2.onMessage.addListener(msg => {
+ // 7. onMessage by port2.postMessage in CS.
+ browser.test.assertEq("from CS to port2", msg);
+ browser.test.assertEq(1, ++postMessageCount2,
+ "BG port2.onMessage should be called once");
+
+ // 8. Should trigger port.onMessage in CS.
+ port.postMessage("from BG to port");
+ });
+ });
+
+ // 1. Notify test runner to create a new tab.
+ browser.test.sendMessage("ready");
+ }
+
+ function contentScript() {
+ let onConnectCount = 0;
+ let port;
+ browser.runtime.onConnect.addListener(port2 => {
+ // 5. onConnect by connect() from BG.
+ browser.test.assertEq("from-bg", port2.name);
+ browser.test.assertEq(1, ++onConnectCount,
+ "CS onConnect should be called once");
+
+ let postMessageCount2 = 0;
+ port2.onMessage.addListener(msg => {
+ // 12. port2.onMessage by port2.postMessage in BG.
+ browser.test.assertEq("from BG to port2", msg);
+ browser.test.assertEq(1, ++postMessageCount2,
+ "CS port2.onMessage should be called once");
+
+ // TODO(robwu): Do not explicitly disconnect, it should not be a problem
+ // if we keep the ports open. However, not closing the ports causes the
+ // test to fail with NS_ERROR_NOT_INITIALIZED in ExtensionUtils.jsm, in
+ // Port.prototype.disconnect (nsIMessageSender.sendAsyncMessage).
+ port.disconnect();
+ port2.disconnect();
+ browser.test.notifyPass("ping pong done");
+ });
+ // 6. should trigger port2.onMessage in BG.
+ port2.postMessage("from CS to port2");
+ });
+
+ // 2. should trigger onConnect in BG.
+ port = browser.runtime.connect({name: "from-cs"});
+ let postMessageCount1 = 0;
+ port.onMessage.addListener(msg => {
+ // 9. onMessage by port.postMessage in BG.
+ browser.test.assertEq("from BG to port", msg);
+ browser.test.assertEq(1, ++postMessageCount1,
+ "CS port.onMessage should be called once");
+
+ // 10. should trigger port.onMessage in BG.
+ port.postMessage("from CS to port");
+ });
+ }
+
+ let extensionData = {
+ background,
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ info("extension loaded");
+
+ await extension.awaitMessage("ready");
+
+ let win = window.open("file_sample.html");
+ await extension.awaitFinish("ping pong done");
+ win.close();
+
+ await extension.unload();
+ info("extension unloaded");
+});
+</script>
+</body>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq(port.name, "ernie", "port name correct");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads");
+ // Closing an already-disconnected port is a no-op.
+ port.disconnect();
+ port.disconnect();
+ browser.test.sendMessage("disconnected");
+ });
+ browser.test.sendMessage("connected");
+ });
+}
+
+function contentScript() {
+ browser.runtime.connect({name: "ernie"});
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(async function test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+ await Promise.all([waitForLoad(win), extension.awaitMessage("connected")]);
+ win.close();
+ await extension.awaitMessage("disconnected");
+
+ info("win.close() succeeded");
+
+ win = window.open("file_sample.html");
+ await Promise.all([waitForLoad(win), extension.awaitMessage("connected")]);
+
+ // Add an "unload" listener so that we don't put the window in the
+ // bfcache. This way it gets destroyed immediately upon navigation.
+ win.addEventListener("unload", function() {}); // eslint-disable-line mozilla/balanced-listeners
+
+ win.location = "http://example.com";
+ await extension.awaitMessage("disconnected");
+ win.close();
+
+ await extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Script Filenames Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_tabs_executeScript() {
+ let validFileName = "script.js";
+ let invalidFileName = "script.xyz";
+
+ async function background() {
+ await browser.tabs.executeScript({ file: "script.js" });
+
+ await browser.test.assertRejects(
+ browser.tabs.executeScript({ file: "script.xyz" }),
+ Error,
+ "invalid filename does not execute"
+ );
+ browser.test.notifyPass("execute-script");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["<all_urls>"],
+ },
+
+ background,
+
+ files: {
+ [validFileName]: function contentScript1() {
+ browser.test.sendMessage("content-script-loaded");
+ },
+ [invalidFileName]: function contentScript2() {
+ browser.test.fail("this script should not be loaded");
+ },
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "http://mochi.test:8888/",
+ true
+ );
+ await extension.startup();
+
+ await extension.awaitMessage("content-script-loaded");
+ await extension.awaitFinish("execute-script");
+
+ await extension.unload();
+ await AppTestDelegate.removeTab(window, tab);
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests scripting.*ContentScripts()</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+
+"use strict";
+
+const MOCHITEST_HOST_PERMISSIONS = [
+ "*://mochi.test/",
+ "*://mochi.xorigin-test/",
+ "*://test1.example.com/",
+];
+
+const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ permissions: ["scripting"],
+ host_permissions: [
+ ...MOCHITEST_HOST_PERMISSIONS,
+ // Used in `file_contains_iframe.html`
+ "*://example.org/",
+ ],
+ granted_host_permissions: true,
+ ...manifestProps,
+ },
+ useAddonManager: "temporary",
+ ...otherProps,
+ });
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_validate_registerContentScripts_params() {
+ let extension = makeExtension({
+ async background() {
+ const TEST_CASES = [
+ {
+ title: "no js and no css",
+ params: [
+ {
+ id: "script",
+ matches: ["*://mochi.test/*"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "At least one js or css must be specified.",
+ },
+ {
+ title: "empty js",
+ params: [
+ {
+ id: "script",
+ js: [],
+ matches: ["*://mochi.test/*"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "At least one js or css must be specified.",
+ },
+ {
+ title: "empty css",
+ params: [
+ {
+ id: "script",
+ css: [],
+ matches: ["*://mochi.test/*"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "At least one js or css must be specified.",
+ },
+ {
+ title: "no matches",
+ params: [
+ {
+ id: "script",
+ js: ["script.js"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "matches must be specified.",
+ },
+ {
+ title: "empty matches",
+ params: [
+ {
+ id: "script",
+ js: ["script.js"],
+ matches: [],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "matches must be specified.",
+ },
+ {
+ title: "one empty match",
+ params: [
+ {
+ id: "script",
+ js: ["script.js"],
+ matches: [""],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "Invalid url pattern: ",
+ },
+ {
+ title: "invalid match",
+ params: [
+ {
+ id: "script",
+ js: ["script.js"],
+ matches: ["not-a-pattern"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "Invalid url pattern: not-a-pattern",
+ },
+ {
+ title: "invalid match and valid match",
+ params: [
+ {
+ id: "script",
+ js: ["script.js"],
+ matches: ["*://mochi.test/*", "not-a-pattern"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "Invalid url pattern: not-a-pattern",
+ },
+ {
+ title: "one empty value in excludeMatches",
+ params: [
+ {
+ id: "script",
+ js: ["script.js"],
+ matches: ["*://mochi.test/*"],
+ excludeMatches: [""],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "Invalid url pattern: ",
+ },
+ {
+ title: "invalid value in excludeMatches",
+ params: [
+ {
+ id: "script",
+ js: ["script.js"],
+ matches: ["*://mochi.test/*"],
+ excludeMatches: ["not-a-pattern"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "Invalid url pattern: not-a-pattern",
+ },
+ {
+ title: "duplicate IDs",
+ params: [
+ {
+ id: "script-1",
+ js: ["script.js"],
+ matches: ["*://mochi.test/*"],
+ persistAcrossSessions: false,
+ },
+ {
+ id: "script-1",
+ js: ["script.js"],
+ matches: ["*://mochi.test/*"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: `Script ID "script-1" found more than once in 'scripts' array.`,
+ },
+ {
+ title: "empty id",
+ params: [
+ {
+ id: "",
+ js: ["script.js"],
+ matches: ["*://mochi.test/*"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "Invalid content script id.",
+ },
+ {
+ title: "id starting with _",
+ params: [
+ {
+ id: "_foo",
+ js: ["script.js"],
+ matches: ["*://mochi.test/*"],
+ persistAcrossSessions: false,
+ },
+ ],
+ expectedError: "Invalid content script id.",
+ },
+ ];
+
+ for (const { title, params, expectedError } of TEST_CASES) {
+ await browser.test.assertRejects(
+ browser.scripting.registerContentScripts(params),
+ expectedError,
+ `${title} - got expected error`
+ );
+ }
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no registered script");
+
+ browser.test.notifyPass("test-finished");
+ },
+ files: {
+ "script.js": "",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
+
+add_task(async function test_registerContentScripts_with_already_registered_id() {
+ let extension = makeExtension({
+ async background() {
+ const script = {
+ id: "script-1",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ };
+
+ await browser.scripting.registerContentScripts([script]);
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(1, scripts.length, "expected 1 registered script");
+
+ await browser.test.assertRejects(
+ browser.scripting.registerContentScripts([script]),
+ `Content script with id "${script.id}" is already registered.`,
+ "got expected error"
+ );
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(1, scripts.length, "expected 1 registered script");
+
+ browser.test.notifyPass("test-finished");
+ },
+ files: {
+ "script.js": "",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
+
+add_task(async function test_validate_getRegisteredContentScripts_params() {
+ let extension = makeExtension({
+ async background() {
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no registered scripts");
+
+ scripts = await browser.scripting.getRegisteredContentScripts({
+ ids: ["non-existent-id"]
+ });
+ browser.test.assertEq(0, scripts.length, "expected no registered scripts");
+
+ browser.test.log("test call with undefined filter and a chrome-compatible callback");
+ scripts = await new Promise(resolve => {
+ browser.scripting.getRegisteredContentScripts(undefined, resolve);
+ });
+ browser.test.assertEq(0, scripts.length, "expected no registered scripts");
+
+ browser.test.log("test call with only the chrome-compatible callback");
+ scripts = await new Promise(resolve => {
+ browser.scripting.getRegisteredContentScripts(resolve);
+ });
+ browser.test.assertEq(0, scripts.length, "expected no registered scripts");
+
+ browser.test.notifyPass("test-finished");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
+
+add_task(async function test_getRegisteredContentScripts() {
+ let extension = makeExtension({
+ async background() {
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no registered scripts");
+
+ const aScript = {
+ id: "a-script",
+ js: ["script.js"],
+ matches: ["<all_urls>"],
+ persistAcrossSessions: false,
+ };
+
+ await browser.scripting.registerContentScripts([aScript]);
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(1, scripts.length, "expected 1 registered script");
+ browser.test.assertEq(aScript.id, scripts[0].id, "expected correct id");
+
+ // This should return no registered scripts.
+ scripts = await browser.scripting.getRegisteredContentScripts({ ids: [] });
+ browser.test.assertEq(0, scripts.length, "expected 0 registered script");
+
+ // Verify that invalid IDs are omitted but valid IDs are used to return
+ // registered scripts.
+ scripts = await browser.scripting.getRegisteredContentScripts({
+ ids: ["non-existent-id", aScript.id]
+ });
+ browser.test.assertEq(1, scripts.length, "expected 1 registered script");
+ browser.test.assertEq(aScript.id, scripts[0].id, "expected correct id");
+
+ browser.test.notifyPass("test-finished");
+ },
+ files: {
+ "script.js": "",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
+
+add_task(async function test_registerContentScripts_js() {
+ let extension = makeExtension({
+ async background() {
+ const TEST_CASES = [
+ // This should have no effect but it should not throw.
+ {
+ title: "no script",
+ params: [],
+ },
+ {
+ title: "one script",
+ params: [
+ {
+ id: "script-1",
+ js: ["script-1.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ }
+ ],
+ },
+ {
+ title: "one script in all frames",
+ params: [
+ {
+ id: "script-2",
+ js: ["script-2.js"],
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ allFrames: true,
+ persistAcrossSessions: false,
+ }
+ ],
+ },
+ {
+ title: "one script in all frames with excludeMatches set",
+ params: [
+ {
+ id: "script-3",
+ js: ["script-3.js"],
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ allFrames: true,
+ excludeMatches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ }
+ ],
+ },
+ {
+ title: "one script, two js paths",
+ params: [
+ {
+ id: "script-4",
+ js: ["script-4-1.js", "script-4-2.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ }
+ ],
+ },
+ {
+ title: "empty excludeMatches",
+ params: [
+ {
+ id: "script-5",
+ // This path should be normalized.
+ js: ["/script-5.js"],
+ matches: ["*://test1.example.com/*"],
+ excludeMatches: [],
+ persistAcrossSessions: false,
+ }
+ ],
+ },
+ ];
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no registered script");
+
+ for (const { title, params } of TEST_CASES) {
+ const res = await browser.scripting.registerContentScripts(params);
+ browser.test.assertEq(
+ undefined,
+ res,
+ `${title} - expected no result`
+ );
+
+ const script = await browser.scripting.getRegisteredContentScripts({
+ ids: params.map(param => param.id)
+ });
+ browser.test.assertEq(
+ params.length,
+ script.length,
+ `${title} - got the expected number of registered scripts`
+ );
+ }
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(
+ // A test case declared above does not contain any script to register.
+ TEST_CASES.length - 1,
+ scripts.length,
+ "got the expected number of registered scripts"
+ );
+ browser.test.assertEq(
+ JSON.stringify([
+ {
+ id: "script-1",
+ allFrames: false,
+ matches: ["*://test1.example.com/*"],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: ["script-1.js"],
+ },
+ {
+ id: "script-2",
+ allFrames: true,
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: ["script-2.js"],
+ },
+ {
+ id: "script-3",
+ allFrames: true,
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ excludeMatches: ["*://test1.example.com/*"],
+ js: ["script-3.js"],
+ },
+ {
+ id: "script-4",
+ allFrames: false,
+ matches: ["*://test1.example.com/*"],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: ["script-4-1.js", "script-4-2.js"],
+ },
+ {
+ id: "script-5",
+ allFrames: false,
+ matches: ["*://test1.example.com/*"],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: ["script-5.js"],
+ },
+ ]),
+ JSON.stringify(scripts),
+ "got expected scripts"
+ );
+
+ browser.test.sendMessage("background-ready");
+ },
+ files: {
+ "script-1.js": () => {
+ browser.test.sendMessage(
+ "script-ran",
+ { file: "script-1.js", value: document.title }
+ );
+ },
+ "script-2.js": () => {
+ browser.test.sendMessage(
+ "script-ran",
+ { file: "script-2.js", value: document.title }
+ );
+ },
+ "script-3.js": () => {
+ browser.test.sendMessage(
+ "script-ran",
+ { file: "script-3.js", value: document.title }
+ );
+ },
+ "script-4-1.js": () => {
+ // We inject this script (first) as well as the one defined right
+ // after. The order should be respected, which is why we define a
+ // property here and check it in the second script.
+ window.SCRIPT_4_INJECTED = "SCRIPT_4_INJECTED";
+ },
+ "script-4-2.js": () => {
+ browser.test.sendMessage(
+ "script-ran",
+ { file: "script-4-2.js", value: window.SCRIPT_4_INJECTED }
+ );
+ delete window.SCRIPT_4_INJECTED;
+ },
+ "script-5.js": () => {
+ browser.test.sendMessage(
+ "script-ran",
+ { file: "script-5.js", value: document.title }
+ );
+ },
+ },
+ });
+
+ let scriptsRan = 0;
+ let results = [];
+ let completePromise = new Promise(resolve => {
+ extension.onMessage("script-ran", result => {
+ results.push(result);
+ scriptsRan++;
+
+ // The value below should be updated when TEST_CASES above is changed.
+ if (scriptsRan === 6) {
+ resolve();
+ }
+ });
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ // Load a page that will trigger the content scripts previously registered.
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ // Wait for all content scripts to be executed.
+ await completePromise;
+
+ // Verify that the scripts have been executed correctly. We sort the results
+ // to compare them against expected values.
+ results.sort((a, b) => {
+ return a.file.localeCompare(b.file) || a.value.localeCompare(b.value);
+ });
+ ok(
+ JSON.stringify([
+ { file: "script-1.js", value: "file contains iframe" },
+ // script-2.js should be injected in two frames
+ { file: "script-2.js", value: "file contains iframe" },
+ { file: "script-2.js", value: "file contains img" },
+ { file: "script-3.js", value: "file contains img" },
+ // script-4-1.js will add a prop to the `window` object, which should be
+ // read by `script-4-2.js`.
+ { file: "script-4-2.js", value: "SCRIPT_4_INJECTED" },
+ { file: "script-5.js", value: "file contains iframe" },
+ ]) === JSON.stringify(results),
+ "got expected script results" + JSON.stringify(results)
+ );
+
+ await AppTestDelegate.removeTab(window, tab);
+ await extension.unload();
+});
+
+add_task(async function test_registerContentScripts_are_not_unregistered() {
+ let extension = makeExtension({
+ files: {
+ "background.html": `<!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <script src="background.js"><\/script>
+ </body>
+ </html>
+ `,
+ "background.js": async () => {
+ await browser.scripting.registerContentScripts([
+ {
+ id: "a-script",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.sendMessage("background-executed");
+ },
+ "script.js": () => {
+ browser.test.sendMessage("script-executed");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ // Load the background page that registers a content script.
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ `moz-extension://${extension.uuid}/background.html`,
+ true
+ );
+ await extension.awaitMessage("background-executed");
+ await AppTestDelegate.removeTab(window, tab);
+
+ // Load a page that will trigger the content scripts previously registered.
+ tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ await extension.awaitMessage("script-executed");
+
+ await AppTestDelegate.removeTab(window, tab);
+ await extension.unload();
+});
+
+add_task(async function test_scripts_dont_run_after_shutdown() {
+ let extension = makeExtension({
+ async background() {
+ await browser.scripting.registerContentScripts([
+ {
+ id: "script-that-should-not-run",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.sendMessage("background-ready");
+ },
+ files: {
+ "script.js": () => {
+ browser.test.fail("this script should not be executed.");
+ },
+ },
+ });
+ // We use a second extension to wait enough time to confirm that the script
+ // registered in the previous extension has not been executed at all, in case
+ // the tab closes before the scheduled content script has had a chance to
+ // run.
+ let anotherExtension = makeExtension({
+ async background() {
+ await browser.scripting.registerContentScripts([
+ {
+ id: "this-script-should-run",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.sendMessage("background-ready");
+ },
+ files: {
+ "script.js": () => {
+ browser.test.sendMessage("script-ran");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ await anotherExtension.startup();
+ await anotherExtension.awaitMessage("background-ready");
+
+ await extension.unload();
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+ await anotherExtension.awaitMessage("script-ran");
+ await AppTestDelegate.removeTab(window, tab);
+
+ await anotherExtension.unload();
+});
+
+add_task(async function test_registerContentScripts_with_wrong_matches() {
+ let extension = makeExtension({
+ async background() {
+ // Register a content script that should not be injected in this test
+ // case because the `matches` values don't match the host permissions.
+ await browser.scripting.registerContentScripts([
+ {
+ id: "script-that-should-not-run",
+ js: ["script.js"],
+ matches: ["*://mozilla.org/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.sendMessage("background-ready");
+ },
+ files: {
+ "script.js": () => {
+ browser.test.fail("this script should not be executed.");
+ },
+ },
+ });
+ // We use a second extension to wait enough time to confirm that the script
+ // registered in the previous extension has not been executed at all, in case
+ // the tab closes before the scheduled content script has had a chance to
+ // run.
+ let anotherExtension = makeExtension({
+ async background() {
+ await browser.scripting.registerContentScripts([
+ {
+ id: "this-script-should-run",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.sendMessage("background-ready");
+ },
+ files: {
+ "script.js": () => {
+ browser.test.sendMessage("script-ran");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ await anotherExtension.startup();
+ await anotherExtension.awaitMessage("background-ready");
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+ await anotherExtension.awaitMessage("script-ran");
+
+ await extension.unload();
+ await anotherExtension.unload();
+
+ // We remove the tab after having unloaded the extensions to avoid failures
+ // on Windows, see: Bug 1761550.
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_registerContentScripts_twice_with_same_id() {
+ let extension = makeExtension({
+ async background() {
+ const script = {
+ id: "script-that-should-not-run",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ };
+
+ const results = await Promise.allSettled([
+ browser.scripting.registerContentScripts([script]),
+ browser.scripting.registerContentScripts([script]),
+ ]);
+
+ browser.test.assertEq(2, results.length, "got expected length");
+ browser.test.assertEq(
+ "fulfilled",
+ results[0].status,
+ "expected fulfilled promise"
+ );
+ browser.test.assertEq(
+ "rejected",
+ results[1].status,
+ "expected rejected promise"
+ );
+ browser.test.assertEq(
+ `Content script with id "script-that-should-not-run" is already registered.`,
+ results[1].reason.message,
+ "expected reason"
+ );
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(1, scripts.length, "expected 1 registered script");
+
+ browser.test.sendMessage("background-done");
+ },
+ files: {
+ "script.js": "",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_getRegisteredContentScripts_during_a_registration() {
+ let extension = makeExtension({
+ async background() {
+ browser.scripting.registerContentScripts([
+ {
+ id: "a-script",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ const scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(
+ JSON.stringify([
+ {
+ id: "a-script",
+ allFrames: false,
+ matches: ["*://test1.example.com/*"],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: ["script.js"],
+ },
+ ]),
+ JSON.stringify(scripts),
+ "expected 1 registered script"
+ );
+
+ browser.test.sendMessage("background-done");
+ },
+ files: {
+ "script.js": "",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_validate_unregisterContentScripts_params() {
+ let extension = makeExtension({
+ async background() {
+ const TEST_CASES = [
+ {
+ title: "unknown id",
+ params: {
+ ids: ["non-existent-id"],
+ },
+ expectedError: `Content script with id "non-existent-id" does not exist.`
+ },
+ {
+ title: "invalid id",
+ params: {
+ ids: ["_invalid-id"],
+ },
+ expectedError: "Invalid content script id.",
+ },
+ ];
+
+ for (const { title, params, expectedError } of TEST_CASES) {
+ await browser.test.assertRejects(
+ browser.scripting.unregisterContentScripts(params),
+ expectedError,
+ `${title} - got expected error`
+ );
+ }
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no script");
+
+ browser.test.sendMessage("background-done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_unregisterContentScripts_with_chrome_compatible_callback() {
+ let extension = makeExtension({
+ async background() {
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no script");
+
+ // Register a script that we can unregister after.
+ await browser.scripting.registerContentScripts([
+ {
+ id: "script-1",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(1, scripts.length, "expected 1 registered script");
+
+ browser.test.log("test call with undefined filter and a chrome-compatible callback");
+ await new Promise(resolve => {
+ browser.scripting.unregisterContentScripts(undefined, resolve);
+ });
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no registered scripts");
+
+ // Re-register a script that we can unregister after.
+ await browser.scripting.registerContentScripts([
+ {
+ id: "script-1",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.log("test call with only the chrome-compatible callback");
+ await new Promise(resolve => {
+ browser.scripting.unregisterContentScripts(resolve);
+ });
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no registered scripts");
+
+ browser.test.sendMessage("background-done");
+ },
+ files: {
+ "script.js": "",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_unregisterContentScripts() {
+ let extension = makeExtension({
+ async background() {
+ const script1 = {
+ id: "script-1",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ };
+ const script2 = {
+ id: "script-2",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ }
+ const script3 = {
+ id: "script-3",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ }
+
+ let res = await browser.scripting.registerContentScripts([
+ script1,
+ script2,
+ script3,
+ ]);
+ browser.test.assertEq(undefined, res, "expected no result");
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(3, scripts.length, "expected 3 scripts");
+ browser.test.assertEq(script1.id, scripts[0].id, "expected correct id");
+ browser.test.assertEq(script2.id, scripts[1].id, "expected correct id");
+ browser.test.assertEq(script3.id, scripts[2].id, "expected correct id");
+
+ // No unregistration when unknown IDs are passed along with valid IDs.
+ await browser.test.assertRejects(
+ browser.scripting.unregisterContentScripts({
+ ids: [script2.id, "non-existent-id"],
+ }),
+ `Content script with id "non-existent-id" does not exist.`
+ );
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(3, scripts.length, "expected 3 scripts");
+
+ // Unregister 1 script.
+ res = await browser.scripting.unregisterContentScripts({
+ ids: [script2.id]
+ });
+ browser.test.assertEq(undefined, res, "expected no result");
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(2, scripts.length, "expected 2 scripts");
+ browser.test.assertEq(script1.id, scripts[0].id, "expected correct id");
+ browser.test.assertEq(script3.id, scripts[1].id, "expected correct id");
+
+ // This should unregister all the remaining registered scripts.
+ res = await browser.scripting.unregisterContentScripts();
+ browser.test.assertEq(undefined, res, "expected no result");
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected no script");
+
+ browser.test.sendMessage("background-done");
+ },
+ files: {
+ "script.js": "",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_unregisterContentScripts_twice_with_same_id() {
+ let extension = makeExtension({
+ async background() {
+ const script = {
+ id: "script-to-unregister",
+ js: ["script.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ };
+
+ await browser.scripting.registerContentScripts([script]);
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(1, scripts.length, "expected 1 registered script");
+
+ const results = await Promise.allSettled([
+ browser.scripting.unregisterContentScripts({ ids: [script.id] }),
+ browser.scripting.unregisterContentScripts({ ids: [script.id] }),
+ ]);
+
+ browser.test.assertEq(2, results.length, "got expected length");
+ browser.test.assertEq(
+ "fulfilled",
+ results[0].status,
+ "expected fulfilled promise"
+ );
+ browser.test.assertEq(
+ "rejected",
+ results[1].status,
+ "expected rejected promise"
+ );
+ browser.test.assertEq(
+ `Content script with id "script-to-unregister" does not exist.`,
+ results[1].reason.message,
+ "expected reason"
+ );
+
+ scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(0, scripts.length, "expected 0 registered script");
+
+ browser.test.sendMessage("background-done");
+ },
+ files: {
+ "script.js": "",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_validate_updateContentScripts_params() {
+ let extension = makeExtension({
+ async background() {
+ const script = {
+ id: "registered-script",
+ js: ["script-1.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ };
+
+ const TEST_CASES = [
+ {
+ title: "invalid script ID",
+ params: [
+ {
+ id: "_invalid-id",
+ },
+ ],
+ expectedError: 'Invalid content script id.',
+ },
+ {
+ title: "empty script ID",
+ params: [
+ {
+ id: "",
+ },
+ ],
+ expectedError: 'Invalid content script id.',
+ },
+ {
+ title: "unknown script ID",
+ params: [
+ {
+ id: "unknown-id",
+ },
+ ],
+ expectedError: 'Content script with id "unknown-id" does not exist.',
+ },
+ {
+ title: "duplicate valid script IDs",
+ params: [
+ {
+ id: script.id,
+ },
+ {
+ id: script.id,
+ },
+ ],
+ expectedError: `Script ID "${script.id}" found more than once in 'scripts' array.`,
+ },
+ {
+ title: "empty matches",
+ params: [
+ {
+ id: script.id,
+ matches: [],
+ },
+ ],
+ expectedError: "matches must be specified.",
+ },
+ {
+ title: "one empty match",
+ params: [
+ {
+ id: script.id,
+ matches: [""],
+ },
+ ],
+ expectedError: "Invalid url pattern: ",
+ },
+ {
+ title: "invalid match",
+ params: [
+ {
+ id: script.id,
+ matches: ["not-a-pattern"],
+ },
+ ],
+ expectedError: "Invalid url pattern: not-a-pattern",
+ },
+ {
+ title: "invalid match and valid match",
+ params: [
+ {
+ id: script.id,
+ matches: ["*://mochi.test/*", "not-a-pattern"],
+ },
+ ],
+ expectedError: "Invalid url pattern: not-a-pattern",
+ },
+ {
+ title: "one empty value in excludeMatches",
+ params: [
+ {
+ id: script.id,
+ excludeMatches: [""],
+ },
+ ],
+ expectedError: "Invalid url pattern: ",
+ },
+ {
+ title: "invalid value in excludeMatches",
+ params: [
+ {
+ id: script.id,
+ excludeMatches: ["not-a-pattern"],
+ },
+ ],
+ expectedError: "Invalid url pattern: not-a-pattern",
+ },
+ {
+ title: "empty js",
+ params: [
+ {
+ id: script.id,
+ js: [],
+ },
+ ],
+ expectedError: "At least one js or css must be specified.",
+ },
+ {
+ title: "empty js and css",
+ params: [
+ {
+ id: script.id,
+ js: [],
+ css: [],
+ },
+ ],
+ expectedError: "At least one js or css must be specified.",
+ },
+ ];
+
+ // Register a valid script so that we can verify update params beyond
+ // script IDs.
+ await browser.scripting.registerContentScripts([script]);
+
+ for (const { title, params, expectedError } of TEST_CASES) {
+ await browser.test.assertRejects(
+ browser.scripting.updateContentScripts(params),
+ expectedError,
+ `${title} - got expected error`
+ );
+ }
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(1, scripts.length, "expected 1 registered script");
+ browser.test.assertEq(
+ JSON.stringify([
+ {
+ id: script.id,
+ allFrames: false,
+ matches: script.matches,
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: script.js,
+ },
+ ]),
+ JSON.stringify(scripts),
+ "expected script to not have been modified"
+ );
+
+ browser.test.sendMessage("background-done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_updateContentScripts() {
+ let extension = makeExtension({
+ async background() {
+ const SCRIPT_ID = "script-to-update";
+
+ await browser.scripting.registerContentScripts([
+ {
+ id: SCRIPT_ID,
+ js: ["script-1.js"],
+ matches: ["*://test1.example.com/*"],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.onMessage.addListener(async (msg, params) => {
+ switch (msg) {
+ case "updateContentScripts": {
+ const {
+ title,
+ updateContentScriptsParams,
+ expectedRegisteredContentScript
+ } = params;
+
+ let result = await browser.scripting.updateContentScripts([
+ updateContentScriptsParams,
+ ]);
+ browser.test.assertEq(
+ undefined,
+ result,
+ `${title} - expected no return value`
+ );
+
+ let scripts = await browser.scripting.getRegisteredContentScripts();
+ browser.test.assertEq(
+ JSON.stringify([expectedRegisteredContentScript]),
+ JSON.stringify(scripts),
+ `${title} - expected registered script`
+ );
+
+ browser.test.sendMessage(`${msg}-done`);
+ break;
+ }
+
+ default:
+ browser.test.fail(`invalid message received: ${msg}`);
+ }
+ });
+
+ browser.test.sendMessage("background-ready");
+ },
+ files: {
+ "script-1.js": () => {
+ browser.test.sendMessage(
+ `script-1 executed in ${location.pathname.split("/").pop()}`
+ );
+ },
+ "script-2.js": () => {
+ browser.test.sendMessage(
+ `script-2 executed in ${location.pathname.split("/").pop()}`
+ );
+ },
+ "script-3.js": () => {
+ browser.test.sendMessage(
+ `script-3 executed in ${location.pathname.split("/").pop()}`
+ );
+ },
+ "script-4.js": () => {
+ browser.test.sendMessage(
+ `script-4 executed in ${location.pathname.split("/").pop()}`
+ );
+ },
+ "style.css": "body { background-color: rgb(0, 255, 0); }",
+ "script-check-style.js": () => {
+ browser.test.assertEq(
+ "rgb(0, 255, 0)",
+ getComputedStyle(document.querySelector('body')).backgroundColor,
+ "expected background color"
+ );
+ browser.test.sendMessage(
+ `script-check-style executed in ${location.pathname.split("/").pop()}`
+ );
+ },
+ },
+ });
+
+ const SCRIPT_ID = "script-to-update";
+ const TEST_PAGE = "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html";
+
+ const runTestCase = async ({
+ title,
+ updateContentScriptsParams,
+ expectedRegisteredContentScript,
+ expectedMessages
+ }) => {
+ // Register content script and verify results.
+ extension.sendMessage("updateContentScripts", {
+ title,
+ updateContentScriptsParams,
+ expectedRegisteredContentScript,
+ });
+ await extension.awaitMessage("updateContentScripts-done");
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ TEST_PAGE,
+ true
+ );
+
+ await Promise.all(expectedMessages.map(msg => extension.awaitMessage(msg)));
+
+ await AppTestDelegate.removeTab(window, tab);
+ };
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ // Load a page that will trigger the content script initially registered.
+ let tab = await AppTestDelegate.openNewForegroundTab(window, TEST_PAGE, true);
+ await extension.awaitMessage("script-1 executed in file_contains_iframe.html");
+ await AppTestDelegate.removeTab(window, tab);
+
+ // Now, let's update this content script a few times.
+ await runTestCase({
+ title: "update ID only",
+ updateContentScriptsParams: {
+ id: SCRIPT_ID,
+ },
+ expectedRegisteredContentScript: {
+ id: SCRIPT_ID,
+ allFrames: false,
+ matches: ["*://test1.example.com/*"],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: ["script-1.js"],
+ },
+ expectedMessages: ["script-1 executed in file_contains_iframe.html"],
+ });
+
+ await runTestCase({
+ title: "update js",
+ updateContentScriptsParams: {
+ id: SCRIPT_ID,
+ js: ["script-2.js"],
+ },
+ expectedRegisteredContentScript: {
+ id: SCRIPT_ID,
+ allFrames: false,
+ matches: ["*://test1.example.com/*"],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: ["script-2.js"],
+ },
+ expectedMessages: ["script-2 executed in file_contains_iframe.html"],
+ });
+
+ await runTestCase({
+ title: "update allFrames and matches",
+ updateContentScriptsParams: {
+ id: SCRIPT_ID,
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ allFrames: true,
+ },
+ expectedRegisteredContentScript: {
+ id: SCRIPT_ID,
+ allFrames: true,
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ js: ["script-2.js"],
+ },
+ expectedMessages: [
+ "script-2 executed in file_contains_iframe.html",
+ "script-2 executed in file_contains_img.html",
+ ],
+ });
+
+ await runTestCase({
+ title: "update excludeMatches and js",
+ updateContentScriptsParams: {
+ id: SCRIPT_ID,
+ js: ["script-3.js"],
+ excludeMatches: ["*://test1.example.com/*"],
+ allFrames: true,
+ },
+ expectedRegisteredContentScript: {
+ id: SCRIPT_ID,
+ allFrames: true,
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ excludeMatches: ["*://test1.example.com/*"],
+ js: ["script-3.js"],
+ },
+ expectedMessages: [
+ "script-3 executed in file_contains_img.html",
+ ],
+ });
+
+ await runTestCase({
+ title: "update allFrames, excludeMatches, js and runAt",
+ updateContentScriptsParams: {
+ id: SCRIPT_ID,
+ allFrames: false,
+ excludeMatches: [],
+ js: ["script-4.js"],
+ runAt: "document_start",
+ },
+ expectedRegisteredContentScript: {
+ id: SCRIPT_ID,
+ allFrames: false,
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ runAt: "document_start",
+ persistAcrossSessions: false,
+ js: ["script-4.js"],
+ },
+ expectedMessages: [
+ "script-4 executed in file_contains_iframe.html",
+ ],
+ });
+
+ await runTestCase({
+ title: "update allFrames, css, js and runAt",
+ updateContentScriptsParams: {
+ id: SCRIPT_ID,
+ allFrames: true,
+ css: ["style.css"],
+ js: ["script-check-style.js"],
+ runAt: "document_idle",
+ },
+ expectedRegisteredContentScript: {
+ id: SCRIPT_ID,
+ allFrames: true,
+ matches: [
+ "*://test1.example.com/*",
+ "*://example.org/*",
+ ],
+ runAt: "document_idle",
+ persistAcrossSessions: false,
+ css: ["style.css"],
+ js: ["script-check-style.js"],
+ },
+ expectedMessages: [
+ "script-check-style executed in file_contains_iframe.html",
+ "script-check-style executed in file_contains_img.html",
+ ],
+ });
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests scripting.executeScript()</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<iframe src="https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"></iframe>
+
+<script type="text/javascript">
+
+"use strict";
+
+const MOCHITEST_HOST_PERMISSIONS = [
+ "*://mochi.test/",
+ "*://mochi.xorigin-test/",
+ "*://test1.example.com/",
+];
+
+const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ permissions: ["scripting"],
+ host_permissions: [
+ ...MOCHITEST_HOST_PERMISSIONS,
+ "https://example.com/",
+ // Used in `file_contains_iframe.html`
+ "https://example.org/",
+ ],
+ granted_host_permissions: true,
+ ...manifestProps,
+ },
+ useAddonManager: "temporary",
+ ...otherProps,
+ });
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_executeScript_params_validation() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ const tabId = tabs[0].id;
+
+ const TEST_CASES = [
+ {
+ title: "no files and no func",
+ executeScriptParams: {},
+ expectedError: /Exactly one of files and func must be specified/,
+ },
+ {
+ title: "both files and func are passed",
+ executeScriptParams: { files: ["script.js"], func() {} },
+ expectedError: /Exactly one of files and func must be specified/,
+ },
+ {
+ title: "non-empty args is passed with files",
+ executeScriptParams: { files: ["script.js"], args: [123] },
+ expectedError: /'args' may not be used with file injections/,
+ },
+ {
+ title: "empty args is passed with files",
+ executeScriptParams: { files: ["script.js"], args: [] },
+ expectedError: /'args' may not be used with file injections/,
+ },
+ {
+ title: "unserializable argument",
+ executeScriptParams: { func() {}, args: [window] },
+ expectedError: /Unserializable arguments/,
+ },
+ {
+ title: "both allFrames and frameIds are passed",
+ executeScriptParams: {
+ target: {
+ tabId,
+ allFrames: true,
+ frameIds: [1, 2, 3],
+ },
+ files: ["script.js"],
+ },
+ expectedError: /Cannot specify both 'allFrames' and 'frameIds'/,
+ },
+ {
+ title: "invalid IDs in frameIds",
+ executeScriptParams: {
+ target: { tabId, frameIds: [0, 1, 2] },
+ func: () => {},
+ },
+ expectedError: "Invalid frame IDs: [1, 2].",
+ },
+ {
+ title: "throw non-structurally cloneable data in all frames",
+ executeScriptParams: {
+ target: {
+ tabId,
+ allFrames: true,
+ },
+ func: () => {
+ throw window;
+ },
+ },
+ expectedError: /Script '<anonymous code>' result is non-structured-clonable data/,
+ },
+ ];
+
+ for (const { title, executeScriptParams, expectedError } of TEST_CASES) {
+ await browser.test.assertRejects(
+ browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ ...executeScriptParams,
+ }),
+ expectedError,
+ `expected error when: ${title}`
+ );
+ }
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_main_world() {
+ let extension = makeExtension({
+ async background() {
+ browser.test.assertThrows(
+ () => {
+ browser.scripting.executeScript({
+ target: { tabId: 123 },
+ func: () => {},
+ world: "MAIN",
+ });
+ },
+ /world: Invalid enumeration value "MAIN"/,
+ "expected 'MAIN' world to not be supported yet"
+ );
+
+ browser.test.notifyPass("background-done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_isolated_world() {
+ let extension = makeExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "@isolated-addon-id" },
+ },
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ let results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: () => {
+ globalThis.defaultWorldVar = browser.runtime.id;
+ return "default world";
+ },
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "default world",
+ results[0].result,
+ "got expected return value"
+ );
+
+ results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: () => {
+ return `isolated: ${browser.runtime.id}; existing default var: ${typeof defaultWorldVar}`;
+ },
+ world: "ISOLATED",
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "isolated: @isolated-addon-id; existing default var: string",
+ results[0].result,
+ "got expected return value"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_execution_world_constants() {
+ let extension = makeExtension({
+ async background() {
+ browser.test.assertTrue(
+ !!browser.scripting.ExecutionWorld,
+ "expected scripting.ExecutionWorld to be defined"
+ );
+ browser.test.assertEq(
+ 1,
+ Object.keys(browser.scripting.ExecutionWorld).length,
+ "expected 1 ExecutionWorld constant"
+ );
+ browser.test.assertEq(
+ "ISOLATED",
+ browser.scripting.ExecutionWorld.ISOLATED,
+ "expected ISOLATED constant to be defined"
+ );
+ // TODO: Bug 1736575 - Add support for other execution worlds like MAIN.
+ browser.test.assertEq(
+ undefined,
+ browser.scripting.ExecutionWorld.MAIN,
+ "expected MAIN constant to be undefined"
+ );
+
+ browser.test.notifyPass("background-done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("background-done");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_with_wrong_host_permissions() {
+ let extension = makeExtension({
+ manifest: {
+ host_permissions: [],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ await browser.test.assertRejects(
+ browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: () => {
+ browser.test.fail("Unexpected execution");
+ },
+ }),
+ "Missing host permission for the tab",
+ "expected host permission error"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_with_invalid_tabId() {
+ let extension = makeExtension({
+ async background() {
+ // This tab ID should not exist.
+ const tabId = 123456789;
+
+ await browser.test.assertRejects(
+ browser.scripting.executeScript({
+ target: { tabId },
+ func: () => {
+ browser.test.fail("Unexpected execution");
+ },
+ }),
+ `Invalid tab ID: ${tabId}`
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_with_func() {
+ let extension = makeExtension({
+ async background() {
+ const getTitle = () => {
+ return document.title;
+ };
+
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: getTitle,
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "file sample",
+ results[0].result,
+ "got the expected title"
+ );
+ browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_executeScript_with_func_and_args() {
+ let extension = makeExtension({
+ async background() {
+ const formatArgs = (a, b, c) => {
+ return `received ${a}, ${b} and ${c}`;
+ };
+
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: formatArgs,
+ args: [true, undefined, "str"],
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ // undefined is converted to null when json-stringified in an array.
+ "received true, null and str",
+ results[0].result,
+ "got the expected return value"
+ );
+ browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_returns_nothing() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: () => {},
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ undefined,
+ results[0].result,
+ "got expected undefined result"
+ );
+ browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_returns_null() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: () => {
+ return null;
+ },
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ null,
+ results[0].result,
+ "got expected null result"
+ );
+ browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_with_error_in_func() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: () => {
+ throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
+ },
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
+ browser.test.assertEq(
+ "Thrown at file_sample.html",
+ results[0].error.message,
+ "got the expected error message"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_executeScript_with_a_file() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ files: ["script.js"],
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "value from script.js",
+ results[0].result,
+ "got the expected result"
+ );
+ browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
+
+ browser.test.notifyPass("execute-script");
+ },
+ files: {
+ "script.js": function() {
+ return "value from script.js";
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_in_one_frame() {
+ let extension = makeExtension({
+ manifest: {
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame with the MochiTest runner
+ // 2. Frame for this file
+ // 3. Frame that loads `file_sample.html` at the top of this file
+ browser.test.assertEq(3, frames.length, "expected 3 frames");
+
+ const fileSampleFrameId = frames[2].frameId;
+ browser.test.assertTrue(
+ frames[2].url.includes("file_sample.html"),
+ "expected frame URL"
+ );
+
+ const TEST_CASES = [
+ {
+ title: "with a file and a frame ID",
+ params: {
+ target: { tabId, frameIds: [fileSampleFrameId] },
+ files: ["script.js"],
+ },
+ expectedResults: [
+ {
+ frameId: fileSampleFrameId,
+ result: "Sample text",
+ },
+ ],
+ },
+ {
+ title: "with no frame ID",
+ params: {
+ target: { tabId },
+ func: () => {
+ return 123;
+ },
+ },
+ expectedResults: [{ frameId: 0, result: 123 }],
+ },
+ ];
+
+ for (const { title, params, expectedResults } of TEST_CASES) {
+ const results = await browser.scripting.executeScript(params);
+
+ browser.test.assertEq(
+ expectedResults.length,
+ results.length,
+ `${title} - got expected number of results`
+ );
+ expectedResults.forEach(({ frameId, result }, index) => {
+ browser.test.assertEq(
+ result,
+ results[index].result,
+ `${title} - got the expected results[${index}].result`
+ );
+ browser.test.assertEq(
+ frameId,
+ results[index].frameId,
+ `${title} - got the expected results[${index}].frameId`
+ );
+ });
+ }
+
+ browser.test.notifyPass("execute-script");
+ },
+ files: {
+ "script.js": function() {
+ return document.getElementById("test").textContent;
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_in_multiple_frameIds() {
+ let extension = makeExtension({
+ manifest: {
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame that loads `file_contains_iframe.html`
+ // 2. Frame that loads `file_contains_img.html`
+ browser.test.assertEq(2, frames.length, "expected 2 frames");
+
+ const frameIds = frames.map(frame => frame.frameId);
+
+ const getTitle = () => {
+ return document.title;
+ };
+
+ const TEST_CASES = [
+ {
+ title: "multiple frame IDs",
+ params: {
+ target: { tabId, frameIds },
+ func: getTitle,
+ },
+ expectedResults: [
+ {
+ frameId: frameIds[0],
+ result: "file contains iframe",
+ },
+ {
+ frameId: frameIds[1],
+ result: "file contains img",
+ },
+ ],
+ },
+ {
+ title: "empty list of frame IDs",
+ params: {
+ target: { tabId, frameIds: [] },
+ func: getTitle,
+ },
+ expectedResults: [],
+ },
+ ];
+
+ for (const { title, params, expectedResults } of TEST_CASES) {
+ const results = await browser.scripting.executeScript(params);
+
+ browser.test.assertEq(
+ expectedResults.length,
+ results.length,
+ `${title} - got expected number of results`
+ );
+ // Sort injection results by frameId to always assert the results in
+ // the same order.
+ results.sort((a, b) => a.frameId - b.frameId);
+
+ browser.test.assertEq(
+ JSON.stringify(expectedResults),
+ JSON.stringify(results),
+ `${title} - got expected results`
+ );
+ }
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_executeScript_with_errors_in_multiple_frameIds() {
+ let extension = makeExtension({
+ manifest: {
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame that loads `file_contains_iframe.html`
+ // 2. Frame that loads `file_contains_img.html`
+ browser.test.assertEq(2, frames.length, "expected 2 frames");
+
+ const frameIds = frames.map(frame => frame.frameId);
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId, frameIds },
+ func: () => {
+ throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
+ },
+ });
+
+ browser.test.assertEq(
+ 2,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "Thrown at file_contains_iframe.html",
+ results[0].error.message,
+ "got expected error message in results[0]"
+ );
+ browser.test.assertEq(
+ "Thrown at file_contains_img.html",
+ results[1].error.message,
+ "got expected error message in results[1]"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_executeScript_with_frameId_and_wrong_host_permission() {
+ let extension = makeExtension({
+ manifest: {
+ host_permissions: MOCHITEST_HOST_PERMISSIONS,
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame with the MochiTest runner
+ // 2. Frame for this file
+ // 3. Frame that loads `file_sample.html` at the top of this file
+ browser.test.assertEq(3, frames.length, "expected 3 frames");
+
+ const frameIds = frames.map(frame => frame.frameId);
+
+ await browser.test.assertRejects(
+ browser.scripting.executeScript({
+ target: { tabId, frameIds: [frameIds[2]] },
+ func: () => {
+ browser.test.fail("Unexpected execution");
+ },
+ }),
+ "Missing host permission for the tab or frames",
+ "got the expected error message"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_with_multiple_frameIds_and_wrong_host_permissions() {
+ let extension = makeExtension({
+ manifest: {
+ host_permissions: MOCHITEST_HOST_PERMISSIONS,
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame with the MochiTest runner
+ // 2. Frame for this file
+ // 3. Frame that loads `file_sample.html` at the top of this file
+ browser.test.assertEq(3, frames.length, "expected 3 frames");
+
+ const frameIds = frames.map(frame => frame.frameId);
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId, frameIds },
+ func: () => {},
+ });
+
+ // We get 2 results because we cannot inject into the 3rd frame.
+ browser.test.assertEq(
+ 2,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertTrue(
+ typeof results[0].error === "undefined",
+ "expected no error in results[0]"
+ );
+ browser.test.assertTrue(
+ typeof results[1].error === "undefined",
+ "expected no error in results[1]"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_with_iframe_srcdoc_and_aboutblank() {
+ let iframe = document.createElement("iframe");
+ iframe.srcdoc = `<!DOCTYPE html>
+ <html>
+ <head><title>iframe with srcdoc</title></head>
+ </html>`;
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ document.body.appendChild(iframe);
+ });
+
+ let iframeAboutBlank = document.createElement("iframe");
+ iframeAboutBlank.src = "about:blank";
+ await new Promise(resolve => {
+ iframeAboutBlank.onload = resolve;
+ document.body.appendChild(iframeAboutBlank);
+ });
+
+ let extension = makeExtension({
+ manifest: {
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame with the MochiTest runner
+ // 2. Frame for this file
+ // 3. Frame that loads `file_sample.html` at the top of this file
+ // 4. Frame that loads the `srcdoc`
+ // 5. Frame for `about:blank`
+ browser.test.assertEq(5, frames.length, "expected 5 frames");
+
+ const frameIds = frames.map(frame => frame.frameId);
+
+ const TEST_CASES = [
+ {
+ title: "with frameIds for all frames",
+ params: {
+ target: { tabId, frameIds },
+ },
+ expectedResults: {
+ count: 5,
+ entriesAtIndex: {
+ 3: {
+ frameId: frameIds[3],
+ result: "iframe with srcdoc",
+ },
+ 4: {
+ frameId: frameIds[4],
+ result: "about:blank",
+ },
+ },
+ },
+ },
+ {
+ title: "with allFrames: true",
+ params: {
+ target: { tabId, allFrames: true },
+ },
+ expectedResults: {
+ count: 5,
+ entriesAtIndex: {
+ 3: {
+ frameId: frameIds[3],
+ result: "iframe with srcdoc",
+ },
+ 4: {
+ frameId: frameIds[4],
+ result: "about:blank",
+ },
+ },
+ },
+ },
+ {
+ title: "with a single frame specified",
+ params: {
+ target: { tabId, frameIds: [frameIds[3]] },
+ },
+ expectedResults: {
+ count: 1,
+ entriesAtIndex: {
+ 0: {
+ frameId: frameIds[3],
+ result: "iframe with srcdoc",
+ },
+ },
+ },
+ },
+ ];
+
+ for (const { title, params, expectedResults } of TEST_CASES) {
+ const results = await browser.scripting.executeScript({
+ ...params,
+ func: () => {
+ return document.title || document.URL;
+ },
+ });
+ // Sort injection results by frameId to always assert the results in
+ // the same order.
+ results.sort((a, b) => a.frameId - b.frameId);
+
+ browser.test.assertEq(
+ expectedResults.count,
+ results.length,
+ `${title} - got the expected number of results`
+ );
+ Object.keys(expectedResults.entriesAtIndex).forEach(index => {
+ browser.test.assertEq(
+ JSON.stringify(expectedResults.entriesAtIndex[index]),
+ JSON.stringify(results[index]),
+ `${title} - got expected results[${index}]`
+ );
+ });
+ }
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ iframe.remove();
+ iframeAboutBlank.remove();
+});
+
+add_task(async function test_executeScript_with_multiple_files() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ files: ["1.js", "2.js"],
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "value from 2.js",
+ results[0].result,
+ "got the expected result"
+ );
+ browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
+
+ browser.test.notifyPass("execute-script");
+ },
+ files: {
+ "1.js": function() {
+ return "value from 1.js";
+ },
+ "2.js": function() {
+ return "value from 2.js";
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+});
+
+add_task(async function test_executeScript_with_multiple_files_and_an_error() {
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ files: ["1.js", "2.js"],
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
+ browser.test.assertEq(
+ "Thrown at file_contains_iframe.html",
+ results[0].error.message,
+ "got the expected error message"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ files: {
+ "1.js": function() {
+ throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
+ },
+ "2.js": function() {
+ return "value from 2.js";
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_executeScript_with_file_not_in_extension() {
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ await browser.test.assertRejects(
+ browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ files: ["https://example.com/script.js"],
+ }),
+ /Files to be injected must be within the extension/,
+ "got the expected error message"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_executeScript_allFrames() {
+ let extension = makeExtension({
+ manifest: {
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame that loads `file_contains_iframe.html`
+ // 2. Frame that loads `file_contains_img.html`
+ browser.test.assertEq(2, frames.length, "expected 2 frames");
+ const frameIds = frames.map(frame => frame.frameId);
+
+ const getTitle = () => {
+ return document.title;
+ };
+
+ const TEST_CASES = [
+ {
+ title: "allFrames set to true",
+ scriptingParams: {
+ target: { tabId, allFrames: true },
+ func: getTitle,
+ },
+ expectedResults: [
+ {
+ frameId: frameIds[0],
+ result: "file contains iframe",
+ },
+ {
+ frameId: frameIds[1],
+ result: "file contains img",
+ },
+ ],
+ },
+ {
+ title: "allFrames set to false",
+ scriptingParams: {
+ target: { tabId, allFrames: false },
+ func: getTitle,
+ },
+ expectedResults: [
+ {
+ frameId: frameIds[0],
+ result: "file contains iframe",
+ },
+ ],
+ },
+ ];
+
+ for (const { title, scriptingParams, expectedResults } of TEST_CASES) {
+ const results = await browser.scripting.executeScript(scriptingParams);
+ // Sort injection results by frameId to always assert the results in
+ // the same order.
+ results.sort((a, b) => a.frameId - b.frameId);
+
+ browser.test.assertDeepEq(
+ expectedResults,
+ results,
+ `${title} - got expected results`
+ );
+
+ // Make sure the `error` prop is never set.
+ for (const result of results) {
+ browser.test.assertFalse(
+ "error" in result,
+ `${title} - expected error property to be unset`
+ );
+ }
+ }
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_executeScript_runtime_errors() {
+ let extension = makeExtension({
+ manifest: {
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame that loads `file_contains_iframe.html`
+ // 2. Frame that loads `file_contains_img.html`
+ browser.test.assertEq(2, frames.length, "expected 2 frames");
+
+ const TEST_CASES = [
+ {
+ title: "reference error",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ // We do not define `e` on purpose.
+ // eslint-disable-next-line no-undef
+ return String(e);
+ },
+ },
+ expectedErrors: [
+ { type: "Error", stringRepr: "ReferenceError: e is not defined" },
+ ],
+ },
+ {
+ title: "eval error",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ // We use `eval()` on purpose.
+ // eslint-disable-next-line no-eval
+ eval("");
+ },
+ },
+ expectedErrors: [
+ { type: "Error", stringRepr: "EvalError: call to eval() blocked by CSP" },
+ ],
+ },
+ {
+ title: "errors thrown in allFrames",
+ scriptingParams: {
+ target: { tabId, allFrames: true },
+ func: () => {
+ throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
+ },
+ },
+ expectedErrors: [
+ { type: "Error", stringRepr: "Error: Thrown at file_contains_iframe.html" },
+ { type: "Error", stringRepr: "Error: Thrown at file_contains_img.html" },
+ ],
+ },
+ {
+ title: "custom error",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ class CustomError extends Error {
+ constructor(message) {
+ super(message);
+
+ this.name = 'CustomError';
+ }
+ }
+
+ throw new CustomError("a custom error message");
+ },
+ },
+ // See Bug 1556604 for why a custom (derived) error looks like a
+ // normal error object after cloning.
+ expectedErrors: [
+ { type: "Error", stringRepr: "Error: a custom error message" },
+ ],
+ },
+ {
+ title: "promise rejection with a string value",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ // eslint-disable-next-line no-throw-literal
+ throw 'an error message';
+ },
+ },
+ expectedErrors: [
+ { type: "String", stringRepr: "an error message" },
+ ],
+ },
+ {
+ title: "promise rejection with an error",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ throw new Error('ooops');
+ },
+ },
+ expectedErrors: [
+ { type: "Error", stringRepr: "Error: ooops" },
+ ],
+ },
+ {
+ title: "promise rejection with null",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ throw null; // eslint-disable-line no-throw-literal
+ },
+ },
+ expectedErrors: [
+ // This means we would receive `error: null`.
+ { type: "Null", stringRepr: "null" },
+ ],
+ },
+ {
+ title: "promise rejection with undefined",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ return new Promise((resolve, reject) => {
+ reject(undefined);
+ });
+ },
+ },
+ expectedErrors: [
+ // This means we would receive `error: undefined`.
+ { type: "Undefined", stringRepr: "undefined" },
+ ],
+ },
+ {
+ title: "promise rejection with empty string",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ throw ""; // eslint-disable-line no-throw-literal
+ },
+ },
+ expectedErrors: [
+ { type: "String", stringRepr: "" },
+ ],
+ },
+ {
+ title: "promise rejection with zero",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ throw 0; // eslint-disable-line no-throw-literal
+ },
+ },
+ expectedErrors: [
+ { type: "Number", stringRepr: "0" },
+ ],
+ },
+ {
+ title: "promise rejection with false",
+ scriptingParams: {
+ target: { tabId },
+ func: () => {
+ throw false; // eslint-disable-line no-throw-literal
+ },
+ },
+ expectedErrors: [
+ { type: "Boolean", stringRepr: "false" },
+ ],
+ },
+ ];
+
+ for (const { title, scriptingParams, expectedErrors } of TEST_CASES) {
+ const results = await browser.scripting.executeScript(scriptingParams);
+ // Sort injection results by frameId to always assert the results in
+ // the same order.
+ results.sort((a, b) => a.frameId - b.frameId);
+
+ browser.test.assertEq(
+ expectedErrors.length,
+ results.length,
+ `expected ${expectedErrors.length} results`
+ );
+
+ for (const [i, { type, stringRepr }] of expectedErrors.entries()) {
+ browser.test.assertTrue(
+ "error" in results[i],
+ `${title} - expected error property to be set`
+ );
+ browser.test.assertFalse(
+ "result" in results[i],
+ `${title} - expected result property to be unset`
+ );
+
+ const { frameId, error } = results[i];
+
+ browser.test.assertEq(
+ `[object ${type}]`,
+ Object.prototype.toString.call(error),
+ `${title} - expected instance of ${type} - ${frameId}`
+ );
+ browser.test.assertEq(
+ stringRepr,
+ String(error),
+ `${title} - got expected errors - ${frameId}`
+ );
+ }
+ }
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(
+ async function test_executeScript_with_allFrames_and_wrong_host_permissions() {
+ let extension = makeExtension({
+ manifest: {
+ host_permissions: MOCHITEST_HOST_PERMISSIONS,
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame with the MochiTest runner
+ // 2. Frame for this file
+ // 3. Frame that loads `file_sample.html` at the top of this file
+ browser.test.assertEq(3, frames.length, "expected 3 frames");
+
+ const results = await browser.scripting.executeScript({
+ target: { tabId, allFrames: true },
+ func: () => {},
+ });
+
+ browser.test.assertEq(
+ 2,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertTrue(
+ typeof results[0].error === "undefined",
+ "expected no error in results[0]"
+ );
+ browser.test.assertTrue(
+ typeof results[1].error === "undefined",
+ "expected no error in results[1]"
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+ }
+);
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests scripting.executeScript() and activeTab</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+
+"use strict";
+
+const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ ...manifestProps,
+ },
+ ...otherProps,
+ });
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+async function verifyExecuteScriptActiveTab(permissions, host_permissions) {
+ let extension = makeExtension({
+ manifest: {
+ permissions: ["scripting", ...permissions],
+ host_permissions,
+ },
+ background() {
+ browser.action.onClicked.addListener(async tab => {
+ const results = await browser.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: () => document.title,
+ });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "file sample",
+ results[0].result,
+ "got the expected title"
+ );
+ browser.test.assertEq(
+ 0,
+ results[0].frameId,
+ "got the expected frameId"
+ );
+
+ browser.test.sendMessage("execute-script");
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ switch (msg) {
+ case "reload-and-execute":
+ const tabs = await browser.tabs.query({ active: true });
+ const tabId = tabs[0].id;
+
+ let promiseTabLoad = new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(updatedTabId, changeInfo) {
+ browser.test.assertEq(tabId, updatedTabId, "got expected tabId");
+
+ if (tabId === updatedTabId && changeInfo.status === "complete") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+
+ await browser.tabs.reload();
+ await promiseTabLoad;
+
+ await browser.test.assertRejects(
+ browser.scripting.executeScript({
+ target: { tabId },
+ func: () => {
+ browser.test.fail("Unexpected execution");
+ },
+ }),
+ "Missing host permission for the tab",
+ "expected host permission error"
+ );
+
+ browser.test.sendMessage("execute-script-after-reload");
+
+ break;
+ default:
+ browser.test.fail(`invalid message received: ${msg}`);
+ }
+ });
+
+ browser.test.sendMessage("background-ready");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ await AppTestDelegate.clickBrowserAction(window, extension);
+ await extension.awaitMessage("execute-script");
+ await AppTestDelegate.closeBrowserAction(window, extension);
+
+ extension.sendMessage("reload-and-execute");
+ await extension.awaitMessage("execute-script-after-reload");
+
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+}
+
+// Test executeScript works with the standard activeTab permission.
+add_task(async function test_executeScript_activeTab_permission() {
+ await verifyExecuteScriptActiveTab(["activeTab"], []);
+});
+
+// Test executeScript works with automatic activeTab granted from optional
+// host permissions.
+add_task(async function test_executeScript_activeTab_automatic_originControls() {
+ await verifyExecuteScriptActiveTab([], ["*://test1.example.com/*"]);
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+ <html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests scripting.executeScript() and injectImmediately</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+
+"use strict";
+
+const MOCHITEST_HOST_PERMISSIONS = [
+ "*://mochi.test/",
+ "*://mochi.xorigin-test/",
+ "*://test1.example.com/",
+];
+
+const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ permissions: ["scripting"],
+ host_permissions: [
+ ...MOCHITEST_HOST_PERMISSIONS,
+ // Used in `file_contains_iframe.html`
+ "https://example.org/",
+ ],
+ granted_host_permissions: true,
+ ...manifestProps,
+ },
+ useAddonManager: "temporary",
+ ...otherProps,
+ });
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_executeScript_injectImmediately() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ const tabId = tabs[0].id;
+
+ let onUpdatedPromise = (tabId, url, status) => {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(_, changed, tab) {
+ if (tabId == tab.id && changed.status == status && tab.url == url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ };
+
+ const url = [
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/",
+ `file_slowed_document.sjs?with-iframe&r=${Math.random()}`,
+ ].join("");
+ const loadingPromise = onUpdatedPromise(tabId, url, "loading");
+ const completePromise = onUpdatedPromise(tabId, url, "complete");
+
+ await browser.tabs.update(tabId, { url });
+ await loadingPromise;
+
+ const func = () => {
+ window.counter = (window.counter || 0) + 1;
+
+ return window.counter;
+ };
+
+ let results = await Promise.all([
+ // counter = 1
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ injectImmediately: true,
+ }),
+ // counter = 3
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ injectImmediately: false,
+ }),
+ // counter = 4
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ // `injectImmediately` is `false` by default
+ }),
+ // counter = 2
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ injectImmediately: true,
+ }),
+ // counter = 5
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ injectImmediately: false,
+ }),
+ ]);
+ browser.test.assertEq(
+ 5,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "1 3 4 2 5",
+ results.map(res => res[0].result).join(" "),
+ `got expected results: ${JSON.stringify(results)}`
+ );
+
+ await completePromise;
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+add_task(async function test_executeScript_injectImmediately_after_document_idle() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+
+ const func = () => {
+ window.counter = (window.counter || 0) + 1;
+
+ return window.counter;
+ };
+
+ let results = await Promise.all([
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ injectImmediately: true,
+ }),
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ injectImmediately: false,
+ }),
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ // `injectImmediately` is `false` by default
+ }),
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ injectImmediately: true,
+ }),
+ browser.scripting.executeScript({
+ target: { tabId },
+ func,
+ injectImmediately: false,
+ }),
+ ]);
+ browser.test.assertEq(
+ 5,
+ results.length,
+ "got expected number of results"
+ );
+ browser.test.assertEq(
+ "1 2 3 4 5",
+ results.map(res => res[0].result).join(" "),
+ `got expected results: ${JSON.stringify(results)}`
+ );
+
+ browser.test.notifyPass("execute-script");
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("execute-script");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests scripting.insertCSS()</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const MOCHITEST_HOST_PERMISSIONS = [
+ "*://mochi.test/",
+ "*://mochi.xorigin-test/",
+ "*://test1.example.com/",
+];
+
+const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ permissions: ["scripting"],
+ host_permissions: [
+ ...MOCHITEST_HOST_PERMISSIONS,
+ // Used in `file_contains_iframe.html`
+ "https://example.org/",
+ ],
+ granted_host_permissions: true,
+ ...manifestProps,
+ },
+ useAddonManager: "temporary",
+ ...otherProps,
+ });
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_insertCSS_and_removeCSS_params_validation() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ const TEST_CASES = [
+ {
+ title: "no files and no css",
+ cssParams: {},
+ expectedError: "Exactly one of files and css must be specified.",
+ },
+ {
+ title: "both files and css are passed",
+ cssParams: {
+ files: ["styles.css"],
+ css: "* { background: rgb(1, 1, 1) }",
+ },
+ expectedError: "Exactly one of files and css must be specified.",
+ },
+ {
+ title: "both allFrames and frameIds are passed",
+ cssParams: {
+ target: {
+ tabId: tabs[0].id,
+ allFrames: true,
+ frameIds: [1, 2, 3],
+ },
+ files: ["styles.css"],
+ },
+ expectedError: "Cannot specify both 'allFrames' and 'frameIds'.",
+ },
+ {
+ title: "empty css string with a file",
+ cssParams: {
+ css: "",
+ files: ["styles.css"],
+ },
+ expectedError: "Exactly one of files and css must be specified.",
+ },
+ ];
+
+ for (const { title, cssParams, expectedError } of TEST_CASES) {
+ await browser.test.assertRejects(
+ browser.scripting.insertCSS({
+ target: { tabId: tabs[0].id },
+ ...cssParams,
+ }),
+ expectedError,
+ `${title} - expected error for insertCSS()`
+ );
+
+ await browser.test.assertRejects(
+ browser.scripting.removeCSS({
+ target: { tabId: tabs[0].id },
+ ...cssParams,
+ }),
+ expectedError,
+ `${title} - expected error for removeCSS()`
+ );
+ }
+
+ browser.test.notifyPass("checks-done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("checks-done");
+ await extension.unload();
+});
+
+add_task(async function test_insertCSS_with_invalid_tabId() {
+ let extension = makeExtension({
+ async background() {
+ // This tab ID should not exist.
+ const tabId = 123456789;
+
+ await browser.test.assertRejects(
+ browser.scripting.insertCSS({
+ target: { tabId },
+ css: "* { background: rgb(1, 1, 1) }",
+ }),
+ `Invalid tab ID: ${tabId}`
+ );
+
+ browser.test.notifyPass("insert-css");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("insert-css");
+ await extension.unload();
+});
+
+add_task(async function test_insertCSS_with_wrong_host_permissions() {
+ let extension = makeExtension({
+ manifest: {
+ host_permissions: [],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ browser.test.assertRejects(
+ browser.scripting.insertCSS({
+ target: { tabId: tabs[0].id },
+ css: "* { background: rgb(1, 1, 1) }",
+ }),
+ /Missing host permission for the tab/,
+ "expected host permission error"
+ );
+
+ browser.test.notifyPass("insert-css");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("insert-css");
+ await extension.unload();
+});
+
+add_task(async function test_insertCSS_and_removeCSS() {
+ let extension = makeExtension({
+ manifest: {
+ permissions: ["scripting", "webNavigation"],
+ },
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const tabId = tabs[0].id;
+
+ const frames = await browser.webNavigation.getAllFrames({ tabId });
+ // 1. Top-level frame that loads `file_contains_iframe.html`
+ // 2. Frame that loads `file_contains_img.html`
+ browser.test.assertEq(2, frames.length, "expected 2 frames");
+ const frameIds = frames.map(frame => frame.frameId);
+
+ const cssColor1 = "rgb(1, 1, 1)";
+ const cssColor2 = "rgb(2, 2, 2)";
+ const cssColorInFile1 = "rgb(3, 3, 3)";
+ const defaultColor = "rgba(0, 0, 0, 0)";
+
+ const TEST_CASES = [
+ {
+ title: "with css prop",
+ elementId: "div-1",
+ cssParams: [
+ {
+ target: { tabId },
+ css: `#div-1 { background: ${cssColor1} }`,
+ },
+ ],
+ expectedResults: [cssColor1, defaultColor],
+ },
+ {
+ title: "with a file",
+ elementId: "div-2",
+ cssParams: [
+ {
+ target: { tabId },
+ files: ["file1.css"],
+ },
+ ],
+ expectedResults: [cssColorInFile1, defaultColor],
+ },
+ {
+ title: "css prop in a single frame",
+ elementId: "div-3",
+ cssParams: [
+ {
+ target: { tabId, frameIds: [frameIds[0]] },
+ css: `#div-3 { background: ${cssColor2} }`,
+ },
+ ],
+ expectedResults: [cssColor2, defaultColor],
+ },
+ {
+ title: "css prop in multiple frames",
+ elementId: "div-4",
+ cssParams: [
+ {
+ target: { tabId, frameIds },
+ css: `#div-4 { background: ${cssColor1} }`,
+ },
+ ],
+ expectedResults: [cssColor1, cssColor1],
+ },
+ {
+ title: "allFrames is true",
+ elementId: "div-5",
+ cssParams: [
+ {
+ target: { tabId, allFrames: true },
+ css: `#div-5 { background: ${defaultColor} }`,
+ },
+ ],
+ expectedResults: [defaultColor, defaultColor],
+ },
+ {
+ title: "origin: 'AUTHOR'",
+ elementId: "div-6",
+ cssParams: [
+ {
+ target: { tabId },
+ css: `#div-6 { background: ${cssColor1} }`,
+ origin: "AUTHOR",
+ },
+ {
+ target: { tabId },
+ css: `#div-6 { background: ${cssColor2} }`,
+ origin: "AUTHOR",
+ },
+ ],
+ expectedResults: [cssColor2, defaultColor],
+ },
+ {
+ title: "origin: 'USER'",
+ elementId: "div-7",
+ cssParams: [
+ {
+ target: { tabId },
+ css: `#div-7 { background: ${cssColor1} !important }`,
+ origin: "USER",
+ },
+ {
+ target: { tabId },
+ css: `#div-7 { background: ${cssColor2} !important }`,
+ origin: "AUTHOR",
+ },
+ ],
+ // User has higher importance.
+ expectedResults: [cssColor1, defaultColor],
+ },
+ {
+ title: "empty css string",
+ elementId: "div-8",
+ cssParams: [
+ {
+ target: { tabId },
+ css: "",
+ },
+ ],
+ expectedResults: [defaultColor, defaultColor],
+ },
+ {
+ title: "allFrames is false",
+ elementId: "div-9",
+ cssParams: [
+ {
+ target: { tabId, allFrames: false },
+ css: `#div-9 { background: ${cssColor1} }`,
+ },
+ ],
+ expectedResults: [cssColor1, defaultColor],
+ },
+ ];
+
+ const getBackgroundColor = elementId => {
+ return window.getComputedStyle(document.getElementById(elementId))
+ .backgroundColor;
+ };
+
+ for (const {
+ title,
+ elementId,
+ cssParams,
+ expectedResults,
+ } of TEST_CASES) {
+ // Create a unique element for the current test case.
+ await browser.scripting.executeScript({
+ target: { tabId, allFrames: true },
+ func: elementId => {
+ const element = document.createElement("div");
+ element.setAttribute("id", elementId);
+ document.body.appendChild(element);
+ },
+ args: [elementId],
+ });
+
+ for (const params of cssParams) {
+ const result = await browser.scripting.insertCSS(params);
+ // `insertCSS()` should not resolve to a value.
+ browser.test.assertEq(undefined, result, "got expected empty result");
+ }
+
+ let results = await browser.scripting.executeScript({
+ target: { tabId, allFrames: true },
+ func: getBackgroundColor,
+ args: [elementId],
+ });
+ results.sort((a, b) => a.frameId - b.frameId);
+
+ browser.test.assertEq(
+ expectedResults.length,
+ results.length,
+ `${title} - got the expected number of results`
+ );
+ results.forEach((result, index) => {
+ browser.test.assertEq(
+ expectedResults[index],
+ result.result,
+ `${title} - got expected result (index=${index}): ${title}`
+ );
+ });
+
+ results = await Promise.all(
+ cssParams.map(params => browser.scripting.removeCSS(params))
+ );
+ // `removeCSS()` should not resolve to a value.
+ results.forEach(result => {
+ browser.test.assertEq(undefined, result, "got expected empty result");
+ });
+
+ results = await browser.scripting.executeScript({
+ target: { tabId, allFrames: true },
+ func: getBackgroundColor,
+ args: [elementId],
+ });
+
+ browser.test.assertTrue(
+ results.every(({ result }) => result === defaultColor),
+ "got expected default color in all frames"
+ );
+ }
+
+ browser.test.notifyPass("insert-and-remove-css");
+ },
+ files: {
+ "file1.css": "#div-2 { background: rgb(3, 3, 3) }",
+ },
+ });
+
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
+ true
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("insert-and-remove-css");
+ await extension.unload();
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests scripting APIs and permissions</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+
+"use strict";
+
+const verifyRegisterContentScripts = async ({ manifest_version }) => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ permissions: ["scripting"],
+ host_permissions: ["*://example.com/*"],
+ optional_permissions: ["*://example.org/*"],
+
+ },
+ async background() {
+ browser.test.onMessage.addListener(async (msg, value) => {
+ switch (msg) {
+ case "grant-permission":
+ let granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(browser.permissions.request(value));
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+ browser.test.sendMessage("permission-granted");
+ break;
+
+ default:
+ browser.test.fail(`invalid message received: ${msg}`);
+ }
+ });
+
+ await browser.scripting.registerContentScripts([
+ {
+ id: "script",
+ js: ["script.js"],
+ matches: [
+ "*://example.com/*",
+ "*://example.net/*",
+ "*://example.org/*",
+ ],
+ persistAcrossSessions: false,
+ },
+ ]);
+
+ browser.test.sendMessage("background-ready");
+ },
+ files: {
+ "script.js": () => {
+ browser.test.sendMessage(
+ "script-ran",
+ window.location.host + window.location.search
+ );
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ if (manifest_version > 2) {
+ extension.sendMessage("grant-permission", {
+ origins: ["*://example.com/*"],
+ });
+ await extension.awaitMessage("permission-granted");
+ }
+
+ // `example.net` is not declared in the list of `permissions`.
+ let tabExampleNet = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://example.net/",
+ true
+ );
+ // `example.org` is listed in `optional_permissions`.
+ let tabExampleOrg = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://example.org/",
+ true
+ );
+ // `example.com` is listed in `permissions`.
+ let tabExampleCom = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://example.com/",
+ true
+ );
+
+ let value = await extension.awaitMessage("script-ran");
+ ok(
+ value === "example.com",
+ `expected: example.com, received: ${value}`
+ );
+
+ extension.sendMessage("grant-permission", {
+ origins: ["*://example.org/*"],
+ });
+ await extension.awaitMessage("permission-granted");
+
+ let tabExampleOrg2 = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "https://example.org/?2",
+ true
+ );
+
+ value = await extension.awaitMessage("script-ran");
+ ok(
+ value === "example.org?2",
+ `expected: example.org?2, received: ${value}`
+ );
+
+ await AppTestDelegate.removeTab(window, tabExampleNet);
+ await AppTestDelegate.removeTab(window, tabExampleOrg);
+ await AppTestDelegate.removeTab(window, tabExampleCom);
+ await AppTestDelegate.removeTab(window, tabExampleOrg2);
+
+ await extension.unload();
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.manifestV3.enabled", true],
+ ["extensions.webextOptionalPermissionPrompts", false],
+ ],
+ });
+});
+
+add_task(async function test_scripting_registerContentScripts_mv2() {
+ await verifyRegisterContentScripts({ manifest_version: 2 });
+});
+
+add_task(async function test_scripting_registerContentScripts_mv3() {
+ await verifyRegisterContentScripts({ manifest_version: 3 });
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests scripting.removeCSS()</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const MOCHITEST_HOST_PERMISSIONS = [
+ "*://mochi.test/",
+ "*://mochi.xorigin-test/",
+ "*://test1.example.com/",
+];
+
+const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ permissions: ["scripting"],
+ host_permissions: [...MOCHITEST_HOST_PERMISSIONS],
+ granted_host_permissions: true,
+ ...manifestProps,
+ },
+ useAddonManager: "temporary",
+ ...otherProps,
+ });
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+});
+
+add_task(async function test_removeCSS_with_invalid_tabId() {
+ let extension = makeExtension({
+ async background() {
+ // This tab ID should not exist.
+ const tabId = 123456789;
+
+ await browser.test.assertRejects(
+ browser.scripting.removeCSS({
+ target: { tabId },
+ css: "* { background: rgb(42, 42, 42) }",
+ }),
+ `Invalid tab ID: ${tabId}`
+ );
+
+ browser.test.notifyPass("remove-css");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("remove-css");
+ await extension.unload();
+});
+
+add_task(async function test_removeCSS_without_insertCSS_called_before() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ browser.scripting
+ .removeCSS({
+ target: { tabId: tabs[0].id },
+ css: "* { background: rgb(42, 42, 42) }",
+ })
+ .then(() => {
+ browser.test.notifyPass("remove-css");
+ })
+ .catch(() => {
+ browser.test.notifyFail("remove-css");
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("remove-css");
+ await extension.unload();
+});
+
+add_task(async function test_removeCSS_with_origin_mismatch() {
+ let extension = makeExtension({
+ async background() {
+ const tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(1, tabs.length, "expected 1 tab");
+
+ const cssColor = "rgb(42, 42, 42)";
+ const cssParams = {
+ target: { tabId: tabs[0].id },
+ css: `* { background: ${cssColor} !important }`,
+ };
+
+ await browser.scripting.insertCSS({ ...cssParams, origin: "AUTHOR" });
+
+ let results = await browser.scripting.executeScript({
+ target: { tabId: tabs[0].id },
+ func: () => {
+ return window.getComputedStyle(document.body).backgroundColor;
+ },
+ });
+
+ browser.test.assertEq(cssColor, results[0].result, "got expected color");
+
+ // Here, we pass a different origin, which should result in no CSS
+ // removal.
+ await browser.scripting.removeCSS({ ...cssParams, origin: "USER" });
+
+ browser.test.assertEq(
+ cssColor,
+ results[0].result,
+ "got expected color after removeCSS"
+ );
+
+ browser.test.notifyPass("remove-css");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("remove-css");
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ // Add two listeners that both send replies. We're supposed to ignore all but one
+ // of them. Which one is chosen is non-deterministic.
+
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == "getreply") {
+ sendReply("reply1");
+ }
+ });
+
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == "getreply") {
+ sendReply("reply2");
+ }
+ });
+
+ function sleep(callback, n = 10) {
+ if (n == 0) {
+ callback();
+ } else {
+ setTimeout(function() { sleep(callback, n - 1); }, 0);
+ }
+ }
+
+ let done_count = 0;
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == "done") {
+ done_count++;
+ browser.test.assertEq(done_count, 1, "got exactly one reply");
+
+ // Go through the event loop a few times to make sure we don't get multiple replies.
+ sleep(function() {
+ browser.test.notifyPass("sendmessage_doublereply");
+ });
+ }
+ });
+}
+
+function contentScript() {
+ browser.runtime.sendMessage("getreply", function(resp) {
+ if (resp != "reply1" && resp != "reply2") {
+ return; // test failed
+ }
+ browser.runtime.sendMessage("done");
+ });
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(async function test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ await Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_doublereply")]);
+
+ win.close();
+
+ await extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
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 @@
+<!doctype html>
+<head>
+ <title>Test sendMessage frameId</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+"use strict";
+
+add_task(async function test_sendMessage_frameId() {
+ const html = `<!doctype html><meta charset="utf-8"><script src="script.js"><\/script>`;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.sendMessage(msg, sender);
+ });
+ browser.tabs.create({url: "tab.html"});
+ },
+ files: {
+ "iframe.html": html,
+ "tab.html": `${html}<iframe src="iframe.html"></iframe>`,
+ "script.js": () => {
+ browser.runtime.sendMessage(window.top === window ? "tab" : "iframe");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ const tab = await extension.awaitMessage("tab");
+ ok(tab.url.endsWith("tab.html"), "Got the message from the tab");
+ is(tab.frameId, 0, "And sender.frameId is zero");
+
+ const iframe = await extension.awaitMessage("iframe");
+ ok(iframe.url.endsWith("iframe.html"), "Got the message from the iframe");
+ is(typeof iframe.frameId, "number", "With sender.frameId of type number");
+ ok(iframe.frameId > 0, "And sender.frameId greater than zero");
+
+ await extension.unload();
+});
+
+</script>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+
+async function testFn(expectPromise) {
+ browser.test.assertEq(null, chrome.runtime.lastError, "no lastError before call");
+ let retval = chrome.runtime.sendMessage("msg");
+ browser.test.assertEq(null, chrome.runtime.lastError, "no lastError after call");
+ if (expectPromise) {
+ browser.test.assertTrue(retval instanceof Promise, "chrome.runtime.sendMessage should return a promise");
+ } else {
+ browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback");
+ }
+
+ let isAsyncCall = false;
+ retval = chrome.runtime.sendMessage("msg", reply => {
+ browser.test.assertEq(undefined, reply, "no reply");
+ browser.test.assertTrue(isAsyncCall, "chrome.runtime.sendMessage's callback must be called asynchronously");
+ browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback");
+ browser.test.assertEq("Could not establish connection. Receiving end does not exist.", chrome.runtime.lastError.message);
+ browser.test.sendMessage("finished", retval);
+ });
+ isAsyncCall = true;
+}
+
+add_task(async function test_content_script_sendMessage_without_listener() {
+ async function contentScript() {
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage("msg"),
+ "Could not establish connection. Receiving end does not exist.");
+
+ browser.test.notifyPass("sendMessage callback was invoked");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ },
+ });
+
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+ await extension.awaitFinish("sendMessage callback was invoked");
+ win.close();
+
+ await extension.unload();
+});
+
+add_task(async function test_content_script_chrome_sendMessage_without_listener() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ // In MV2, chrome namespace in content scripts do get promises, however in background pages they do not.
+ background: `(${testFn})(false)`,
+ files: {
+ "contentscript.js": `(${testFn})(true)`,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished");
+
+ let win = window.open("file_sample.html");
+ await extension.awaitMessage("finished");
+ win.close();
+
+ await extension.unload();
+});
+
+add_task(async function test_chrome_sendMessage_without_listener_v3() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+
+ // We only test the background here because content script behavior
+ // is independant of the manifest version.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ },
+ background: `(${testFn})(true)`,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("finished");
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == 0) {
+ sendReply("reply1");
+ } else if (msg == 1) {
+ window.setTimeout(function() {
+ sendReply("reply2");
+ }, 0);
+ return true;
+ } else if (msg == 2) {
+ browser.test.notifyPass("sendmessage_reply");
+ }
+ });
+}
+
+function contentScript() {
+ browser.runtime.sendMessage(0, function(resp1) {
+ if (resp1 != "reply1") {
+ return; // test failed
+ }
+ browser.runtime.sendMessage(1, function(resp2) {
+ if (resp2 != "reply2") {
+ return; // test failed
+ }
+ browser.runtime.sendMessage(2);
+ });
+ });
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(async function test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ await Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_reply")]);
+
+ win.close();
+
+ await extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function backgroundScript(token, id, otherId) {
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+ if (msg === `content-${token}`) {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"),
+ `${id}: sender url correct`);
+
+ let tabId = sender.tab.id;
+ browser.tabs.sendMessage(tabId, `${token}-contentMessage`);
+
+ sendReply(`${token}-done`);
+ } else if (msg === `tab-${token}`) {
+ browser.runtime.sendMessage(otherId, `${otherId}-tabMessage`);
+ browser.runtime.sendMessage(`${token}-tabMessage`);
+
+ sendReply(`${token}-done`);
+ } else {
+ browser.test.fail(`${id}: Unexpected runtime message received: ${msg} ${uneval(sender)}`);
+ }
+ });
+
+ browser.runtime.onMessageExternal.addListener((msg, sender, sendReply) => {
+ browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`);
+
+ if (msg === `content-${id}`) {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"),
+ `${id}: external sender url correct`);
+
+ sendReply(`${otherId}-done`);
+ } else if (msg === `tab-${id}`) {
+ sendReply(`${otherId}-done`);
+ } else if (msg !== `${id}-tabMessage`) {
+ browser.test.fail(`${id}: Unexpected runtime external message received: ${msg} ${uneval(sender)}`);
+ }
+ });
+
+ browser.tabs.create({url: "tab.html"});
+}
+
+function contentScript(token, id, otherId) {
+ let gotContentMessage = false;
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+ browser.test.assertEq(`${token}-contentMessage`, msg,
+ `${id}: Correct content script message`);
+ if (msg === `${token}-contentMessage`) {
+ gotContentMessage = true;
+ }
+ });
+
+ Promise.all([
+ browser.runtime.sendMessage(otherId, `content-${otherId}`).then(resp => {
+ browser.test.assertEq(`${id}-done`, resp, `${id}: Correct content script external response token`);
+ }),
+
+ browser.runtime.sendMessage(`content-${token}`).then(resp => {
+ browser.test.assertEq(`${token}-done`, resp, `${id}: Correct content script response token`);
+ }).catch(e => {
+ browser.test.fail(`content-${token} rejected with ${e.message}`);
+ }),
+ ]).then(() => {
+ browser.test.assertTrue(gotContentMessage, `${id}: Got content script message`);
+
+ browser.test.sendMessage("content-script-done");
+ });
+}
+
+async function tabScript(token, id, otherId) {
+ let gotTabMessage = false;
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+ if (String(msg).startsWith("content-")) {
+ return;
+ }
+
+ browser.test.assertEq(`${token}-tabMessage`, msg,
+ `${id}: Correct tab script message`);
+ if (msg === `${token}-tabMessage`) {
+ gotTabMessage = true;
+ }
+ });
+
+ browser.test.sendMessage("tab-script-loaded");
+
+ await new Promise(resolve => {
+ const listener = (msg) => {
+ if (msg !== "run-tab-script") {
+ return;
+ }
+ browser.test.onMessage.removeListener(listener);
+ resolve();
+ };
+ browser.test.onMessage.addListener(listener);
+ });
+
+ Promise.all([
+ browser.runtime.sendMessage(otherId, `tab-${otherId}`).then(resp => {
+ browser.test.assertEq(`${id}-done`, resp, `${id}: Correct tab script external response token`);
+ }),
+
+ browser.runtime.sendMessage(`tab-${token}`).then(resp => {
+ browser.test.assertEq(`${token}-done`, resp, `${id}: Correct tab script response token`);
+ }),
+ ]).then(() => {
+ browser.test.assertTrue(gotTabMessage, `${id}: Got tab script message`);
+
+ window.close();
+
+ browser.test.sendMessage("tab-script-done");
+ });
+}
+
+function makeExtension(id, otherId) {
+ let token = Math.random();
+
+ let args = `${token}, ${JSON.stringify(id)}, ${JSON.stringify(otherId)}`;
+
+ let extensionData = {
+ background: `(${backgroundScript})(${args})`,
+ manifest: {
+ "browser_specific_settings": {"gecko": {id}},
+
+ "permissions": ["tabs"],
+
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "tab.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="tab.js"><\/script>
+ </head>
+ </html>`,
+
+ "tab.js": `(${tabScript})(${args})`,
+
+ "content_script.js": `(${contentScript})(${args})`,
+ },
+ };
+ return extensionData;
+}
+
+add_task(async function test_contentscript() {
+ const ID1 = "sendmessage1@mochitest.mozilla.org";
+ const ID2 = "sendmessage2@mochitest.mozilla.org";
+
+ let extension1 = ExtensionTestUtils.loadExtension(makeExtension(ID1, ID2));
+ let extension2 = ExtensionTestUtils.loadExtension(makeExtension(ID2, ID1));
+
+ await Promise.all([
+ extension1.startup(),
+ extension2.startup(),
+ extension1.awaitMessage("tab-script-loaded"),
+ extension2.awaitMessage("tab-script-loaded"),
+ ]);
+
+ extension1.sendMessage("run-tab-script");
+ extension2.sendMessage("run-tab-script");
+
+ let win = window.open("file_sample.html");
+
+ await waitForLoad(win);
+
+ await Promise.all([
+ extension1.awaitMessage("content-script-done"),
+ extension2.awaitMessage("content-script-done"),
+ extension1.awaitMessage("tab-script-done"),
+ extension2.awaitMessage("tab-script-done"),
+ ]);
+
+ win.close();
+
+ await extension1.unload();
+ await extension2.unload();
+});
+</script>
+
+</body>
+</html>
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..71ef3a2214
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html
@@ -0,0 +1,277 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const { ExtensionStorageIDB } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/ExtensionStorageIDB.jsm"
+);
+
+const storageTestHelpers = {
+ storageLocal: {
+ async writeData() {
+ await browser.storage.local.set({hello: "world"});
+ browser.test.sendMessage("finished");
+ },
+
+ async readData() {
+ const matchBrowserStorage = await browser.storage.local.get("hello").then(result => {
+ return (Object.keys(result).length == 1 && result.hello == "world");
+ });
+
+ browser.test.sendMessage("results", {matchBrowserStorage});
+ },
+
+ assertResults({results, keepOnUninstall}) {
+ if (keepOnUninstall) {
+ is(results.matchBrowserStorage, true, "browser.storage.local data is still present");
+ } else {
+ is(results.matchBrowserStorage, false, "browser.storage.local data was cleared");
+ }
+ },
+ },
+ storageSync: {
+ async writeData() {
+ await browser.storage.sync.set({hello: "world"});
+ browser.test.sendMessage("finished");
+ },
+
+ async readData() {
+ const matchBrowserStorage = await browser.storage.sync.get("hello").then(result => {
+ return (Object.keys(result).length == 1 && result.hello == "world");
+ });
+
+ browser.test.sendMessage("results", {matchBrowserStorage});
+ },
+
+ assertResults({results, keepOnUninstall}) {
+ if (keepOnUninstall) {
+ is(results.matchBrowserStorage, true, "browser.storage.sync data is still present");
+ } else {
+ is(results.matchBrowserStorage, false, "browser.storage.sync data was cleared");
+ }
+ },
+ },
+ webAPIs: {
+ async readData() {
+ let matchLocalStorage = (localStorage.getItem("hello") == "world");
+
+ let idbPromise = new Promise((resolve, reject) => {
+ let req = indexedDB.open("test");
+ req.onerror = e => {
+ reject(new Error(`indexedDB open failed with ${e.errorCode}`));
+ };
+
+ req.onupgradeneeded = e => {
+ // no database, data is not present
+ resolve(false);
+ };
+
+ req.onsuccess = e => {
+ let db = e.target.result;
+ let transaction = db.transaction("store", "readwrite");
+ let addreq = transaction.objectStore("store").get("hello");
+ addreq.onerror = addreqError => {
+ reject(new Error(`read from indexedDB failed with ${addreqError.errorCode}`));
+ };
+ addreq.onsuccess = () => {
+ let match = (addreq.result.value == "world");
+ resolve(match);
+ };
+ };
+ });
+
+ await idbPromise.then(matchIDB => {
+ let result = {matchLocalStorage, matchIDB};
+ browser.test.sendMessage("results", result);
+ });
+ },
+
+ async writeData() {
+ localStorage.setItem("hello", "world");
+
+ let idbPromise = new Promise((resolve, reject) => {
+ let req = indexedDB.open("test");
+ req.onerror = e => {
+ reject(new Error(`indexedDB open failed with ${e.errorCode}`));
+ };
+
+ req.onupgradeneeded = e => {
+ let db = e.target.result;
+ db.createObjectStore("store", {keyPath: "name"});
+ };
+
+ req.onsuccess = e => {
+ let db = e.target.result;
+ let transaction = db.transaction("store", "readwrite");
+ let addreq = transaction.objectStore("store")
+ .add({name: "hello", value: "world"});
+ addreq.onerror = addreqError => {
+ reject(new Error(`add to indexedDB failed with ${addreqError.errorCode}`));
+ };
+ addreq.onsuccess = () => {
+ resolve();
+ };
+ };
+ });
+
+ await idbPromise.then(() => {
+ browser.test.sendMessage("finished");
+ });
+ },
+
+ assertResults({results, keepOnUninstall}) {
+ if (keepOnUninstall) {
+ is(results.matchLocalStorage, true, "localStorage data is still present");
+ is(results.matchIDB, true, "indexedDB data is still present");
+ } else {
+ is(results.matchLocalStorage, false, "localStorage data was cleared");
+ is(results.matchIDB, false, "indexedDB data was cleared");
+ }
+ },
+ },
+};
+
+async function test_uninstall({extensionId, writeData, readData, assertResults}) {
+ // Set the pref to prevent cleaning up storage on uninstall in a separate prefEnv
+ // so we can pop it below, leaving flags set in the previous prefEnvs unmodified.
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.keepStorageOnUninstall", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: writeData,
+ manifest: {
+ browser_specific_settings: {gecko: {id: extensionId}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished");
+ await extension.unload();
+
+ // Check that we can still see data we wrote to storage but clear the
+ // "leave storage" flag so our storaged gets cleared on the next uninstall.
+ // This effectively tests the keepUuidOnUninstall logic, which ensures
+ // that when we read storage again and check that it is cleared, that
+ // it is actually a meaningful test!
+ await SpecialPowers.popPrefEnv();
+
+ extension = ExtensionTestUtils.loadExtension({
+ background: readData,
+ manifest: {
+ browser_specific_settings: {gecko: {id: extensionId}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ let results = await extension.awaitMessage("results");
+
+ assertResults({results, keepOnUninstall: true});
+
+ await extension.unload();
+
+ // Read again. This time, our data should be gone.
+ extension = ExtensionTestUtils.loadExtension({
+ background: readData,
+ manifest: {
+ browser_specific_settings: {gecko: {id: extensionId}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ results = await extension.awaitMessage("results");
+
+ assertResults({results, keepOnUninstall: false});
+
+ await extension.unload();
+}
+
+
+add_task(async function test_setup_keep_uuid_on_uninstall() {
+ // Use a test-only pref to leave the addonid->uuid mapping around after
+ // uninstall so that we can re-attach to the same storage (this prefEnv
+ // is kept for this entire file and cleared automatically once all the
+ // tests in this file have been executed).
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.keepUuidOnUninstall", true]],
+ });
+});
+
+// Test extension indexedDB and localStorage storages get cleaned up when the
+// extension is uninstalled.
+add_task(async function test_uninstall_with_webapi_storages() {
+ await test_uninstall({
+ extensionId: "storage.cleanup-WebAPIStorages@tests.mozilla.org",
+ ...(storageTestHelpers.webAPIs),
+ });
+});
+
+// Test browser.storage.local with JSONFile backend gets cleaned up when the
+// extension is uninstalled.
+add_task(async function test_uninistall_with_storage_local_file_backend() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[ExtensionStorageIDB.BACKEND_ENABLED_PREF, false]],
+ });
+
+ await test_uninstall({
+ extensionId: "storage.cleanup-JSONFileBackend@tests.mozilla.org",
+ ...(storageTestHelpers.storageLocal),
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Repeat the cleanup test when the storage.local IndexedDB backend is enabled.
+add_task(async function test_uninistall_with_storage_local_idb_backend() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[ExtensionStorageIDB.BACKEND_ENABLED_PREF, true]],
+ });
+
+ await test_uninstall({
+ extensionId: "storage.cleanup-IDBBackend@tests.mozilla.org",
+ ...(storageTestHelpers.storageLocal),
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Legacy storage.sync backend is still being used on GeckoView builds.
+const storageSyncOldKintoBackend = SpecialPowers.Services.prefs.getBoolPref(
+ "webextensions.storage.sync.kinto",
+ false
+);
+
+// Verify browser.storage.sync rust backend is also cleared on uninstall.
+async function test_uninistall_with_storage_sync() {
+ await test_uninstall({
+ extensionId: "storage.cleanup-sync@tests.mozilla.org",
+ ...(storageTestHelpers.storageSync),
+ });
+}
+
+// NOTE: ideally we would be using a skip_if option on the add_task call,
+// but we don't support that in the add_task defined in mochitest-plain.
+if (!storageSyncOldKintoBackend) {
+ add_task(test_uninistall_with_storage_sync);
+}
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Storage API </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.storageManager.enabled", true],
+ ["dom.storageManager.prompt.testing", true],
+ ["dom.storageManager.prompt.testing.allow", true],
+ ],
+ });
+});
+
+add_task(async function test_backgroundScript() {
+ function background() {
+ browser.test.assertTrue(navigator.storage !== undefined, "Has storage api interface");
+
+ // Test estimate.
+ browser.test.assertTrue("estimate" in navigator.storage, "Has estimate function");
+ browser.test.assertEq("function", typeof navigator.storage.estimate, "estimate is function");
+ browser.test.assertTrue(navigator.storage.estimate() instanceof Promise, "estimate returns a promise");
+
+ return browser.test.notifyPass("navigation_storage_api.done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("navigation_storage_api.done");
+ await extension.unload();
+});
+
+add_task(async function test_contentScript() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]],
+ });
+
+ function contentScript() {
+ // Should not access storage api in non-secure context.
+ browser.test.assertEq(undefined, navigator.storage,
+ "A page from the unsecure http protocol " +
+ "doesn't have access to the navigator.storage API");
+
+ return browser.test.notifyPass("navigation_storage_api.done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://example.com/*/file_sample.html"],
+ "js": ["content_script.js"],
+ }],
+ },
+
+ files: {
+ "content_script.js": `(${contentScript})()`,
+ },
+ });
+
+ await extension.startup();
+
+ // Open an explicit URL for testing Storage API in an insecure context.
+ let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+
+ await extension.awaitFinish("navigation_storage_api.done");
+
+ await extension.unload();
+ win.close();
+});
+
+add_task(async function test_contentScriptSecure() {
+ function contentScript() {
+ browser.test.assertTrue(navigator.storage !== undefined, "Has storage api interface");
+
+ // Test estimate.
+ browser.test.assertTrue("estimate" in navigator.storage, "Has estimate function");
+ browser.test.assertEq("function", typeof navigator.storage.estimate, "estimate is function");
+
+ // The promise that estimate function returns belongs to the content page,
+ // but the Promise constructor belongs to the content script sandbox.
+ // Check window.Promise here.
+ browser.test.assertTrue(navigator.storage.estimate() instanceof window.Promise, "estimate returns a promise");
+
+ return browser.test.notifyPass("navigation_storage_api.done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ "matches": ["https://example.com/*/file_sample.html"],
+ "js": ["content_script.js"],
+ }],
+ },
+
+ files: {
+ "content_script.js": `(${contentScript})()`,
+ },
+ });
+
+ await extension.startup();
+
+ // Open an explicit URL for testing Storage API in a secure context.
+ let win = window.open("file_sample.html");
+
+ await extension.awaitFinish("navigation_storage_api.done");
+
+ await extension.unload();
+ win.close();
+});
+
+add_task(async function cleanup() {
+ await SpecialPowers.popPrefEnv();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// The purpose of this test is making sure that the implementation enabled by
+// default for the storage.local and storage.sync APIs does work across all
+// platforms/builds/apps
+add_task(async function test_storage_smoke_test() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ for (let storageArea of ["sync", "local"]) {
+ let storage = browser.storage[storageArea];
+
+ browser.test.assertTrue(!!storage, `StorageArea ${storageArea} is present.`)
+
+ let data = await storage.get();
+ browser.test.assertEq(0, Object.keys(data).length,
+ `Storage starts out empty for ${storageArea}`);
+
+ data = await storage.get("test");
+ browser.test.assertEq(0, Object.keys(data).length,
+ `Can read non-existent keys for ${storageArea}`);
+
+ await storage.set({
+ "test1": "test-value1",
+ "test2": "test-value2",
+ "test3": "test-value3"
+ });
+
+ browser.test.assertEq(
+ "test-value1",
+ (await storage.get("test1")).test1,
+ `Can set and read back single values for ${storageArea}`);
+
+ browser.test.assertEq(
+ "test-value2",
+ (await storage.get("test2")).test2,
+ `Can set and read back single values for ${storageArea}`);
+
+ data = await storage.get();
+ browser.test.assertEq(3, Object.keys(data).length,
+ `Can set and read back all values for ${storageArea}`);
+ browser.test.assertEq("test-value1", data.test1,
+ `Can set and read back all values for ${storageArea}`);
+ browser.test.assertEq("test-value2", data.test2,
+ `Can set and read back all values for ${storageArea}`);
+ browser.test.assertEq("test-value3", data.test3,
+ `Can set and read back all values for ${storageArea}`);
+
+ data = await storage.get(["test1", "test2"]);
+ browser.test.assertEq(2, Object.keys(data).length,
+ `Can set and read back array of values for ${storageArea}`);
+ browser.test.assertEq("test-value1", data.test1,
+ `Can set and read back array of values for ${storageArea}`);
+ browser.test.assertEq("test-value2", data.test2,
+ `Can set and read back array of values for ${storageArea}`);
+
+ await storage.remove("test1");
+ data = await storage.get(["test1", "test2"]);
+ browser.test.assertEq(1, Object.keys(data).length,
+ `Data can be removed for ${storageArea}`);
+ browser.test.assertEq("test-value2", data.test2,
+ `Data can be removed for ${storageArea}`);
+
+ data = await storage.get({
+ test1: 1,
+ test2: 2,
+ });
+ browser.test.assertEq(2, Object.keys(data).length,
+ `Expected a key-value pair for every property for ${storageArea}`);
+ browser.test.assertEq(1, data.test1,
+ `Use default value if key was deleted for ${storageArea}`);
+ browser.test.assertEq("test-value2", data.test2,
+ `Use stored value if found for ${storageArea}`);
+
+ await storage.clear();
+ data = await storage.get();
+ browser.test.assertEq(0, Object.keys(data).length,
+ `Data is empty after clear for ${storageArea}`);
+ }
+
+ browser.test.sendMessage("done");
+ },
+ manifest: {
+ permissions: ["storage"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for multiple extensions trying to filterResponseData on the same request</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const TEST_URL =
+ "http://example.org/tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt";
+
+add_task(async () => {
+ const firstExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(
+ ({ requestId }) => {
+ const filter = browser.webRequest.filterResponseData(requestId);
+ filter.ondata = event => {
+ filter.write(new TextEncoder().encode("Start "));
+ filter.write(event.data);
+ filter.disconnect();
+ };
+ },
+ {
+ urls: [
+ "http://example.org/*/file_streamfilter.txt",
+ ],
+ },
+ ["blocking"]
+ );
+ },
+ });
+
+ const secondExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(
+ ({ requestId }) => {
+ const filter = browser.webRequest.filterResponseData(requestId);
+ filter.ondata = event => {
+ filter.write(event.data);
+ };
+ filter.onstop = event => {
+ filter.write(new TextEncoder().encode(" End"));
+ filter.close();
+ };
+ },
+ {
+ urls: [
+ "http://example.org/tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt",
+ ],
+ },
+ ["blocking"]
+ );
+ },
+ });
+
+ await firstExtension.startup();
+ await secondExtension.startup();
+
+ let iframe = document.createElement("iframe");
+ iframe.src = TEST_URL;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true}));
+
+ let content = await SpecialPowers.spawn(iframe, [], async () => {
+ return this.content.document.body.textContent;
+ });
+ SimpleTest.is(content, "Start Middle\n End", "Correctly intercepted page content");
+
+ await firstExtension.unload();
+ await secondExtension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for using filterResponseData to intercept a cross-origin navigation that will involve a process switch with fission</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const TEST_HOST = "http://example.com/";
+const CROSS_ORIGIN_HOST = "http://example.org/";
+const TEST_PATH =
+ "tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt";
+
+const TEST_URL = TEST_HOST + TEST_PATH;
+const CROSS_ORIGIN_URL = CROSS_ORIGIN_HOST + TEST_PATH;
+
+add_task(async () => {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(
+ ({ requestId }) => {
+ const filter = browser.webRequest.filterResponseData(requestId);
+ filter.onerror = () => browser.test.fail(
+ `Unexpected filterResponseData error: ${filter.error}`
+ );
+ filter.ondata = event => {
+ filter.write(event.data);
+ };
+ filter.onstop = event => {
+ filter.write(new TextEncoder().encode(" End"));
+ filter.close();
+ };
+ },
+ {
+ urls: [
+ "http://example.org/*/file_streamfilter.txt",
+ ],
+ },
+ ["blocking"]
+ );
+ },
+ });
+
+ await extension.startup();
+
+ let iframe = document.createElement("iframe");
+ iframe.src = TEST_URL;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true}));
+
+
+ iframe.src = CROSS_ORIGIN_URL;
+ await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true}));
+
+ let content = await SpecialPowers.spawn(iframe, [], async () => {
+ return this.content.document.body.textContent;
+ });
+ SimpleTest.is(content, "Middle\n End", "Correctly intercepted page content");
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>WebExtension test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(async function test_webext_tab_subframe_privileges() {
+ function background() {
+ browser.runtime.onMessage.addListener(async ({msg, success, tabId, error}) => {
+ if (msg == "webext-tab-subframe-privileges") {
+ if (success) {
+ await browser.tabs.remove(tabId);
+
+ browser.test.notifyPass(msg);
+ } else {
+ browser.test.log(`Got an unexpected error: ${error}`);
+
+ let tabs = await browser.tabs.query({active: true});
+ await browser.tabs.remove(tabs[0].id);
+
+ browser.test.notifyFail(msg);
+ }
+ }
+ });
+ browser.tabs.create({url: browser.runtime.getURL("/tab.html")});
+ }
+
+ async function tabSubframeScript() {
+ browser.test.assertTrue(browser.tabs != undefined,
+ "Subframe of a privileged page has access to privileged APIs");
+ if (browser.tabs) {
+ try {
+ let tab = await browser.tabs.getCurrent();
+ browser.runtime.sendMessage({
+ msg: "webext-tab-subframe-privileges",
+ success: true,
+ tabId: tab.id,
+ });
+ } catch (e) {
+ browser.runtime.sendMessage({msg: "webext-tab-subframe-privileges", success: false, error: `${e}`});
+ }
+ } else {
+ browser.runtime.sendMessage({
+ msg: "webext-tab-subframe-privileges",
+ success: false,
+ error: `Privileged APIs missing in WebExtension tab sub-frame`,
+ });
+ }
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "tab.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe src="tab-subframe.html"></iframe>
+ </body>
+ </html>`,
+ "tab-subframe.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ <script src="tab-subframe.js"><\/script>
+ </head>
+ </html>`,
+ "tab-subframe.js": tabSubframeScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+
+ await extension.awaitFinish("webext-tab-subframe-privileges");
+ await extension.unload();
+});
+
+add_task(async function test_webext_background_subframe_privileges() {
+ function backgroundSubframeScript() {
+ browser.test.assertTrue(browser.tabs != undefined,
+ "Subframe of a background page has access to privileged APIs");
+ browser.test.notifyPass("webext-background-subframe-privileges");
+ }
+
+ let extensionData = {
+ manifest: {
+ background: {
+ page: "background.html",
+ },
+ },
+ files: {
+ "background.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe src="background-subframe.html"></iframe>
+ </body>
+ </html>`,
+ "background-subframe.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ <script src="background-subframe.js"><\/script>
+ </head>
+ </html>`,
+ "background-subframe.js": backgroundSubframeScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+
+ await extension.awaitFinish("webext-background-subframe-privileges");
+ await extension.unload();
+});
+
+add_task(async function test_webext_contentscript_iframe_subframe_privileges() {
+ function background() {
+ browser.runtime.onMessage.addListener(({name, hasTabsAPI, hasStorageAPI}) => {
+ if (name == "contentscript-iframe-loaded") {
+ browser.test.assertFalse(hasTabsAPI,
+ "Subframe of a content script privileged iframes has no access to privileged APIs");
+ browser.test.assertTrue(hasStorageAPI,
+ "Subframe of a content script privileged iframes has access to content script APIs");
+
+ browser.test.notifyPass("webext-contentscript-subframe-privileges");
+ }
+ });
+ }
+
+ function subframeScript() {
+ browser.runtime.sendMessage({
+ name: "contentscript-iframe-loaded",
+ hasTabsAPI: browser.tabs != undefined,
+ hasStorageAPI: browser.storage != undefined,
+ });
+ }
+
+ function contentScript() {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", browser.runtime.getURL("/contentscript-iframe.html"));
+ document.body.appendChild(iframe);
+ }
+
+ let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["storage"],
+ "content_scripts": [{
+ "matches": ["https://example.com/*"],
+ "js": ["contentscript.js"],
+ }],
+ web_accessible_resources: [
+ "contentscript-iframe.html",
+ ],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ "contentscript-iframe.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe src="contentscript-iframe-subframe.html"></iframe>
+ </body>
+ </html>`,
+ "contentscript-iframe-subframe.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ <script src="contentscript-iframe-subframe.js"><\/script>
+ </head>
+ </html>`,
+ "contentscript-iframe-subframe.js": subframeScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+
+ let win = window.open("https://example.com");
+
+ await extension.awaitFinish("webext-contentscript-subframe-privileges");
+
+ win.close();
+
+ await extension.unload();
+});
+
+add_task(async function test_webext_background_remote_subframe_privileges() {
+ function backgroundSubframeScript() {
+ window.addEventListener("message", evt => {
+ browser.test.assertEq("http://mochi.test:8888", evt.origin, "postmessage origin ok");
+ browser.test.assertFalse(evt.data.tabs, "remote frame cannot access webextension APIs");
+ browser.test.assertEq("cookie=monster", evt.data.cookie, "Expected cookie value");
+ browser.test.notifyPass("webext-background-subframe-privileges");
+ }, {once: true});
+ browser.cookies.set({url: "http://mochi.test:8888", name: "cookie", "value": "monster"});
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["cookies", "*://mochi.test/*", "tabs"],
+ background: {
+ page: "background.html",
+ },
+ },
+ files: {
+ "background.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ <script src="background-subframe.js"><\/script>
+ </head>
+ <body>
+ <iframe src='${SimpleTest.getTestFileURL("file_remote_frame.html")}'></iframe>
+ </body>
+ </html>`,
+ "background-subframe.js": backgroundSubframeScript,
+ },
+ };
+ // Need remote webextensions to be able to load remote content from a background page.
+ if (!SpecialPowers.getBoolPref("extensions.webextensions.remote", true)) {
+ return;
+ }
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+
+ await extension.awaitFinish("webext-background-subframe-privileges");
+ await extension.unload();
+});
+
+// Test a moz-extension:// iframe inside a content iframe in an extension page.
+add_task(async function test_sub_subframe_conduit_verified_env() {
+ let manifest = {
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html"],
+ all_frames: true,
+ js: ["cs.js"],
+ }],
+ background: {
+ page: "background.html",
+ },
+ web_accessible_resources: ["iframe.html"],
+ };
+
+ let files = {
+ "iframe.html": `<!DOCTYPE html><meta charset=utf-8> iframe`,
+ "cs.js"() {
+ // A compromised content sandbox shouldn't be able to trick the parent
+ // process into giving it extension privileges by sending false metadata.
+ async function faker(extensionId, envType) {
+ try {
+ let id = envType + "-xyz1234";
+ let wgc = this.content.windowGlobalChild;
+
+ let conduit = wgc.getActor("Conduits").openConduit({}, {
+ id,
+ envType,
+ extensionId,
+ query: ["CreateProxyContext"],
+ });
+
+ return await conduit.queryCreateProxyContext({
+ childId: id,
+ extensionId,
+ envType: "addon_parent",
+ url: this.content.location.href,
+ viewType: "tab",
+ });
+ } catch (e) {
+ return e.message;
+ }
+ }
+
+ let iframe = document.createElement("iframe");
+ iframe.src = browser.runtime.getURL("iframe.html");
+
+ iframe.onload = async () => {
+ for (let envType of ["content_child", "addon_child"]) {
+ let msg = await this.wrappedJSObject.SpecialPowers.spawn(
+ iframe, [browser.runtime.id, envType], faker);
+ browser.test.sendMessage(envType, msg);
+ }
+ };
+ document.body.appendChild(iframe);
+ },
+ "background.html": `<!DOCTYPE html>
+ <meta charset=utf-8>
+ <iframe src="${SimpleTest.getTestFileURL("file_sample.html")}">
+ </iframe>
+ page
+ `,
+ };
+
+ async function expectErrors(ext, log) {
+ let err = await ext.awaitMessage("content_child");
+ is(err, "Bad sender context envType: content_child");
+
+ err = await ext.awaitMessage("addon_child");
+ is(err, "Unknown sender or wrong actor for recvCreateProxyContext");
+ }
+
+ let remote = SpecialPowers.getBoolPref("extensions.webextensions.remote");
+
+ let badProcess = { message: /Bad {[\w-]+} process: web/ };
+ let badPrincipal = { message: /Bad {[\w-]+} principal: http/ };
+ consoleMonitor.start(remote ? [badPrincipal, badProcess] : [badProcess]);
+
+ let extension = ExtensionTestUtils.loadExtension({ manifest, files });
+ await extension.startup();
+
+ if (remote) {
+ info("Need OOP to spoof from a web iframe inside background page.");
+ await expectErrors(extension);
+ }
+
+ info("Try spoofing from the web process.");
+ let win = window.open("./file_sample.html");
+ await expectErrors(extension);
+ win.close();
+
+ await extension.unload();
+ await consoleMonitor.finished();
+ info("Conduit creation logged correct exception(s).");
+});
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests tabs.captureTab and tabs.captureVisibleTab</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+async function runTest({ html, fullZoom, coords, rect, scale }) {
+ let url = `data:text/html,${encodeURIComponent(html)}#scroll`;
+
+ async function background({ coords, rect, scale, method, fullZoom }) {
+ try {
+ // Wait for the page to load
+ await new Promise(resolve => {
+ browser.webNavigation.onCompleted.addListener(
+ () => resolve(),
+ {url: [{schemes: ["data"]}]});
+ });
+
+ let [tab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ // TODO: Bug 1665429 - on mobile we ignore zoom for now
+ if (browser.tabs.setZoom) {
+ await browser.tabs.setZoom(tab.id, fullZoom ?? 1);
+ }
+
+ let id = method === "captureVisibleTab" ? tab.windowId : tab.id;
+
+ let [jpeg, png, ...pngs] = await Promise.all([
+ browser.tabs[method](id, { format: "jpeg", quality: 95, rect, scale }),
+ browser.tabs[method](id, { format: "png", quality: 95, rect, scale }),
+ browser.tabs[method](id, { quality: 95, rect, scale }),
+ browser.tabs[method](id, { rect, scale }),
+ ]);
+
+ browser.test.assertTrue(
+ pngs.every(url => url == png),
+ "All PNGs are identical"
+ );
+
+ browser.test.assertTrue(
+ jpeg.startsWith("data:image/jpeg;base64,"),
+ "jpeg is JPEG"
+ );
+ browser.test.assertTrue(
+ png.startsWith("data:image/png;base64,"),
+ "png is PNG"
+ );
+
+ let promises = [jpeg, png].map(
+ url =>
+ new Promise(resolve => {
+ let img = new Image();
+ img.src = url;
+ img.onload = () => resolve(img);
+ })
+ );
+
+ let width = (rect?.width ?? tab.width) * (scale ?? devicePixelRatio);
+ let height = (rect?.height ?? tab.height) * (scale ?? devicePixelRatio);
+
+ [jpeg, png] = await Promise.all(promises);
+ let images = { jpeg, png };
+ for (let format of Object.keys(images)) {
+ let img = images[format];
+
+ // WGP.drawSnapshot() deals in int coordinates, and rounds down.
+ browser.test.assertTrue(
+ Math.abs(width - img.width) <= 1,
+ `${format} ok image width: ${img.width}, expected: ${width}`
+ );
+ browser.test.assertTrue(
+ Math.abs(height - img.height) <= 1,
+ `${format} ok image height ${img.height}, expected: ${height}`
+ );
+
+ let canvas = document.createElement("canvas");
+ canvas.width = img.width;
+ canvas.height = img.height;
+ canvas.mozOpaque = true;
+
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0);
+
+ for (let { x, y, color } of coords) {
+ x = (x + img.width) % img.width;
+ y = (y + img.height) % img.height;
+ let imageData = ctx.getImageData(x, y, 1, 1).data;
+
+ if (format == "png") {
+ browser.test.assertEq(
+ `rgba(${color},255)`,
+ `rgba(${[...imageData]})`,
+ `${format} image color is correct at (${x}, ${y})`
+ );
+ } else {
+ // Allow for some deviation in JPEG version due to lossy compression.
+ const SLOP = 3;
+
+ browser.test.log(
+ `Testing ${format} image color at (${x}, ${y}), have rgba(${[
+ ...imageData,
+ ]}), expecting approx. rgba(${color},255)`
+ );
+
+ browser.test.assertTrue(
+ Math.abs(color[0] - imageData[0]) <= SLOP,
+ `${format} image color.red is correct at (${x}, ${y})`
+ );
+ browser.test.assertTrue(
+ Math.abs(color[1] - imageData[1]) <= SLOP,
+ `${format} image color.green is correct at (${x}, ${y})`
+ );
+ browser.test.assertTrue(
+ Math.abs(color[2] - imageData[2]) <= SLOP,
+ `${format} image color.blue is correct at (${x}, ${y})`
+ );
+ browser.test.assertEq(
+ 255,
+ imageData[3],
+ `${format} image color.alpha is correct at (${x}, ${y})`
+ );
+ }
+ }
+ }
+
+ browser.test.notifyPass("captureTab");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("captureTab");
+ }
+ }
+
+ for (let method of ["captureTab", "captureVisibleTab"]) {
+ let options = { coords, rect, scale, method, fullZoom };
+ info(`Testing configuration: ${JSON.stringify(options)}`);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["<all_urls>", "webNavigation"],
+ },
+
+ background: `(${background})(${JSON.stringify(options)})`,
+ });
+
+ await extension.startup();
+
+ let testWindow = window.open(url);
+ await extension.awaitFinish("captureTab");
+
+ testWindow.close();
+ await extension.unload();
+ }
+}
+
+async function testEdgeToEdge({ color, fullZoom }) {
+ let neutral = [0xaa, 0xaa, 0xaa];
+
+ let html = `
+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ </head>
+ <body style="background-color: rgb(${color})">
+ <!-- Fill most of the image with a neutral color to test edge-to-edge scaling. -->
+ <div style="position: absolute;
+ left: 2px;
+ right: 2px;
+ top: 2px;
+ bottom: 2px;
+ background: rgb(${neutral});"></div>
+ </body>
+ </html>
+ `;
+
+ // Check the colors of the first and last pixels of the image, to make
+ // sure we capture the entire frame, and scale it correctly.
+ let coords = [
+ { x: 0, y: 0, color },
+ { x: -1, y: -1, color },
+ { x: 300, y: 200, color: neutral },
+ ];
+
+ info(`Test edge to edge color ${color} at fullZoom=${fullZoom}`);
+ await runTest({ html, fullZoom, coords });
+}
+
+add_task(async function testCaptureEdgeToEdge() {
+ await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 1 });
+ await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 2 });
+ await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 0.5 });
+ await testEdgeToEdge({ color: [255, 255, 255], fullZoom: 1 });
+});
+
+const tallDoc = `<!DOCTYPE html>
+ <meta charset=utf-8>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <div style="background: yellow; width: 50%; height: 500px;"></div>
+ <div id=scroll style="background: red; width: 25%; height: 5000px;"></div>
+ Opened with the #scroll fragment, scrolls the div ^ into view.
+`;
+
+// Test currently visible viewport is captured if scrolling is involved.
+add_task(async function testScrolledViewport() {
+ await runTest({
+ html: tallDoc,
+ coords: [
+ { x: 50, y: 50, color: [255, 0, 0] },
+ { x: 50, y: -50, color: [255, 0, 0] },
+ { x: -50, y: -50, color: [255, 255, 255] },
+ ],
+ });
+});
+
+// Test rect and scale options.
+add_task(async function testRectAndScale() {
+ await runTest({
+ html: tallDoc,
+ rect: { x: 50, y: 50, width: 10, height: 1000 },
+ scale: 4,
+ coords: [
+ { x: 0, y: 0, color: [255, 255, 0] },
+ { x: -1, y: 0, color: [255, 255, 0] },
+ { x: 0, y: -1, color: [255, 0, 0] },
+ { x: -1, y: -1, color: [255, 0, 0] },
+ ],
+ });
+});
+
+// Test OOP iframes are captured, for Fission compatibility.
+add_task(async function testOOPiframe() {
+ await runTest({
+ html: `<!DOCTYPE html>
+ <meta charset=utf-8>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <iframe src="http://example.net/tests/toolkit/components/extensions/test/mochitest/file_green.html"></iframe>
+ `,
+ coords: [
+ { x: 50, y: 50, color: [0, 255, 0] },
+ { x: 50, y: -50, color: [255, 255, 255] },
+ { x: -50, y: 50, color: [255, 255, 255] },
+ ],
+ });
+});
+
+add_task(async function testOOPiframeScale() {
+ let scale = 2;
+ await runTest({
+ html: `<!DOCTYPE html>
+ <meta charset=utf-8>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <style>
+ body {
+ background: yellow;
+ margin: 0;
+ }
+ </style>
+ <iframe frameborder="0" style="width: 300px; height: 300px" src="http://example.net/tests/toolkit/components/extensions/test/mochitest/file_green_blue.html"></iframe>
+ `,
+ coords: [
+ { x: 20 * scale, y: 20 * scale, color: [0, 255, 0] },
+ { x: 200 * scale, y: 20 * scale, color: [0, 0, 255] },
+ { x: 20 * scale, y: 200 * scale, color: [0, 0, 255] },
+ ],
+ scale,
+ });
+});
+
+add_task(async function testCaptureTabPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background() {
+ browser.test.assertEq(
+ undefined,
+ browser.tabs.captureTab,
+ 'Extension without "<all_urls>" permission should not have access to captureTab'
+ );
+ browser.test.notifyPass("captureTabPermissions");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("captureTabPermissions");
+ await extension.unload();
+});
+
+add_task(async function testCaptureVisibleTabPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background() {
+ browser.test.assertEq(
+ undefined,
+ browser.tabs.captureVisibleTab,
+ 'Extension without "<all_urls>" permission should not have access to captureVisibleTab'
+ );
+ browser.test.notifyPass("captureVisibleTabPermissions");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("captureVisibleTabPermissions");
+ await extension.unload();
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <title>Test tabs.create(cookieStoreId)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+add_task(async function no_cookies_permission() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-container-1" }),
+ /No permission for cookieStoreId/,
+ "cookieStoreId requires cookies permission"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function invalid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "not-firefox-container-1" }),
+ /Illegal cookieStoreId/,
+ "cookieStoreId must be valid"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-private" }),
+ /Illegal to set private cookieStoreId in a non-private window/,
+ "cookieStoreId cannot be private in a non-private window"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function perma_private_browsing_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are unavailable in permanent private browsing mode/,
+ "cookieStoreId cannot be a container tab ID in perma-private browsing mode"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function userContext_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are currently disabled/,
+ "cookieStoreId cannot be a container tab ID when contextual identities are disabled"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function valid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ const testCases = [
+ {
+ description: "no explicit URL",
+ createProperties: {
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreId: "firefox-container-1",
+ },
+ {
+ description: "pass explicit url",
+ createProperties: {
+ url: "about:blank",
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreId: "firefox-container-1",
+ },{
+ description: "pass explicit not-blank url",
+ createProperties: {
+ url: "https://example.com/",
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreId: "firefox-container-1",
+ },{
+ description: "pass extension page url",
+ createProperties: {
+ url: "blank.html",
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreId: "firefox-container-1",
+ }
+ ];
+
+ async function background(testCases) {
+ for (let { createProperties, expectedCookieStoreId } of testCases) {
+ const { url } = createProperties;
+ const updatedPromise = new Promise(resolve => {
+ const onUpdated = (changedTabId, changed) => {
+ // Loading an extension page causes two `about:blank` messages
+ // because of the process switch
+ if (changed.url && (url == "about:blank" || changed.url != "about:blank")) {
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ resolve({tabId: changedTabId, url: changed.url});
+ }
+ };
+ browser.tabs.onUpdated.addListener(onUpdated);
+ });
+
+ const tab = await browser.tabs.create(createProperties);
+ browser.test.assertEq(
+ expectedCookieStoreId,
+ tab.cookieStoreId,
+ "Expected cookieStoreId for container tab"
+ );
+
+ if (url && url !== "about:blank") {
+ // Make sure tab can load successfully
+ const updated = await updatedPromise;
+ browser.test.assertEq(tab.id, updated.tabId, `Expected value for tab.id`);
+ if (updated.url.startsWith("moz-extension")) {
+ browser.test.assertEq(browser.runtime.getURL(url), updated.url,
+ `Expected value for extension page url`);
+ } else {
+ browser.test.assertEq(url, updated.url, `Expected value for tab.url`);
+ }
+ }
+
+ await browser.tabs.remove(tab.id);
+ }
+ browser.test.sendMessage("done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ files: {
+ "blank.html": `<html><head><meta charset="utf-8"></head></html>`,
+ },
+ background: `(${background})(${JSON.stringify(testCases)})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tabs executeScript Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+async function testHasPermission(params) {
+ let contentSetup = params.contentSetup || (() => Promise.resolve());
+
+ async function background(contentSetup) {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq(msg, "script ran", "script ran");
+ browser.test.notifyPass("executeScript");
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "execute-script");
+
+ browser.tabs.executeScript({
+ file: "script.js",
+ });
+ });
+
+ await contentSetup();
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: params.manifest,
+
+ background: `(${background})(${contentSetup})`,
+
+ files: {
+ "panel.html": `<!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ </body>
+ </html>`,
+ "script.js": function() {
+ browser.runtime.sendMessage("script ran");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (params.setup) {
+ await params.setup(extension);
+ }
+
+ extension.sendMessage("execute-script");
+
+ await extension.awaitFinish("executeScript");
+
+ if (params.tearDown) {
+ await params.tearDown(extension);
+ }
+
+ await extension.unload();
+}
+
+add_task(async function testGoodPermissions() {
+ let tab = await AppTestDelegate.openNewForegroundTab(
+ window,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ info("Test explicit host permission");
+ await testHasPermission({
+ manifest: { permissions: ["http://mochi.test/"] },
+ });
+
+ info("Test explicit host subdomain permission");
+ await testHasPermission({
+ manifest: { permissions: ["http://*.mochi.test/"] },
+ });
+
+ info("Test explicit <all_urls> permission");
+ await testHasPermission({
+ manifest: { permissions: ["<all_urls>"] },
+ });
+
+ info("Test activeTab permission with a browser action click");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ contentSetup: function() {
+ browser.browserAction.onClicked.addListener(() => {
+ browser.test.log("Clicked.");
+ });
+ return Promise.resolve();
+ },
+ setup: extension => AppTestDelegate.clickBrowserAction(window, extension),
+ tearDown: extension => AppTestDelegate.closeBrowserAction(window, extension),
+ });
+
+ info("Test activeTab permission with a page action click");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ page_action: {},
+ },
+ contentSetup: async () => {
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ },
+ setup: extension => AppTestDelegate.clickPageAction(window, extension),
+ tearDown: extension => AppTestDelegate.closePageAction(window, extension),
+ });
+
+ info("Test activeTab permission with a browser action w/popup click");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: { default_popup: "panel.html" },
+ },
+ setup: async extension => {
+ await AppTestDelegate.clickBrowserAction(window, extension);
+ return AppTestDelegate.awaitExtensionPanel(window, extension);
+ },
+ tearDown: extension => AppTestDelegate.closeBrowserAction(window, extension),
+ });
+
+ info("Test activeTab permission with a page action w/popup click");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ page_action: { default_popup: "panel.html" },
+ },
+ contentSetup: async () => {
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ },
+ setup: extension => AppTestDelegate.clickPageAction(window, extension),
+ tearDown: extension => AppTestDelegate.closePageAction(window, extension),
+ });
+
+ await AppTestDelegate.removeTab(window, tab);
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tabs permissions test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const URL1 =
+ "https://www.example.com/tests/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html";
+const URL2 =
+ "https://example.net/tests/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html";
+
+const helperExtensionDef = {
+ manifest: {
+ permissions: ["webNavigation", "<all_urls>"],
+ },
+
+ async background() {
+ browser.test.onMessage.addListener(async message => {
+ switch (message.subject) {
+ case "createTab": {
+ const tabLoaded = new Promise(resolve => {
+ browser.webNavigation.onCompleted.addListener(function listener(
+ details
+ ) {
+ if (details.url === message.data.url) {
+ browser.webNavigation.onCompleted.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+
+ const tab = await browser.tabs.create({ url: message.data.url });
+ await tabLoaded;
+ browser.test.sendMessage("tabCreated", tab.id);
+ break;
+ }
+
+ case "changeTabURL": {
+ const tabLoaded = new Promise(resolve => {
+ browser.webNavigation.onCompleted.addListener(function listener(
+ details
+ ) {
+ if (details.url === message.data.url) {
+ browser.webNavigation.onCompleted.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+
+ await browser.tabs.update(message.data.tabId, {
+ url: message.data.url,
+ });
+ await tabLoaded;
+ browser.test.sendMessage("tabURLChanged", message.data.tabId);
+ break;
+ }
+
+ case "changeTabHashAndTitle": {
+ const tabChanged = new Promise(resolve => {
+ let hasURLChangeInfo = false,
+ hasTitleChangeInfo = false;
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId,
+ changeInfo,
+ tab
+ ) {
+ if (changeInfo.url?.endsWith(message.data.urlHash)) {
+ hasURLChangeInfo = true;
+ }
+ if (changeInfo.title === message.data.title) {
+ hasTitleChangeInfo = true;
+ }
+ if (hasURLChangeInfo && hasTitleChangeInfo) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+
+ await browser.tabs.executeScript(message.data.tabId, {
+ code: `
+ document.location.hash = ${JSON.stringify(message.data.urlHash)};
+ document.title = ${JSON.stringify(message.data.title)};
+ `,
+ });
+ await tabChanged;
+ browser.test.sendMessage("tabHashAndTitleChanged");
+ break;
+ }
+
+ case "removeTab": {
+ await browser.tabs.remove(message.data.tabId);
+ browser.test.sendMessage("tabRemoved");
+ break;
+ }
+
+ default:
+ browser.test.fail(`Received unexpected message: ${message}`);
+ }
+ });
+ },
+};
+
+/*
+ * Test tabs.query function
+ * Check if the correct tabs are queried by url or title based on the granted permissions
+ */
+async function test_query(testCases, permissions) {
+ const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef);
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions,
+ },
+
+ async background() {
+ // wait for start message
+ const [testCases, tabIdFromURL1, tabIdFromURL2] = await new Promise(
+ resolve => {
+ browser.test.onMessage.addListener(message => resolve(message));
+ }
+ );
+
+ for (const testCase of testCases) {
+ const query = testCase.query;
+ const matchingTabs = testCase.matchingTabs;
+
+ let tabQuery = await browser.tabs.query(query);
+ // ignore other tabs in the window
+ tabQuery = tabQuery.filter(tab => {
+ return tab.id === tabIdFromURL1 || tab.id === tabIdFromURL2;
+ });
+
+ browser.test.assertEq(matchingTabs, tabQuery.length, `Tabs queried`);
+ }
+ // send end message
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await helperExtension.startup();
+ await extension.startup();
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: { url: URL1 },
+ });
+ const tabIdFromURL1 = await helperExtension.awaitMessage("tabCreated");
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: { url: URL2 },
+ });
+ const tabIdFromURL2 = await helperExtension.awaitMessage("tabCreated");
+
+ if (permissions.includes("activeTab")) {
+ extension.grantActiveTab(tabIdFromURL2);
+ }
+
+ extension.sendMessage([testCases, tabIdFromURL1, tabIdFromURL2]);
+ await extension.awaitFinish("tabs.query");
+
+ helperExtension.sendMessage({
+ subject: "removeTab",
+ data: { tabId: tabIdFromURL1 },
+ });
+ await helperExtension.awaitMessage("tabRemoved");
+
+ helperExtension.sendMessage({
+ subject: "removeTab",
+ data: { tabId: tabIdFromURL2 },
+ });
+ await helperExtension.awaitMessage("tabRemoved");
+
+ await extension.unload();
+ await helperExtension.unload();
+}
+
+// https://www.example.com host permission
+add_task(function query_with_host_permission_url1() {
+ return test_query(
+ [
+ {
+ query: { url: "*://www.example.com/*" },
+ matchingTabs: 1,
+ },
+ {
+ query: { url: "<all_urls>" },
+ matchingTabs: 1,
+ },
+ {
+ query: { url: ["*://www.example.com/*", "*://example.net/*"] },
+ matchingTabs: 1,
+ },
+ {
+ query: { title: "The Title" },
+ matchingTabs: 1,
+ },
+ {
+ query: { title: "Another Title" },
+ matchingTabs: 0,
+ },
+ {
+ query: {},
+ matchingTabs: 2,
+ },
+ ],
+ ["*://www.example.com/*"]
+ );
+});
+
+// https://example.net host permission
+add_task(function query_with_host_permission_url2() {
+ return test_query(
+ [
+ {
+ query: { url: "*://www.example.com/*" },
+ matchingTabs: 0,
+ },
+ {
+ query: { url: "<all_urls>" },
+ matchingTabs: 1,
+ },
+ {
+ query: { url: ["*://www.example.com/*", "*://example.net/*"] },
+ matchingTabs: 1,
+ },
+ {
+ query: { title: "The Title" },
+ matchingTabs: 0,
+ },
+ {
+ query: { title: "Another Title" },
+ matchingTabs: 1,
+ },
+ {
+ query: {},
+ matchingTabs: 2,
+ },
+ ],
+ ["*://example.net/*"]
+ );
+});
+
+// <all_urls> permission
+add_task(function query_with_host_permission_all_urls() {
+ return test_query(
+ [
+ {
+ query: { url: "*://www.example.com/*" },
+ matchingTabs: 1,
+ },
+ {
+ query: { url: "<all_urls>" },
+ matchingTabs: 2,
+ },
+ {
+ query: { url: ["*://www.example.com/*", "*://example.net/*"] },
+ matchingTabs: 2,
+ },
+ {
+ query: { title: "The Title" },
+ matchingTabs: 1,
+ },
+ {
+ query: { title: "Another Title" },
+ matchingTabs: 1,
+ },
+ {
+ query: {},
+ matchingTabs: 2,
+ },
+ ],
+ ["<all_urls>"]
+ );
+});
+
+// tabs permission
+add_task(function query_with_tabs_permission() {
+ return test_query(
+ [
+ {
+ query: { url: "*://www.example.com/*" },
+ matchingTabs: 1,
+ },
+ {
+ query: { url: "<all_urls>" },
+ matchingTabs: 2,
+ },
+ {
+ query: { url: ["*://www.example.com/*", "*://example.net/*"] },
+ matchingTabs: 2,
+ },
+ {
+ query: { title: "The Title" },
+ matchingTabs: 1,
+ },
+ {
+ query: { title: "Another Title" },
+ matchingTabs: 1,
+ },
+ {
+ query: {},
+ matchingTabs: 2,
+ },
+ ],
+ ["tabs"]
+ );
+});
+
+// activeTab permission
+add_task(function query_with_activeTab_permission() {
+ return test_query(
+ [
+ {
+ query: { url: "*://www.example.com/*" },
+ matchingTabs: 0,
+ },
+ {
+ query: { url: "<all_urls>" },
+ matchingTabs: 1,
+ },
+ {
+ query: { url: ["*://www.example.com/*", "*://example.net/*"] },
+ matchingTabs: 1,
+ },
+ {
+ query: { title: "The Title" },
+ matchingTabs: 0,
+ },
+ {
+ query: { title: "Another Title" },
+ matchingTabs: 1,
+ },
+ {
+ query: {},
+ matchingTabs: 2,
+ },
+ ],
+ ["activeTab"]
+ );
+});
+// no permission
+add_task(function query_without_permission() {
+ return test_query(
+ [
+ {
+ query: { url: "*://www.example.com/*" },
+ matchingTabs: 0,
+ },
+ {
+ query: { url: "<all_urls>" },
+ matchingTabs: 0,
+ },
+ {
+ query: { url: ["*://www.example.com/*", "*://example.net/*"] },
+ matchingTabs: 0,
+ },
+ {
+ query: { title: "The Title" },
+ matchingTabs: 0,
+ },
+ {
+ query: { title: "Another Title" },
+ matchingTabs: 0,
+ },
+ {
+ query: {},
+ matchingTabs: 2,
+ },
+ ],
+ []
+ );
+});
+
+/*
+ * Test tabs.onUpdate and tabs.get function
+ * Check if the changeInfo or tab object contains the restricted properties
+ * url and title only when the right permissions are granted
+ * The tab is updated without causing navigation in order to also test activeTab permission
+ */
+async function test_restricted_properties(
+ permissions,
+ hasRestrictedProperties
+) {
+ const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef);
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions,
+ },
+
+ async background() {
+ // wait for test start signal and data
+ const [
+ hasRestrictedProperties,
+ tabId,
+ urlHash,
+ title,
+ ] = await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => {
+ resolve(message);
+ });
+ });
+
+ let hasURLChangeInfo = false,
+ hasTitleChangeInfo = false;
+ function onUpdateListener(tabId, changeInfo, tab) {
+ if (changeInfo.url?.endsWith(urlHash)) {
+ hasURLChangeInfo = true;
+ }
+ if (changeInfo.title === title) {
+ hasTitleChangeInfo = true;
+ }
+ }
+ browser.tabs.onUpdated.addListener(onUpdateListener);
+
+ // wait for test evaluation signal and data
+ await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => {
+ if (message === "collectTestResults") {
+ resolve(message);
+ }
+ });
+ browser.test.sendMessage("waitingForTabPropertyChanges");
+ });
+
+ // check onUpdate changeInfo
+ browser.test.assertEq(
+ hasRestrictedProperties,
+ hasURLChangeInfo,
+ `Has changeInfo property "url"`
+ );
+ browser.test.assertEq(
+ hasRestrictedProperties,
+ hasTitleChangeInfo,
+ `Has changeInfo property "title"`
+ );
+ // check tab properties
+ const tabGet = await browser.tabs.get(tabId);
+ browser.test.assertEq(
+ hasRestrictedProperties,
+ !!tabGet.url?.endsWith(urlHash),
+ `Has tab property "url"`
+ );
+ browser.test.assertEq(
+ hasRestrictedProperties,
+ tabGet.title === title,
+ `Has tab property "title"`
+ );
+ // send end message
+ browser.test.notifyPass("tabs.restricted_properties");
+ },
+ });
+
+ const urlHash = "#ChangedURL";
+ const title = "Changed Title";
+
+ await helperExtension.startup();
+ await extension.startup();
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: { url: URL1 },
+ });
+ const tabId = await helperExtension.awaitMessage("tabCreated");
+
+ if (permissions.includes("activeTab")) {
+ extension.grantActiveTab(tabId);
+ }
+ // send test start signal and data
+ extension.sendMessage([hasRestrictedProperties, tabId, urlHash, title]);
+ await extension.awaitMessage("waitingForTabPropertyChanges");
+
+ helperExtension.sendMessage({
+ subject: "changeTabHashAndTitle",
+ data: {
+ tabId,
+ urlHash,
+ title,
+ },
+ });
+ await helperExtension.awaitMessage("tabHashAndTitleChanged");
+
+ // send end signal and evaluate results
+ extension.sendMessage("collectTestResults");
+ await extension.awaitFinish("tabs.restricted_properties");
+
+ helperExtension.sendMessage({
+ subject: "removeTab",
+ data: { tabId },
+ });
+ await helperExtension.awaitMessage("tabRemoved");
+
+ await extension.unload();
+ await helperExtension.unload();
+}
+
+// https://www.example.com host permission
+add_task(function has_restricted_properties_with_host_permission_url1() {
+ return test_restricted_properties(["*://www.example.com/*"], true);
+});
+// https://example.net host permission
+add_task(function has_restricted_properties_with_host_permission_url2() {
+ return test_restricted_properties(["*://example.net/*"], false);
+});
+// <all_urls> permission
+add_task(function has_restricted_properties_with_host_permission_all_urls() {
+ return test_restricted_properties(["<all_urls>"], true);
+});
+// tabs permission
+add_task(function has_restricted_properties_with_tabs_permission() {
+ return test_restricted_properties(["tabs"], true);
+});
+// activeTab permission
+add_task(function has_restricted_properties_with_activeTab_permission() {
+ return test_restricted_properties(["activeTab"], true);
+}).skip(); // TODO bug 1686080: support changeInfo.url with activeTab
+// no permission
+add_task(function has_restricted_properties_without_permission() {
+ return test_restricted_properties([], false);
+});
+
+
+/*
+ * Test tabs.onUpdate filter functionality
+ * Check if the restricted filter properties only work if the
+ * right permissions are granted
+ */
+async function test_onUpdateFilter(testCases, permissions) {
+ // Filters for onUpdated are not supported on Android.
+ if (AppConstants.platform === "android") {
+ return;
+ }
+
+ const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef);
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions,
+ },
+
+ async background() {
+ let listenerGotCalled = false;
+ function onUpdateListener(tabId, changeInfo, tab) {
+ listenerGotCalled = true;
+ }
+
+ browser.test.onMessage.addListener(async message => {
+ switch (message.subject) {
+ case "setup": {
+ browser.tabs.onUpdated.addListener(
+ onUpdateListener,
+ message.data.filter
+ );
+ browser.test.sendMessage("done");
+ break;
+ }
+
+ case "collectTestResults": {
+ browser.test.assertEq(
+ message.data.expectEvent,
+ listenerGotCalled,
+ `Update listener called`
+ );
+ browser.tabs.onUpdated.removeListener(onUpdateListener);
+ listenerGotCalled = false;
+ browser.test.sendMessage("done");
+ break;
+ }
+
+ default:
+ browser.test.fail(`Received unexpected message: ${message}`);
+ }
+ });
+ },
+ });
+
+ await helperExtension.startup();
+ await extension.startup();
+
+ for (const testCase of testCases) {
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: { url: URL1 },
+ });
+ const tabId = await helperExtension.awaitMessage("tabCreated");
+
+ extension.sendMessage({
+ subject: "setup",
+ data: {
+ filter: testCase.filter,
+ },
+ });
+ await extension.awaitMessage("done");
+
+ helperExtension.sendMessage({
+ subject: "changeTabURL",
+ data: {
+ tabId,
+ url: URL2,
+ },
+ });
+ await helperExtension.awaitMessage("tabURLChanged");
+
+ extension.sendMessage({
+ subject: "collectTestResults",
+ data: {
+ expectEvent: testCase.expectEvent,
+ },
+ });
+ await extension.awaitMessage("done");
+
+ helperExtension.sendMessage({
+ subject: "removeTab",
+ data: { tabId },
+ });
+ await helperExtension.awaitMessage("tabRemoved");
+ }
+
+ await extension.unload();
+ await helperExtension.unload();
+}
+
+// https://mozilla.org host permission
+add_task(function onUpdateFilter_with_host_permission_url3() {
+ return test_onUpdateFilter(
+ [
+ {
+ filter: { urls: ["*://mozilla.org/*"] },
+ expectEvent: false,
+ },
+ {
+ filter: { urls: ["<all_urls>"] },
+ expectEvent: false,
+ },
+ {
+ filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] },
+ expectEvent: false,
+ },
+ {
+ filter: { properties: ["title"] },
+ expectEvent: false,
+ },
+ {
+ filter: {},
+ expectEvent: true,
+ },
+ ],
+ ["*://mozilla.org/*"]
+ );
+});
+
+// https://example.net host permission
+add_task(function onUpdateFilter_with_host_permission_url2() {
+ return test_onUpdateFilter(
+ [
+ {
+ filter: { urls: ["*://mozilla.org/*"] },
+ expectEvent: false,
+ },
+ {
+ filter: { urls: ["<all_urls>"] },
+ expectEvent: true,
+ },
+ {
+ filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] },
+ expectEvent: true,
+ },
+ {
+ filter: { properties: ["title"] },
+ expectEvent: true,
+ },
+ {
+ filter: {},
+ expectEvent: true,
+ },
+ ],
+ ["*://example.net/*"]
+ );
+});
+
+// <all_urls> permission
+add_task(function onUpdateFilter_with_host_permission_all_urls() {
+ return test_onUpdateFilter(
+ [
+ {
+ filter: { urls: ["*://mozilla.org/*"] },
+ expectEvent: false,
+ },
+ {
+ filter: { urls: ["<all_urls>"] },
+ expectEvent: true,
+ },
+ {
+ filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] },
+ expectEvent: true,
+ },
+ {
+ filter: { properties: ["title"] },
+ expectEvent: true,
+ },
+ {
+ filter: {},
+ expectEvent: true,
+ },
+ ],
+ ["<all_urls>"]
+ );
+});
+
+// tabs permission
+add_task(function onUpdateFilter_with_tabs_permission() {
+ return test_onUpdateFilter(
+ [
+ {
+ filter: { urls: ["*://mozilla.org/*"] },
+ expectEvent: false,
+ },
+ {
+ filter: { urls: ["<all_urls>"] },
+ expectEvent: true,
+ },
+ {
+ filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] },
+ expectEvent: true,
+ },
+ {
+ filter: { properties: ["title"] },
+ expectEvent: true,
+ },
+ {
+ filter: {},
+ expectEvent: true,
+ },
+ ],
+ ["tabs"]
+ );
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tabs create Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_setup(async () => {
+ // TODO bug 1799344: remove this when the pref is true by default.
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["extensions.openPopupWithoutUserGesture.enabled", true],
+ ],
+ });
+});
+
+async function test_query(query) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "current-window@tests.mozilla.org",
+ }
+ },
+ permissions: ["tabs"],
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ },
+
+ useAddonManager: "permanent",
+
+ background: async function() {
+ let query = await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => {
+ resolve(message);
+ });
+ });
+ let tab = await browser.tabs.create({ url: "http://www.example.com", active: true });
+ browser.runtime.onMessage.addListener(message => {
+ if (message === "popup-loaded") {
+ browser.runtime.sendMessage({ tab, query });
+ }
+ });
+ browser.browserAction.openPopup();
+ },
+
+ files: {
+ "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`,
+ "popup.js"() {
+ browser.runtime.onMessage.addListener(async function({ tab, query }) {
+ let tabs = await browser.tabs.query(query);
+ browser.test.assertEq(tabs.length, 1, `Got one tab`);
+ browser.test.assertEq(tabs[0].id, tab.id, "The tab is the right one");
+
+ // Create a new tab and verify that we still see the right result
+ let newTab = await browser.tabs.create({ url: "http://www.example.com", active: true });
+ tabs = await browser.tabs.query(query);
+ browser.test.assertEq(tabs.length, 1, `Got one tab`);
+ browser.test.assertEq(tabs[0].id, newTab.id, "Got the newly-created tab");
+
+ await browser.tabs.remove(newTab.id);
+
+ // Remove the tab and verify that we see the old tab
+ tabs = await browser.tabs.query(query);
+ browser.test.assertEq(tabs.length, 1, `Got one tab`);
+ browser.test.assertEq(tabs[0].id, tab.id, "Got the tab that was active before");
+
+ // Cleanup
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("tabs.query");
+ });
+ browser.runtime.sendMessage("popup-loaded");
+ },
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(query);
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+}
+
+add_task(function test_query_currentWindow_from_popup() {
+ return test_query({ currentWindow: true, active: true });
+});
+
+add_task(function test_query_lastActiveWindow_from_popup() {
+ return test_query({ lastFocusedWindow: true, active: true });
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test tabs.sendMessage</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+"use strict";
+
+add_task(async function test_tabs_sendMessage_to_extension_page_frame() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html?tabs.sendMessage"],
+ js: ["cs.js"],
+ }],
+ web_accessible_resources: ["page.html", "page.js"],
+ },
+
+ async background() {
+ let tab;
+
+ browser.runtime.onMessage.addListener(async (msg, sender) => {
+ browser.test.assertEq(msg, "page-script-ready");
+ browser.test.assertEq(sender.url, browser.runtime.getURL("page.html"));
+
+ let tabId = sender.tab.id;
+ let response = await browser.tabs.sendMessage(tabId, "tab-sendMessage");
+
+ switch (response) {
+ case "extension-tab":
+ browser.test.assertEq(tab.id, tabId, "Extension tab responded");
+ browser.test.assertEq(sender.frameId, 0, "Response from top level");
+ await browser.tabs.remove(tab.id);
+ browser.test.sendMessage("extension-tab-responded");
+ break;
+
+ case "extension-frame":
+ browser.test.assertTrue(sender.frameId > 0, "Response from iframe");
+ browser.test.sendMessage("extension-frame-responded");
+ break;
+
+ default:
+ browser.test.fail("Unexpected response: " + response);
+ }
+ });
+
+ tab = await browser.tabs.create({ url: "page.html" });
+ },
+
+ files: {
+ "cs.js"() {
+ let iframe = document.createElement("iframe");
+ iframe.src = browser.runtime.getURL("page.html");
+ document.body.append(iframe);
+ browser.test.sendMessage("content-script-done");
+ },
+
+ "page.html": `<!DOCTYPE html>
+ <meta charset=utf-8>
+ <script src=page.js><\/script>
+ Extension page`,
+
+ "page.js"() {
+ browser.runtime.onMessage.addListener(async msg => {
+ browser.test.assertEq(msg, "tab-sendMessage");
+ return window.parent === window ? "extension-tab" : "extension-frame";
+ });
+ browser.runtime.sendMessage("page-script-ready");
+ },
+ }
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("extension-tab-responded");
+
+ let win = window.open("file_sample.html?tabs.sendMessage");
+ await extension.awaitMessage("content-script-done");
+ await extension.awaitMessage("extension-frame-responded");
+ win.close();
+
+ await extension.unload();
+});
+
+add_task(async function test_tabs_sendMessage_using_frameId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://mochi.test/*/file_contains_iframe.html"],
+ run_at: "document_start",
+ js: ["cs_top.js"],
+ },
+ {
+ matches: ["http://example.org/*/file_contains_img.html"],
+ js: ["cs_iframe.js"],
+ all_frames: true,
+ },
+ ],
+ },
+
+ background() {
+ browser.runtime.onMessage.addListener(async (msg, sender) => {
+ let { tab, frameId } = sender;
+ browser.test.assertEq(msg, "cs_iframe_ready", "Iframe cs ready.");
+ browser.test.assertTrue(frameId > 0, "Not from the top frame.");
+
+ let response = await browser.tabs.sendMessage(tab.id, "msg");
+ browser.test.assertEq(response, "cs_top", "Top cs responded first.");
+
+ response = await browser.tabs.sendMessage(tab.id, "msg", { frameId });
+ browser.test.assertEq(response, "cs_iframe", "Iframe cs reponded.");
+
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("ready");
+ },
+
+ files: {
+ "cs_top.js"() {
+ browser.test.log("Top content script loaded.")
+ browser.runtime.onMessage.addListener(async () => "cs_top");
+ },
+ "cs_iframe.js"() {
+ browser.test.log("Iframe content script loaded.")
+ browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
+ browser.test.log("Iframe content script received message.")
+ setTimeout(() => sendResponse("cs_iframe"), 100);
+ return true;
+ });
+ browser.runtime.sendMessage("cs_iframe_ready");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let win = window.open("file_contains_iframe.html");
+ await extension.awaitMessage("done");
+ win.close();
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Testing test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+function loadExtensionAndInterceptTest(extensionData) {
+ let results = [];
+ let testResolve;
+ let testDone = new Promise(resolve => { testResolve = resolve; });
+ let handler = {
+ testResult(...result) {
+ result.pop();
+ results.push(result);
+ SimpleTest.info(`Received test result: ${JSON.stringify(result)}`);
+ },
+
+ testMessage(msg, ...args) {
+ results.push(["test-message", msg, ...args]);
+ SimpleTest.info(`Received message: ${msg} ${JSON.stringify(args)}`);
+ if (msg === "This is the last browser.test call") {
+ testResolve();
+ }
+ },
+ };
+ let extension = SpecialPowers.loadExtension(extensionData, handler);
+ SimpleTest.registerCleanupFunction(() => {
+ if (extension.state == "pending" || extension.state == "running") {
+ SimpleTest.ok(false, "Extension left running at test shutdown");
+ return extension.unload();
+ } else if (extension.state == "unloading") {
+ SimpleTest.ok(false, "Extension not fully unloaded at test shutdown");
+ }
+ });
+ extension.awaitResults = () => testDone.then(() => results);
+ return extension;
+}
+
+// NOTE: This test does not verify the behavior expected by calling the browser.test API methods.
+//
+// On the contrary it tests what messages ext-test.js sends to the parent process as a result of
+// processing different kind of parameters (e.g. how a dom element or a JS object with a custom
+// toString method are being serialized into strings).
+//
+// All browser.test calls results are intercepted by the test itself, see verifyTestResults for
+// the expectations of each browser.test call.
+function testScript() {
+ browser.test.notifyPass("dot notifyPass");
+ browser.test.notifyFail("dot notifyFail");
+ browser.test.log("dot log");
+ browser.test.fail("dot fail");
+ browser.test.succeed("dot succeed");
+ browser.test.assertTrue(true);
+ browser.test.assertFalse(false);
+ browser.test.assertEq("", "");
+
+ let obj = {};
+ let arr = [];
+ browser.test.assertTrue(obj, "Object truthy");
+ browser.test.assertTrue(arr, "Array truthy");
+ browser.test.assertTrue(true, "True truthy");
+ browser.test.assertTrue(false, "False truthy");
+ browser.test.assertTrue(null, "Null truthy");
+ browser.test.assertTrue(undefined, "Void truthy");
+
+ browser.test.assertFalse(obj, "Object falsey");
+ browser.test.assertFalse(arr, "Array falsey");
+ browser.test.assertFalse(true, "True falsey");
+ browser.test.assertFalse(false, "False falsey");
+ browser.test.assertFalse(null, "Null falsey");
+ browser.test.assertFalse(undefined, "Void falsey");
+
+ browser.test.assertEq(obj, obj, "Object equality");
+ browser.test.assertEq(arr, arr, "Array equality");
+ browser.test.assertEq(null, null, "Null equality");
+ browser.test.assertEq(undefined, undefined, "Void equality");
+
+ browser.test.assertEq({}, {}, "Object reference inequality");
+ browser.test.assertEq([], [], "Array reference inequality");
+ browser.test.assertEq(true, 1, "strict: true and 1 inequality");
+ browser.test.assertEq("1", 1, "strict: '1' and 1 inequality");
+ browser.test.assertEq(null, undefined, "Null and void inequality");
+
+ browser.test.assertDeepEq({a: 1, b: 1}, {b: 1, a: 1}, "Object deep eq");
+ browser.test.assertDeepEq([[2], [1]], [[2], [1]], "Array deep eq");
+ browser.test.assertDeepEq(true, 1, "strict: true and 1 deep ineq");
+ browser.test.assertDeepEq("1", 1, "strict: '1' and 1 deep ineq");
+ // Key with undefined value should be different from object without key:
+ browser.test.assertDeepEq(null, undefined, "Null and void deep ineq");
+ browser.test.assertDeepEq({c: undefined}, {c: null}, "void+null deep ineq");
+ browser.test.assertDeepEq({a: undefined, b: 1}, {b: 1}, "void/- deep ineq");
+
+ browser.test.assertDeepEq(NaN, NaN, "NaN deep eq");
+ browser.test.assertDeepEq(NaN, null, "NaN+null deep ineq");
+ browser.test.assertDeepEq(Infinity, Infinity, "Infinity deep eq");
+ browser.test.assertDeepEq(Infinity, null, "Infinity+null deep ineq");
+
+ obj = {
+ toString() {
+ return "Dynamic toString";
+ },
+ };
+ browser.test.assertEq(obj, obj, "obj with dynamic toString()");
+
+ browser.test.assertThrows(
+ () => { throw new Error("dummy"); },
+ /dummy2/,
+ "intentional failure"
+ );
+ browser.test.assertThrows(
+ () => { throw new Error("dummy2"); },
+ /dummy3/
+ );
+ browser.test.assertThrows(
+ () => {},
+ /dummy/
+ );
+
+ // The WebIDL version of assertDeepEq structurally clones before sending the
+ // params to the main thread. This check verifies that the behavior is
+ // consistent between the WebIDL and Schemas.jsm-generated API bindings.
+ browser.test.assertThrows(
+ () => browser.test.assertDeepEq(obj, obj, "obj with func"),
+ /An unexpected error occurred/,
+ "assertDeepEq obj with function throws"
+ );
+ browser.test.assertThrows(
+ () => browser.test.assertDeepEq(() => {}, () => {}, "func to assertDeepEq"),
+ /An unexpected error occurred/,
+ "assertDeepEq with function throws"
+ );
+ browser.test.assertThrows(
+ () => browser.test.assertDeepEq(/./, /./, "regexp"),
+ /Unsupported obj type: RegExp/,
+ "assertDeepEq with RegExp throws"
+ );
+
+ // Set of additional tests to only run on background page and content script
+ // (but skip on background service worker).
+ if (self === self.window) {
+ let dom = document.createElement("body");
+ browser.test.assertTrue(dom, "Element truthy");
+ browser.test.assertTrue(false, document.createElement("html"));
+ browser.test.assertFalse(dom, "Element falsey");
+ browser.test.assertFalse(true, document.createElement("head"));
+ browser.test.assertEq(dom, dom, "Element equality");
+ browser.test.assertEq(dom, document.createElement("body"), "Element inequality");
+ browser.test.assertEq(true, false, document.createElement("div"));
+ }
+
+ browser.test.sendMessage("Ran test at", location.protocol);
+ browser.test.sendMessage("This is the last browser.test call");
+}
+
+function verifyTestResults(results, shortName, expectedProtocol, useServiceWorker) {
+ let expectations = [
+ ["test-done", true, "dot notifyPass"],
+ ["test-done", false, "dot notifyFail"],
+ ["test-log", true, "dot log"],
+ ["test-result", false, "dot fail"],
+ ["test-result", true, "dot succeed"],
+ ["test-result", true, "undefined"],
+ ["test-result", true, "undefined"],
+ ["test-eq", true, "undefined", "", ""],
+
+ ["test-result", true, "Object truthy"],
+ ["test-result", true, "Array truthy"],
+ ["test-result", true, "True truthy"],
+ ["test-result", false, "False truthy"],
+ ["test-result", false, "Null truthy"],
+ ["test-result", false, "Void truthy"],
+
+ ["test-result", false, "Object falsey"],
+ ["test-result", false, "Array falsey"],
+ ["test-result", false, "True falsey"],
+ ["test-result", true, "False falsey"],
+ ["test-result", true, "Null falsey"],
+ ["test-result", true, "Void falsey"],
+
+ ["test-eq", true, "Object equality", "[object Object]", "[object Object]"],
+ ["test-eq", true, "Array equality", "", ""],
+ ["test-eq", true, "Null equality", "null", "null"],
+ ["test-eq", true, "Void equality", "undefined", "undefined"],
+
+ ["test-eq", false, "Object reference inequality", "[object Object]", "[object Object] (different)"],
+ ["test-eq", false, "Array reference inequality", "", " (different)"],
+ ["test-eq", false, "strict: true and 1 inequality", "true", "1"],
+ ["test-eq", false, "strict: '1' and 1 inequality", "1", "1 (different)"],
+ ["test-eq", false, "Null and void inequality", "null", "undefined"],
+
+ ["test-eq", true, "Object deep eq", `{"a":1,"b":1}`, `{"b":1,"a":1}`],
+ ["test-eq", true, "Array deep eq", "[[2],[1]]", "[[2],[1]]"],
+ ["test-eq", false, "strict: true and 1 deep ineq", "true", "1"],
+ ["test-eq", false, "strict: '1' and 1 deep ineq", `"1"`, "1"],
+ ["test-eq", false, "Null and void deep ineq", "null", "undefined"],
+ ["test-eq", false, "void+null deep ineq", `{"c":"undefined"}`, `{"c":null}`],
+ ["test-eq", false, "void/- deep ineq", `{"a":"undefined","b":1}`, `{"b":1}`],
+
+ ["test-eq", true, "NaN deep eq", `NaN`, `NaN`],
+ ["test-eq", false, "NaN+null deep ineq", `NaN`, `null`],
+ ["test-eq", true, "Infinity deep eq", `Infinity`, `Infinity`],
+ ["test-eq", false, "Infinity+null deep ineq", `Infinity`, `null`],
+
+ [
+ "test-eq",
+ true,
+ "obj with dynamic toString()",
+ // - Privileged JS API Bindings: the ext-test.js module will get a XrayWrapper and so when
+ // the object is being stringified the custom `toString()` method will not be called and
+ // "[object Object]" is the value we expect.
+ // - WebIDL API Bindngs: the parameter is being serialized into a string on the worker thread,
+ // the object is stringified using the worker principal and so there is no XrayWrapper
+ // involved and the value expected is the value returned by the custom toString method the.
+ // object does provide.
+ useServiceWorker ? "Dynamic toString" : "[object Object]",
+ useServiceWorker ? "Dynamic toString" : "[object Object]",
+ ],
+
+ [
+ "test-result", false,
+ "Function threw, expecting error to match '/dummy2/', got \'Error: dummy\': intentional failure"
+ ],
+ [
+ "test-result", false,
+ "Function threw, expecting error to match '/dummy3/', got \'Error: dummy2\'"
+ ],
+ [
+ "test-result", false,
+ "Function did not throw, expected error '/dummy/'"
+ ],
+ [
+ "test-result", true,
+ "Function threw, expecting error to match '/An unexpected error occurred/', got 'Error: An unexpected error occurred': assertDeepEq obj with function throws",
+ ],
+ [
+ "test-result", true,
+ "Function threw, expecting error to match '/An unexpected error occurred/', got 'Error: An unexpected error occurred': assertDeepEq with function throws",
+ ],
+ [
+ "test-result", true,
+ "Function threw, expecting error to match '/Unsupported obj type: RegExp/', got 'Error: Unsupported obj type: RegExp': assertDeepEq with RegExp throws",
+ ],
+ ];
+
+ if (!useServiceWorker) {
+ expectations.push(...[
+ ["test-result", true, "Element truthy"],
+ ["test-result", false, "[object HTMLHtmlElement]"],
+ ["test-result", false, "Element falsey"],
+ ["test-result", false, "[object HTMLHeadElement]"],
+ ["test-eq", true, "Element equality", "[object HTMLBodyElement]", "[object HTMLBodyElement]"],
+ ["test-eq", false, "Element inequality", "[object HTMLBodyElement]", "[object HTMLBodyElement] (different)"],
+ ["test-eq", false, "[object HTMLDivElement]", "true", "false"],
+ ]);
+ }
+
+ expectations.push(...[
+ ["test-message", "Ran test at", expectedProtocol],
+ ["test-message", "This is the last browser.test call"],
+ ]);
+
+ expectations.forEach((expectation, i) => {
+ let msg = expectation.slice(2).join(" - ");
+ isDeeply(results[i], expectation, `${shortName} (${msg})`);
+ });
+ is(results[expectations.length], undefined, "No more results");
+}
+
+add_task(async function test_test_in_background() {
+ let extensionData = {
+ background: `(${testScript})()`,
+ // This test case should never run the background script in a worker,
+ // even if this test file is running when "extensions.backgroundServiceWorker.forceInTest"
+ // pref is true
+ useServiceWorker: false,
+ };
+
+ let extension = loadExtensionAndInterceptTest(extensionData);
+ await extension.startup();
+ let results = await extension.awaitResults();
+ verifyTestResults(results, "background page", "moz-extension:", false);
+ await extension.unload();
+});
+
+add_task(async function test_test_in_background_service_worker() {
+ if (!ExtensionTestUtils.isInBackgroundServiceWorkerTests()) {
+ is(
+ ExtensionTestUtils.getBackgroundServiceWorkerEnabled(),
+ false,
+ "This test should only be skipped with background service worker disabled"
+ )
+ info("Test intentionally skipped on 'extensions.backgroundServiceWorker.enabled=false'");
+ return;
+ }
+
+ let extensionData = {
+ background: `(${testScript})()`,
+ // This test case should always run the background script in a worker,
+ // or be skipped if the background service worker is disabled by prefs.
+ useServiceWorker: true,
+ };
+
+ let extension = loadExtensionAndInterceptTest(extensionData);
+ await extension.startup();
+ let results = await extension.awaitResults();
+ verifyTestResults(results, "background service worker", "moz-extension:", true);
+ await extension.unload();
+});
+
+add_task(async function test_test_in_content_script() {
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html"],
+ js: ["contentscript.js"],
+ }],
+ },
+ files: {
+ "contentscript.js": `(${testScript})()`,
+ },
+ };
+
+ let extension = loadExtensionAndInterceptTest(extensionData);
+ await extension.startup();
+ let win = window.open("file_sample.html");
+ let results = await extension.awaitResults();
+ win.close();
+ verifyTestResults(results, "content script", "http:", false);
+ await extension.unload();
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_unlimitedStorage.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+
+"use strict";
+
+async function test_background_storagePersist(EXTENSION_ID) {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.storageManager.enabled", true],
+ ["dom.storageManager.prompt.testing", false],
+ ["dom.storageManager.prompt.testing.allow", false],
+ ],
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+
+ manifest: {
+ permissions: ["storage", "unlimitedStorage"],
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ },
+
+ background: async function() {
+ const PROMISE_RACE_TIMEOUT = 8000;
+
+ browser.test.sendMessage("extension-uuid", window.location.host);
+
+ await browser.storage.local.set({testkey: "testvalue"});
+ await browser.test.sendMessage("storage-local-called");
+
+ const requestStoragePersist = async () => {
+ const persistAllowed = await navigator.storage.persist();
+ if (!persistAllowed) {
+ throw new Error("navigator.storage.persist() has been denied");
+ }
+ };
+
+ await Promise.race([
+ requestStoragePersist(),
+ new Promise((resolve, reject) => {
+ setTimeout(() => {
+ reject(new Error("Timeout opening persistent db from background page"));
+ }, PROMISE_RACE_TIMEOUT);
+ }),
+ ]).then(
+ () => {
+ browser.test.notifyPass("indexeddb-storagePersistent-unlimitedStorage-done");
+ },
+ (error) => {
+ browser.test.fail(`error while testing persistent IndexedDB storage: ${error}`);
+ browser.test.notifyFail("indexeddb-storagePersistent-unlimitedStorage-done");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+
+ const uuid = await extension.awaitMessage("extension-uuid");
+
+ await extension.awaitMessage("storage-local-called");
+
+ let chromeScript = SpecialPowers.loadChromeScript(function test_country_data() {
+ /* eslint-env mozilla/chrome-script */
+ const {addMessageListener, sendAsyncMessage} = this;
+
+ addMessageListener("getPersistedStatus", (uuid) => {
+ const {
+ ExtensionStorageIDB,
+ } = ChromeUtils.import("resource://gre/modules/ExtensionStorageIDB.jsm");
+
+ const {WebExtensionPolicy} = Cu.getGlobalForObject(ExtensionStorageIDB);
+ const policy = WebExtensionPolicy.getByHostname(uuid);
+ const storagePrincipal = ExtensionStorageIDB.getStoragePrincipal(policy.extension);
+ const request = Services.qms.persisted(storagePrincipal);
+ request.callback = () => {
+ // request.result will be undeinfed if the request failed (request.resultCode !== Cr.NS_OK).
+ sendAsyncMessage("gotPersistedStatus", request.result);
+ };
+ });
+ });
+
+ const persistedPromise = chromeScript.promiseOneMessage("gotPersistedStatus");
+ chromeScript.sendAsyncMessage("getPersistedStatus", uuid);
+ is(await persistedPromise, true, "Got the expected persist status for the storagePrincipal");
+
+ await extension.awaitFinish("indexeddb-storagePersistent-unlimitedStorage-done");
+ await extension.unload();
+
+ checkSitePermissions(uuid, Services.perms.UNKNOWN_ACTION, "has been cleared");
+}
+
+add_task(async function test_unlimitedStorage() {
+ const EXTENSION_ID = "test-storagePersist@mozilla";
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["extensions.webextensions.ExtensionStorageIDB.enabled", true],
+ ],
+ });
+
+ // Verify persist mode enabled when the storage.local IDB database is opened from
+ // the main process (from parent/ext-storage.js).
+ info("Test unlimitedStorage on an extension migrating to the IndexedDB storage.local backend)");
+ await test_background_storagePersist(EXTENSION_ID);
+
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ [`extensions.webextensions.ExtensionStorageIDB.migrated.` + EXTENSION_ID, true],
+ ],
+ });
+
+ // Verify persist mode enabled when the storage.local IDB database is opened from
+ // the child process (from child/ext-storage.js).
+ info("Test unlimitedStorage on an extension migrated to the IndexedDB storage.local backend");
+ await test_background_storagePersist(EXTENSION_ID);
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the web_accessible_resources incognito</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+async function testImageLoading(src, expectedAction) {
+ let imageLoadingPromise = new Promise((resolve, reject) => {
+ let cleanupListeners;
+ let testImage = new window.Image();
+ // Set the src via wrappedJSObject so the load is triggered with the
+ // content page's principal rather than ours.
+ testImage.wrappedJSObject.setAttribute("src", src);
+
+ let loadListener = () => {
+ cleanupListeners();
+ resolve(expectedAction === "loaded");
+ };
+
+ let errorListener = (event) => {
+ cleanupListeners();
+ resolve(expectedAction === "blocked");
+ browser.test.log(`+++ image loading ${event.error}`);
+ };
+
+ cleanupListeners = () => {
+ testImage.removeEventListener("load", loadListener);
+ testImage.removeEventListener("error", errorListener);
+ };
+
+ testImage.addEventListener("load", loadListener);
+ testImage.addEventListener("error", errorListener);
+ });
+
+ let success = await imageLoadingPromise;
+ browser.runtime.sendMessage({name: "image-loading", expectedAction, success});
+}
+
+function testScript() {
+ window.postMessage("test-script-loaded", "*");
+}
+
+add_task(async function test_web_accessible_resources_incognito() {
+ // This extension will not have access to private browsing so its
+ // accessible resources should not be able to load in them.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "web_accessible_resources": [
+ "image.png",
+ "test_script.js",
+ "accessible.html",
+ ],
+ },
+ background() {
+ browser.test.sendMessage("url", browser.runtime.getURL(""));
+ },
+ files: {
+ "image.png": IMAGE_ARRAYBUFFER,
+ "test_script.js": testScript,
+ "accessible.html": `<html><head>
+ <meta charset="utf-8">
+ </head></html>`,
+ },
+ });
+
+ await extension.startup();
+ let baseUrl = await extension.awaitMessage("url");
+
+ async function content() {
+ let baseUrl = await browser.runtime.sendMessage({name: "get-url"});
+ testImageLoading(`${baseUrl}image.png`, "loaded");
+
+ let testScriptElement = document.createElement("script");
+ // Set the src via wrappedJSObject so the load is triggered with the
+ // content page's principal rather than ours.
+ testScriptElement.wrappedJSObject.setAttribute("src", `${baseUrl}test_script.js`);
+ document.head.appendChild(testScriptElement);
+
+ let iframe = document.createElement("iframe");
+ // Set the src via wrappedJSObject so the load is triggered with the
+ // content page's principal rather than ours.
+ iframe.wrappedJSObject.setAttribute("src", `${baseUrl}accessible.html`);
+ document.body.appendChild(iframe);
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener("message", event => {
+ browser.runtime.sendMessage({"name": event.data});
+ });
+ }
+
+ let pb_extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs"],
+ content_scripts: [{
+ "matches": ["*://example.com/*/file_sample.html"],
+ "run_at": "document_end",
+ "js": ["content_script_helper.js", "content_script.js"],
+ }],
+ },
+ files: {
+ "content_script_helper.js": `${testImageLoading}`,
+ "content_script.js": content,
+ },
+ background() {
+ let url = "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html";
+ let baseUrl;
+ let window;
+
+ browser.runtime.onMessage.addListener(async msg => {
+ switch (msg.name) {
+ case "image-loading":
+ browser.test.assertFalse(msg.success, `Image was ${msg.expectedAction}`);
+ browser.test.sendMessage(`image-${msg.expectedAction}`);
+ break;
+ case "get-url":
+ return baseUrl;
+ default:
+ browser.test.fail(`unexepected message ${msg.name}`);
+ }
+ });
+
+ browser.test.onMessage.addListener(async (msg, data) => {
+ if (msg == "start") {
+ baseUrl = data;
+ window = await browser.windows.create({url, incognito: true});
+ }
+ if (msg == "close") {
+ browser.windows.remove(window.id);
+ }
+ });
+ },
+ });
+ await pb_extension.startup();
+
+ consoleMonitor.start([
+ {message: /may not load or link to.*image.png/},
+ {message: /may not load or link to.*test_script.js/},
+ {message: /\<script\> source URI is not allowed in this document/},
+ {message: /may not load or link to.*accessible.html/},
+ ]);
+
+ pb_extension.sendMessage("start", baseUrl);
+
+ await pb_extension.awaitMessage("image-loaded");
+
+ pb_extension.sendMessage("close");
+
+ await extension.unload();
+ await pb_extension.unload();
+
+ await consoleMonitor.finished();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the web_accessible_resources manifest directive</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// add_setup not available in mochitest
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({set: [["extensions.manifestV3.enabled", true]]});
+})
+
+let image = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
+);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0))
+ .buffer;
+
+const ANDROID = navigator.userAgent.includes("Android");
+
+async function testImageLoading(src, expectedAction) {
+ let imageLoadingPromise = new Promise((resolve, reject) => {
+ let cleanupListeners;
+ let testImage = document.createElement("img");
+ // Set the src via wrappedJSObject so the load is triggered with the
+ // content page's principal rather than ours.
+ testImage.wrappedJSObject.setAttribute("src", src);
+
+ let loadListener = () => {
+ cleanupListeners();
+ resolve(expectedAction === "loaded");
+ };
+
+ let errorListener = () => {
+ cleanupListeners();
+ resolve(expectedAction === "blocked");
+ };
+
+ cleanupListeners = () => {
+ testImage.removeEventListener("load", loadListener);
+ testImage.removeEventListener("error", errorListener);
+ };
+
+ testImage.addEventListener("load", loadListener);
+ testImage.addEventListener("error", errorListener);
+
+ document.body.appendChild(testImage);
+ });
+
+ let success = await imageLoadingPromise;
+ browser.runtime.sendMessage({
+ name: "image-loading",
+ expectedAction,
+ success,
+ });
+}
+
+async function _test_web_accessible_resources({
+ manifest,
+ expectShouldLoadByDefault = true,
+ usePagePrincipal = false,
+}) {
+ function background(shouldLoad, usePagePrincipal) {
+ let gotURL;
+ let tabId;
+ let expectBrowserAPI;
+
+ function loadFrame(url, sandbox = null, srcdoc = false) {
+ return new Promise(resolve => {
+ browser.tabs.sendMessage(
+ tabId,
+ ["load-iframe", url, sandbox, srcdoc, usePagePrincipal],
+ reply => {
+ resolve(reply);
+ }
+ );
+ });
+ }
+
+ // shouldLoad will be true unless we expect all attempts to fail.
+ let urls = [
+ // { url, shouldLoad, sandbox, srcdoc }
+ {
+ url: browser.runtime.getURL("accessible.html"),
+ shouldLoad,
+ },
+ {
+ url: browser.runtime.getURL("accessible.html") + "?foo=bar",
+ shouldLoad,
+ },
+ {
+ url: browser.runtime.getURL("accessible.html") + "#!foo=bar",
+ shouldLoad,
+ },
+ {
+ url: browser.runtime.getURL("accessible.html"),
+ shouldLoad,
+ sandbox: "allow-scripts",
+ },
+ {
+ url: browser.runtime.getURL("accessible.html"),
+ shouldLoad,
+ sandbox: "allow-same-origin allow-scripts",
+ },
+ {
+ url: browser.runtime.getURL("accessible.html"),
+ shouldLoad,
+ sandbox: "allow-scripts",
+ srcdoc: true,
+ },
+ {
+ url: browser.runtime.getURL("inaccessible.html"),
+ shouldLoad: false,
+ },
+ {
+ url: browser.runtime.getURL("inaccessible.html"),
+ shouldLoad: false,
+ sandbox: "allow-same-origin allow-scripts",
+ },
+ {
+ url: browser.runtime.getURL("inaccessible.html"),
+ shouldLoad: false,
+ sandbox: "allow-same-origin allow-scripts",
+ srcdoc: true,
+ },
+ {
+ url: browser.runtime.getURL("wild1.html"),
+ shouldLoad,
+ },
+ {
+ url: browser.runtime.getURL("wild2.htm"),
+ shouldLoad: false,
+ },
+ ];
+
+ async function runTests() {
+ for (let { url, shouldLoad, sandbox, srcdoc } of urls) {
+ // Sandboxed pages with an opaque origin do not get browser api.
+ expectBrowserAPI = !sandbox || sandbox.includes("allow-same-origin");
+ let success = await loadFrame(url, sandbox, srcdoc);
+
+ browser.test.assertEq(shouldLoad, success, "Load was successful");
+ if (shouldLoad && !srcdoc) {
+ browser.test.assertEq(url, gotURL, "Got expected url");
+ } else {
+ browser.test.assertEq(undefined, gotURL, "Got no url");
+ }
+ gotURL = undefined;
+ }
+
+ browser.test.notifyPass("web-accessible-resources");
+ }
+
+ browser.runtime.onMessage.addListener(
+ ([msg, url, hasBrowserAPI], sender) => {
+ if (msg == "content-script-ready") {
+ tabId = sender.tab.id;
+ runTests();
+ } else if (msg == "page-script") {
+ browser.test.assertEq(
+ undefined,
+ gotURL,
+ "Should have gotten only one message"
+ );
+ browser.test.assertEq("string", typeof url, "URL should be a string");
+ browser.test.assertEq(
+ expectBrowserAPI,
+ hasBrowserAPI,
+ "has access to browser api"
+ );
+ gotURL = url;
+ }
+ }
+ );
+
+ browser.test.sendMessage("ready");
+ }
+
+ function contentScript() {
+ window.addEventListener("message", event => {
+ // bounce the postmessage to the background script
+ if (event.data[0] == "page-script") {
+ browser.runtime.sendMessage(event.data);
+ }
+ });
+
+ browser.runtime.onMessage.addListener(
+ ([msg, url, sandboxed, srcdoc, usePagePrincipal], sender, respond) => {
+ if (msg == "load-iframe") {
+ // construct the frame using srcdoc if requested.
+ if (srcdoc) {
+ sandboxed = sandboxed !== null ? `sandbox="${sandboxed}"` : "";
+ let frameSrc = `<iframe ${sandboxed} src="${url}" onload="parent.postMessage(true, '*')" onerror="parent.postMessage(false, '*')">`;
+ let frame = document.createElement("iframe");
+ frame.setAttribute("srcdoc", frameSrc);
+ window.addEventListener("message", function listener(event) {
+ if (event.source === frame.contentWindow) {
+ window.removeEventListener("message", listener);
+ respond(event.data);
+ }
+ });
+ document.body.appendChild(frame);
+ return true;
+ }
+
+ let iframe = document.createElement("iframe");
+ if (sandboxed !== null) {
+ iframe.setAttribute("sandbox", sandboxed);
+ }
+
+ if (usePagePrincipal) {
+ // Test using the page principal
+ iframe.wrappedJSObject.src = url;
+ } else {
+ // Test using the expanded principal
+ iframe.src = url;
+ }
+ iframe.addEventListener("load", () => {
+ respond(true);
+ });
+ iframe.addEventListener("error", () => {
+ respond(false);
+ });
+ document.body.appendChild(iframe);
+ return true;
+ }
+ }
+ );
+ browser.runtime.sendMessage(["content-script-ready"]);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["https://example.com/"],
+ js: ["content_script.js"],
+ run_at: "document_idle",
+ },
+ ],
+ ...manifest,
+ },
+
+ background: `(${background})(${expectShouldLoadByDefault}, ${usePagePrincipal})`,
+
+ files: {
+ "content_script.js": contentScript,
+
+ "accessible.html": `<!DOCTYPE html><html><head>
+ <meta charset="utf-8">
+ <script src="pagescript.js"><\/script>
+ </head></html>`,
+
+ "inaccessible.html": `<!DOCTYPE html><html><head>
+ <meta charset="utf-8">
+ <script src="pagescript.js"><\/script>
+ </head></html>`,
+
+ "wild1.html": `<!DOCTYPE html><html><head>
+ <meta charset="utf-8">
+ <script src="pagescript.js"><\/script>
+ </head></html>`,
+
+ "wild2.htm": `<!DOCTYPE html><html><head>
+ <meta charset="utf-8">
+ <script src="pagescript.js"><\/script>
+ </head></html>`,
+
+ "pagescript.js":
+ // We postmessage so we can determine when browser is not available
+ 'window.parent.postMessage(["page-script", location.href, typeof browser !== "undefined"], "*");',
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("ready");
+
+ let win = window.open("https://example.com/");
+
+ await extension.awaitFinish("web-accessible-resources");
+
+ win.close();
+
+ await extension.unload();
+};
+
+add_task(async function test_web_accessible_resources_v2() {
+ await SpecialPowers.pushPrefEnv({set: [["extensions.content_web_accessible.enabled", true]]});
+ consoleMonitor.start([
+ {message: /Content at https:\/\/example.com\/ may not load or link to.*inaccessible.html/},
+ ]);
+ await _test_web_accessible_resources({
+ manifest: {
+ manifest_version: 2,
+ web_accessible_resources: ["/accessible.html", "wild*.html"],
+ }
+ });
+ await consoleMonitor.finished();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Same test as above, but using only the content principal
+add_task(async function test_web_accessible_resources_v2_content() {
+ await SpecialPowers.pushPrefEnv({set: [["extensions.content_web_accessible.enabled", true]]});
+ consoleMonitor.start([
+ {message: /Content at https:\/\/example.com\/ may not load or link to.*inaccessible.html/},
+ ]);
+ await _test_web_accessible_resources({
+ manifest: {
+ manifest_version: 2,
+ web_accessible_resources: ["/accessible.html", "wild*.html"],
+ },
+ usePagePrincipal: true,
+ });
+ await consoleMonitor.finished();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_web_accessible_resources_v3() {
+ // MV3 always requires this, pref off to ensure it works.
+ await SpecialPowers.pushPrefEnv({set: [["extensions.content_web_accessible.enabled", false]]});
+ consoleMonitor.start([
+ {message: /Content at https:\/\/example.com\/ may not load or link to.*inaccessible.html/},
+ ]);
+ await _test_web_accessible_resources({
+ manifest: {
+ manifest_version: 3,
+ web_accessible_resources: [
+ {
+ resources: ["/accessible.html", "wild*.html"],
+ matches: ["*://example.com/*"]
+ },
+ ],
+ host_permissions: ["*://example.com/*"],
+ granted_host_permissions: true,
+ }
+ });
+ await consoleMonitor.finished();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_web_accessible_resources_v3_by_id() {
+ consoleMonitor.start([
+ {message: /Content at https:\/\/example.com\/ may not load or link to.*accessible.html/},
+ {message: /Content at https:\/\/example.com\/ may not load or link to.*inaccessible.html/},
+ ]);
+ await _test_web_accessible_resources({
+ manifest: {
+ manifest_version: 3,
+ browser_specific_settings: {
+ gecko: {
+ id: "extension_wac@mochitest",
+ },
+ },
+ web_accessible_resources: [
+ {
+ resources: ["/accessible.html", "wild*.html"],
+ extension_ids: ["extension_wac@mochitest"]
+ },
+ ],
+ host_permissions: ["*://example.com/*"],
+ // Work-around for bug 1766752 to allow content_scripts to run:
+ granted_host_permissions: true,
+ },
+ expectShouldLoadByDefault: false,
+ });
+ await consoleMonitor.finished();
+});
+
+add_task(async function test_web_accessible_resources_mixed_content() {
+ function background() {
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg.name === "image-loading") {
+ browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`);
+ browser.test.sendMessage(`image-${msg.expectedAction}`);
+ } else {
+ browser.test.sendMessage(msg);
+ if (msg === "accessible-script-loaded") {
+ browser.test.notifyPass("mixed-test");
+ }
+ }
+ });
+
+ browser.test.sendMessage("background-ready");
+ }
+
+ async function content() {
+ await testImageLoading(
+ "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png",
+ "blocked"
+ );
+ await testImageLoading(browser.runtime.getURL("image.png"), "loaded");
+
+ let testScriptElement = document.createElement("script");
+ // Set the src via wrappedJSObject so the load is triggered with the
+ // content page's principal rather than ours.
+ testScriptElement.wrappedJSObject.setAttribute(
+ "src",
+ browser.runtime.getURL("test_script.js")
+ );
+ document.head.appendChild(testScriptElement);
+
+ window.addEventListener("message", event => {
+ browser.runtime.sendMessage(event.data);
+ });
+ }
+
+ function testScript() {
+ window.postMessage("accessible-script-loaded", "*");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["https://example.com/*/file_mixed.html"],
+ run_at: "document_end",
+ js: ["content_script_helper.js", "content_script.js"],
+ },
+ ],
+ web_accessible_resources: ["image.png", "test_script.js"],
+ },
+ background,
+ files: {
+ "content_script_helper.js": `${testImageLoading}`,
+ "content_script.js": content,
+ "test_script.js": testScript,
+ "image.png": IMAGE_ARRAYBUFFER,
+ },
+ });
+
+ await SpecialPowers.pushPrefEnv({set: [
+ ["security.mixed_content.upgrade_display_content", false],
+ ["security.mixed_content.block_display_content", true],
+ ]});
+
+ await Promise.all([
+ extension.startup(),
+ extension.awaitMessage("background-ready"),
+ ]);
+
+ let win = window.open(
+ "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_mixed.html"
+ );
+
+ await Promise.all([
+ extension.awaitMessage("image-blocked"),
+ extension.awaitMessage("image-loaded"),
+ extension.awaitMessage("accessible-script-loaded"),
+ ]);
+ await extension.awaitFinish("mixed-test");
+ win.close();
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+// test that MV2 extensions continue to open other MV2 extension pages
+// when they are not listed in web_accessible_resources. This test also
+// covers mobile/android tab creation.
+add_task(async function test_web_accessible_resources_extensions_MV2() {
+ function background() {
+ let newtab;
+ let win;
+ let expectUrl;
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
+ if (!expectUrl || tab.url != expectUrl || changeInfo.status !== "complete") {
+ return;
+ }
+ expectUrl = undefined;
+ browser.test.log(`onUpdated ${JSON.stringify(changeInfo)} ${tab.url}`);
+ browser.test.sendMessage("onUpdated", tab.url);
+ });
+ browser.test.onMessage.addListener(async (msg, url) => {
+ browser.test.log(`onMessage ${msg} ${url}`);
+ expectUrl = url;
+ if (msg == "create") {
+ newtab = await browser.tabs.create({ url });
+ browser.test.assertTrue(
+ newtab.id !== browser.tabs.TAB_ID_NONE,
+ "New tab was created."
+ );
+ } else if (msg == "update") {
+ await browser.tabs.update(newtab.id, { url });
+ } else if (msg == "remove") {
+ await browser.tabs.remove(newtab.id);
+ newtab = null;
+ browser.test.sendMessage("completed");
+ } else if (msg == "open-window") {
+ win = await browser.windows.create({ url });
+ } else if (msg == "close-window") {
+ await browser.windows.remove(win.id);
+ browser.test.sendMessage("completed");
+ win = null;
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "this-mv2@mochitest" } },
+ },
+ background,
+ files: {
+ "page.html": `<!DOCTYPE html><html><head>
+ <meta charset="utf-8">
+ </head></html>`,
+ },
+ });
+
+ async function testTabsAction(ext, action, url) {
+ ext.sendMessage(action, url);
+ is(await ext.awaitMessage("onUpdated"), url, "extension url was loaded");
+ }
+
+ await extension.startup();
+ let extensionUrl = `moz-extension://${extension.uuid}/page.html`;
+
+ // Test opening its own pages
+ await testTabsAction(extension, "create", `${extensionUrl}?q=1`);
+ await testTabsAction(extension, "update", `${extensionUrl}?q=2`);
+ extension.sendMessage("remove");
+ await extension.awaitMessage("completed");
+ if (!ANDROID) {
+ await testTabsAction(extension, "open-window", `${extensionUrl}?q=3`);
+ extension.sendMessage("close-window");
+ await extension.awaitMessage("completed");
+ }
+
+ // Extension used to open the homepage in a new window.
+ let other = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "<all_urls>"],
+ },
+ background,
+ });
+ await other.startup();
+
+ // Test opening another extensions pages
+ await testTabsAction(other, "create", `${extensionUrl}?q=4`);
+ await testTabsAction(other, "update", `${extensionUrl}?q=5`);
+ other.sendMessage("remove");
+ await other.awaitMessage("completed");
+ if (!ANDROID) {
+ await testTabsAction(other, "open-window", `${extensionUrl}?q=6`);
+ other.sendMessage("close-window");
+ await other.awaitMessage("completed");
+ }
+
+ await extension.unload();
+ await other.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+if (AppConstants.platform === "android") {
+ SimpleTest.requestLongerTimeout(3);
+}
+
+/* globals sendMouseEvent */
+
+function backgroundScript() {
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+ const URL = BASE + "/file_WebNavigation_page1.html";
+
+ const EVENTS = [
+ "onTabReplaced",
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ "onErrorOccurred",
+ "onReferenceFragmentUpdated",
+ "onHistoryStateUpdated",
+ ];
+
+ let expectedTabId = -1;
+
+ function gotEvent(event, details) {
+ if (!details.url.startsWith(BASE)) {
+ return;
+ }
+ browser.test.log(`Got ${event} ${details.url} ${details.frameId} ${details.parentFrameId}`);
+
+ if (expectedTabId == -1) {
+ browser.test.assertTrue(details.tabId !== undefined, "tab ID defined");
+ expectedTabId = details.tabId;
+ }
+
+ browser.test.assertEq(details.tabId, expectedTabId, "correct tab");
+
+ browser.test.sendMessage("received", {url: details.url, event});
+
+ if (details.url == URL) {
+ browser.test.assertEq(0, details.frameId, "root frame ID correct");
+ browser.test.assertEq(-1, details.parentFrameId, "root parent frame ID correct");
+ } else {
+ browser.test.assertEq(0, details.parentFrameId, "parent frame ID correct");
+ browser.test.assertTrue(details.frameId != 0, "frame ID probably okay");
+ }
+
+ browser.test.assertTrue(details.frameId !== undefined, "frameId != undefined");
+ browser.test.assertTrue(details.parentFrameId !== undefined, "parentFrameId != undefined");
+ }
+
+ let listeners = {};
+ for (let event of EVENTS) {
+ listeners[event] = gotEvent.bind(null, event);
+ browser.webNavigation[event].addListener(listeners[event]);
+ }
+
+ browser.test.sendMessage("ready");
+}
+
+const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+const URL = BASE + "/file_WebNavigation_page1.html";
+const FORM_URL = URL + "?";
+const FRAME = BASE + "/file_WebNavigation_page2.html";
+const FRAME2 = BASE + "/file_WebNavigation_page3.html";
+const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html";
+const REDIRECT = BASE + "/redirection.sjs";
+const REDIRECTED = BASE + "/dummy_page.html";
+const CLIENT_REDIRECT = BASE + "/file_webNavigation_clientRedirect.html";
+const CLIENT_REDIRECT_HTTPHEADER = BASE + "/file_webNavigation_clientRedirect_httpHeaders.html";
+const FRAME_CLIENT_REDIRECT = BASE + "/file_webNavigation_frameClientRedirect.html";
+const FRAME_REDIRECT = BASE + "/file_webNavigation_frameRedirect.html";
+const FRAME_MANUAL = BASE + "/file_webNavigation_manualSubframe.html";
+const FRAME_MANUAL_PAGE1 = BASE + "/file_webNavigation_manualSubframe_page1.html";
+const FRAME_MANUAL_PAGE2 = BASE + "/file_webNavigation_manualSubframe_page2.html";
+const INVALID_PAGE = "https://invalid.localhost/";
+
+const REQUIRED = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+];
+
+var received = [];
+var completedResolve;
+var waitingURL, waitingEvent;
+
+function loadAndWait(win, event, url, script) {
+ received = [];
+ waitingEvent = event;
+ waitingURL = url;
+ dump(`RUN ${script}\n`);
+ script();
+ return new Promise(resolve => { completedResolve = resolve; });
+}
+
+add_task(async function webnav_transitions_props() {
+ function backgroundScriptTransitions() {
+ const EVENTS = [
+ "onCommitted",
+ "onHistoryStateUpdated",
+ "onReferenceFragmentUpdated",
+ "onCompleted",
+ ];
+
+ function gotEvent(event, details) {
+ browser.test.log(`Got ${event} ${details.url} ${details.transitionType} ${details.transitionQualifiers && JSON.stringify(details.transitionQualifiers)}`);
+
+ browser.test.sendMessage("received", {url: details.url, details, event});
+ }
+
+ let listeners = {};
+ for (let event of EVENTS) {
+ listeners[event] = gotEvent.bind(null, event);
+ browser.webNavigation[event].addListener(listeners[event]);
+ }
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background: backgroundScriptTransitions,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ extension.onMessage("received", ({url, event, details}) => {
+ received.push({url, event, details});
+
+ if (event == waitingEvent && url == waitingURL) {
+ completedResolve();
+ }
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ await loadAndWait(win, "onCompleted", URL, () => { win.location = URL; });
+
+ // transitionType: reload
+ received = [];
+ await loadAndWait(win, "onCompleted", URL, () => { win.location.reload(); });
+
+ let found = received.find((data) => (data.event == "onCommitted" && data.url == URL));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "reload",
+ "Got the expected 'reload' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers),
+ "transitionQualifiers found in the OnCommitted events");
+ }
+
+ // transitionType: auto_subframe
+ found = received.find((data) => (data.event == "onCommitted" && data.url == FRAME));
+
+ ok(found, "Got the sub-frame onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers),
+ "transitionQualifiers found in the OnCommitted events");
+ }
+
+ // transitionType: form_submit
+ received = [];
+ await loadAndWait(win, "onCompleted", FORM_URL, () => {
+ win.document.querySelector("form").submit();
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == FORM_URL));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "form_submit",
+ "Got the expected 'form_submit' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers),
+ "transitionQualifiers found in the OnCommitted events");
+ }
+
+ // transitionQualifier: server_redirect
+ received = [];
+ await loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = REDIRECT; });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "link",
+ "Got the expected 'link' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "server_redirect"),
+ "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: forward_back
+ received = [];
+ await loadAndWait(win, "onCompleted", FORM_URL, () => { win.history.back(); });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == FORM_URL));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "link",
+ "Got the expected 'link' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "forward_back"),
+ "Got the expected 'forward_back' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: client_redirect
+ // (from meta http-equiv tag)
+ received = [];
+ await loadAndWait(win, "onCompleted", REDIRECTED, () => {
+ win.location = CLIENT_REDIRECT;
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "link",
+ "Got the expected 'link' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "client_redirect"),
+ "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: client_redirect
+ // (from http headers)
+ received = [];
+ await loadAndWait(win, "onCompleted", REDIRECTED, () => {
+ win.location = CLIENT_REDIRECT_HTTPHEADER;
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "link",
+ "Got the expected 'link' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "client_redirect"),
+ "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: client_redirect (sub-frame)
+ // (from meta http-equiv tag)
+ received = [];
+ await loadAndWait(win, "onCompleted", REDIRECTED, () => {
+ win.location = FRAME_CLIENT_REDIRECT;
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "client_redirect"),
+ "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: server_redirect (sub-frame)
+ received = [];
+ await loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = FRAME_REDIRECT; });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECT));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+ // TODO BUG 1264936: currently the server_redirect is not detected in sub-frames
+ // once we fix it we can test it here:
+ //
+ // ok(Array.isArray(found.details.transitionQualifiers) &&
+ // found.details.transitionQualifiers.find((q) => q == "server_redirect"),
+ // "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionType: manual_subframe
+ received = [];
+ await loadAndWait(win, "onCompleted", FRAME_MANUAL, () => { win.location = FRAME_MANUAL; });
+ found = received.find((data) => (data.event == "onCommitted" &&
+ data.url == FRAME_MANUAL_PAGE1));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+ }
+
+ received = [];
+ await loadAndWait(win, "onCompleted", FRAME_MANUAL_PAGE2, () => {
+ let el = win.document.querySelector("iframe")
+ .contentDocument.querySelector("a");
+ sendMouseEvent({type: "click"}, el, win);
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" &&
+ data.url == FRAME_MANUAL_PAGE2));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ if (AppConstants.MOZ_BUILD_APP === "browser") {
+ is(found.details.transitionType, "manual_subframe",
+ "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+ } else {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+ }
+ }
+
+ // Test transitions properties on onHistoryStateUpdated events.
+
+ received = [];
+ await loadAndWait(win, "onCompleted", FRAME2, () => { win.location = FRAME2; });
+
+ received = [];
+ await loadAndWait(win, "onHistoryStateUpdated", `${FRAME2}/pushState`, () => {
+ win.history.pushState({}, "History PushState", `${FRAME2}/pushState`);
+ });
+
+ found = received.find((data) => (data.event == "onHistoryStateUpdated" &&
+ data.url == `${FRAME2}/pushState`));
+
+ ok(found, "Got the onHistoryStateUpdated event");
+
+ if (found) {
+ is(typeof found.details.transitionType, "string",
+ "Got transitionType in the onHistoryStateUpdated event");
+ ok(Array.isArray(found.details.transitionQualifiers),
+ "Got transitionQualifiers in the onHistoryStateUpdated event");
+ }
+
+ // Test transitions properties on onReferenceFragmentUpdated events.
+
+ received = [];
+ await loadAndWait(win, "onReferenceFragmentUpdated", `${FRAME2}/pushState#ref2`, () => {
+ win.history.pushState({}, "ReferenceFragment Update", `${FRAME2}/pushState#ref2`);
+ });
+
+ found = received.find((data) => (data.event == "onReferenceFragmentUpdated" &&
+ data.url == `${FRAME2}/pushState#ref2`));
+
+ ok(found, "Got the onReferenceFragmentUpdated event");
+
+ if (found) {
+ is(typeof found.details.transitionType, "string",
+ "Got transitionType in the onReferenceFragmentUpdated event");
+ ok(Array.isArray(found.details.transitionQualifiers),
+ "Got transitionQualifiers in the onReferenceFragmentUpdated event");
+ }
+
+ // cleanup phase
+ win.close();
+
+ await extension.unload();
+ info("webnavigation extension unloaded");
+});
+
+add_task(async function webnav_ordering() {
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background: backgroundScript,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ extension.onMessage("received", ({url, event}) => {
+ received.push({url, event});
+
+ if (event == waitingEvent && url == waitingURL) {
+ completedResolve();
+ }
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ await loadAndWait(win, "onCompleted", URL, () => { win.location = URL; });
+
+ function checkRequired(url) {
+ for (let event of REQUIRED) {
+ let found = false;
+ for (let r of received) {
+ if (r.url == url && r.event == event) {
+ found = true;
+ }
+ }
+ ok(found, `Received event ${event} from ${url}`);
+ }
+ }
+
+ checkRequired(URL);
+ checkRequired(FRAME);
+
+ function checkBefore(action1, action2) {
+ function find(action) {
+ for (let i = 0; i < received.length; i++) {
+ if (received[i].url == action.url && received[i].event == action.event) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ let index1 = find(action1);
+ let index2 = find(action2);
+ ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`);
+ ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`);
+ ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`);
+ }
+
+ // As required in the webNavigation API documentation:
+ // If a navigating frame contains subframes, its onCommitted is fired before any
+ // of its children's onBeforeNavigate; while onCompleted is fired after
+ // all of its children's onCompleted.
+ checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"});
+ checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"});
+
+ // As required in the webNAvigation API documentation, check the event sequence:
+ // onBeforeNavigate -> onCommitted -> onDOMContentLoaded -> onCompleted
+ let expectedEventSequence = [
+ "onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted",
+ ];
+
+ for (let i = 1; i < expectedEventSequence.length; i++) {
+ let after = expectedEventSequence[i];
+ let before = expectedEventSequence[i - 1];
+ checkBefore({url: URL, event: before}, {url: URL, event: after});
+ checkBefore({url: FRAME, event: before}, {url: FRAME, event: after});
+ }
+
+ await loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; });
+
+ checkRequired(FRAME2);
+
+ let navigationSequence = [
+ {
+ action: () => { win.frames[0].document.getElementById("elt").click(); },
+ waitURL: `${FRAME2}#ref`,
+ expectedEvent: "onReferenceFragmentUpdated",
+ description: "clicked an anchor link",
+ },
+ {
+ action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
+ waitURL: `${FRAME2}#ref2`,
+ expectedEvent: "onReferenceFragmentUpdated",
+ description: "history.pushState, same pathname, different hash",
+ },
+ {
+ action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
+ waitURL: `${FRAME2}#ref2`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, same hash",
+ },
+ {
+ action: () => {
+ win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param1=value#ref2`);
+ },
+ waitURL: `${FRAME2}?query_param1=value#ref2`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, same hash, different query params",
+ },
+ {
+ action: () => {
+ win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param2=value#ref3`);
+ },
+ waitURL: `${FRAME2}?query_param2=value#ref3`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, different hash, different query params",
+ },
+ {
+ action: () => { win.frames[0].history.pushState(null, "History PushState", FRAME_PUSHSTATE); },
+ waitURL: FRAME_PUSHSTATE,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, different pathname",
+ },
+ ];
+
+ for (let navigation of navigationSequence) {
+ let {expectedEvent, waitURL, action, description} = navigation;
+ info(`Waiting ${expectedEvent} from ${waitURL} - ${description}`);
+ await loadAndWait(win, expectedEvent, waitURL, action);
+ info(`Received ${expectedEvent} from ${waitURL} - ${description}`);
+ }
+
+ for (let i = navigationSequence.length - 1; i > 0; i--) {
+ let {waitURL: fromURL, expectedEvent} = navigationSequence[i];
+ let {waitURL} = navigationSequence[i - 1];
+ info(`Waiting ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
+ await loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.back(); });
+ info(`Received ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
+ }
+
+ for (let i = 0; i < navigationSequence.length - 1; i++) {
+ let {waitURL: fromURL} = navigationSequence[i];
+ let {waitURL, expectedEvent} = navigationSequence[i + 1];
+ info(`Waiting ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+ await loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); });
+ info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+ }
+
+ win.close();
+
+ await extension.unload();
+ info("webnavigation extension unloaded");
+});
+
+add_task(async function webnav_error_event() {
+ function backgroundScriptErrorEvent() {
+ browser.webNavigation.onErrorOccurred.addListener((details) => {
+ browser.test.log(`Got onErrorOccurred ${details.url} ${details.error}`);
+
+ browser.test.sendMessage("received", {url: details.url, details, event: "onErrorOccurred"});
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background: backgroundScriptErrorEvent,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ extension.onMessage("received", ({url, event, details}) => {
+ received.push({url, event, details});
+
+ if (event == waitingEvent && url == waitingURL) {
+ completedResolve();
+ }
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ received = [];
+ await loadAndWait(win, "onErrorOccurred", INVALID_PAGE, () => { win.location = INVALID_PAGE; });
+
+ let found = received.find((data) => (data.event == "onErrorOccurred" &&
+ data.url == INVALID_PAGE));
+
+ ok(found, "Got the onErrorOccurred event");
+
+ if (found) {
+ ok(found.details.error.match(/Error code [0-9]+/),
+ "Got the expected error string in the onErrorOccurred event");
+ }
+
+ // cleanup phase
+ win.close();
+
+ await extension.unload();
+ info("webnavigation extension unloaded");
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_webnav_unresolved_uri_on_expected_URI_scheme() {
+ function background() {
+ let listeners = [];
+
+ function cleanupTestListeners() {
+ browser.test.log(`Cleanup previous test event listeners`);
+ for (let {event, listener} of listeners.splice(0)) {
+ browser.webNavigation[event].removeListener(listener);
+ }
+ }
+
+ function createTestListener(event, fail, urlFilter) {
+ return new Promise(resolve => {
+ function listener(details) {
+ let log = JSON.stringify({url: details.url, urlFilter});
+ if (fail) {
+ browser.test.fail(`Got an unexpected ${event} on the failure listener: ${log}`);
+ } else {
+ browser.test.succeed(`Got the expected ${event} on the success listener: ${log}`);
+ }
+
+ resolve();
+ }
+
+ browser.webNavigation[event].addListener(listener, {url: urlFilter});
+ listeners.push({event, listener});
+ });
+ }
+
+ browser.test.onMessage.addListener((msg, events, data) => {
+ if (msg !== "test-filters") {
+ return;
+ }
+
+ let promises = [];
+
+ for (let {okFilter, failFilter} of data.filters) {
+ for (let event of events) {
+ promises.push(
+ Promise.race([
+ createTestListener(event, false, okFilter),
+ createTestListener(event, true, failFilter),
+ ]));
+ }
+ }
+
+ Promise.all(promises).catch(e => {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ }).then(() => {
+ cleanupTestListeners();
+ browser.test.sendMessage("test-filter-next");
+ });
+
+ browser.test.sendMessage("test-filter-ready");
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+
+ let win = window.open();
+
+ let testFilterScenarios = [
+ {
+ url: "https://example.net/browser",
+ filters: [
+ // schemes
+ {
+ okFilter: [{schemes: ["https"]}],
+ failFilter: [{schemes: ["http"]}],
+ },
+ // ports
+ {
+ okFilter: [{ports: [80, 22, 443]}],
+ failFilter: [{ports: [81, 82, 83]}],
+ },
+ {
+ okFilter: [{ports: [22, 443, [10, 80]]}],
+ failFilter: [{ports: [22, 23, [81, 100]]}],
+ },
+ // multiple criteria in a single filter:
+ // if one of the criteria is not verified, the event should not be received.
+ {
+ okFilter: [{schemes: ["https"], ports: [80, 22, 443]}],
+ failFilter: [{schemes: ["https"], ports: [81, 82, 83]}],
+ },
+ {
+ okFilter: [{hostEquals: "example.net", ports: [80, 22, 443]}],
+ failFilter: [{hostEquals: "example.org", ports: [80, 22, 443]}],
+ },
+ // multiple urlFilters on the same listener
+ // if at least one of the criteria is verified, the event should be received.
+ {
+ okFilter: [{schemes: ["http"]}, {ports: [80, 22, 443]}],
+ failFilter: [{schemes: ["http"]}, {ports: [81, 82, 83]}],
+ },
+ ],
+ },
+ {
+ url: "https://example.net/browser?param=1#ref",
+ filters: [
+ // host: Equals, Contains, Prefix, Suffix
+ {
+ okFilter: [{hostEquals: "example.net"}],
+ failFilter: [{hostEquals: "example.com"}],
+ },
+ {
+ okFilter: [{hostContains: ".example"}],
+ failFilter: [{hostContains: ".www"}],
+ },
+ {
+ okFilter: [{hostPrefix: "example"}],
+ failFilter: [{hostPrefix: "www"}],
+ },
+ {
+ okFilter: [{hostSuffix: "net"}],
+ failFilter: [{hostSuffix: "com"}],
+ },
+ // path: Equals, Contains, Prefix, Suffix
+ {
+ okFilter: [{pathEquals: "/browser"}],
+ failFilter: [{pathEquals: "/"}],
+ },
+ {
+ okFilter: [{pathContains: "brow"}],
+ failFilter: [{pathContains: "tool"}],
+ },
+ {
+ okFilter: [{pathPrefix: "/bro"}],
+ failFilter: [{pathPrefix: "/tool"}],
+ },
+ {
+ okFilter: [{pathSuffix: "wser"}],
+ failFilter: [{pathSuffix: "kit"}],
+ },
+ // query: Equals, Contains, Prefix, Suffix
+ {
+ okFilter: [{queryEquals: "param=1"}],
+ failFilter: [{queryEquals: "wrongparam=2"}],
+ },
+ {
+ okFilter: [{queryContains: "param"}],
+ failFilter: [{queryContains: "wrongparam"}],
+ },
+ {
+ okFilter: [{queryPrefix: "param="}],
+ failFilter: [{queryPrefix: "wrong"}],
+ },
+ {
+ okFilter: [{querySuffix: "=1"}],
+ failFilter: [{querySuffix: "=2"}],
+ },
+ // urlMatches, originAndPathMatches
+ {
+ okFilter: [{urlMatches: "example.net/.*\?param=1"}],
+ failFilter: [{urlMatches: "example.net/.*\?wrongparam=2"}],
+ },
+ {
+ okFilter: [{originAndPathMatches: "example.net\/browser"}],
+ failFilter: [{originAndPathMatches: "example.net/.*\?param=1"}],
+ },
+ ],
+ },
+ ];
+
+ info("WebNavigation event filters test scenarios starting...");
+
+ const EVENTS = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ ];
+
+ for (let data of testFilterScenarios) {
+ info(`Prepare the new test scenario: ${JSON.stringify(data)}`);
+
+ win.location = "about:blank";
+
+ // Wait for the about:blank load to finish before continuing, in case this
+ // load is causing a process switch back into our process.
+ await SimpleTest.promiseWaitForCondition(() => {
+ try {
+ return win.location.href == "about:blank" &&
+ win.document.readyState == "complete";
+ } catch (e) {
+ return false;
+ }
+ });
+
+ extension.sendMessage("test-filters", EVENTS, data);
+ await extension.awaitMessage("test-filter-ready");
+
+ info(`Loading the test url: ${data.url}`);
+ win.location = data.url;
+
+ await extension.awaitMessage("test-filter-next");
+
+ info("Test scenario completed. Moving to the next test scenario.");
+ }
+
+ info("WebNavigation event filters test onReferenceFragmentUpdated scenario starting...");
+
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+ let url = BASE + "/file_WebNavigation_page3.html";
+
+ let okFilter = [{urlContains: "_page3.html"}];
+ let failFilter = [{ports: [444]}];
+ let data = {filters: [{okFilter, failFilter}]};
+ let event = "onCompleted";
+
+ info(`Loading the initial test url: ${url}`);
+ extension.sendMessage("test-filters", [event], data);
+
+ await extension.awaitMessage("test-filter-ready");
+ win.location = url;
+ await extension.awaitMessage("test-filter-next");
+
+ event = "onReferenceFragmentUpdated";
+ extension.sendMessage("test-filters", [event], data);
+
+ await extension.awaitMessage("test-filter-ready");
+ win.location = url + "#ref1";
+ await extension.awaitMessage("test-filter-next");
+
+ info("WebNavigation event filters test onHistoryStateUpdated scenario starting...");
+
+ event = "onHistoryStateUpdated";
+ extension.sendMessage("test-filters", [event], data);
+ await extension.awaitMessage("test-filter-ready");
+
+ win.history.pushState({}, "", BASE + "/pushState_page3.html");
+ await extension.awaitMessage("test-filter-next");
+
+ // TODO: add additional specific tests for the other webNavigation events:
+ // onErrorOccurred (and onCreatedNavigationTarget on supported)
+
+ info("WebNavigation event filters test scenarios completed.");
+
+ await extension.unload();
+
+ win.close();
+});
+
+add_task(async function test_webnav_empty_filter_validation_error() {
+ function background() {
+ let catchedException;
+
+ try {
+ browser.webNavigation.onCompleted.addListener(
+ // Empty callback (not really used)
+ () => {},
+ // Empty filter (which should raise a validation error exception).
+ {url: []}
+ );
+ } catch (e) {
+ catchedException = e;
+ browser.test.log(`Got an exception`);
+ }
+
+ if (catchedException &&
+ catchedException.message.includes("Type error for parameter filters") &&
+ catchedException.message.includes("Array requires at least 1 items; you have 0")) {
+ browser.test.notifyPass("webNav.emptyFilterValidationError");
+ } else {
+ browser.test.notifyFail("webNav.emptyFilterValidationError");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("webNav.emptyFilterValidationError");
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function webnav_test_incognito() {
+ // Monitor will fail if it gets any event.
+ let monitor = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["webNavigation", "*://mochi.test/*"],
+ },
+ background() {
+ const EVENTS = [
+ "onTabReplaced",
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ "onErrorOccurred",
+ "onReferenceFragmentUpdated",
+ "onHistoryStateUpdated",
+ ];
+
+ function onEvent(event, details) {
+ browser.test.fail(`not_allowed - Got ${event} ${details.url} ${details.frameId} ${details.parentFrameId}`);
+ }
+
+ let listeners = {};
+ for (let event of EVENTS) {
+ listeners[event] = onEvent.bind(null, event);
+ browser.webNavigation[event].addListener(listeners[event]);
+ }
+
+ browser.test.onMessage.addListener(async (message, tabId) => {
+ // try to access the private window
+ await browser.test.assertRejects(browser.webNavigation.getAllFrames({tabId}),
+ /Invalid tab ID/,
+ "should not be able to get incognito frames");
+ await browser.test.assertRejects(browser.webNavigation.getFrame({tabId, frameId: 0}),
+ /Invalid tab ID/,
+ "should not be able to get incognito frames");
+ browser.test.notifyPass("completed");
+ });
+ },
+ });
+
+ // extension loads a private window and waits for the onCompleted event.
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs", "webNavigation", "*://mochi.test/*"],
+ },
+ async background() {
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+ const url = BASE + "/file_WebNavigation_page1.html";
+ let window;
+
+ browser.webNavigation.onCompleted.addListener(async (details) => {
+ if (details.url !== url) {
+ return;
+ }
+ browser.test.log(`spanning - Got onCompleted ${details.url} ${details.frameId} ${details.parentFrameId}`);
+ browser.test.sendMessage("completed");
+ });
+ browser.test.onMessage.addListener(async () => {
+ await browser.windows.remove(window.id);
+ browser.test.notifyPass("done");
+ });
+ window = await browser.windows.create({url, incognito: true});
+ let tabs = await browser.tabs.query({active: true, windowId: window.id});
+ browser.test.sendMessage("tabId", tabs[0].id);
+ },
+ });
+
+ await monitor.startup();
+ await extension.startup();
+
+ await extension.awaitMessage("completed");
+ let tabId = await extension.awaitMessage("tabId");
+
+ await monitor.sendMessage("tab", tabId);
+ await monitor.awaitFinish("completed");
+
+ await extension.sendMessage("close");
+ await extension.awaitFinish("done");
+
+ await extension.unload();
+ await monitor.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<script>
+"use strict";
+
+// Check that the windowId and tabId filter work as expected in the webRequest
+// and proxy API:
+// - A non-matching windowId / tabId listener won't trigger events.
+// - A matching tabId from a tab triggers the event.
+// - A matching windowId from a tab triggers the event.
+// (unlike test_ext_webrequest_filter.html, this also works on Android)
+// - Requests from background pages can be matched with windowId and tabId -1.
+add_task(async function test_filter_tabId_and_windowId() {
+ async function tabScript() {
+ let pendingExpectations = new Set();
+ // Helper to detect completion of expected requests.
+ function watchExpected(filter, desc) {
+ desc += ` - ${JSON.stringify(filter)}`;
+ const DESC_PROXY = `${desc} (proxy)`;
+ const DESC_WEBREQUEST = `${desc} (webRequest)`;
+ pendingExpectations.add(DESC_PROXY);
+ pendingExpectations.add(DESC_WEBREQUEST);
+ browser.proxy.onRequest.addListener(() => {
+ pendingExpectations.delete(DESC_PROXY);
+ }, filter);
+ browser.webRequest.onBeforeRequest.addListener(
+ () => {
+ pendingExpectations.delete(DESC_WEBREQUEST);
+ },
+ filter,
+ ["blocking"]
+ );
+ }
+
+ // Helper to detect unexpected requests.
+ function watchUnexpected(filter, desc) {
+ desc += ` - ${JSON.stringify(filter)}`;
+ browser.proxy.onRequest.addListener(() => {
+ browser.test.fail(`${desc} - unexpected proxy event`);
+ }, filter);
+ browser.webRequest.onBeforeRequest.addListener(() => {
+ browser.test.fail(`${desc} - unexpected webRequest event`);
+ }, filter);
+ }
+
+ function registerExpectations(url, windowId, tabId) {
+ const urls = [url];
+ watchUnexpected({ urls, windowId: 0 }, "non-matching windowId");
+ watchUnexpected({ urls, tabId: 0 }, "non-matching tabId");
+
+ watchExpected({ urls, windowId }, "windowId matches");
+ watchExpected({ urls, tabId }, "tabId matches");
+ }
+
+ try {
+ let { windowId, tabId } = await browser.runtime.sendMessage("getIds");
+ browser.test.log(`Dummy tab has: tabId=${tabId} windowId=${windowId}`);
+ registerExpectations("http://example.com/?tab", windowId, tabId);
+ registerExpectations("http://example.com/?bg", -1, -1);
+
+ // Call an API method implemented in the parent process to ensure that
+ // the listeners have been registered (workaround for bug 1300234).
+ // There is a .catch() at the end because the call is rejected on Android.
+ await browser.proxy.settings.get({}).catch(() => {});
+
+ browser.test.log("Triggering request from background page.");
+ await browser.runtime.sendMessage("triggerBackgroundRequest");
+
+ browser.test.log("Triggering request from tab.");
+ await fetch("http://example.com/?tab");
+
+ browser.test.assertEq(0, pendingExpectations.size, "got all events");
+ for (let description of pendingExpectations) {
+ browser.test.fail(`Event not observed: ${description}`);
+ }
+ } catch (e) {
+ browser.test.fail(`Unexpected test failure: ${e} :: ${e.stack}`);
+ }
+ browser.runtime.sendMessage("testCompleted");
+ }
+
+ function background() {
+ browser.runtime.onMessage.addListener(async (msg, sender) => {
+ if (msg === "getIds") {
+ return { windowId: sender.tab.windowId, tabId: sender.tab.id };
+ }
+ if (msg === "triggerBackgroundRequest") {
+ await fetch("http://example.com/?bg");
+ }
+ if (msg === "testCompleted") {
+ await browser.tabs.remove(sender.tab.id);
+ browser.test.sendMessage("testCompleted");
+ }
+ });
+ browser.tabs.create({
+ url: browser.runtime.getURL("tab.html"),
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "proxy",
+ "webRequest",
+ "webRequestBlocking",
+ "http://example.com/*",
+ ],
+ },
+ background,
+ files: {
+ "tab.html": `<!DOCTYPE html><script src="tab.js"><\/script>`,
+ "tab.js": tabScript,
+ },
+ });
+ await extension.startup();
+
+ await extension.awaitMessage("testCompleted");
+ await extension.unload();
+});
+</script>
+</head>
+<body>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head_webrequest.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+// This file defines content scripts.
+
+let baseUrl = "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/authenticate.sjs";
+function testXHR(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.onload = resolve;
+ xhr.onabort = reject;
+ xhr.onerror = reject;
+ xhr.send();
+ });
+}
+
+function getAuthHandler(result, blocking = true) {
+ function background(result) {
+ browser.webRequest.onAuthRequired.addListener((details) => {
+ browser.test.succeed(`authHandler.onAuthRequired called with ${details.requestId} ${details.url} result ${JSON.stringify(result)}`);
+ browser.test.sendMessage("onAuthRequired");
+ return result;
+ }, {urls: ["*://mochi.test/*"]}, ["blocking"]);
+ browser.webRequest.onCompleted.addListener((details) => {
+ browser.test.succeed(`authHandler.onCompleted called with ${details.requestId} ${details.url}`);
+ browser.test.sendMessage("onCompleted");
+ }, {urls: ["*://mochi.test/*"]});
+ browser.webRequest.onErrorOccurred.addListener((details) => {
+ browser.test.succeed(`authHandler.onErrorOccurred called with ${details.requestId} ${details.url}`);
+ browser.test.sendMessage("onErrorOccurred");
+ }, {urls: ["*://mochi.test/*"]});
+ }
+
+ let permissions = [
+ "webRequest",
+ "*://mochi.test/*",
+ ];
+ if (blocking) {
+ permissions.push("webRequestBlocking");
+ }
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions,
+ },
+ background: `(${background})(${JSON.stringify(result)})`,
+ });
+}
+
+add_task(async function test_webRequest_auth_nonblocking_forwardAuthProvider() {
+ // The chrome script sets up a default auth handler on the channel, the
+ // extension does not return anything in the authRequred call. We should
+ // get the call in the extension first, then in the chrome code where we
+ // cancel the request to avoid dealing with the prompt dialog here. The test
+ // is to ensure that WebRequest calls the previous notificationCallbacks
+ // if the authorization is not handled by the onAuthRequired handler.
+
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ let observer = channel => {
+ if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test" &&
+ channel.URI.spec.includes("authenticate.sjs"))) {
+ return;
+ }
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ channel.notificationCallbacks = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor",
+ "nsIAuthPromptProvider",
+ "nsIAuthPrompt2"]),
+ getInterface: ChromeUtils.generateQI(["nsIAuthPromptProvider",
+ "nsIAuthPrompt2"]),
+ promptAuth(channel, level, authInfo) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+ getAuthPrompt(reason, iid) {
+ return this;
+ },
+ asyncPromptAuth(channel, callback, context, level, authInfo) {
+ // We just cancel here, we're only ensuring that non-webrequest
+ // notificationcallbacks get called if webrequest doesn't handle it.
+ Promise.resolve().then(() => {
+ callback.onAuthCancelled(context, false);
+ channel.cancel(Cr.NS_BINDING_ABORTED);
+ sendAsyncMessage("callback-complete");
+ });
+ },
+ };
+ };
+ Services.obs.addObserver(observer, "http-on-modify-request");
+ sendAsyncMessage("chrome-ready");
+ });
+ await chromeScript.promiseOneMessage("chrome-ready");
+ let callbackComplete = chromeScript.promiseOneMessage("callback-complete");
+
+ let handlingExt = getAuthHandler();
+ await handlingExt.startup();
+
+ await Assert.rejects(testXHR(`${baseUrl}?realm=auth_nonblocking_forwardAuth&user=auth_nonblocking_forwardAuth&pass=auth_nonblocking_forwardAuth`),
+ ProgressEvent,
+ "caught rejected xhr");
+
+ await callbackComplete;
+ await handlingExt.awaitMessage("onAuthRequired");
+ // We expect onErrorOccurred because the "default" authprompt above cancelled
+ // the auth request to avoid a dialog.
+ await handlingExt.awaitMessage("onErrorOccurred");
+ await handlingExt.unload();
+ chromeScript.destroy();
+});
+
+add_task(async function test_webRequest_auth_nonblocking_forwardAuthPrompt2() {
+ // The chrome script sets up a default auth handler on the channel, the
+ // extension does not return anything in the authRequred call. We should
+ // get the call in the extension first, then in the chrome code where we
+ // cancel the request to avoid dealing with the prompt dialog here. The test
+ // is to ensure that WebRequest calls the previous notificationCallbacks
+ // if the authorization is not handled by the onAuthRequired handler.
+
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ let observer = channel => {
+ if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test" &&
+ channel.URI.spec.includes("authenticate.sjs"))) {
+ return;
+ }
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ channel.notificationCallbacks = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor",
+ "nsIAuthPrompt2"]),
+ getInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+ promptAuth(request, level, authInfo) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+ asyncPromptAuth(request, callback, context, level, authInfo) {
+ // We just cancel here, we're only ensuring that non-webrequest
+ // notificationcallbacks get called if webrequest doesn't handle it.
+ Promise.resolve().then(() => {
+ request.cancel(Cr.NS_BINDING_ABORTED);
+ sendAsyncMessage("callback-complete");
+ });
+ },
+ };
+ };
+ Services.obs.addObserver(observer, "http-on-modify-request");
+ sendAsyncMessage("chrome-ready");
+ });
+ await chromeScript.promiseOneMessage("chrome-ready");
+ let callbackComplete = chromeScript.promiseOneMessage("callback-complete");
+
+ let handlingExt = getAuthHandler();
+ await handlingExt.startup();
+
+ await Assert.rejects(testXHR(`${baseUrl}?realm=auth_nonblocking_forwardAuthPromptProvider&user=auth_nonblocking_forwardAuth&pass=auth_nonblocking_forwardAuth`),
+ ProgressEvent,
+ "caught rejected xhr");
+
+ await callbackComplete;
+ await handlingExt.awaitMessage("onAuthRequired");
+ // We expect onErrorOccurred because the "default" authprompt above cancelled
+ // the auth request to avoid a dialog.
+ await handlingExt.awaitMessage("onErrorOccurred");
+ await handlingExt.unload();
+ chromeScript.destroy();
+});
+</script>
+</head>
+<body>
+<div id="test">Authorization Test</div>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_webRequest_serviceworker_events() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.serviceWorkers.testing.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "<all_urls>",
+ ],
+ },
+ background() {
+ let eventNames = new Set([
+ "onBeforeRequest",
+ "onBeforeSendHeaders",
+ "onSendHeaders",
+ "onHeadersReceived",
+ "onResponseStarted",
+ "onCompleted",
+ "onErrorOccurred",
+ ]);
+
+ function listener(name, details) {
+ browser.test.assertTrue(eventNames.has(name), `received ${name}`);
+ eventNames.delete(name);
+ if (name == "onCompleted") {
+ eventNames.delete("onErrorOccurred");
+ } else if (name == "onErrorOccurred") {
+ eventNames.delete("onCompleted");
+ }
+ if (eventNames.size == 0) {
+ browser.test.sendMessage("done");
+ }
+ }
+
+ for (let name of eventNames) {
+ browser.webRequest[name].addListener(
+ listener.bind(null, name),
+ {urls: ["https://example.com/*"]}
+ );
+ }
+ },
+ });
+
+ await extension.startup();
+ let registration = await navigator.serviceWorker.register("webrequest_worker.js", {scope: "."});
+ await waitForState(registration.installing, "activated");
+ await extension.awaitMessage("done");
+ await registration.unregister();
+ await extension.unload();
+});
+
+add_task(async function test_webRequest_background_events() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "<all_urls>",
+ ],
+ },
+ background() {
+ let eventNames = new Set([
+ "onBeforeRequest",
+ "onBeforeSendHeaders",
+ "onSendHeaders",
+ "onHeadersReceived",
+ "onResponseStarted",
+ "onCompleted",
+ ]);
+
+ function listener(name, details) {
+ browser.test.assertTrue(eventNames.has(name), `received ${name}`);
+ eventNames.delete(name);
+
+ if (eventNames.size === 0) {
+ browser.test.assertEq("xmlhttprequest", details.type, "correct type for fetch [see bug 1366710]");
+ browser.test.assertEq(0, eventNames.size, "messages received");
+ browser.test.sendMessage("done");
+ }
+ }
+
+ for (let name of eventNames) {
+ browser.webRequest[name].addListener(
+ listener.bind(null, name),
+ {urls: ["https://example.com/*"]}
+ );
+ }
+
+ fetch("https://example.com/example.txt").then(() => {
+ browser.test.succeed("Fetch succeeded.");
+ }, () => {
+ browser.test.fail("fetch received");
+ browser.test.sendMessage("done");
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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..9d57d55681
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
@@ -0,0 +1,445 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head_webrequest.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+const expectedBaseProps = {
+ // On Desktop builds, if "browse.chrome.guess_favicon" is set to true,
+ // a favicon requests may be triggered at a random time while the test
+ // cases are running, we include it the ignore list by default to prevent
+ // intermittent failures (e.g. see Bug 1733781 and Bug 1633189).
+ ignore: ["favicon.ico"],
+};
+
+function promiseWindowEvent(name, accept) {
+ return new Promise(resolve => {
+ window.addEventListener(name, function listener(event) {
+ if (event.data !== accept) {
+ return;
+ }
+ window.removeEventListener(name, listener);
+ resolve(event);
+ });
+ });
+}
+
+if (AppConstants.platform === "android") {
+ SimpleTest.requestLongerTimeout(3);
+}
+
+let extension;
+add_task(async function setup() {
+ // Clear the image cache, since it gets in the way otherwise.
+ let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
+ let cache = imgTools.getImgCacheForDocument(document);
+ cache.clearCache(false);
+ await SpecialPowers.spawnChrome([], async () => {
+ Services.cache2.clear();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.rcwn.enabled", false]],
+ });
+
+ extension = makeExtension();
+ await extension.startup();
+});
+
+// expect is a set of test values used by the background script.
+//
+// type: type of request action
+// events: optional, If defined only the events listed are expected for the
+// request. If undefined, all events except onErrorOccurred
+// and onBeforeRedirect are expected. Must be in order received.
+// redirect: url to redirect to during onBeforeSendHeaders
+// status: number expected status during onHeadersReceived, 200 default
+// cancel: event in which we return cancel=true. cancelled message is sent.
+// cached: expected fromCache value, default is false, checked in onCompletion
+// headers: request or response headers to modify
+// origin: The expected originUrl, a default origin can be passed for all files
+
+add_task(async function test_webRequest_links() {
+ let expect = {
+ "file_style_bad.css": {
+ type: "stylesheet",
+ events: ["onBeforeRequest", "onErrorOccurred"],
+ cancel: "onBeforeRequest",
+ },
+ "file_style_redirect.css": {
+ type: "stylesheet",
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+ optional_events: ["onHeadersReceived"],
+ redirect: "file_style_good.css",
+ },
+ "file_style_good.css": {
+ type: "stylesheet",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+ addStylesheet("file_style_bad.css");
+ await extension.awaitMessage("cancelled");
+ // we redirect to style_good which completes the test
+ addStylesheet(`file_style_redirect.css?nocache=${Math.random()}`);
+ await extension.awaitMessage("done");
+});
+
+add_task(async function test_webRequest_images() {
+ let expect = {
+ "file_image_bad.png": {
+ type: "image",
+ events: ["onBeforeRequest", "onErrorOccurred"],
+ cancel: "onBeforeRequest",
+ },
+ "file_image_redirect.png": {
+ type: "image",
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+ optional_events: ["onHeadersReceived"],
+ redirect: "file_image_good.png",
+ },
+ "file_image_good.png": {
+ type: "image",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+ addImage("file_image_bad.png");
+ await extension.awaitMessage("cancelled");
+ // we redirect to image_good which completes the test
+ addImage("file_image_redirect.png");
+ await extension.awaitMessage("done");
+});
+
+add_task(async function test_webRequest_scripts() {
+ let expect = {
+ "file_script_bad.js": {
+ type: "script",
+ events: ["onBeforeRequest", "onErrorOccurred"],
+ cancel: "onBeforeRequest",
+ },
+ "file_script_redirect.js": {
+ type: "script",
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+ optional_events: ["onHeadersReceived"],
+ redirect: "file_script_good.js",
+ },
+ "file_script_good.js": {
+ type: "script",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+ let message = promiseWindowEvent("message", "test1");
+ addScript("file_script_bad.js");
+ await extension.awaitMessage("cancelled");
+ // we redirect to script_good which completes the test
+ addScript("file_script_redirect.js?q=test1");
+ await extension.awaitMessage("done");
+
+ is((await message).data, "test1", "good script ran");
+});
+
+add_task(async function test_webRequest_xhr_get() {
+ let expect = {
+ "file_script_xhr.js": {
+ type: "script",
+ },
+ "xhr_resource": {
+ status: 404,
+ type: "xmlhttprequest",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+ addScript("file_script_xhr.js");
+ await extension.awaitMessage("done");
+});
+
+add_task(async function test_webRequest_nonexistent() {
+ let expect = {
+ "nonexistent_script_url.js": {
+ status: 404,
+ type: "script",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+ addScript("nonexistent_script_url.js");
+ await extension.awaitMessage("done");
+});
+
+add_task(async function test_webRequest_checkCached() {
+ let expect = {
+ "file_image_good.png": {
+ type: "image",
+ cached: true,
+ },
+ "file_script_good.js": {
+ type: "script",
+ cached: true,
+ },
+ "file_style_good.css": {
+ type: "stylesheet",
+ cached: false,
+ },
+ "nonexistent_script_url.js": {
+ status: 404,
+ type: "script",
+ cached: false,
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+ let message = promiseWindowEvent("message", "test1");
+
+ addImage("file_image_good.png");
+ addScript("file_script_good.js?q=test1");
+
+ is((await message).data, "test1", "good script ran");
+
+ addStylesheet(`file_style_good.css?nocache=${Math.random()}`);
+ addScript("nonexistent_script_url.js");
+ await extension.awaitMessage("done");
+});
+
+add_task(async function test_webRequest_headers() {
+ let expect = {
+ "file_script_nonexistent.js": {
+ type: "script",
+ status: 404,
+ headers: {
+ request: {
+ add: {
+ "X-WebRequest-request": "text",
+ "X-WebRequest-request-binary": "binary",
+ },
+ modify: {
+ "user-agent": "WebRequest",
+ },
+ remove: [
+ "referer",
+ ],
+ },
+ response: {
+ add: {
+ "X-WebRequest-response": "text",
+ "X-WebRequest-response-binary": "binary",
+ },
+ modify: {
+ "server": "WebRequest",
+ "content-type": "text/html; charset=utf-8",
+ },
+ remove: [
+ "connection",
+ ],
+ },
+ },
+ completion: "onCompleted",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+ addScript("file_script_nonexistent.js");
+ await extension.awaitMessage("done");
+});
+
+add_task(async function test_webRequest_tabId() {
+ function background() {
+ let tab;
+ browser.tabs.onCreated.addListener(newTab => {
+ tab = newTab;
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "close-tab") {
+ browser.tabs.remove(tab.id);
+ browser.test.sendMessage("tab-closed");
+ }
+ });
+ }
+
+ let tabExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "tabs",
+ ],
+ },
+ background,
+ });
+ await tabExt.startup();
+
+ let linkUrl = `file_WebRequest_page3.html?trigger=a&nocache=${Math.random()}`;
+ let expect = {
+ "file_WebRequest_page3.html": {
+ type: "main_frame",
+ },
+ };
+
+ extension.sendMessage("set-expected", {
+ ...expectedBaseProps,
+ expect,
+ origin: location.href,
+ });
+ await extension.awaitMessage("continue");
+ let a = addLink(linkUrl);
+ a.click();
+ await extension.awaitMessage("done");
+
+ let closed = tabExt.awaitMessage("tab-closed");
+ tabExt.sendMessage("close-tab");
+ await closed;
+
+ await tabExt.unload();
+});
+
+add_task(async function test_webRequest_tabId_browser() {
+ async function background(url) {
+ let tabId;
+ browser.test.onMessage.addListener(async (msg, expected) => {
+ if (msg == "create") {
+ let tab = await browser.tabs.create({url});
+ tabId = tab.id;
+ return;
+ }
+ if (msg == "done") {
+ await browser.tabs.remove(tabId);
+ browser.test.sendMessage("done");
+ }
+ });
+ browser.test.sendMessage("origin", browser.runtime.getURL("/"));
+ }
+
+ let pageUrl = `${SimpleTest.getTestFileURL("file_sample.html")}?nocache=${Math.random()}`;
+ let tabExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "tabs",
+ ],
+ },
+ background: `(${background})('${pageUrl}')`,
+ });
+
+ let expect = {
+ "file_sample.html": {
+ type: "main_frame",
+ },
+ };
+
+ await tabExt.startup();
+ let origin = await tabExt.awaitMessage("origin");
+
+ // expecting origin == extension baseUrl
+ extension.sendMessage("set-expected", {
+ ...expectedBaseProps,
+ expect,
+ origin,
+ });
+ await extension.awaitMessage("continue");
+
+ // open a tab from an extension principal
+ tabExt.sendMessage("create");
+ await extension.awaitMessage("done");
+ tabExt.sendMessage("done");
+ await tabExt.awaitMessage("done");
+ await tabExt.unload();
+});
+
+add_task(async function test_webRequest_frames() {
+ let expect = {
+ "redirection.sjs": {
+ status: 302,
+ type: "sub_frame",
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onBeforeRedirect"],
+ },
+ "dummy_page.html": {
+ type: "sub_frame",
+ status: 404,
+ },
+ "badrobot": {
+ type: "sub_frame",
+ status: 404,
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onErrorOccurred"],
+ // When an url's hostname fails to be resolved, an NS_ERROR_NET_ON_RESOLVED/RESOLVING
+ // onError event may be fired right before the NS_ERROR_UNKNOWN_HOST
+ // (See Bug 1516862 for a rationale).
+ optional_events: ["onErrorOccurred"],
+ error: ["NS_ERROR_UNKNOWN_HOST", "NS_ERROR_NET_ON_RESOLVED", "NS_ERROR_NET_ON_RESOLVING"],
+ },
+ };
+ extension.sendMessage("set-expected", {
+ ...expectedBaseProps,
+ expect,
+ origin: location.href
+ });
+ await extension.awaitMessage("continue");
+ addFrame("redirection.sjs");
+ addFrame("https://nonresolvablehostname.invalid/badrobot");
+ await extension.awaitMessage("done");
+});
+
+add_task(async function teardown() {
+ await extension.unload();
+});
+
+add_task(async function test_case_preserving() {
+ const manifest = {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "http://mochi.test/",
+ ],
+ };
+
+ async function background() {
+ // This is testing if header names preserve case,
+ // so the case-sensitive comparison is on purpose.
+ function ua({url, requestHeaders}) {
+ if (url.endsWith("?blind-add")) {
+ requestHeaders.push({name: "user-agent", value: "Blind/Add"});
+ return {requestHeaders};
+ }
+ for (const header of requestHeaders) {
+ if (header.name === "User-Agent") {
+ header.value = "Case/Sensitive";
+ }
+ }
+ return {requestHeaders};
+ }
+
+ await browser.webRequest.onBeforeSendHeaders.addListener(ua, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]);
+ browser.test.sendMessage("ready");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({manifest, background});
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ const response1 = await fetch(SimpleTest.getTestFileURL("return_headers.sjs"));
+ const headers1 = JSON.parse(await response1.text());
+
+ is(headers1["user-agent"], "Case/Sensitive", "User-Agent header matched and changed.");
+
+ const response2 = await fetch(SimpleTest.getTestFileURL("return_headers.sjs?blind-add"));
+ const headers2 = JSON.parse(await response2.text());
+
+ is(headers2["user-agent"], "Blind/Add", "User-Agent header blindly added.");
+
+ await extension.unload();
+});
+
+</script>
+</head>
+<body>
+<div id="test">Sample text</div>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebRequest errors</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+async function test_connection_refused(url, expectedError) {
+ async function background(url, expectedError) {
+ browser.test.log(`background url is ${url}`);
+ browser.webRequest.onErrorOccurred.addListener(details => {
+ if (details.url != url) {
+ return;
+ }
+ browser.test.assertTrue(details.error.startsWith(expectedError), "error correct");
+ browser.test.sendMessage("onErrorOccurred");
+ }, {urls: ["<all_urls>"]});
+
+ let tabId;
+ browser.test.onMessage.addListener(async (msg, expected) => {
+ await browser.tabs.remove(tabId);
+ browser.test.sendMessage("done");
+ });
+
+ let tab = await browser.tabs.create({url});
+ tabId = tab.id;
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["webRequest", "tabs", "*://badchain.include-subdomains.pinning.example.com/*"],
+ },
+ background: `(${background})("${url}", "${expectedError}")`,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ await extension.awaitMessage("onErrorOccurred");
+ extension.sendMessage("close-tab");
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+}
+
+add_task(function test_bad_cert() {
+ return test_connection_refused("https://badchain.include-subdomains.pinning.example.com/", "Unable to communicate securely with peer");
+});
+
+</script>
+
+</body>
+</html>
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..5ccbf761ec
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
@@ -0,0 +1,226 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_webrequest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+if (AppConstants.platform === "android") {
+ SimpleTest.requestLongerTimeout(6);
+}
+
+let windowData, testWindow;
+
+add_task(async function setup() {
+ await SpecialPowers.spawnChrome([], async () => {
+ Services.cache2.clear();
+ });
+
+ testWindow = window.open("about:blank", "_blank", "width=100,height=100");
+ await waitForLoad(testWindow);
+
+ // Fetch the windowId and tabId we need to filter with WebRequest.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "tabs",
+ ],
+ },
+ background() {
+ browser.tabs.query({currentWindow: true}).then(tabs => {
+ let tab = tabs.find(tab => tab.active);
+ let {windowId} = tab;
+
+ browser.test.log(`current window ${windowId} tabs: ${JSON.stringify(tabs.map(tab => [tab.id, tab.url]))}`);
+ browser.test.sendMessage("windowData", {windowId, tabId: tab.id});
+ });
+ },
+ });
+ await extension.startup();
+ windowData = await extension.awaitMessage("windowData");
+ info(`window is ${JSON.stringify(windowData)}`);
+ await extension.unload();
+});
+
+add_task(async function test_webRequest_filter_window() {
+ if (AppConstants.MOZ_BUILD_APP !== "browser") {
+ // Android does not support multiple windows.
+ return;
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.serviceWorkers.testing.enabled", true],
+ ["network.http.rcwn.enabled", false]],
+ });
+
+ let events = {
+ "onBeforeRequest": [{urls: ["<all_urls>"], windowId: windowData.windowId}],
+ "onBeforeSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]],
+ "onSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]],
+ "onBeforeRedirect": [{urls: ["<all_urls>"], windowId: windowData.windowId}],
+ "onHeadersReceived": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["responseHeaders"]],
+ "onResponseStarted": [{urls: ["<all_urls>"], windowId: windowData.windowId}],
+ "onCompleted": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["responseHeaders"]],
+ "onErrorOccurred": [{urls: ["<all_urls>"], windowId: windowData.windowId}],
+ };
+ let expect = {
+ "file_image_bad.png": {
+ optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"],
+ type: "main_frame",
+ },
+ };
+
+ if (AppConstants.platform != "android") {
+ expect["favicon.ico"] = {
+ // These events only happen in non-e10s. See bug 1472156.
+ optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"],
+ type: "image",
+ origin: SimpleTest.getTestFileURL("file_image_bad.png"),
+ };
+ }
+
+ let extension = makeExtension(events);
+ await extension.startup();
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+
+ // We should not get events for a new window load.
+ let newWindow = window.open("file_image_good.png", "_blank", "width=100,height=100");
+ await waitForLoad(newWindow);
+ newWindow.close();
+
+ // We should not get background events.
+ let registration = await navigator.serviceWorker.register("webrequest_worker.js?test0", {scope: "."});
+ await waitForState(registration.installing, "activated");
+
+ // We should get events for the reload.
+ testWindow.location = "file_image_bad.png";
+ await extension.awaitMessage("done");
+
+ testWindow.location = "about:blank";
+ await registration.unregister();
+ await extension.unload();
+});
+
+add_task(async function test_webRequest_filter_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.serviceWorkers.testing.enabled", true]],
+ });
+
+ let img = `file_image_good.png?r=${Math.random()}`;
+
+ let events = {
+ "onBeforeRequest": [{urls: ["<all_urls>"], tabId: windowData.tabId}],
+ "onBeforeSendHeaders": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["requestHeaders"]],
+ "onSendHeaders": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["requestHeaders"]],
+ "onBeforeRedirect": [{urls: ["<all_urls>"], tabId: windowData.tabId}],
+ "onHeadersReceived": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["responseHeaders"]],
+ "onResponseStarted": [{urls: ["<all_urls>"], tabId: windowData.tabId}],
+ "onCompleted": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["responseHeaders"]],
+ "onErrorOccurred": [{urls: ["<all_urls>"], tabId: windowData.tabId}],
+ };
+ let expect = {
+ "file_image_good.png": {
+ // These events only happen in non-e10s. See bug 1472156.
+ optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"],
+ type: "main_frame",
+ // cached: AppConstants.MOZ_BUILD_APP === "browser",
+ },
+ };
+
+ if (AppConstants.platform != "android") {
+ // A favicon request may be initiated, and complete or be aborted.
+ expect["favicon.ico"] = {
+ optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onResponseStarted", "onCompleted", "onErrorOccurred"],
+ type: "image",
+ origin: SimpleTest.getTestFileURL(img),
+ };
+ }
+
+ let extension = makeExtension(events);
+ await extension.startup();
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+
+ if (AppConstants.MOZ_BUILD_APP === "browser") {
+ // We should not get events for a new window load.
+ let newWindow = window.open(img, "_blank", "width=100,height=100");
+ await waitForLoad(newWindow);
+ newWindow.close();
+ }
+
+ // We should not get background events.
+ let registration = await navigator.serviceWorker.register("webrequest_worker.js?test1", {scope: "."});
+ await waitForState(registration.installing, "activated");
+
+ // We should get events for the reload.
+ testWindow.location = img;
+ await extension.awaitMessage("done");
+
+ testWindow.location = "about:blank";
+ await registration.unregister();
+ await extension.unload();
+});
+
+
+add_task(async function test_webRequest_filter_background() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.serviceWorkers.testing.enabled", true]],
+ });
+
+ let events = {
+ "onBeforeRequest": [{urls: ["<all_urls>"], tabId: -1}],
+ "onBeforeSendHeaders": [{urls: ["<all_urls>"], tabId: -1}, ["requestHeaders"]],
+ "onSendHeaders": [{urls: ["<all_urls>"], tabId: -1}, ["requestHeaders"]],
+ "onBeforeRedirect": [{urls: ["<all_urls>"], tabId: -1}],
+ "onHeadersReceived": [{urls: ["<all_urls>"], tabId: -1}, ["responseHeaders"]],
+ "onResponseStarted": [{urls: ["<all_urls>"], tabId: -1}],
+ "onCompleted": [{urls: ["<all_urls>"], tabId: -1}, ["responseHeaders"]],
+ "onErrorOccurred": [{urls: ["<all_urls>"], tabId: -1}],
+ };
+ let expect = {
+ "webrequest_worker.js": {
+ type: "script",
+ },
+ "example.txt": {
+ status: 404,
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onResponseStarted"],
+ optional_events: ["onCompleted", "onErrorOccurred"],
+ type: "xmlhttprequest",
+ origin: SimpleTest.getTestFileURL("webrequest_worker.js?test2"),
+ },
+ };
+
+ let extension = makeExtension(events);
+ await extension.startup();
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ await extension.awaitMessage("continue");
+
+ // We should not get events for a window.
+ testWindow.location = "file_image_bad.png";
+
+ // We should get events for the background page.
+ let registration = await navigator.serviceWorker.register(SimpleTest.getTestFileURL("webrequest_worker.js?test2"), {scope: "."});
+ await waitForState(registration.installing, "activated");
+ await extension.awaitMessage("done");
+ testWindow.location = "about:blank";
+ await registration.unregister();
+
+ await extension.unload();
+});
+
+add_task(async function teardown() {
+ testWindow.close();
+});
+</script>
+</head>
+<body>
+
+</body>
+</html>
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..76a13be1af
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html
@@ -0,0 +1,213 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head_webrequest.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+let extensionData = {
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>", "tabs"],
+ },
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(details => {
+ if (details.url.endsWith("/favicon.ico")) {
+ // We don't care about favicon.ico in this test. It is hard to control
+ // whether the request happens.
+ browser.test.log(`Ignoring favicon request: ${details.url}`);
+ return;
+ }
+ browser.test.sendMessage("onBeforeRequest", details);
+ }, {urls: ["<all_urls>"]}, ["blocking"]);
+
+ let tab;
+ browser.tabs.onCreated.addListener(newTab => {
+ browser.test.sendMessage("tab-created");
+ tab = newTab;
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "close-tab") {
+ browser.tabs.remove(tab.id);
+ browser.test.sendMessage("tab-closed");
+ }
+ });
+ },
+};
+
+let expected = {
+ "file_simple_xhr.html": {
+ type: "main_frame",
+ toplevel: true,
+ },
+ "file_image_good.png": {
+ type: "image",
+ toplevel: true,
+ origin: "file_simple_xhr.html",
+ },
+ "example.txt": {
+ type: "xmlhttprequest",
+ toplevel: true,
+ origin: "file_simple_xhr.html",
+ },
+ // sub frames will have the origin and first ancestor is the
+ // parent document
+ "file_simple_xhr_frame.html": {
+ type: "sub_frame",
+ toplevelParent: true,
+ origin: "file_simple_xhr.html",
+ parent: "file_simple_xhr.html",
+ },
+ // a resource in a sub frame will have origin of the subframe,
+ // but the ancestor chain starts with the parent document
+ "xhr_resource": {
+ type: "xmlhttprequest",
+ origin: "file_simple_xhr_frame.html",
+ parent: "file_simple_xhr.html",
+ },
+ "file_image_bad.png": {
+ type: "image",
+ depth: 2,
+ origin: "file_simple_xhr_frame.html",
+ parent: "file_simple_xhr.html",
+ },
+ "file_simple_xhr_frame2.html": {
+ type: "sub_frame",
+ depth: 2,
+ origin: "file_simple_xhr_frame.html",
+ parent: "file_simple_xhr_frame.html",
+ },
+ "file_image_redirect.png": {
+ type: "image",
+ depth: 2,
+ origin: "file_simple_xhr_frame2.html",
+ parent: "file_simple_xhr_frame.html",
+ },
+ "xhr_resource_2": {
+ type: "xmlhttprequest",
+ depth: 2,
+ origin: "file_simple_xhr_frame2.html",
+ parent: "file_simple_xhr_frame.html",
+ },
+ // This is loaded in a sandbox iframe. originUrl is not available for that,
+ // and requests within a sandboxed iframe will additionally have an empty
+ // url on their immediate parent/ancestor.
+ "file_simple_sandboxed_frame.html": {
+ type: "sub_frame",
+ depth: 3,
+ parent: "file_simple_xhr_frame2.html",
+ },
+ "xhr_sandboxed": {
+ type: "xmlhttprequest",
+ sandboxed: true,
+ depth: 3,
+ parent: "",
+ },
+ "file_image_great.png": {
+ type: "image",
+ sandboxed: true,
+ depth: 3,
+ parent: "",
+ },
+ "file_simple_sandboxed_subframe.html": {
+ type: "sub_frame",
+ depth: 4,
+ parent: "",
+ },
+};
+
+function checkDetails(details) {
+ let url = new URL(details.url);
+ let filename = url.pathname.split("/").pop();
+ ok(filename in expected, `Should be expecting a request for ${filename}`);
+ let expect = expected[filename];
+ is(expect.type, details.type, `${details.type} type matches`);
+ if (details.parentFrameId == -1) {
+ is(details.frameAncestors.length, 0, "no ancestors for main_frame requests");
+ } else if (details.parentFrameId == 0) {
+ is(details.frameAncestors.length, 1, "one ancestors for sub_frame requests");
+ } else {
+ ok(details.frameAncestors.length > 1, "have multiple ancestors for deep subframe requests");
+ is(details.frameAncestors.length, expect.depth, "have multiple ancestors for deep subframe requests");
+ }
+ if (details.parentFrameId > -1) {
+ ok(!expect.origin || details.originUrl.includes(expect.origin), "origin url is correct");
+ is(details.frameAncestors[0].frameId, details.parentFrameId, "first ancestor matches request.parentFrameId");
+ ok(details.frameAncestors[0].url.includes(expect.parent), "ancestor parent page correct");
+ is(details.frameAncestors[details.frameAncestors.length - 1].frameId, 0, "last ancestor is always zero");
+ // All our tests should be somewhere within the frame that we set topframe in the query string. That
+ // frame will always be the last ancestor.
+ ok(details.frameAncestors[details.frameAncestors.length - 1].url.includes("topframe=true"), "last ancestor is always topframe");
+ }
+ if (expect.toplevel) {
+ is(details.frameId, 0, "expect load at top level");
+ is(details.parentFrameId, -1, "expect top level frame to have no parent");
+ } else if (details.type == "sub_frame") {
+ ok(details.frameId > 0, "expect sub_frame to load into a new frame");
+ if (expect.toplevelParent) {
+ is(details.parentFrameId, 0, "expect sub_frame to have top level parent");
+ is(details.frameAncestors.length, 1, "one ancestor for top sub_frame request");
+ } else {
+ ok(details.parentFrameId > 0, "expect sub_frame to have parent");
+ ok(details.frameAncestors.length > 1, "sub_frame has ancestors");
+ }
+ expect.subframeId = details.frameId;
+ expect.parentId = details.parentFrameId;
+ } else if (expect.sandboxed) {
+ is(details.documentUrl, undefined, "null principal documentUrl for sandboxed request");
+ } else {
+ // get the parent frame.
+ let purl = new URL(details.documentUrl);
+ let pfilename = purl.pathname.split("/").pop();
+ let parent = expected[pfilename];
+ is(details.frameId, parent.subframeId, "expect load in subframe");
+ is(details.parentFrameId, parent.parentId, "expect subframe parent");
+ }
+ return filename;
+}
+
+add_task(async function test_webRequest_main_frame() {
+ // Clear the image cache, since it gets in the way otherwise.
+ let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
+ let cache = imgTools.getImgCacheForDocument(document);
+ cache.clearCache(false);
+ await SpecialPowers.spawnChrome([], async () => {
+ Services.cache2.clear();
+ });
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let a = addLink(`file_simple_xhr.html?topframe=true&nocache=${Math.random()}`);
+ a.click();
+
+ let remaining = new Set(Object.keys(expected));
+ let totalExpectedCount = remaining.size;
+ for (let i = 0; i < totalExpectedCount; i++) {
+ info(`Waiting for request ${i + 1} out of ${totalExpectedCount}`);
+ info(`Expecting one of: ${Array.from(remaining)}`);
+ let details = await extension.awaitMessage("onBeforeRequest");
+ info(`Checking details for request ${i}: ${JSON.stringify(details)}`);
+ let filename = checkDetails(details);
+ ok(remaining.delete(filename), `Got only one request for ${filename}`);
+ }
+
+ await extension.awaitMessage("tab-created");
+ extension.sendMessage("close-tab");
+ await extension.awaitMessage("tab-closed");
+
+ await extension.unload();
+});
+</script>
+</head>
+<body>
+<div id="test">Sample text</div>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_getSecurityInfo.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_getSecurityInfo.html
new file mode 100644
index 0000000000..5628109483
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_getSecurityInfo.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>browser.webRequest.getSecurityInfo()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(async function test_getSecurityInfo() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>"
+ ],
+ },
+ async background() {
+ const url = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_sample.html";
+
+ let tab;
+ browser.webRequest.onHeadersReceived.addListener(async details => {
+ const securityInfo = await browser.webRequest.getSecurityInfo(
+ details.requestId,
+ {}
+ );
+
+ // Some properties have dynamic values so let's take them out of the
+ // `securityInfo` object before asserting all the other props with deep
+ // equality.
+ const {
+ cipherSuite,
+ secretKeyLength,
+ keaGroupName,
+ signatureSchemeName,
+ protocolVersion,
+ certificates,
+ ...otherProps
+ } = securityInfo;
+
+ browser.test.assertTrue(cipherSuite.length, "expected cipher suite");
+ browser.test.assertTrue(
+ Number.isInteger(secretKeyLength),
+ "expected secret key length"
+ );
+ browser.test.assertTrue(
+ keaGroupName.length,
+ "expected kea group name"
+ );
+ browser.test.assertTrue(
+ signatureSchemeName.length,
+ "expected signature scheme name"
+ );
+ browser.test.assertTrue(
+ protocolVersion.length,
+ "expected protocol version"
+ );
+ browser.test.assertTrue(
+ Array.isArray(certificates),
+ "expected an array of certificates"
+ );
+
+ browser.test.assertDeepEq({
+ state: "secure",
+ isExtendedValidation: false,
+ certificateTransparencyStatus: "not_applicable",
+ hsts: false,
+ hpkp: false,
+ usedEch: false,
+ usedDelegatedCredentials: false,
+ usedOcsp: false,
+ usedPrivateDns: false,
+ }, otherProps, "expected security info");
+
+ await browser.tabs.remove(tab.id);
+ browser.test.notifyPass("success");
+ }, { urls: [url] } , ["blocking"]);
+
+ tab = await browser.tabs.create({ url });
+ },
+ });
+ await extension.startup();
+
+ await extension.awaitFinish("success");
+
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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..e66b5c471a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html
@@ -0,0 +1,252 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_webrequest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+function getExtension() {
+ async function background() {
+ let expect;
+ let urls = ["*://*.example.org/tests/*"];
+ browser.webRequest.onBeforeRequest.addListener(details => {
+ browser.test.assertEq(expect.shift(), "onBeforeRequest");
+ }, {urls}, ["blocking"]);
+ browser.webRequest.onBeforeSendHeaders.addListener(details => {
+ browser.test.assertEq(expect.shift(), "onBeforeSendHeaders");
+ }, {urls}, ["blocking", "requestHeaders"]);
+ browser.webRequest.onSendHeaders.addListener(details => {
+ browser.test.assertEq(expect.shift(), "onSendHeaders");
+ }, {urls}, ["requestHeaders"]);
+
+ async function testSecurityInfo(securityInfo, options) {
+ if (options.certificateChain) {
+ // Some of the tests here only produce a single cert in the chain.
+ browser.test.assertTrue(securityInfo.certificates.length >= 1, "have certificate chain");
+ } else {
+ browser.test.assertTrue(securityInfo.certificates.length == 1, "no certificate chain");
+ }
+ let cert = securityInfo.certificates[0];
+ let now = Date.now();
+ browser.test.assertTrue(Number.isInteger(cert.validity.start), "cert start is integer");
+ browser.test.assertTrue(Number.isInteger(cert.validity.end), "cert end is integer");
+ browser.test.assertTrue(cert.validity.start < now, "cert start validity is correct");
+ browser.test.assertTrue(now < cert.validity.end, "cert end validity is correct");
+ if (options.rawDER) {
+ for (let cert of securityInfo.certificates) {
+ browser.test.assertTrue(!!cert.rawDER.length, "have rawDER");
+ }
+ }
+ }
+
+ function stripQuery(url) {
+ // In this whole test we are not interested in the query part of the URL.
+ // Most tests include a cache buster (bustcache) param in the URL.
+ return url.split("?")[0];
+ }
+
+ browser.webRequest.onHeadersReceived.addListener(async (details) => {
+ browser.test.assertEq(expect.shift(), "onHeadersReceived");
+
+ // We expect all requests to have been upgraded at this point.
+ browser.test.assertTrue(details.url.startsWith("https"), "connection is https");
+ let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {});
+ browser.test.assertTrue(securityInfo && securityInfo.state == "secure",
+ "security info reflects https");
+ await testSecurityInfo(securityInfo, {});
+ securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {certificateChain: true});
+ await testSecurityInfo(securityInfo, {certificateChain: true});
+ securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {rawDER: true});
+ await testSecurityInfo(securityInfo, {rawDER: true});
+ securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {certificateChain: true, rawDER: true});
+ await testSecurityInfo(securityInfo, {certificateChain: true, rawDER: true});
+
+ browser.test.sendMessage("hsts", securityInfo.hsts);
+ let headers = details.responseHeaders || [];
+ for (let header of headers) {
+ if (header.name.toLowerCase() === "strict-transport-security") {
+ return;
+ }
+ }
+ if (details.url.includes("addHsts")) {
+ headers.push({
+ name: "Strict-Transport-Security",
+ value: "max-age=31536000000",
+ });
+ }
+ return {responseHeaders: headers};
+ }, {urls}, ["blocking", "responseHeaders"]);
+ browser.webRequest.onBeforeRedirect.addListener(details => {
+ browser.test.assertEq(expect.shift(), "onBeforeRedirect");
+ }, {urls});
+ browser.webRequest.onResponseStarted.addListener(details => {
+ browser.test.assertEq(expect.shift(), "onResponseStarted");
+ }, {urls});
+ browser.webRequest.onCompleted.addListener(details => {
+ browser.test.assertEq(expect.shift(), "onCompleted");
+ browser.test.sendMessage("onCompleted", stripQuery(details.url));
+ }, {urls});
+ browser.webRequest.onErrorOccurred.addListener(details => {
+ browser.test.notifyFail(`onErrorOccurred ${JSON.stringify(details)}`);
+ }, {urls});
+
+ async function onUpdated(tabId, tabInfo, tab) {
+ if (tabInfo.status !== "complete" || tab.url === "about:blank") {
+ return;
+ }
+ browser.tabs.remove(tabId);
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ browser.test.sendMessage("tabs-done", stripQuery(tab.url));
+ }
+ browser.test.onMessage.addListener((url, expected) => {
+ expect = expected;
+ browser.tabs.onUpdated.addListener(onUpdated);
+ browser.tabs.create({url});
+ });
+ }
+
+ let manifest = {
+ "permissions": [
+ "tabs",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ ],
+ };
+ return ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ });
+}
+
+add_setup(async () => {
+ // In bug 1605515, we repeatedly saw a missing onHeadersReceived event,
+ // possibly related to bug 1595610. As a workaround, clear the cache.
+ await SpecialPowers.spawnChrome([], async () => {
+ Services.cache2.clear();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_HSTS, resolve);
+ });
+ });
+});
+
+// This test makes a request against a server that redirects with a 302.
+add_task(async function test_hsts_request() {
+ const testPath = "example.org/tests/toolkit/components/extensions/test/mochitest";
+
+ let extension = getExtension();
+ await extension.startup();
+
+ // simple redirect
+ let sample = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_sample.html";
+ extension.sendMessage(
+ `https://${testPath}/redirect_auto.sjs?redirect_uri=${sample}?bustcache1=${Math.random()}`,
+ ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders",
+ "onHeadersReceived", "onBeforeRedirect", "onBeforeRequest",
+ "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived",
+ "onResponseStarted", "onCompleted"]);
+ is(await extension.awaitMessage("hsts"), false, "First request to this host, not receiving a hsts header");
+ is(await extension.awaitMessage("hsts"), false, "second (redirected) reqiest to the same host, still no knowledge about the hosts hsts preference");
+ // Note: stripQuery strips query string added by redirect_auto.
+ is(await extension.awaitMessage("tabs-done"), sample, "redirection ok");
+ is(await extension.awaitMessage("onCompleted"), sample, "redirection ok");
+
+ // priming hsts
+ extension.sendMessage(
+ `https://${testPath}/hsts.sjs`,
+ ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders",
+ "onHeadersReceived", "onResponseStarted", "onCompleted"]);
+ is(await extension.awaitMessage("hsts"), false, "First request to this host, receiving hsts header and saving the hosts STS preference for the next request");
+ is(await extension.awaitMessage("tabs-done"),
+ "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs",
+ "hsts primed");
+ is(await extension.awaitMessage("onCompleted"),
+ "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs");
+
+ // test upgrade
+ extension.sendMessage(
+ `http://${testPath}/hsts.sjs`,
+ ["onBeforeRequest", "onBeforeRedirect", "onBeforeRequest",
+ "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived",
+ "onResponseStarted", "onCompleted"]);
+ is(await extension.awaitMessage("hsts"), true, "second (redirected) reqiest to the same host, we know about the hsts status of the host this time");
+ is(await extension.awaitMessage("tabs-done"),
+ "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs",
+ "hsts upgraded");
+ is(await extension.awaitMessage("onCompleted"),
+ "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs");
+
+ await extension.unload();
+});
+
+// This test makes a priming request and adds the STS header, then tests the upgrade.
+add_task(async function test_hsts_header() {
+ const testPath = "test1.example.org/tests/toolkit/components/extensions/test/mochitest";
+
+ let extension = getExtension();
+ await extension.startup();
+
+ // priming hsts, this time there is no STS header, onHeadersReceived adds it.
+ let completed = extension.awaitMessage("onCompleted");
+ let tabdone = extension.awaitMessage("tabs-done");
+ extension.sendMessage(
+ `https://${testPath}/file_sample.html?bustcache2=${Math.random()}&addHsts=true`,
+ ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders",
+ "onHeadersReceived", "onResponseStarted", "onCompleted"]);
+ is(await extension.awaitMessage("hsts"), false, "First reqeuest to this host, we don't know about the hosts STS setting yet");
+ is(await tabdone, `https://${testPath}/file_sample.html`, "priming request done");
+ is(await completed, `https://${testPath}/file_sample.html`, "priming request done");
+
+ // test upgrade from http to https due to onHeadersReceived adding STS header
+ completed = extension.awaitMessage("onCompleted");
+ tabdone = extension.awaitMessage("tabs-done");
+ extension.sendMessage(
+ `http://${testPath}/file_sample.html?bustcache3=${Math.random()}`,
+ ["onBeforeRequest", "onBeforeRedirect", "onBeforeRequest",
+ "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived",
+ "onResponseStarted", "onCompleted"]);
+ is(await extension.awaitMessage("hsts"), true, "We have received an hsts header last request via oneadersReceived");
+ is(await tabdone, `https://${testPath}/file_sample.html`, "hsts upgraded");
+ is(await completed, `https://${testPath}/file_sample.html`, "request upgraded");
+
+ await extension.unload();
+});
+
+add_task(async function test_nonBlocking_securityInfo() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": [
+ "webRequest",
+ "<all_urls>",
+ ],
+ },
+ async background() {
+ let tab;
+ browser.webRequest.onHeadersReceived.addListener(async (details) => {
+ let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {});
+ browser.test.assertTrue(!securityInfo, "securityInfo undefined on http request");
+ browser.tabs.remove(tab.id);
+ browser.test.notifyPass("success");
+ }, {urls: ["<all_urls>"], types: ["main_frame"]});
+ tab = await browser.tabs.create({
+ url: `https://example.org/tests/toolkit/components/extensions/test/mochitest/file_sample.html?bustcache4=${Math.random()}`,
+ });
+ },
+ });
+ await extension.startup();
+
+ await extension.awaitFinish("success");
+ await extension.unload();
+});
+</script>
+</head>
+<body>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1450965: Skip Cors Check for Early WebExtention Redirects </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* Description of the test:
+ * We try to Check if a WebExtention can redirect a request and bypass CORS
+ * We're redirecting a fetch request in onBeforeRequest
+ * which should not be blocked, even though we do not have
+ * the CORS information yet.
+ */
+
+const WIN_URL =
+ "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html";
+
+
+add_task(async function test_webRequest_redirect_cors_bypass() {
+ // disable third-party storage isolation so the test works as expected
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ ],
+ },
+ background() {
+ browser.webRequest.onBeforeRequest.addListener((details) => {
+ if (details.url.includes("file_cors_blocked.txt")) {
+ // File_cors_blocked does not need to exist, because we're redirecting anyway.
+ const testPath = "example.org/tests/toolkit/components/extensions/test/mochitest";
+ let redirectUrl = `https://${testPath}/file_sample.txt`;
+
+ // If the WebExtion cant bypass CORS, the fetch will throw a CORS-Exception
+ // because we do not have the CORS header yet for 'file-cors-blocked.txt'
+ return {redirectUrl};
+ }
+ }, {urls: ["<all_urls>"]}, ["blocking"]);
+ },
+
+ });
+
+ await extension.startup();
+ let win = window.open(WIN_URL);
+ // Creating a message channel to the new tab.
+ const channel = new BroadcastChannel("test_bus");
+ await new Promise((resolve, reject) => {
+ channel.onmessage = async function(fetch_result) {
+ // Fetch result data will either be the text content of file_sample.txt -> 'Sample'
+ // or a network-Error.
+ // In case it's 'Sample' the redirect did happen correctly.
+ ok(fetch_result.data == "Sample", "Cors was Bypassed");
+ win.close();
+ await extension.unload();
+ resolve();
+ };
+ });
+});
+
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1434357: Allow Web Request API to redirect to data: URI</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* Description of the test:
+ * We load a *.js file which gets redirected to a data: URI.
+ * Since there is no good way to communicate loaded data: URI scripts
+ * we use updating a divContainer as a detour to verify the data: URI
+ * script has loaded.
+ */
+
+const WIN_URL =
+ "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html";
+
+add_task(async function test_webRequest_redirect_data_uri() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "*://mochi.test/tests/*",
+ ],
+ content_scripts: [{
+ matches: ["*://mochi.test/tests/*/file_redirect_data_uri.html"],
+ run_at: "document_end",
+ js: ["content_script.js"],
+ "all_frames": true,
+ }],
+ },
+
+ background() {
+ browser.webRequest.onBeforeRequest.addListener((details) => {
+ if (details.url.includes("dummy_non_existend_file.js")) {
+ let redirectUrl =
+ "data:text/javascript,document.getElementById('testdiv').textContent='loaded'";
+ return {redirectUrl};
+ }
+ }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]);
+ },
+
+ files: {
+ "content_script.js": function() {
+ let scriptEl = document.createElement("script");
+ // please note that dummy_non_existend_file.js file does not really need
+ // to exist because we redirect the load within onBeforeRequest().
+ scriptEl.src = "dummy_non_existend_file.js";
+ document.body.appendChild(scriptEl);
+
+ scriptEl.onload = function() {
+ let divContent = document.getElementById("testdiv").textContent;
+ browser.test.assertEq(divContent, "loaded",
+ "redirect to data: URI allowed");
+ browser.test.sendMessage("finished");
+ };
+ scriptEl.onerror = function() {
+ browser.test.fail("script load failure");
+ browser.test.sendMessage("finished");
+ };
+ },
+ },
+ });
+
+ await extension.startup();
+ let win = window.open(WIN_URL);
+ await extension.awaitMessage("finished");
+ win.close();
+ await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function test_webRequest_upgrade() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "*://mochi.test/tests/*",
+ ],
+ },
+ background() {
+ browser.webRequest.onSendHeaders.addListener((details) => {
+ // At this point, the request should have been upgraded.
+ browser.test.assertTrue(details.url.startsWith("https:"), "request is upgraded");
+ browser.test.assertTrue(details.url.includes("file_sample"), "redirect after upgrade worked");
+ // Note: although not significant for the test assertions, note that
+ // the requested file won't load - https://mochi.test:8888/ does not
+ // resolve to anything on the test server.
+ browser.test.sendMessage("finished");
+ }, {urls: ["*://mochi.test/tests/*"]});
+
+ browser.webRequest.onBeforeRequest.addListener((details) => {
+ browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`);
+ let url = new URL(details.url);
+ if (url.protocol == "http:") {
+ return {upgradeToSecure: true};
+ }
+ // After the channel is initially upgraded, we get another onBeforeRequest
+ // call. Here we can redirect again to a new url.
+ if (details.url.includes("file_mixed.html")) {
+ let redirectUrl = new URL("file_sample.html", details.url).href;
+ return {redirectUrl};
+ }
+ }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]);
+ },
+ });
+
+ await extension.startup();
+ let win = window.open("http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_mixed.html");
+ await extension.awaitMessage("finished");
+ win.close();
+ await extension.unload();
+});
+
+add_task(async function test_webRequest_redirect_wins() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "*://mochi.test/tests/*",
+ ],
+ },
+ background() {
+ browser.webRequest.onSendHeaders.addListener((details) => {
+ // At this point, the request should have been redirected instead of upgraded.
+ browser.test.assertTrue(details.url.includes("file_sample"), "request was redirected");
+ browser.test.sendMessage("finished");
+ }, {urls: ["*://mochi.test/tests/*"]});
+
+ browser.webRequest.onBeforeRequest.addListener((details) => {
+ if (details.url.includes("file_mixed.html")) {
+ let redirectUrl = new URL("file_sample.html", details.url).href;
+ return {upgradeToSecure: true, redirectUrl};
+ }
+ }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]);
+ },
+ });
+
+ await extension.startup();
+ let win = window.open("http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_mixed.html");
+ await extension.awaitMessage("finished");
+ win.close();
+ await extension.unload();
+});
+
+// Test that there is no infinite redirect loop when upgradeToSecure is used on
+// https. This test checks that the redirect chain is: http -> https -> done.
+add_task(async function upgradeToSecure_for_https_is_noop() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "*://example.com/tests/*",
+ ],
+ },
+ background() {
+ let count = 0;
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`);
+ ++count;
+ if (details.url.startsWith("http:")) {
+ browser.test.assertEq(1, count, "Initial request is http:");
+ } else {
+ browser.test.assertEq(2, count, "Second request is https:");
+ }
+ return {upgradeToSecure: true};
+ },
+ { urls: ["*://example.com/tests/*file_sample.html"] },
+ ["blocking"]
+ );
+ browser.webRequest.onCompleted.addListener(
+ details => {
+ browser.test.log(`onCompleted ${details.requestId} ${details.url}`);
+ browser.test.assertTrue(details.url.startsWith("https"), "is https");
+ browser.test.assertEq(2, count, "Seen two requests (http + https)");
+ browser.test.sendMessage("finished");
+ },
+ { urls: ["*://example.com/tests/*file_sample.html"] },
+ );
+ },
+ });
+
+ await extension.startup();
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html");
+ await extension.awaitMessage("finished");
+ win.close();
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<form method="post"
+ action="file_WebRequest_page3.html?trigger=form"
+ target="_blank"
+ enctype="multipart/form-data"
+ >
+<input type="text" name="&quot;special&quot; &#x0D;&#x0A; ch�rs" value="sp�cial">
+<input type="file" name="testFile">
+<input type="file" name="emptyFile">
+<input type="text" name="textInput1" value="value1">
+</form>
+
+<form method="post"
+ action="file_WebRequest_page3.html?trigger=form"
+ target="_blank"
+ enctype="multipart/form-data"
+ >
+<input type="text" name="textInput2" value="value2">
+<input type="file" name="testFile">
+<input type="file" name="emptyFile">
+</form>
+
+</form>
+<form method="post"
+ action="file_WebRequest_page3.html?trigger=form"
+ target="_blank"
+ >
+<input type="text" name="textInput" value="value1">
+<input type="text" name="textInput" value="value2">
+</form>
+<script>
+"use strict";
+
+let files, testFile, blob, file, uploads;
+add_task(async function test_setup() {
+ files = await new Promise(resolve => {
+ SpecialPowers.createFiles([{name: "testFile.pdf", data: "Not really a PDF file :)", "type": "application/x-pdf"}], (result) => {
+ resolve(result);
+ });
+ });
+ testFile = files[0];
+ blob = {
+ name: "blobAsFile",
+ content: new Blob(["A blob sent as a file"], {type: "text/csv"}),
+ fileName: "blobAsFile.csv",
+ };
+ file = {
+ name: "testFile",
+ fileName: testFile.name,
+ };
+ uploads = {
+ [blob.name]: blob,
+ [file.name]: file,
+ "emptyFile": {fileName: ""}
+ };
+});
+
+function background() {
+ const FILTERS = {urls: ["<all_urls>"]};
+
+ function onUpload(details) {
+ let url = new URL(details.url);
+ let upload = url.searchParams.get("upload");
+ if (!upload) {
+ return;
+ }
+
+ let requestBody = details.requestBody;
+ browser.test.log(`onBeforeRequest upload: ${details.url} ${JSON.stringify(details.requestBody)}`);
+ browser.test.assertTrue(!!requestBody, `Intercepted upload ${details.url} #${details.requestId} ${upload} have a requestBody`);
+ if (!requestBody) {
+ return;
+ }
+ let byteLength = parseInt(upload, 10);
+ if (byteLength) {
+ browser.test.assertTrue(!!requestBody.raw, `Binary upload ${details.url} #${details.requestId} ${upload} have a raw attribute`);
+ browser.test.assertEq(byteLength, requestBody.raw && requestBody.raw.map(r => r.bytes ? r.bytes.byteLength : 0).reduce((a, b) => a + b), `Binary upload size matches`);
+ return;
+ }
+ if ("raw" in requestBody) {
+ browser.test.assertEq(upload, JSON.stringify(requestBody.raw).replace(/(\bfile: ")[^"]+/, "$1<file>"), `Upload ${details.url} #${details.requestId} matches raw data`);
+ } else {
+ browser.test.assertEq(upload, JSON.stringify(requestBody.formData), `Upload ${details.url} #${details.requestId} matches form data.`);
+ }
+ }
+
+ browser.webRequest.onCompleted.addListener(
+ details => {
+ browser.test.log(`onCompleted ${details.requestId} ${details.url}`);
+ // See bug 1471387
+ if (details.url.endsWith("/favicon.ico") || details.originUrl == "about:newtab") {
+ return;
+ }
+
+ browser.test.sendMessage("done");
+ },
+ FILTERS);
+
+ let onBeforeRequest = details => {
+ browser.test.log(`${name} ${details.requestId} ${details.url}`);
+ // See bug 1471387
+ if (details.url.endsWith("/favicon.ico") || details.originUrl == "about:newtab") {
+ return;
+ }
+
+ onUpload(details);
+ };
+
+ browser.webRequest.onBeforeRequest.addListener(
+ onBeforeRequest, FILTERS, ["requestBody"]);
+
+ let tab;
+ browser.tabs.onCreated.addListener(newTab => {
+ tab = newTab;
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "close-tab") {
+ browser.tabs.remove(tab.id);
+ browser.test.sendMessage("tab-closed");
+ }
+ });
+}
+
+add_task(async function test_xhr_forms() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "tabs",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ ],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ async function doneAndTabClosed() {
+ await extension.awaitMessage("done");
+ let closed = extension.awaitMessage("tab-closed");
+ extension.sendMessage("close-tab");
+ await closed;
+ }
+
+ for (let form of document.forms) {
+ if (file.name in form.elements) {
+ SpecialPowers.wrap(form.elements[file.name]).mozSetFileArray(files);
+ }
+ let action = new URL(form.action);
+ let formData = new FormData(form);
+ let webRequestFD = {};
+
+ let updateActionURL = () => {
+ for (let name of formData.keys()) {
+ webRequestFD[name] = name in uploads ? [uploads[name].fileName] : formData.getAll(name);
+ }
+ action.searchParams.set("upload", JSON.stringify(webRequestFD));
+ action.searchParams.set("enctype", form.enctype);
+ };
+
+ updateActionURL();
+
+ form.action = action;
+ form.submit();
+ await doneAndTabClosed();
+
+ if (form.enctype !== "multipart/form-data") {
+ continue;
+ }
+
+ let post = (data) => {
+ let xhr = new XMLHttpRequest();
+ action.searchParams.set("xhr", "1");
+ xhr.open("POST", action.href);
+ xhr.send(data);
+ action.searchParams.delete("xhr");
+ return doneAndTabClosed();
+ };
+
+ formData.append(blob.name, blob.content, blob.fileName);
+ formData.append("formDataField", "some value");
+ updateActionURL();
+ await post(formData);
+
+ action.searchParams.set("upload", JSON.stringify([{file: "<file>"}]));
+ await post(testFile);
+
+ action.searchParams.set("upload", `${blob.content.size} bytes`);
+ await post(blob.content);
+
+ let byteLength = 16;
+ action.searchParams.set("upload", `${byteLength} bytes`);
+ await post(new ArrayBuffer(byteLength));
+ }
+
+ // Testing the decoding of percent escapes even in cases where the
+ // multipart/form-data serializer won't emit them.
+ {
+ let boundary = "-".repeat(27);
+ for (let i = 0; i < 3; i++) {
+ const randomNumber = Math.floor(Math.random() * (2 ** 32));
+ boundary += String(randomNumber);
+ }
+
+ const formPayload = [
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="percent escapes other than%20quotes and newlines"',
+ "",
+ "",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="valid UTF-8: %F0%9F%92%A9"',
+ "",
+ "",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="broken UTF-8: %F0%9F %92%A9"',
+ "",
+ "",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="percent escapes aren\'t decoded in filenames"; filename="%0D%0A%22"',
+ "Content-Type: application/octet-stream",
+ "",
+ "",
+ `--${boundary}--`,
+ ""
+ ].join("\r\n");
+
+ const action = new URL("file_WebRequest_page3.html?trigger=form", document.location.href);
+ action.searchParams.set("xhr", "1");
+ action.searchParams.set("upload", JSON.stringify({
+ "percent escapes other than quotes and newlines": [""],
+ "valid UTF-8: 💩": [""],
+ "broken UTF-8: � ��": [""],
+ "percent escapes aren't decoded in filenames": ["%0D%0A%22"]
+ }));
+ action.searchParams.set("enctype", "multipart/form-data");
+
+ await fetch(
+ action.href,
+ {
+ method: "POST",
+ headers: {"Content-Type": `multipart/form-data; boundary=${boundary}`},
+ body: formPayload
+ },
+ );
+ await doneAndTabClosed();
+ }
+
+ await extension.unload();
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_worker.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_worker.html
new file mode 100644
index 0000000000..9fc3e00f01
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_worker.html
@@ -0,0 +1,192 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head_webrequest.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+let tabId;
+
+let extensionData = {
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>", "tabs"],
+ },
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(details => {
+ if (details.url.endsWith("/favicon.ico")) {
+ // We don't care about favicon.ico in this test. It is hard to control
+ // whether the request happens.
+ browser.test.log(`Ignoring favicon request: ${details.url}`);
+ return;
+ }
+ browser.test.sendMessage("onBeforeRequest", details);
+ }, {urls: ["<all_urls>"]}, ["blocking"]);
+
+ let tab;
+ browser.tabs.onCreated.addListener(newTab => {
+ tab = newTab;
+ browser.test.sendMessage("tab-created", tab.id);
+ });
+
+ browser.test.onMessage.addListener(async (msg) => {
+ if (msg === "close-tab") {
+ await browser.tabs.remove(tab.id);
+ browser.test.sendMessage("tab-closed");
+ }
+ });
+ },
+};
+
+let expected = {
+ "file_simple_webrequest_worker.html?topframe=true": {
+ type: "main_frame",
+ toplevel: true,
+ origin: "test_ext_webrequest_worker.html",
+ tabId: true,
+ parentFrameId: -1,
+ },
+ "file_simple_iframe_worker.html": {
+ type: "sub_frame",
+ toplevel: false,
+ origin: "file_simple_webrequest_worker.html?topframe=true",
+ tabId: true,
+ parentFrameId: 0,
+ },
+ "file_simple_toplevel.txt": {
+ type: "xmlhttprequest",
+ toplevel: true,
+ origin: "file_simple_webrequest_worker.html?topframe=true",
+ tabId: true,
+ parentFrameId: -1,
+ },
+ "file_simple_iframe.txt": {
+ type: "xmlhttprequest",
+ toplevel: false,
+ origin: "file_simple_iframe_worker.html",
+ tabId: true,
+ parentFrameId: 0,
+ },
+ "file_simple_worker.txt": {
+ type: "xmlhttprequest",
+ toplevel: true,
+ origin: "file_simple_worker.js",
+ tabId: true,
+ parentFrameId: -1,
+ },
+ "file_simple_iframe_worker.txt": {
+ type: "xmlhttprequest",
+ toplevel: false,
+ origin: "file_simple_worker.js?iniframe=true",
+ tabId: true,
+ parentFrameId: 0,
+ },
+ "file_simple_sharedworker.txt": {
+ type: "xmlhttprequest",
+ toplevel: undefined,
+ origin: "file_simple_sharedworker.js",
+ tabId: false,
+ parentFrameId: -1,
+ },
+ "file_simple_iframe_sharedworker.txt": {
+ type: "xmlhttprequest",
+ toplevel: undefined,
+ origin: "file_simple_sharedworker.js?iniframe=true",
+ tabId: false,
+ parentFrameId: -1,
+ },
+ "file_simple_worker.js": {
+ type: "script",
+ toplevel: true,
+ origin: "file_simple_webrequest_worker.html?topframe=true",
+ tabId: true,
+ parentFrameId: -1,
+ },
+ "file_simple_sharedworker.js": {
+ type: "script",
+ toplevel: undefined,
+ origin: "file_simple_webrequest_worker.html?topframe=true",
+ tabId: false,
+ parentFrameId: -1,
+ },
+ "file_simple_worker.js?iniframe=true": {
+ type: "script",
+ toplevel: false,
+ origin: "file_simple_iframe_worker.html",
+ tabId: true,
+ parentFrameId: 0,
+ },
+ "file_simple_sharedworker.js?iniframe=true": {
+ type: "script",
+ toplevel: undefined,
+ origin: "file_simple_iframe_worker.html",
+ tabId: false,
+ parentFrameId: -1,
+ },
+
+
+};
+
+function checkDetails(details) {
+ let filename = details.url.split("/").pop();
+ ok(filename in expected, `Should be expecting a request for ${filename}`);
+ let expect = expected[filename];
+ is(expect.type, details.type, `${details.type} type matches`);
+ const originUrlSuffix = details.originUrl?.split("/").pop();
+ ok(expect.origin === originUrlSuffix || originUrlSuffix.startsWith(expect.origin), `origin url is correct`);
+ is(details.parentFrameId, expect.parentFrameId, "parentFrameId matches");
+ is(expect.tabId ? tabId : -1, details.tabId, "tabId matches");
+ // TODO: When expect.toplevel is "undefined", the details.frameId is supposed
+ // to be -1.
+ // details in https://phabricator.services.mozilla.com/D182705#inline-1030548.
+ if (expect.toplevel === undefined || expect.toplevel) {
+ is(details.frameId, 0, "expect zero frameId");
+ } else {
+ ok(details.frameId > 0, "expect non-zero frameId");
+ }
+ return filename;
+}
+
+add_task(async function test_webRequest_worker() {
+ await SpecialPowers.spawnChrome([], async () => {
+ Services.cache2.clear();
+ });
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let a = addLink(`file_simple_webrequest_worker.html?topframe=true`);
+ a.click();
+ tabId = await extension.awaitMessage("tab-created");
+ info(`Get created tab(${tabId}`);
+
+ let remaining = new Set(Object.keys(expected));
+ let totalExpectedCount = remaining.size;
+ let currentExpectedCount = 0;
+ while (remaining.size !== 0) {
+ info(`Waiting for request ${currentExpectedCount + 1} out of ${totalExpectedCount}`);
+ info(`Expecting one of: ${Array.from(remaining)}`);
+ let details = await extension.awaitMessage("onBeforeRequest");
+ info(`Checking details for request: ${JSON.stringify(details)}`);
+ let filename = checkDetails(details);
+ ok(remaining.delete(filename), `Got only one request for ${filename}`);
+ currentExpectedCount = currentExpectedCount + 1;
+ }
+
+ extension.sendMessage("close-tab");
+ await extension.awaitMessage("tab-closed");
+ await extension.unload();
+});
+
+</script>
+</head>
+<body>
+<div id="test">Sample text</div>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(async function test_postMessage() {
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ "all_frames": true,
+ },
+ ],
+
+ web_accessible_resources: ["iframe.html"],
+ },
+
+ background() {
+ browser.test.sendMessage("iframe-url", browser.runtime.getURL("iframe.html"));
+ },
+
+ files: {
+ "content_script.js": function() {
+ window.addEventListener("message", event => {
+ if (event.data == "ping") {
+ event.source.postMessage({pong: location.href},
+ event.origin);
+ }
+ });
+ },
+
+ "iframe.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="content_script.js"><\/script>
+ </head>
+ </html>`,
+ },
+ };
+
+ let createIframe = url => {
+ let iframe = document.createElement("iframe");
+ return new Promise(resolve => {
+ iframe.src = url;
+ iframe.onload = resolve;
+ document.body.appendChild(iframe);
+ }).then(() => {
+ return iframe;
+ });
+ };
+
+ let awaitMessage = () => {
+ return new Promise(resolve => {
+ let listener = event => {
+ if (event.data.pong) {
+ window.removeEventListener("message", listener);
+ resolve(event.data);
+ }
+ };
+ window.addEventListener("message", listener);
+ });
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let iframeURL = await extension.awaitMessage("iframe-url");
+ let testURL = SimpleTest.getTestFileURL("file_sample.html");
+
+ for (let url of [iframeURL, testURL]) {
+ info(`Testing URL ${url}`);
+
+ let iframe = await createIframe(url);
+
+ iframe.contentWindow.postMessage(
+ "ping", url);
+
+ let pong = await awaitMessage();
+ is(pong.pong, url, "Got expected pong");
+
+ iframe.remove();
+ }
+
+ await extension.unload();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Check StartupCache</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// The startup canary file is removed sometime after the startup, with a delay,
+// e.g. 30 seconds on desktop:
+// https://searchfox.org/mozilla-central/rev/aa46c2dcccbc6fd4265edca05d3d00cccdfc97b9/browser/components/BrowserGlue.jsm#2486-2490
+// e.g. up to 15 seconds (as an idle timeout) on Android:
+// https://searchfox.org/mozilla-central/rev/aa46c2dcccbc6fd4265edca05d3d00cccdfc97b9/mobile/android/chrome/geckoview/geckoview.js#510
+//
+// This test completes quickly if run sequentially after the many tests in this
+// directory. Otherwise the test may wait for up to MAX_DELAY_SEC seconds.
+const MAX_DELAY_SEC = 30;
+SimpleTest.requestFlakyTimeout("trackStartupCrashEnd() is called with a delay");
+
+// This test is not extension-specific, but placed in the extensions/ directory
+// because it complements the test_check_startupcache.html test, and because
+// the directory has many other tests, to minimize the amount of time wasted on
+// waiting.
+
+add_task(async function check_startup_canary() {
+ // The ".startup-incomplete" file is created at the startup, and supposedly
+ // cleared "soon" after startup (when the application knows that the startup
+ // succeeded without crash). Bug 1624724 and bug 1728461 show that this has
+ // not always been the case, so this regression test verifies that the file
+ // is actually non-existent when this test start, see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1728461#c12
+
+ // This test is opened as a web page in the browser, so that should have been
+ // a point where the startup should have been considered done.
+
+ async function canaryExists() {
+ let chromeScript = loadChromeScript(async () => {
+ // This file is called FILE_STARTUP_INCOMPLETE in nsAppRunner.cpp and
+ // referenced via mozilla::startup::GetIncompleteStartupFile:
+ let file = Services.dirsvc.get("ProfLD", Ci.nsIFile);
+ file.append(".startup-incomplete");
+ this.sendAsyncMessage("canary_exists", file.exists());
+ });
+ let exists = await chromeScript.promiseOneMessage("canary_exists");
+ chromeScript.destroy();
+ return exists;
+ }
+
+ info("Checking if startup canary exists");
+ let i = 0;
+ while (await canaryExists()) {
+ if (i++ > MAX_DELAY_SEC) {
+ info("Canary still exists, giving up on waiting");
+ break;
+ }
+ info(`Startup canary exists, will retry ${i} / ${MAX_DELAY_SEC}.`);
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ is(
+ await canaryExists(),
+ false,
+ "Startup canary should have been removed after early startup"
+ );
+});
+
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Verify non-remote mode</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+add_task(async function verify_extensions_in_parent_process() {
+ // This test ensures we are running with the proper settings.
+ const { WebExtensionPolicy } = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services);
+ SimpleTest.ok(!WebExtensionPolicy.useRemoteWebExtensions, "extensions running in-process");
+
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
+ Assert.ok(WebExtensionPolicy.isExtensionProcess, "parent is extension process");
+ this.sendAsyncMessage("checks_done");
+ });
+ await chromeScript.promiseOneMessage("checks_done");
+ chromeScript.destroy();
+});
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Verify remote mode</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+ "use strict";
+ // This test ensures we are running with the proper settings.
+ const {WebExtensionPolicy} = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services);
+ SimpleTest.ok(WebExtensionPolicy.useRemoteWebExtensions, "extensions running remote");
+ SimpleTest.ok(!WebExtensionPolicy.isExtensionProcess, "testing from remote process");
+</script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Verify WebExtension background service worker mode</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+ "use strict";
+ // This test ensures we are running with the proper settings.
+ const {WebExtensionPolicy} = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services);
+ SimpleTest.ok(WebExtensionPolicy.useRemoteWebExtensions, "extensions running remote");
+ SimpleTest.ok(!WebExtensionPolicy.isExtensionProcess, "testing from remote process");
+ SimpleTest.ok(WebExtensionPolicy.backgroundServiceWorkerEnabled, "extensions background service worker enabled");
+ SimpleTest.ok(AppConstants.MOZ_WEBEXT_WEBIDL_ENABLED, "extensions API webidl bindings enabled");
+</script>
+
+</body>
+</html>
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..14d3ad2bab
--- /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.sys.mjs b/toolkit/components/extensions/test/mochitest/webrequest_test.sys.mjs
new file mode 100644
index 0000000000..33554f3023
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/webrequest_test.sys.mjs
@@ -0,0 +1,16 @@
+export 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");