summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/components/extensions/test
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/extensions/test')
-rw-r--r--toolkit/components/extensions/test/mochitest/.eslintrc.js35
-rw-r--r--toolkit/components/extensions/test/mochitest/chrome.ini35
-rw-r--r--toolkit/components/extensions/test/mochitest/chrome_head.js12
-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.html11
-rw-r--r--toolkit/components/extensions/test/mochitest/file_csp.html14
-rw-r--r--toolkit/components/extensions/test/mochitest/file_csp.html^headers^1
-rw-r--r--toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js12
-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_redirect.pngbin0 -> 5401 bytes
-rw-r--r--toolkit/components/extensions/test/mochitest/file_mixed.html13
-rw-r--r--toolkit/components/extensions/test/mochitest/file_permission_xhr.html55
-rw-r--r--toolkit/components/extensions/test/mochitest/file_privilege_escalation.html13
-rw-r--r--toolkit/components/extensions/test/mochitest/file_sample.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_bad.js3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_good.js3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_redirect.js4
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_xhr.js5
-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_teardown_test.js24
-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/head.js13
-rw-r--r--toolkit/components/extensions/test/mochitest/head_cookies.js167
-rw-r--r--toolkit/components/extensions/test/mochitest/head_webrequest.js331
-rw-r--r--toolkit/components/extensions/test/mochitest/mochitest.ini114
-rw-r--r--toolkit/components/extensions/test/mochitest/redirection.sjs4
-rw-r--r--toolkit/components/extensions/test/mochitest/return_headers.sjs20
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html166
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html84
-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_saveAs.html68
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html106
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html141
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html64
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html50
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html164
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html53
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html83
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html96
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html61
-rw-r--r--toolkit/components/extensions/test/mochitest/test_clipboard.html140
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_all_apis.js158
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html46
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html47
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html47
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html76
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html162
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript.html116
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html117
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html88
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html54
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html81
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html165
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html48
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html81
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html95
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html89
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html59
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html96
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies.html234
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html93
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html72
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html112
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html86
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html92
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_generate.html49
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_geturl.html72
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_i18n.html432
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html116
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html49
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_jsversion.html86
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html63
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_notifications.html224
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html119
-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.html103
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html127
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html78
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html61
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html60
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_schema.html73
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html101
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html83
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html79
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html93
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_storage_content.html330
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html118
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html202
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html150
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_test.html191
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html170
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html353
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html559
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html308
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html116
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html327
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html216
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html199
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html105
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html86
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js8
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_test.jsm22
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_worker.js3
-rw-r--r--toolkit/components/extensions/test/xpcshell/.eslintrc.js9
-rw-r--r--toolkit/components/extensions/test/xpcshell/data/file_download.html12
-rw-r--r--toolkit/components/extensions/test/xpcshell/data/file_download.txt1
-rw-r--r--toolkit/components/extensions/test/xpcshell/head.js111
-rw-r--r--toolkit/components/extensions/test/xpcshell/head_native_messaging.js131
-rw-r--r--toolkit/components/extensions/test/xpcshell/head_sync.js67
-rw-r--r--toolkit/components/extensions/test/xpcshell/native_messaging.ini13
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js38
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_csp_validator.js85
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_alarms.js210
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js33
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js44
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js44
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js64
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js91
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js23
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js24
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js22
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js40
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js72
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js45
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js34
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_contexts.js190
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads.js76
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js354
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js862
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js402
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_experiments.js175
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_extension.js55
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_idle.js202
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js37
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js168
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js188
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js50
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_management.js20
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js135
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js30
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js27
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js13
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js514
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js128
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js82
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js30
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js23
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js26
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js25
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js337
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js79
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js59
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js54
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js51
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas.js1427
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js147
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js102
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js232
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_simple.js69
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_storage.js334
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js1073
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_topSites.js85
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js55
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_locale_converter.js133
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_locale_data.js130
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_native_messaging.js302
-rw-r--r--toolkit/components/extensions/test/xpcshell/xpcshell.ini72
180 files changed, 20067 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 000000000..53938410b
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/.eslintrc.js
@@ -0,0 +1,35 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": "../../../../../testing/mochitest/mochitest.eslintrc.js",
+
+ "env": {
+ "webextensions": true,
+ },
+
+ "globals": {
+ "ChromeWorker": false,
+ "onmessage": true,
+ "sendAsyncMessage": false,
+
+ "waitForLoad": true,
+ "promiseConsoleOutput": true,
+
+ "ExtensionTestUtils": false,
+ "NetUtil": true,
+ "webrequest_test": false,
+ "XPCOMUtils": true,
+
+ // head_webrequest.js symbols
+ "addStylesheet": true,
+ "addLink": true,
+ "addImage": true,
+ "addScript": true,
+ "addFrame": true,
+ "makeExtension": false,
+ },
+
+ "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 000000000..26585cad7
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+support-files =
+ chrome_head.js
+ head.js
+ head_cookies.js
+ file_sample.html
+ webrequest_chromeworker.js
+ webrequest_test.jsm
+tags = webextensions
+
+[test_chrome_ext_background_debug_global.html]
+skip-if = (os == 'android') # android doesn't have devtools
+[test_chrome_ext_background_page.html]
+skip-if = (toolkit == 'android') # android doesn't have devtools
+[test_chrome_ext_eventpage_warning.html]
+[test_chrome_ext_contentscript_unrecognizedprop_warning.html]
+skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
+[test_chrome_ext_hybrid_addons.html]
+[test_chrome_ext_trustworthy_origin.html]
+[test_chrome_ext_webnavigation_resolved_urls.html]
+skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
+[test_chrome_ext_shutdown_cleanup.html]
+[test_chrome_native_messaging_paths.html]
+skip-if = os != "mac" && os != "linux"
+[test_ext_cookies_expiry.html]
+[test_ext_cookies_permissions_bad.html]
+[test_ext_cookies_permissions_good.html]
+[test_ext_cookies_containers.html]
+[test_ext_jsversion.html]
+[test_ext_schema.html]
+[test_chrome_ext_storage_cleanup.html]
+[test_chrome_ext_idle.html]
+[test_chrome_ext_downloads_saveAs.html]
+[test_chrome_ext_webrequest_background_events.html]
+skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
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 000000000..da2f53a02
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/chrome_head.js
@@ -0,0 +1,12 @@
+"use strict";
+
+const {
+ classes: Cc,
+ interfaces: Ci,
+ utils: Cu,
+ results: Cr,
+} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
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 000000000..663ebc611
--- /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 000000000..cc1acc83d
--- /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 000000000..a0a26a2e9
--- /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 000000000..5807dd439
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+"use strict";
+window.close();
+</script>
+</head>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_csp.html b/toolkit/components/extensions/test/mochitest/file_csp.html
new file mode 100644
index 000000000..206e44390
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_csp.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+<img id="bad-image" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" />
+<script id="bad-script" type="text/javascript" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_script_bad.js"></script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_csp.html^headers^ b/toolkit/components/extensions/test/mochitest/file_csp.html^headers^
new file mode 100644
index 000000000..4c6fa3c26
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_csp.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'
diff --git a/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js b/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js
new file mode 100644
index 000000000..06dfae65e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js
@@ -0,0 +1,12 @@
+"use strict";
+
+var {interfaces: Ci} = Components;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+Services.console.registerListener(function listener(message) {
+ if (/WebExt Privilege Escalation/.test(message.message)) {
+ Services.console.unregisterListener(listener);
+ sendAsyncMessage("console-message", {message: message.message});
+ }
+});
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 000000000..4c3be5084
--- /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 000000000..769c63634
--- /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_redirect.png b/toolkit/components/extensions/test/mochitest/file_image_redirect.png
new file mode 100644
index 000000000..4c3be5084
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_image_redirect.png
Binary files differ
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 000000000..f3c7dda58
--- /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_permission_xhr.html b/toolkit/components/extensions/test/mochitest/file_permission_xhr.html
new file mode 100644
index 000000000..22a55f90d
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_permission_xhr.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script>
+"use strict";
+
+/* globals privilegedFetch, privilegedXHR */
+/* eslint-disable mozilla/balanced-listeners */
+
+addEventListener("message", function rcv(event) {
+ removeEventListener("message", rcv, false);
+
+ function assertTrue(condition, description) {
+ postMessage({msg: "assertTrue", condition, description}, "*");
+ }
+
+ function passListener() {
+ assertTrue(true, "Content XHR has no elevated privileges");
+ postMessage({"msg": "finish"}, "*");
+ }
+
+ function failListener() {
+ assertTrue(false, "Content XHR has no elevated privileges");
+ postMessage({"msg": "finish"}, "*");
+ }
+
+ try {
+ new privilegedXHR();
+ assertTrue(false, "Content should not have access to privileged XHR constructor");
+ } catch (e) {
+ assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged XHR constructor");
+ }
+
+ try {
+ new privilegedFetch();
+ assertTrue(false, "Content should not have access to privileged fetch() constructor");
+ } catch (e) {
+ assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged fetch() constructor");
+ }
+
+ let req = new XMLHttpRequest();
+ req.addEventListener("load", failListener);
+ req.addEventListener("error", passListener);
+ req.open("GET", "http://example.org/example.txt");
+ req.send();
+}, false);
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html b/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html
new file mode 100644
index 000000000..258f7058d
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+ <script type="text/javascript">
+ "use strict";
+ throw new Error(`WebExt Privilege Escalation: typeof(browser) = ${typeof(browser)}`);
+ </script>
+</body>
+</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 000000000..a20e49a1f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_sample.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+
+</body>
+</html>
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 000000000..c425122c7
--- /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 000000000..1848edf68
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_script_good.js
@@ -0,0 +1,3 @@
+"use strict";
+
+window.success = window.success ? window.success + 1 : 1;
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 000000000..c89a196c2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_script_redirect.js
@@ -0,0 +1,4 @@
+"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 000000000..07f80eb2e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_script_xhr.js
@@ -0,0 +1,5 @@
+"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_style_bad.css b/toolkit/components/extensions/test/mochitest/file_style_bad.css
new file mode 100644
index 000000000..8dbc8dc7a
--- /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 000000000..46f9774b5
--- /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 000000000..8dbc8dc7a
--- /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_teardown_test.js b/toolkit/components/extensions/test/mochitest/file_teardown_test.js
new file mode 100644
index 000000000..7246012ad
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_teardown_test.js
@@ -0,0 +1,24 @@
+"use strict";
+
+/* globals addMessageListener */
+let {Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+let events = [];
+function record(type, extensionContext) {
+ let eventType = type == "proxy-context-load" ? "load" : "unload";
+ let url = extensionContext.uri.spec;
+ let extensionId = extensionContext.extension.id;
+ events.push({eventType, url, extensionId});
+}
+
+Management.on("proxy-context-load", record);
+Management.on("proxy-context-unload", record);
+addMessageListener("cleanup", () => {
+ Management.off("proxy-context-load", record);
+ Management.off("proxy-context-unload", record);
+});
+
+addMessageListener("get-context-events", extensionId => {
+ sendAsyncMessage("context-events", events);
+ events = [];
+});
+sendAsyncMessage("chromescript-startup");
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 000000000..cba3043f7
--- /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 000000000..c5b436979
--- /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 000000000..574a392a1
--- /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 000000000..d360bcbb1
--- /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 000000000..06dbd4374
--- /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 000000000..307990714
--- /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 000000000..55bb7aa6a
--- /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 000000000..8f589f8bb
--- /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 000000000..af51c2e52
--- /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/head.js b/toolkit/components/extensions/test/mochitest/head.js
new file mode 100644
index 000000000..1b1a29472
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head.js
@@ -0,0 +1,13 @@
+"use strict";
+
+/* exported waitForLoad */
+
+function waitForLoad(win) {
+ return new Promise(resolve => {
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener, true);
+ resolve();
+ }, true);
+ });
+}
+
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 000000000..9f6966551
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_cookies.js
@@ -0,0 +1,167 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported testCookies */
+
+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 cookieSvc = SpecialPowers.Services.cookies;
+
+ let domain = options.domain.replace(/^\.?/, ".");
+
+ // This will be evicted after we add a fourth cookie.
+ cookieSvc.add(domain, "/", "evicted", "bar", options.secure, false, false, options.expiry);
+ // This will be modified by the background script.
+ cookieSvc.add(domain, "/", "foo", "bar", options.secure, false, false, options.expiry);
+ // This will be deleted by the background script.
+ cookieSvc.add(domain, "/", "deleted", "bar", options.secure, false, false, options.expiry);
+
+
+ yield extension.startup();
+
+ yield extension.awaitMessage("change-cookies");
+ cookieSvc.add(domain, "/", "x", "y", options.secure, false, false, options.expiry);
+ cookieSvc.add(domain, "/", "x", "z", options.secure, false, false, options.expiry);
+ cookieSvc.remove(domain, "x", "/", false, {});
+ extension.sendMessage("cookies-changed");
+
+ yield extension.awaitFinish("cookie-permissions");
+ yield extension.unload();
+
+
+ function getCookies(host) {
+ let cookies = [];
+ let enum_ = cookieSvc.getCookiesFromHost(host, {});
+ while (enum_.hasMoreElements()) {
+ cookies.push(enum_.getNext().QueryInterface(SpecialPowers.Ci.nsICookie2));
+ }
+ return cookies.sort((a, b) => String.localeCompare(a.name, b.name));
+ }
+
+ let cookies = getCookies(options.domain);
+ info(`Cookies: ${cookies.map(c => `${c.name}=${c.value}`)}`);
+
+ if (options.shouldPass) {
+ is(cookies.length, 2, "expected two cookies for host");
+
+ is(cookies[0].name, "bar", "correct cookie name");
+ is(cookies[0].value, "quux", "correct cookie value");
+
+ is(cookies[1].name, "foo", "correct cookie name");
+ is(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.
+ is(cookies.length, 3, "expected three cookies for host");
+
+ is(cookies[0].name, "bar", "correct cookie name");
+ is(cookies[0].value, "quux", "correct cookie value");
+
+ is(cookies[1].name, "deleted", "correct cookie name");
+
+ is(cookies[2].name, "foo", "correct cookie name");
+ is(cookies[2].value, "baz", "correct cookie value");
+ } else {
+ is(cookies.length, 2, "expected two cookies for host");
+
+ is(cookies[0].name, "deleted", "correct second cookie name");
+
+ is(cookies[1].name, "foo", "correct cookie name");
+ is(cookies[1].value, "bar", "correct cookie value");
+ }
+
+ for (let cookie of cookies) {
+ cookieSvc.remove(cookie.host, cookie.name, "/", false, {});
+ }
+ // Make sure we don't silently poison subsequent tests if something goes wrong.
+ is(getCookies(options.domain).length, 0, "cookies cleared");
+}
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 000000000..96924e505
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js
@@ -0,0 +1,331 @@
+"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"]],
+ "onResponseStarted": [{urls: ["<all_urls>"]}],
+ "onCompleted": [{urls: ["<all_urls>"]}, ["responseHeaders"]],
+ "onErrorOccurred": [{urls: ["<all_urls>"]}],
+};
+
+function background(events) {
+ let expect;
+ let ignore;
+ let defaultOrigin;
+
+ 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;
+ if (url.protocol == "data:") {
+ // pathname is everything after protocol.
+ filename = url.pathname;
+ } else {
+ 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`);
+ }
+ }
+
+ 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 {
+ expectedEvent = expected.optional_events[0] == name;
+ if (expectedEvent) {
+ expected.optional_events.shift();
+ }
+ }
+ 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") {
+ // 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.assertNotEq(expected.test.requestId, details.requestId,
+ `last requestId ${expected.test.requestId} different from this one ${details.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 bug 1329299");
+ }
+ } else {
+ // 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");
+ }
+ if (name == "onBeforeSendHeaders") {
+ 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);
+ }
+ }
+ if (name == "onSendHeaders") {
+ if (expected.headers && expected.headers.request) {
+ checkHeaders("request", expected, details);
+ }
+ }
+ if (name == "onHeadersReceived") {
+ browser.test.assertEq(expected.status || 200, details.statusCode,
+ `expected HTTP status received for ${details.url}`);
+ if (expected.headers && expected.headers.response) {
+ result.responseHeaders = processHeaders("response", expected, details);
+ }
+ }
+ if (name == "onCompleted") {
+ // 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.
+ 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.assertEq("127.0.0.1", details.ip, `correct ip for ${details.url}`);
+ }
+ if (expected.headers && expected.headers.response) {
+ checkHeaders("response", expected, details);
+ }
+ }
+
+ 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");
+ 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/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini
new file mode 100644
index 000000000..45586237e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -0,0 +1,114 @@
+[DEFAULT]
+support-files =
+ head.js
+ file_mixed.html
+ head_webrequest.js
+ file_csp.html
+ file_csp.html^headers^
+ file_WebRequest_page3.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_WebNavigation_page1.html
+ file_WebNavigation_page2.html
+ file_WebNavigation_page3.html
+ file_with_about_blank.html
+ file_image_good.png
+ file_image_bad.png
+ file_image_redirect.png
+ file_style_good.css
+ file_style_bad.css
+ file_style_redirect.css
+ file_script_good.js
+ file_script_bad.js
+ file_script_redirect.js
+ file_script_xhr.js
+ file_sample.html
+ redirection.sjs
+ file_privilege_escalation.html
+ file_ext_test_api_injection.js
+ file_permission_xhr.html
+ file_teardown_test.js
+ return_headers.sjs
+ webrequest_worker.js
+tags = webextensions
+
+[test_clipboard.html]
+# skip-if = # disabled test case with_permission_allow_copy, see inline comment.
+[test_ext_inIncognitoContext_window.html]
+skip-if = os == 'android' # Android does not currently support windows.
+[test_ext_geturl.html]
+[test_ext_background_canvas.html]
+[test_ext_content_security_policy.html]
+[test_ext_contentscript.html]
+[test_ext_contentscript_api_injection.html]
+[test_ext_contentscript_async_loading.html]
+[test_ext_contentscript_context.html]
+[test_ext_contentscript_create_iframe.html]
+[test_ext_contentscript_devtools_metadata.html]
+[test_ext_contentscript_exporthelpers.html]
+[test_ext_contentscript_css.html]
+[test_ext_contentscript_about_blank.html]
+[test_ext_contentscript_permission.html]
+skip-if = os == 'android' # Android does not support tabs API. Bug 1260250
+[test_ext_contentscript_teardown.html]
+skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
+[test_ext_exclude_include_globs.html]
+[test_ext_i18n_css.html]
+[test_ext_generate.html]
+[test_ext_notifications.html]
+[test_ext_permission_xhr.html]
+[test_ext_runtime_connect.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_runtime_connect_twoway.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_runtime_connect2.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_runtime_disconnect.html]
+[test_ext_runtime_id.html]
+[test_ext_sandbox_var.html]
+[test_ext_sendmessage_reply.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_sendmessage_reply2.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_sendmessage_doublereply.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_sendmessage_no_receiver.html]
+[test_ext_storage_content.html]
+[test_ext_storage_tab.html]
+skip-if = os == 'android' # Android does not currently support tabs.
+[test_ext_test.html]
+[test_ext_cookies.html]
+skip-if = os == 'android' # Bug 1258975 on android.
+[test_ext_background_api_injection.html]
+[test_ext_background_generated_url.html]
+[test_ext_background_teardown.html]
+[test_ext_tab_teardown.html]
+skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
+[test_ext_unload_frame.html]
+[test_ext_i18n.html]
+skip-if = (os == 'android') # Bug 1258975 on android.
+[test_ext_listener_proxies.html]
+[test_ext_web_accessible_resources.html]
+skip-if = (os == 'android') # Bug 1258975 on android.
+[test_ext_webrequest_background_events.html]
+skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
+[test_ext_webrequest_basic.html]
+skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
+[test_ext_webrequest_suspend.html]
+skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
+[test_ext_webrequest_upload.html]
+skip-if = release_or_beta || os == 'android' # webrequest api unsupported (bug 1258975).
+[test_ext_webnavigation.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_webnavigation_filters.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_window_postMessage.html]
+[test_ext_subframes_privileges.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_xhr_capabilities.html]
diff --git a/toolkit/components/extensions/test/mochitest/redirection.sjs b/toolkit/components/extensions/test/mochitest/redirection.sjs
new file mode 100644
index 000000000..370ecd213
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/redirection.sjs
@@ -0,0 +1,4 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 302);
+ aResponse.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 000000000..54e2e5fb4
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/return_headers.sjs
@@ -0,0 +1,20 @@
+/* -*- 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/test_chrome_ext_background_debug_global.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
new file mode 100644
index 000000000..0edf5ea86
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
@@ -0,0 +1,166 @@
+<!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/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+const {
+ XPIProvider,
+} = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+
+/**
+ * This test is asserting that ext-backgroundPage.js successfully sets its
+ * debug global in the AddonWrapper provided by XPIProvider.jsm
+ *
+ * It does _not_ test any functionality in devtools and does not guarantee
+ * debugging is actually working correctly end-to-end.
+ */
+
+function background() {
+ window.testThing = "test!";
+ browser.test.notifyPass("background script ran");
+}
+
+const ID = "debug@tests.mozilla.org";
+let extensionData = {
+ useAddonManager: "temporary",
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ },
+};
+
+add_task(function* () {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("background script ran");
+
+ yield new Promise(function(resolve) {
+ window.BrowserToolboxProcess.emit("connectionchange", "opened", {
+ setAddonOptions(id, options) {
+ if (id === ID) {
+ let context = Cu.waiveXrays(options.global);
+ ok(context.chrome, "global context has a chrome object");
+ ok(context.browser, "global context has a browser object");
+ is("test!", context.testThing, "global context is the background script context");
+ resolve();
+ }
+ },
+ });
+ });
+
+ let addon = yield new Promise((resolve, reject) => {
+ AddonManager.getAddonByID(ID, addon => addon ? resolve(addon) : reject());
+ });
+
+ ok(addon, `Got the addon wrapper for ${addon.id}`);
+
+ function waitForDebugGlobalChanges(times, initialAddonInstanceID) {
+ return new Promise((resolve) => {
+ AddonManager.addAddonListener({
+ count: 0,
+ notNullGlobalsCount: 0,
+ undefinedPrivateWrappersCount: 0,
+ lastAddonInstanceID: initialAddonInstanceID,
+ onPropertyChanged(newAddon, changedPropNames) {
+ if (newAddon.id != addon.id ||
+ !changedPropNames.includes("debugGlobal")) {
+ return;
+ }
+
+ ok(!(newAddon.setDebugGlobal) && !(newAddon.getDebugGlobal),
+ "The addon wrapper should not be a PrivateWrapper");
+
+ let activeAddon = XPIProvider.activeAddons.get(addon.id);
+
+ let addonInstanceID;
+
+ if (!activeAddon) {
+ // The addon has been disable, the preferred global should be null
+ addonInstanceID = this.lastAddonInstanceID;
+ delete this.lastAddonInstanceID;
+ } else {
+ addonInstanceID = activeAddon.instanceID;
+ this.lastAddonInstanceID = addonInstanceID;
+ }
+
+ ok(addonInstanceID, `Got the addon instanceID for ${addon.id}`);
+
+ AddonManager.getAddonByInstanceID(addonInstanceID).then((privateWrapper) => {
+ this.count += 1;
+
+ if (!privateWrapper) {
+ // The addon has been uninstalled
+ this.undefinedPrivateWrappersCount += 1;
+ } else {
+ ok((privateWrapper.getDebugGlobal), "Got the addon PrivateWrapper");
+
+ if (privateWrapper.getDebugGlobal()) {
+ this.notNullGlobalsCount += 1;
+ }
+ }
+
+ if (this.count == times) {
+ AddonManager.removeAddonListener(this);
+ resolve({
+ counters: {
+ count: this.count,
+ notNullGlobalsCount: this.notNullGlobalsCount,
+ undefinedPrivateWrappersCount: this.undefinedPrivateWrappersCount,
+ },
+ lastAddonInstanceID: this.lastAddonInstanceID,
+ });
+ }
+ });
+ },
+ });
+ });
+ }
+
+ // two calls expected, one for the shutdown and one for the startup
+ // of the background page.
+ let waitForDebugGlobalChangesOnReload = waitForDebugGlobalChanges(2);
+
+ info("Addon reload...");
+ yield addon.reload();
+
+ info("Addon completed startup after reload");
+
+ let {
+ counters: reloadCounters,
+ lastAddonInstanceID,
+ } = yield waitForDebugGlobalChangesOnReload;
+
+ isDeeply(reloadCounters, {count: 2, notNullGlobalsCount: 1, undefinedPrivateWrappersCount: 0},
+ "Got the expected number of onPropertyChanged calls on reload");
+
+ // one more call expected for the shutdown.
+ let waitForDebugGlobalChangesOnShutdown = waitForDebugGlobalChanges(1, lastAddonInstanceID);
+
+ info("extension unloading...");
+ yield extension.unload();
+ info("extension unloaded");
+
+ let {counters: unloadCounters} = yield waitForDebugGlobalChangesOnShutdown;
+
+ isDeeply(unloadCounters, {count: 1, notNullGlobalsCount: 0, undefinedPrivateWrappersCount: 1},
+ "Got the expected number of onPropertyChanged calls on shutdown");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html
new file mode 100644
index 000000000..3c4774652
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html
@@ -0,0 +1,84 @@
+<!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/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+Cu.import("resource://testing-common/TestUtils.jsm");
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* testAlertNotShownInBackgroundWindow() {
+ ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(),
+ "Alerts should not be present at the start of the test.");
+
+ let consoleOpened = TestUtils.topicObserved("web-console-created");
+
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ browser.test.log("background script executed");
+
+ alert("I am an alert in the background.");
+
+ browser.test.notifyPass("alertCalled");
+ },
+ });
+
+ yield extension.startup();
+
+ info("startup complete loaded");
+
+ yield extension.awaitFinish("alertCalled");
+
+
+ let alertWindows = Services.wm.getEnumerator("alert:alert");
+ ok(!alertWindows.hasMoreElements(), "Should not show alert");
+
+
+ // Make sure the message we output to the console is seen.
+ // This message is in ext-backgroundPage.js
+ let events = Cc["@mozilla.org/consoleAPI-storage;1"]
+ .getService(Ci.nsIConsoleAPIStorage).getEvents();
+
+ // This is the warning that is output after the first `alert()` call is made.
+ let alertWarningEvent = events[events.length - 2];
+ is(alertWarningEvent.arguments[0], "alert() is not supported in background windows; please use console.log instead.");
+
+ // This is the actual alert text that should be present in the console
+ // instead of as an `alert`.
+ let alertEvent = events[events.length - 1];
+ is(alertEvent.arguments[0], "I am an alert in the background.");
+
+
+ // Wait for the browser console window to open.
+ yield consoleOpened;
+
+ let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ require("devtools/client/framework/devtools-browser");
+ let hudservice = require("devtools/client/webconsole/hudservice");
+
+ // And then double check that we have an actual browser console.
+ let haveConsole = !!hudservice.getBrowserConsole();
+ ok(haveConsole, "Expected browser console to be open");
+
+ if (haveConsole) {
+ yield hudservice.toggleBrowserConsole();
+ }
+
+ yield 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 000000000..e08121a8f
--- /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/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/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(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: Error processing content_scripts.*.unrecognized_property: An unexpected property was found/,
+ }]);
+ });
+
+ yield extension.startup();
+
+ window.open(`${BASE}/file_sample.html`);
+
+ yield Promise.all([extension.awaitFinish("content-script-loaded")]);
+ info("test page loaded");
+
+ yield extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+});
+</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 000000000..c1aaae035
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test downloads.download() saveAs option</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_downloads_saveAs() {
+ function background() {
+ const url = URL.createObjectURL(new Blob(["file content"]));
+ browser.test.onMessage.addListener(async () => {
+ try {
+ let id = await browser.downloads.download({url, saveAs: true});
+ browser.downloads.onChanged.addListener(delta => {
+ if (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 {MockFilePicker} = SpecialPowers;
+ const manifest = {background, manifest: {permissions: ["downloads"]}};
+ const extension = ExtensionTestUtils.loadExtension(manifest);
+
+ MockFilePicker.init(window);
+ MockFilePicker.useAnyFile();
+ const [file] = MockFilePicker.returnFiles;
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("download");
+ let result = yield extension.awaitMessage("done");
+
+ ok(result.ok, "downloads.download() works with saveAs");
+ is(file.fileSize, 12, "downloaded file is the correct size");
+ file.remove(false);
+
+ // Test the user canceling the save dialog.
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+
+ extension.sendMessage("download");
+ result = yield 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(!file.exists(), "file was not downloaded");
+
+ yield extension.unload();
+ MockFilePicker.cleanup();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html
new file mode 100644
index 000000000..ecea8237e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebExtension EventPage Warning</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function createEventPageExtension(eventPage) {
+ function eventPageScript() {
+ browser.test.log("running event page as background script");
+ browser.test.sendMessage("running", 1);
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ "background": eventPage,
+ },
+ files: {
+ "event-page-script.js": eventPageScript,
+ "event-page.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="event-page-script.js"><\/script>
+ </head></html>`,
+ },
+ });
+}
+
+add_task(function* test_eventpages() {
+ // Used in other tests to prevent the monitorConsole to grip.
+ SimpleTest.waitForExplicitFinish();
+
+ let testCases = [
+ {
+ message: "testing event page running as a background page",
+ eventPage: {
+ "page": "event-page.html",
+ "persistent": false,
+ },
+ },
+ {
+ message: "testing event page scripts running as a background page",
+ eventPage: {
+ "scripts": ["event-page-script.js"],
+ "persistent": false,
+ },
+ },
+ ];
+
+ for (let {message, eventPage} of testCases) {
+ info(message);
+
+ // Wait for the expected logged warnings from the manifest validation.
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{message: /Event pages are not currently supported./}]);
+ });
+
+ let extension = createEventPageExtension(eventPage);
+
+ info("load complete");
+ let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ is(x, 1, "got correct value from extension");
+ info("test complete");
+ yield extension.unload();
+ info("extension unloaded successfully");
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+
+ waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{
+ message: /Reading manifest: Error processing background.nonExistentProp: An unexpected property was found/,
+ }]);
+ });
+
+ info("testing additional unrecognized properties on background page");
+
+ extension = createEventPageExtension({
+ "scripts": ["event-page-script.js"],
+ "nonExistentProp": true,
+ });
+
+ info("load complete");
+ [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ is(x, 1, "got correct value from extension");
+ info("test complete");
+ yield extension.unload();
+ info("extension unloaded successfully");
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+ }
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html
new file mode 100644
index 000000000..a74c551f0
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for hybrid addons: SDK or bootstrap.js + embedded WebExtension</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.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/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/**
+ * This test contains additional tests that ensure that an SDK hybrid addon
+ * which is using the new module loader can embed a webextension correctly:
+ *
+ * while the other tests related to the "Embedded WebExtension" are focused
+ * on unit testing a specific component, these tests are testing that a complete
+ * hybrid SDK addon works as expected.
+ *
+ * NOTE: this tests are also the only ones which tests an SDK hybrid addon that
+ * uses the new module loader (the one actually used in production by real world
+ * addons these days), while the Addon SDK "embedded-webextension" test addon
+ * uses the old deprecated module loader (as all the other Addon SDK test addons).
+ */
+
+function generateClassicExtensionFiles({id, files}) {
+ // The addon install.rdf file, as it would be generated by jpm from the addon
+ // package.json metadata.
+ files["install.rdf"] = `<?xml version="1.0" encoding="utf-8"?>
+ <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>${id}</em:id>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+ <em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
+ <em:unpack>false</em:unpack>
+ <em:version>0.1.0</em:version>
+ <em:name>Fake Hybrid Addon</em:name>
+ <em:description>A fake hybrid addon</em:description>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>51.0a1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Fennec -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id>
+ <em:minVersion>51.0a1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </RDF>`;
+
+ // The addon package.json file.
+ files["package.json"] = `{
+ "id": "${id}",
+ "name": "hybrid-addon",
+ "version": "0.1.0",
+ "description": "A fake hybrid addon",
+ "main": "index.js",
+ "engines": {
+ "firefox": ">= 51.0a1",
+ "fennec": ">= 51.0a1"
+ },
+ "license": "MPL-2.0",
+ "hasEmbeddedWebExtension": true
+ }`;
+
+ // The bootstrap file that jpm bundle in any SDK addon built with it.
+ files["bootstrap.js"] = `
+ const { utils: Cu } = Components;
+ const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", "");
+ const COMMONJS_URI = "resource://gre/modules/commonjs";
+ const { require } = Cu.import(COMMONJS_URI + "/toolkit/require.js", {});
+ const { Bootstrap } = require(COMMONJS_URI + "/sdk/addon/bootstrap.js");
+ var { startup, shutdown, install, uninstall } = new Bootstrap(rootURI);
+ `;
+
+ return files;
+}
+
+add_task(function* test_sdk_hybrid_addon_with_jpm_module_loader() {
+ function backgroundScript() {
+ browser.runtime.sendMessage("background message", (reply) => {
+ browser.test.assertEq("sdk received message: background message", reply,
+ "Got the expected reply from the SDK context");
+ browser.test.notifyPass("sdk.webext-api.onmessage");
+ });
+ }
+
+ async function sdkMainScript() {
+ /* globals require */
+ const webext = require("sdk/webextension");
+ let {browser} = await webext.startup();
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ sendReply(`sdk received message: ${msg}`);
+ });
+ }
+
+ let id = "fake@sdk.hybrid.addon";
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files: generateClassicExtensionFiles({
+ id,
+ files: {
+ "index.js": sdkMainScript,
+ "webextension/manifest.json": {
+ name: "embedded webextension name",
+ manifest_version: 2,
+ version: "0.1.0",
+ background: {
+ scripts: ["bg.js"],
+ },
+ },
+ "webextension/bg.js": backgroundScript,
+ },
+ }),
+ }, id);
+
+ extension.startup();
+
+ yield extension.awaitFinish("sdk.webext-api.onmessage");
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html
new file mode 100644
index 000000000..3c3063e67
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html
@@ -0,0 +1,64 @@
+<!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/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService);
+
+add_task(function* testWithRealIdleService() {
+ function background() {
+ browser.test.onMessage.addListener((msg, ...args) => {
+ let detectionInterval = args[0];
+ if (msg == "addListener") {
+ browser.idle.queryState(detectionInterval).then(status => {
+ 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");
+ });
+ } else if (msg == "checkState") {
+ browser.idle.queryState(detectionInterval).then(status => {
+ browser.test.assertEq("idle", status, "Idle status is idle");
+ browser.test.notifyPass("idle");
+ });
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ yield extension.startup();
+ let idleTime = idleService.idleTime;
+ let detectionInterval = Math.max(Math.ceil(idleTime / 1000) + 2, 15);
+ info(`idleTime: ${idleTime}, detectionInterval: ${detectionInterval}`);
+ extension.sendMessage("addListener", detectionInterval);
+ info("Listener added");
+ yield extension.awaitMessage("listenerFired");
+ info("Listener fired");
+ extension.sendMessage("checkState", detectionInterval);
+ yield extension.awaitFinish("idle");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
new file mode 100644
index 000000000..e3098e6b1
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
@@ -0,0 +1,50 @@
+<!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/SpawnTask.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/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/TestUtils.jsm");
+
+const {GlobalManager} = Cu.import("resource://gre/modules/Extension.jsm");
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* testShutdownCleanup() {
+ is(GlobalManager.initialized, false,
+ "GlobalManager start as not initialized");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ browser.test.notifyPass("background page loaded");
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("background page loaded");
+
+ is(GlobalManager.initialized, true,
+ "GlobalManager has been initialized once an extension is started");
+
+ yield extension.unload();
+
+ is(GlobalManager.initialized, false,
+ "GlobalManager has been uninitialized once all the webextensions have been stopped");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
new file mode 100644
index 000000000..010769500
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" 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";
+
+// Test that storage used by a webextension (through localStorage,
+// indexedDB, and browser.storage.local) gets cleaned up when the
+// extension is uninstalled.
+add_task(function* test_uninstall() {
+ function 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();
+ };
+ };
+ });
+
+ let browserStoragePromise = browser.storage.local.set({hello: "world"});
+
+ Promise.all([idbPromise, browserStoragePromise]).then(() => {
+ browser.test.sendMessage("finished");
+ });
+ }
+
+ function 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);
+ };
+ };
+ });
+
+ let browserStoragePromise = browser.storage.local.get("hello").then(result => {
+ return (Object.keys(result).length == 1 && result.hello == "world");
+ });
+
+ Promise.all([idbPromise, browserStoragePromise])
+ .then(([matchIDB, matchBrowserStorage]) => {
+ let result = {matchLocalStorage, matchIDB, matchBrowserStorage};
+ browser.test.sendMessage("results", result);
+ });
+ }
+
+ const ID = "storage.cleanup@tests.mozilla.org";
+
+ // Use a test-only pref to leave the addonid->uuid mapping around after
+ // uninstall so that we can re-attach to the same storage. Also set
+ // the pref to prevent cleaning up storage on uninstall so we can test
+ // that the "keep uuid" logic works correctly. Do the storage flag in
+ // a separate prefEnv so we can pop it below, leaving the uuid flag set.
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.keepUuidOnUninstall", true]],
+ });
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.keepStorageOnUninstall", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: writeData,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+ yield 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 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!
+ yield SpecialPowers.popPrefEnv();
+ extension = ExtensionTestUtils.loadExtension({
+ background: readData,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let results = yield extension.awaitMessage("results");
+ is(results.matchLocalStorage, true, "localStorage data is still present");
+ is(results.matchIDB, true, "indexedDB data is still present");
+ is(results.matchBrowserStorage, true, "browser.storage.local data is still present");
+
+ yield extension.unload();
+
+ // Read again. This time, our data should be gone.
+ extension = ExtensionTestUtils.loadExtension({
+ background: readData,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ results = yield extension.awaitMessage("results");
+ is(results.matchLocalStorage, false, "localStorage data was cleared");
+ is(results.matchIDB, false, "indexedDB data was cleared");
+ is(results.matchBrowserStorage, false, "browser.storage.local data was cleared");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html
new file mode 100644
index 000000000..573c08806
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html
@@ -0,0 +1,53 @@
+<!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/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/**
+ * This test is asserting that moz-extension: URLs are recognized as trustworthy local origins
+ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager",
+ "@mozilla.org/contentsecuritymanager;1",
+ "nsIContentSecurityManager");
+
+add_task(function* () {
+ function background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("/test.html"));
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "test.html": `<html><head></head><body></body></html>`,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let url = yield extension.awaitMessage("ready");
+
+ let uri = NetUtil.newURI(url);
+ let principal = Services.scriptSecurityManager.getCodebasePrincipal(uri);
+ is(gContentSecurityManager.isOriginPotentiallyTrustworthy(principal), true);
+
+ yield extension.unload();
+});
+</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 000000000..768eb31fd
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html
@@ -0,0 +1,83 @@
+<!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/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* webnav_unresolved_uri_on_expected_URI_scheme() {
+ function background() {
+ let checkURLs;
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ if (checkURLs.length > 0) {
+ 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);
+
+ yield extension.startup();
+
+ let checkURLs = [
+ "resource://gre/modules/Services.jsm",
+ "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js",
+ "about:mozilla",
+ ];
+
+ let tabURL = yield extension.awaitMessage("ready");
+ checkURLs.push(tabURL);
+
+ extension.sendMessage("checkURLs", checkURLs);
+
+ for (let url of checkURLs) {
+ window.open(url);
+ yield extension.awaitMessage("next");
+ }
+
+ yield 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 000000000..a13c4d475
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.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";
+
+Cu.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(`recieved ${name}`);
+ }
+
+ for (let name of eventNames) {
+ browser.webRequest[name].addListener(
+ listener.bind(null, name),
+ {urls: ["https://example.com/*"]}
+ );
+ }
+ },
+};
+
+add_task(function* test_webRequest_chromeworker_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ yield extension.startup();
+ yield new Promise(resolve => {
+ let worker = new ChromeWorker("webrequest_chromeworker.js");
+ worker.onmessage = event => {
+ ok("chrome worker fetch finished");
+ resolve();
+ };
+ worker.postMessage("go");
+ });
+ yield extension.unload();
+});
+
+add_task(function* test_webRequest_chromepage_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ yield extension.startup();
+ yield new Promise(resolve => {
+ fetch("https://example.com/example.txt").then(() => {
+ ok("test page loaded");
+ resolve();
+ });
+ });
+ yield extension.unload();
+});
+
+add_task(function* test_webRequest_jsm_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ yield extension.startup();
+ yield testFetch("https://example.com/example.txt").then(() => {
+ ok("fetch page loaded");
+ });
+ yield testXHR("https://example.com/example.txt").then(() => {
+ ok("xhr page loaded");
+ });
+ yield extension.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 000000000..29a148063
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
@@ -0,0 +1,61 @@
+<!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/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* global OS */
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+// Test that the default paths searched for native host manifests
+// are the ones we expect.
+add_task(function* test_default_paths() {
+ let expectUser, expectGlobal;
+ switch (AppConstants.platform) {
+ case "macosx": {
+ expectUser = OS.Path.join(OS.Constants.Path.homeDir,
+ "Library/Application Support/Mozilla/NativeMessagingHosts");
+ expectGlobal = "/Library/Application Support/Mozilla/NativeMessagingHosts";
+
+ break;
+ }
+
+ case "linux": {
+ expectUser = OS.Path.join(OS.Constants.Path.homeDir, ".mozilla/native-messaging-hosts");
+
+ const libdir = AppConstants.HAVE_USR_LIB64_DIR ? "lib64" : "lib";
+ expectGlobal = OS.Path.join("/usr", libdir, "mozilla/native-messaging-hosts");
+ 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("XREUserNativeMessaging", Ci.nsIFile).path;
+ is(userDir, expectUser, "user-specific native messaging directory is correct");
+
+ let globalDir = Services.dirsvc.get("XRESysNativeMessaging", 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_clipboard.html b/toolkit/components/extensions/test/mochitest/test_clipboard.html
new file mode 100644
index 000000000..900ee5f10
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_clipboard.html
@@ -0,0 +1,140 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>clipboard permission test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.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";
+
+function doCopy(txt) {
+ let field = document.createElement("textarea");
+ document.body.appendChild(field);
+ field.value = txt;
+ field.select();
+ return document.execCommand("copy");
+}
+
+add_task(function* no_permission_deny_copy() {
+ function backgroundScript() {
+ browser.test.assertEq(false, doCopy("whatever"),
+ "copy should be denied without permission");
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ background: `${doCopy};(${backgroundScript})();`,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitMessage("ready");
+
+ yield extension.unload();
+});
+
+/** Selecting text in a bg page is not possible, skip test until it's fixed.
+add_task(function* with_permission_allow_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: `${doCopy};(${backgroundScript})();`,
+ manifest: {
+ permissions: [
+ "clipboardWrite",
+ ],
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ const DUMMY_STR = "dummy string to copy";
+ yield new Promise(resolve => {
+ SimpleTest.waitForClipboard(DUMMY_STR, () => {
+ extension.sendMessage(DUMMY_STR);
+ }, resolve, resolve);
+ });
+
+ yield extension.unload();
+}); */
+
+add_task(function* content_script_no_permission_deny_copy() {
+ function contentScript() {
+ browser.test.assertEq(false, doCopy("whatever"),
+ "copy should be denied without permission");
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": `${doCopy};(${contentScript})();`,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitMessage("ready");
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* content_script_with_permission_allow_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: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardWrite",
+ ],
+ },
+ files: {
+ "contentscript.js": `${doCopy};(${contentScript})();`,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitMessage("ready");
+
+ const DUMMY_STR = "dummy string to copy in content script";
+ yield new Promise(resolve => {
+ SimpleTest.waitForClipboard(DUMMY_STR, () => {
+ extension.sendMessage(DUMMY_STR);
+ }, resolve, resolve);
+ });
+
+ win.close();
+
+ yield extension.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 000000000..0f617c37e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
@@ -0,0 +1,158 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// 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.getURL",
+ "runtime.id",
+ "runtime.lastError",
+ "runtime.onConnect",
+ "runtime.onMessage",
+ "runtime.sendMessage",
+ // If you want to add a new powerful test API, please see bug 1287233.
+ "test.assertEq",
+ "test.assertFalse",
+ "test.assertRejects",
+ "test.assertThrows",
+ "test.assertTrue",
+ "test.fail",
+ "test.log",
+ "test.notifyFail",
+ "test.notifyPass",
+ "test.onMessage",
+ "test.sendMessage",
+ "test.succeed",
+];
+
+let expectedContentApis = [
+ ...expectedCommonApis,
+ ...expectedContentApisTargetSpecific,
+];
+
+let expectedBackgroundApis = [
+ ...expectedCommonApis,
+ ...expectedBackgroundApisTargetSpecific,
+ "extension.ViewType",
+ "extension.getBackgroundPage",
+ "extension.getViews",
+ "extension.isAllowedFileSchemeAccess",
+ "extension.isAllowedIncognitoAccess",
+ // Note: extensionTypes is not visible in Chrome.
+ "extensionTypes.ImageFormat",
+ "extensionTypes.RunAt",
+ "management.ExtensionDisabledReason",
+ "management.ExtensionInstallType",
+ "management.ExtensionType",
+ "management.getSelf",
+ "management.uninstallSelf",
+ "runtime.getBackgroundPage",
+ "runtime.getBrowserInfo",
+ "runtime.getPlatformInfo",
+ "runtime.onInstalled",
+ "runtime.onStartup",
+ "runtime.onUpdateAvailable",
+ "runtime.openOptionsPage",
+ "runtime.reload",
+ "runtime.setUninstallURL",
+];
+
+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";
+ }
+ 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 (let key in obj) {
+ let val = obj[key];
+ if (typeof val == "object" && val !== null && mayRecurse(key, val)) {
+ diveDeeper(`${path}.${key}`, val);
+ } else if (val !== undefined) {
+ results.push(`${path}.${key}`);
+ }
+ }
+ }
+ diveDeeper("browser", browser);
+ diveDeeper("chrome", chrome);
+ browser.test.sendMessage("allApis", results.sort());
+}
+
+add_task(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);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ let actualApis = yield extension.awaitMessage("allApis");
+ win.close();
+ let expectedApis = generateExpectations(expectedContentApis);
+ isDeeply(actualApis, expectedApis, "content script APIs");
+
+ yield extension.unload();
+});
+
+add_task(function* test_enumerate_background_script_apis() {
+ let extensionData = {
+ background: sendAllApis,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ let actualApis = yield extension.awaitMessage("allApis");
+ let expectedApis = generateExpectations(expectedBackgroundApis);
+ isDeeply(actualApis, expectedApis, "background script APIs");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html b/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html
new file mode 100644
index 000000000..f43a59f81
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for privilege escalation into content pages</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* testBackgroundWindow() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+
+ browser.test.log("background script executed");
+ window.location = `${BASE}/file_privilege_escalation.html`;
+ },
+ });
+
+ let awaitConsole = new Promise(resolve => {
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_ext_test_api_injection.js"));
+
+ chromeScript.addMessageListener("console-message", resolve);
+ });
+
+ yield extension.startup();
+
+ let message = yield awaitConsole;
+
+ ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"),
+ "Document does not have `browser` APIs.");
+
+ yield 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 000000000..bff7190cb
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for background page canvas rendering</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(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 extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+ yield extension.awaitFinish("background-canvas");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html
new file mode 100644
index 000000000..f4fcf3d34
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test _generated_background_page.html</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+add_task(function* test_url_of_generated_background_page() {
+ function backgroundScript() {
+ const EXPECTED_URL = browser.runtime.getURL("/_generated_background_page.html");
+ browser.test.assertEq(EXPECTED_URL, location.href);
+ browser.test.sendMessage("script done", EXPECTED_URL);
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ background: {
+ scripts: ["bg.js"],
+ },
+ web_accessible_resources: ["_generated_background_page.html"],
+ },
+ files: {
+ "bg.js": backgroundScript,
+ },
+ });
+
+ yield extension.startup();
+ const EXPECTED_URL = yield extension.awaitMessage("script done");
+
+ let win = window.open(EXPECTED_URL);
+ ok(win, "Should open new tab at URL: " + EXPECTED_URL);
+ yield extension.awaitMessage("script done");
+ win.close();
+
+ yield extension.unload();
+});
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html
new file mode 100644
index 000000000..bb6b2e970
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for background script teardown</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+add_task(function* test_background_reload_and_unload() {
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("reload-background", msg);
+ location.reload();
+ });
+ browser.test.sendMessage("background-url", location.href);
+ }
+
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_teardown_test.js"));
+ yield chromeScript.promiseOneMessage("chromescript-startup");
+
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ function* getContextEvents() {
+ chromeScript.sendAsyncMessage("get-context-events");
+ let contextEvents = yield chromeScript.promiseOneMessage("context-events");
+ return contextEvents.filter(event => event.extensionId == extension.id);
+ }
+ yield extension.startup();
+ let backgroundUrl = yield extension.awaitMessage("background-url");
+
+ let contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1,
+ "ExtensionContext state change after loading an extension");
+ is(contextEvents[0].eventType, "load");
+ is(contextEvents[0].url, backgroundUrl,
+ "The ExtensionContext should be the background page");
+
+ extension.sendMessage("reload-background");
+ yield extension.awaitMessage("background-url");
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 2,
+ "ExtensionContext state changes after reloading the background page");
+ is(contextEvents[0].eventType, "unload",
+ "Unload ExtensionContext of background page");
+ is(contextEvents[0].url, backgroundUrl, "ExtensionContext URL = background");
+ is(contextEvents[1].eventType, "load",
+ "Create new ExtensionContext for background page");
+ is(contextEvents[1].url, backgroundUrl, "ExtensionContext URL = background");
+ yield extension.unload();
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1,
+ "ExtensionContext state change after unloading the extension");
+ is(contextEvents[0].eventType, "unload",
+ "Unload ExtensionContext for background page after extension unloads");
+ is(contextEvents[0].url, backgroundUrl, "ExtensionContext URL = background");
+
+ chromeScript.sendAsyncMessage("cleanup");
+ chromeScript.destroy();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html b/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html
new file mode 100644
index 000000000..a36f29563
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension CSP test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+/**
+ * Tests that content security policies for an add-on are actually applied to *
+ * documents that belong to it. This tests both the base policies and add-on
+ * specific policies, and ensures that the parsed policies applied to the
+ * document's principal match what was specified in the policy string.
+ *
+ * @param {object} [customCSP]
+ */
+function* testPolicy(customCSP = null) {
+ let baseURL;
+
+ let baseCSP = {
+ "object-src": ["blob:", "filesystem:", "https://*", "moz-extension:", "'self'"],
+ "script-src": ["'unsafe-eval'", "'unsafe-inline'", "blob:", "filesystem:", "https://*", "moz-extension:", "'self'"],
+ };
+
+ let addonCSP = {
+ "object-src": ["'self'"],
+ "script-src": ["'self'"],
+ };
+
+ let content_security_policy = null;
+
+ if (customCSP) {
+ for (let key of Object.keys(customCSP)) {
+ addonCSP[key] = customCSP[key].split(/\s+/);
+ }
+
+ content_security_policy = Object.keys(customCSP)
+ .map(key => `${key} ${customCSP[key]}`)
+ .join("; ");
+ }
+
+
+ function filterSelf(sources) {
+ return sources.map(src => src == "'self'" ? baseURL : src);
+ }
+
+ function checkSource(name, policy, expected) {
+ is(JSON.stringify(policy[name].sort()),
+ JSON.stringify(filterSelf(expected[name]).sort()),
+ `Expected value for ${name}`);
+ }
+
+ function checkCSP(csp, location) {
+ let policies = csp["csp-policies"];
+
+ info(`Base policy for ${location}`);
+
+ is(policies[0]["report-only"], false, "Policy is not report-only");
+ checkSource("object-src", policies[0], baseCSP);
+ checkSource("script-src", policies[0], baseCSP);
+
+ info(`Add-on policy for ${location}`);
+
+ is(policies[1]["report-only"], false, "Policy is not report-only");
+ checkSource("object-src", policies[1], addonCSP);
+ checkSource("script-src", policies[1], addonCSP);
+ }
+
+
+ function getCSP(window) {
+ let {cspJSON} = SpecialPowers.Cu.getObjectPrincipal(window);
+ return JSON.parse(cspJSON);
+ }
+
+ function background(getCSPFn) {
+ browser.test.sendMessage("base-url", browser.extension.getURL("").replace(/\/$/, ""));
+
+ browser.test.sendMessage("background-csp", getCSPFn(window));
+ }
+
+ function tabScript(getCSPFn) {
+ browser.test.sendMessage("tab-csp", getCSPFn(window));
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${background})(${getCSP})`,
+
+ files: {
+ "tab.html": `<html><head><meta charset="utf-8">
+ <script src="tab.js"></${"script"}></head></html>`,
+
+ "tab.js": `(${tabScript})(${getCSP})`,
+
+ "content.html": `<html><head><meta charset="utf-8"></head></html>`,
+ },
+
+ manifest: {
+ content_security_policy,
+
+ web_accessible_resources: ["content.html", "tab.html"],
+ },
+ });
+
+
+ info(`Testing CSP for policy: ${content_security_policy}`);
+
+ yield extension.startup();
+
+ baseURL = yield extension.awaitMessage("base-url");
+
+
+ let win1 = window.open(`${baseURL}/tab.html`);
+
+ let frame = document.createElement("iframe");
+ frame.src = `${baseURL}/content.html`;
+ document.body.appendChild(frame);
+
+ yield new Promise(resolve => {
+ frame.onload = resolve;
+ });
+
+
+ let backgroundCSP = yield extension.awaitMessage("background-csp");
+ checkCSP(backgroundCSP, "background page");
+
+ let tabCSP = yield extension.awaitMessage("tab-csp");
+ checkCSP(tabCSP, "tab page");
+
+ let contentCSP = getCSP(frame.contentWindow);
+ checkCSP(contentCSP, "content frame");
+
+
+ win1.close();
+ frame.remove();
+
+ yield extension.unload();
+}
+
+add_task(function* testCSP() {
+ yield testPolicy(null);
+
+ let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='";
+
+ yield testPolicy({
+ "object-src": "'self' https://*.example.com",
+ "script-src": `'self' https://*.example.com 'unsafe-eval' ${hash}`,
+ });
+
+ yield testPolicy({
+ "object-src": "'none'",
+ "script-src": `'self'`,
+ });
+});
+</script>
+</body>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
new file mode 100644
index 000000000..39f1bfabd
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
@@ -0,0 +1,116 @@
+<!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/SpawnTask.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(function* test_contentscript() {
+ function background() {
+ browser.runtime.onMessage.addListener(([msg, expectedStates, readyState], sender) => {
+ if (msg == "chrome-namespace-ok") {
+ browser.test.sendMessage(msg);
+ return;
+ }
+
+ browser.test.assertEq("script-run", msg, "message type is correct");
+ browser.test.assertTrue(expectedStates.includes(readyState),
+ `readyState "${readyState}" is one of [${expectedStates}]`);
+ browser.test.sendMessage("script-run-" + expectedStates[0]);
+ });
+ }
+
+ function contentScriptStart() {
+ browser.runtime.sendMessage(["script-run", ["loading"], document.readyState]);
+ }
+ function contentScriptEnd() {
+ browser.runtime.sendMessage(["script-run", ["interactive", "complete"], document.readyState]);
+ }
+ function contentScriptIdle() {
+ browser.runtime.sendMessage(["script-run", ["complete"], document.readyState]);
+ }
+
+ function contentScript() {
+ let manifest = browser.runtime.getManifest();
+ void manifest.applications.gecko.id;
+ chrome.runtime.sendMessage(["chrome-namespace-ok"]);
+ }
+
+ let extensionData = {
+ manifest: {
+ applications: {gecko: {id: "contentscript@tests.mozilla.org"}},
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script_start.js"],
+ "run_at": "document_start",
+ },
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script_end.js"],
+ "run_at": "document_end",
+ },
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script_idle.js"],
+ "run_at": "document_idle",
+ },
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ },
+ background,
+
+ files: {
+ "content_script_start.js": contentScriptStart,
+ "content_script_end.js": contentScriptEnd,
+ "content_script_idle.js": contentScriptIdle,
+ "content_script.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let loadingCount = 0;
+ let interactiveCount = 0;
+ let completeCount = 0;
+ extension.onMessage("script-run-loading", () => { loadingCount++; });
+ extension.onMessage("script-run-interactive", () => { interactiveCount++; });
+
+ let completePromise = new Promise(resolve => {
+ extension.onMessage("script-run-complete", () => { completeCount++; resolve(); });
+ });
+
+ let chromeNamespacePromise = extension.awaitMessage("chrome-namespace-ok");
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), completePromise, chromeNamespacePromise]);
+ info("test page loaded");
+
+ win.close();
+
+ is(loadingCount, 1, "document_start script ran exactly once");
+ is(interactiveCount, 1, "document_end script ran exactly once");
+ is(completeCount, 1, "document_idle script ran exactly once");
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</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 000000000..3766678e7
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html
@@ -0,0 +1,117 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test content script match_about_blank option</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" 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(function* test_contentscript_about_blank() {
+ const manifest = {
+ content_scripts: [
+ {
+ match_about_blank: true,
+ matches: ["http://mochi.test/*/file_with_about_blank.html", "http://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});
+ yield extension.startup();
+
+ let count = 0;
+ extension.onMessage("script", script => {
+ info(`script ran: ${script}`);
+ count++;
+ });
+
+ let win = window.open("http://example.com/" + PATH);
+ yield 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);
+ yield 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, "transparent", "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();
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html
new file mode 100644
index 000000000..abf3d349f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for privilege escalation into iframe with content script APIs</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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>
+
+<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it -->
+<textarea id="test-asset">
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="./content_script_iframe.js">
+ </script>
+ </head>
+ </html>
+</textarea>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_api_injection() {
+ function contentScript() {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html"));
+ document.body.appendChild(iframe);
+ }
+
+ function contentScriptIframe() {
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+ window.location = `${BASE}/file_privilege_escalation.html`;
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ "web_accessible_resources": [
+ "content_script_iframe.html",
+ ],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ "content_script_iframe.js": contentScriptIframe,
+ "content_script_iframe.html": document.querySelector("#test-asset").textContent,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let awaitConsole = new Promise(resolve => {
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_ext_test_api_injection.js"));
+
+ chromeScript.addMessageListener("console-message", resolve);
+ });
+
+ yield extension.startup();
+ info("extension loaded");
+
+ let win = window.open("file_sample.html");
+
+ let message = yield awaitConsole;
+
+ ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"),
+ "Document does not have `browser` APIs.");
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html
new file mode 100644
index 000000000..d78f7ce02
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test content script async loading</title>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <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(function* test_async_loading() {
+ const adder = `(function add(a = 1) { this.count += a; })();\n`;
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ matches: ["https://example.org/"],
+ js: ["first.js", "second.js"],
+ }],
+ },
+ files: {
+ "first.js": `
+ this.count = 0;
+ ${adder.repeat(50000)}; // 2Mb
+ browser.test.assertEq(this.count, 50000, "A 50k line script");
+
+ this.order = (this.order || 0) + 1;
+ browser.test.sendMessage("first", this.order);
+ `,
+ "second.js": `
+ this.order = (this.order || 0) + 1;
+ browser.test.sendMessage("second", this.order);
+ `,
+ },
+ });
+
+ yield extension.startup();
+ const win = window.open("https://example.org/");
+
+ const [first, second] = yield Promise.all([
+ extension.awaitMessage("first"),
+ extension.awaitMessage("second"),
+ ]);
+
+ is(first, 1, "first.js finished execution first.");
+ is(second, 2, "second.js finished execution second.");
+
+ yield extension.unload();
+ win.close();
+});
+
+</script>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
new file mode 100644
index 000000000..97b1645dd
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script contexts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* test_contentscript_context() {
+ function contentScript() {
+ browser.test.sendMessage("content-script-ready");
+
+ window.addEventListener("pagehide", () => {
+ browser.test.sendMessage("content-script-hide");
+ }, true);
+ window.addEventListener("pageshow", () => {
+ browser.test.sendMessage("content-script-show");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://example.com/"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+ });
+
+ yield extension.startup();
+
+ let win = window.open("http://example.com/");
+ yield extension.awaitMessage("content-script-ready");
+ yield extension.awaitMessage("content-script-show");
+
+ // Get the content script context and check that it points to the correct window.
+
+ let {DocumentManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionContent.jsm", {});
+ let context = DocumentManager.getContentScriptContext(extension, win);
+ ok(context != null, "Got content script context");
+
+ is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct");
+
+ // Navigate so that the content page is hidden in the bfcache.
+
+ win.location = "http://example.org/";
+ yield extension.awaitMessage("content-script-hide");
+
+ is(context.contentWindow, null, "Context's contentWindow property is null");
+
+ // Navigate back so the content page is resurrected from the bfcache.
+
+ SpecialPowers.wrap(win).history.back();
+ yield extension.awaitMessage("content-script-show");
+
+ is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct");
+
+ win.close();
+
+ yield extension.awaitMessage("content-script-hide");
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
new file mode 100644
index 000000000..8aac3e213
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
@@ -0,0 +1,165 @@
+<!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/SpawnTask.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>
+
+<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it -->
+<textarea id="test-asset">
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="content_script_iframe.js"></script>
+ </head>
+ </html>
+</textarea>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_create_iframe() {
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ let {name, availableAPIs, manifest, testGetManifest} = msg;
+ let hasExtTabsAPI = availableAPIs.indexOf("tabs") > 0;
+ let hasExtWindowsAPI = availableAPIs.indexOf("windows") > 0;
+
+ browser.test.assertFalse(hasExtTabsAPI, "the created iframe should not be able to use privileged APIs (tabs)");
+ browser.test.assertFalse(hasExtWindowsAPI, "the created iframe should not be able to use privileged APIs (windows)");
+
+ let {applications: {gecko: {id: expectedManifestGeckoId}}} = chrome.runtime.getManifest();
+ let {applications: {gecko: {id: actualManifestGeckoId}}} = manifest;
+
+ browser.test.assertEq(actualManifestGeckoId, expectedManifestGeckoId,
+ "the add-on manifest should be accessible from the created iframe"
+ );
+
+ let {applications: {gecko: {id: testGetManifestGeckoId}}} = testGetManifest;
+
+ browser.test.assertEq(testGetManifestGeckoId, expectedManifestGeckoId,
+ "GET_MANIFEST() returns manifest data before extension unload"
+ );
+
+ browser.test.sendMessage(name);
+ });
+ }
+
+ function contentScript() {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html"));
+ document.body.appendChild(iframe);
+ }
+
+ function contentScriptIframe() {
+ window.GET_MANIFEST = browser.runtime.getManifest.bind(null);
+
+ window.testGetManifestException = () => {
+ try {
+ window.GET_MANIFEST();
+ } catch (exception) {
+ return String(exception);
+ }
+ };
+
+ let testGetManifest = window.GET_MANIFEST();
+
+ let manifest = browser.runtime.getManifest();
+ let availableAPIs = Object.keys(browser);
+
+ browser.runtime.sendMessage({
+ name: "content-script-iframe-loaded",
+ availableAPIs,
+ manifest,
+ testGetManifest,
+ });
+ }
+
+ const ID = "contentscript@tests.mozilla.org";
+ let extensionData = {
+ manifest: {
+ applications: {gecko: {id: ID}},
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ web_accessible_resources: [
+ "content_script_iframe.html",
+ ],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": contentScript,
+ "content_script_iframe.html": document.querySelector("#test-asset").textContent,
+ "content_script_iframe.js": contentScriptIframe,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let contentScriptIframeCreatedPromise = new Promise(resolve => {
+ extension.onMessage("content-script-iframe-loaded", () => { resolve(); });
+ });
+
+ yield extension.startup();
+ info("extension loaded");
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), contentScriptIframeCreatedPromise]);
+ info("content script privileged iframe loaded and executed");
+
+ info("testing APIs availability once the extension is unloaded...");
+
+ let iframeWindow = SpecialPowers.wrap(win)[0];
+
+ ok(iframeWindow, "content script enabled iframe found");
+ ok(/content_script_iframe\.html$/.test(iframeWindow.location), "the found iframe has the expected URL");
+
+ yield extension.unload();
+ info("extension unloaded");
+
+ info("test content script APIs not accessible from the frame once the extension is unloaded");
+
+ let ww = SpecialPowers.Cu.waiveXrays(iframeWindow);
+ let isDeadWrapper = SpecialPowers.Cu.isDeadWrapper(ww.browser);
+ ok(!isDeadWrapper, "the API object should not be a dead object");
+
+ let manifest;
+ let manifestException;
+
+ try {
+ manifest = ww.browser.runtime.getManifest();
+ } catch (e) {
+ manifestException = e;
+ }
+
+ ok(!manifest, "manifest should be undefined");
+
+ is(String(manifestException), "TypeError: ww.browser.runtime is undefined",
+ "expected exception received");
+
+ let getManifestException = ww.testGetManifestException();
+
+ is(getManifestException, "TypeError: can't access dead object",
+ "expected exception received");
+
+ win.close();
+
+ info("done");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html
new file mode 100644
index 000000000..5630a1d68
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html
@@ -0,0 +1,48 @@
+<!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/SpawnTask.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(function* test_content_script_css() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "css": ["content.css"],
+ }],
+ },
+
+ files: {
+ "content.css": "body { max-width: 42px; }",
+ },
+ });
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield waitForLoad(win);
+
+ let style = win.getComputedStyle(win.document.body);
+ is(style.maxWidth, "42px", "Stylesheet correctly applied");
+
+ yield extension.unload();
+
+ style = win.getComputedStyle(win.document.body);
+ is(style.maxWidth, "none", "Stylesheet correctly removed");
+
+ win.close();
+});
+</script>
+
+</body>
+</html>
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 000000000..137a3cda4
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Sandbox metadata on WebExtensions ContentScripts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(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);
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ let innerWindowID = SpecialPowers.wrap(win)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+
+ yield extension.awaitFinish("contentScript.executed");
+
+ const {ExtensionContent} = SpecialPowers.Cu.import(
+ "resource://gre/modules/ExtensionContent.jsm", {}
+ );
+
+ let res = ExtensionContent.getContentScriptGlobalsForWindow(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");
+
+ yield extension.unload();
+ info("extension unloaded");
+
+ res = ExtensionContent.getContentScriptGlobalsForWindow(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_exporthelpers.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html
new file mode 100644
index 000000000..f3414901d
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html
@@ -0,0 +1,95 @@
+<!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/SpawnTask.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";
+
+add_task(function* test_contentscript_exportHelpers() {
+ function contentScript() {
+ browser.test.assertTrue(typeof cloneInto === "function");
+ browser.test.assertTrue(typeof createObjectIn === "function");
+ browser.test.assertTrue(typeof exportFunction === "function");
+
+ /* globals exportFunction, precisePi, reportPi */
+ let value = 3.14;
+ exportFunction(() => value, window, {defineAs: "precisePi"});
+
+ browser.test.assertEq("undefined", typeof precisePi,
+ "exportFunction should export to the page's scope only");
+
+ browser.test.assertEq("undefined", typeof window.precisePi,
+ "exportFunction should export to the page's scope only");
+
+ let results = [];
+ exportFunction(pi => results.push(pi), window, {defineAs: "reportPi"});
+
+ let s = document.createElement("script");
+ s.textContent = `(${function() {
+ let result1 = "unknown 1";
+ let result2 = "unknown 2";
+ try {
+ result1 = precisePi();
+ } catch (e) {
+ result1 = "err:" + e;
+ }
+ try {
+ result2 = window.precisePi();
+ } catch (e) {
+ result2 = "err:" + e;
+ }
+ reportPi(result1);
+ reportPi(result2);
+ }})();`;
+
+ document.documentElement.appendChild(s);
+ // Inline script ought to run synchronously.
+
+ browser.test.assertEq(3.14, results[0],
+ "exportFunction on window should define a global function");
+ browser.test.assertEq(3.14, results[1],
+ "exportFunction on window should export a property to window.");
+
+ browser.test.assertEq(2, results.length,
+ "Expecting the number of results to match the number of method calls");
+
+ browser.test.notifyPass("export helper test completed");
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ run_at: "document_start",
+ }],
+ },
+
+ files: {
+ "contentscript.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield extension.awaitFinish("export helper test completed");
+ win.close();
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
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 000000000..a2f38dce6
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script private browsing ID</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_contentscript_incognito() {
+ let extension = ExtensionTestUtils.loadExtension({
+ 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");
+ }
+ },
+ },
+ });
+
+ yield extension.startup();
+
+ extension.sendMessage(["open-window", SimpleTest.getTestFileURL("file_sample.html")]);
+
+ yield extension.awaitFinish("cookies");
+
+ extension.sendMessage(["close-window"]);
+ yield extension.awaitMessage("done");
+
+ yield extension.unload();
+});
+</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 000000000..eaf815092
--- /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 type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_contentscript() {
+ function background() {
+ browser.test.onMessage.addListener(url => {
+ browser.tabs.create({url}).then(tab => {
+ return browser.tabs.executeScript(tab.id, {code: "true;"})
+ .then(() => {
+ browser.test.sendMessage("executed", true);
+ browser.tabs.remove([tab.id]);
+ }, err => {
+ browser.test.sendMessage("executed", false);
+ browser.tabs.remove([tab.id]);
+ });
+ });
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["<all_urls>"],
+ },
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ extension.sendMessage("https://example.com");
+ let result = yield extension.awaitMessage("executed");
+ is(result, true, "Content script can be run in a page without mozAddonManager");
+
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+
+ extension.sendMessage("https://example.com");
+ result = yield extension.awaitMessage("executed");
+ is(result, false, "Content script cannot be run in a page with mozAddonManager");
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html
new file mode 100644
index 000000000..33a8c4ccc
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script teardown</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+add_task(function* test_contentscript_reload_and_unload() {
+ function contentScript() {
+ browser.test.sendMessage("contentscript-run");
+ }
+ function background() {
+ let removedTabs = 0;
+ browser.tabs.onRemoved.addListener(() => {
+ browser.test.assertEq(1, ++removedTabs,
+ "Expected only one tab to be removed during the test");
+ browser.test.sendMessage("tab-closed");
+ });
+ }
+
+ let extensionData = {
+ background,
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["contentscript.js"],
+ }],
+ },
+
+ files: {
+ "contentscript.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_teardown_test.js"));
+ yield chromeScript.promiseOneMessage("chromescript-startup");
+ function* getContextEvents() {
+ chromeScript.sendAsyncMessage("get-context-events");
+ let contextEvents = yield chromeScript.promiseOneMessage("context-events");
+ return contextEvents.filter(event => event.extensionId == extension.id);
+ }
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitMessage("contentscript-run");
+ let tabUrl = win.location.href;
+
+ let contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1,
+ "ExtensionContext state change after loading a content script");
+ is(contextEvents[0].eventType, "load",
+ "Create ExtensionContext for content script");
+ is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page");
+
+ let promiseReload = extension.awaitMessage("contentscript-run");
+ win.location.reload();
+ yield promiseReload;
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 2,
+ "ExtensionContext state changes after reloading a content script");
+ is(contextEvents[0].eventType, "unload", "Unload old ExtensionContext");
+ is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page");
+ is(contextEvents[1].eventType, "load",
+ "Create new ExtensionContext for content script");
+ is(contextEvents[1].url, tabUrl, "ExtensionContext URL = page");
+
+ let tabClosePromise = extension.awaitMessage("tab-closed");
+ win.close();
+ yield tabClosePromise;
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1,
+ "ExtensionContext state change after unloading a content script");
+ is(contextEvents[0].eventType, "unload",
+ "Unload ExtensionContext after closing the tab with the content script");
+ is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page");
+
+ chromeScript.sendAsyncMessage("cleanup");
+ chromeScript.destroy();
+ yield 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 000000000..d414a4e46
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
@@ -0,0 +1,234 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_cookies() {
+ 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");
+ }
+
+ 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,
+ session: false,
+ expirationDate: THE_FUTURE,
+ storeId: STORE_ID,
+ };
+
+ 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(cookies.length, 1, "one cookie found for matching name");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({domain: "example.org"});
+ browser.test.assertEq(cookies.length, 1, "one cookie found for matching domain");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({domain: "example.net"});
+ browser.test.assertEq(cookies.length, 0, "no cookies found for non-matching domain");
+
+ cookies = await browser.cookies.getAll({secure: false});
+ browser.test.assertEq(cookies.length, 1, "one non-secure cookie found");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({secure: true});
+ browser.test.assertEq(cookies.length, 0, "no secure cookies found");
+
+ cookies = await browser.cookies.getAll({storeId: STORE_ID});
+ browser.test.assertEq(cookies.length, 1, "one cookie found for valid storeId");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({storeId: "invalid_id"});
+ browser.test.assertEq(cookies.length, 0, "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}, 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 tab returned for store");
+
+ {
+ let privateWindow = await browser.windows.create({incognito: true});
+ 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(privateWindow.id);
+ }
+
+ 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}, 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(cookies.length, 1, "one session cookie found");
+ browser.test.assertEq(true, cookies[0].session, "found session cookie");
+
+ cookies = await browser.cookies.getAll({session: false});
+ browser.test.assertEq(cookies.length, 0, "no non-session cookies found");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "name1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, 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(cookies.length, 1, "one secure cookie found");
+ browser.test.assertEq(true, cookies[0].secure, "found secure cookie");
+
+ cookies = await browser.cookies.getAll({secure: false});
+ browser.test.assertEq(cookies.length, 0, "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}, 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(cookies.length, 1, "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(cookies.length, 0, "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}, 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}, 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");
+
+ {
+ let privateWindow = await browser.windows.create({incognito: true});
+
+ // Hacky work-around for bugzil.la/1309637
+ await new Promise(resolve => setTimeout(resolve, 700));
+
+ 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");
+
+ let details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: STORE_ID});
+ assertExpected({url: TEST_URL, name: "store", storeId: STORE_ID}, 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}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+ browser.test.assertEq(null, cookie, "deleted the private cookie");
+
+ await browser.windows.remove(privateWindow.id);
+ }
+
+ browser.test.notifyPass("cookies");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["cookies", "*://example.org/"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("cookies");
+ yield 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 000000000..bc4994eec
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.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";
+
+add_task(function* setup() {
+ // make sure userContext is enabled.
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["privacy.userContext.enabled", true],
+ ]});
+});
+
+add_task(function* test_cookie_containers() {
+ 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");
+ }
+
+ 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,
+ session: false,
+ expirationDate: THE_FUTURE,
+ storeId: "firefox-container-1",
+ };
+
+ 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, cookies.length, "getAll() with default storeId returns an empty array");
+
+ 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"}, 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/"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("cookies");
+ yield 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 000000000..3927d9e94
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.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";
+
+add_task(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 domain = ".example.com";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["http://example.com/", "cookies"],
+ },
+ background,
+ });
+
+ let cookieSvc = SpecialPowers.Services.cookies;
+
+ let cookie = {
+ host: domain,
+ name: "first",
+ path: "/",
+ };
+
+ do {
+ cookieSvc.add(cookie.host, cookie.path, cookie.name, "one", false, false, false, Date.now() / 1000 + 1);
+ } while (!cookieSvc.cookieExists(cookie));
+
+ yield extension.startup();
+ yield extension.awaitMessage("change-cookies");
+
+ cookieSvc.add(cookie.host, cookie.path, cookie.name, "one-again", false, false, false, Date.now() / 1000 + 10);
+
+ yield extension.awaitFinish("cookie-expiry");
+ yield extension.unload();
+});
+
+</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 000000000..15a62855a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.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="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* init() {
+ // We need to trigger a cookie eviction in order to test our batch delete
+ // observer.
+ SpecialPowers.setIntPref("network.cookie.maxPerHost", 3);
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("network.cookie.maxPerHost");
+ });
+});
+
+add_task(function* test_bad_cookie_permissions() {
+ info("Test non-matching, non-secure domain with non-secure cookie");
+ yield 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");
+ yield 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");
+ yield 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)");
+ yield 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");
+ yield 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");
+ yield testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "https://example.com/",
+ domain: "example.com",
+ secure: true,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test non-matching domain");
+ yield testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.com/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test invalid scheme");
+ yield 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 000000000..31e83188c
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.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="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* init() {
+ // We need to trigger a cookie eviction in order to test our batch delete
+ // observer.
+ SpecialPowers.setIntPref("network.cookie.maxPerHost", 3);
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("network.cookie.maxPerHost");
+ });
+});
+
+add_task(function* test_good_cookie_permissions() {
+ info("Test matching, non-secure domain with non-secure cookie");
+ yield 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");
+ yield 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");
+ yield 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)");
+ yield 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)");
+ yield 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)");
+ yield 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_exclude_include_globs.html b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html
new file mode 100644
index 000000000..640522b40
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html
@@ -0,0 +1,92 @@
+<!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/SpawnTask.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(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": ["http://example.org/", "http://*.example.org/"],
+ "exclude_globs": [],
+ "include_globs": ["*"],
+ "js": ["content_script_all.js"],
+ },
+ {
+ "matches": ["http://example.org/", "http://*.example.org/"],
+ "include_globs": ["*test1*"],
+ "js": ["content_script_includes_test1.js"],
+ },
+ {
+ "matches": ["http://example.org/", "http://*.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++;
+ });
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ info("extension loaded");
+
+ let win = window.open("http://example.org/");
+ yield Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-excludes-test1")]);
+ win.close();
+ is(ran, 2);
+
+ win = window.open("http://test1.example.org/");
+ yield Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-includes-test1")]);
+ win.close();
+ is(ran, 4);
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</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 000000000..cfafcbad9
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_generate.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for generating WebExtensions</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+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(function* test_background() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ info("load complete");
+ let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ is(x, 1, "got correct value from extension");
+ info("startup complete");
+ extension.sendMessage(10, 20);
+ yield extension.awaitFinish();
+ info("test complete");
+ yield extension.unload();
+ info("extension unloaded successfully");
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_geturl.html b/toolkit/components/extensions/test/mochitest/test_ext_geturl.html
new file mode 100644
index 000000000..6e39c2f5d
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_geturl.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+function background() {
+ browser.runtime.onMessage.addListener(([url1, url2]) => {
+ let url3 = browser.runtime.getURL("test_file.html");
+ let url4 = browser.extension.getURL("test_file.html");
+
+ browser.test.assertTrue(url1 !== undefined, "url1 defined");
+
+ browser.test.assertTrue(url1.startsWith("moz-extension://"), "url1 has correct scheme");
+ browser.test.assertTrue(url1.endsWith("test_file.html"), "url1 has correct leaf name");
+
+ browser.test.assertEq(url1, url2, "url2 matches");
+ browser.test.assertEq(url1, url3, "url3 matches");
+ browser.test.assertEq(url1, url4, "url4 matches");
+
+ browser.test.notifyPass("geturl");
+ });
+}
+
+function contentScript() {
+ let url1 = browser.runtime.getURL("test_file.html");
+ let url2 = browser.extension.getURL("test_file.html");
+ browser.runtime.sendMessage([url1, url2]);
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish("geturl")]);
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
new file mode 100644
index 000000000..1f7330bbb
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
@@ -0,0 +1,432 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for WebExtension localization APIs</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("intl.accept_languages"); });
+SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("general.useragent.locale"); });
+
+add_task(function* test_i18n() {
+ function runTests(assertEq) {
+ let _ = browser.i18n.getMessage.bind(browser.i18n);
+
+ let url = browser.runtime.getURL("/");
+ assertEq(url, `moz-extension://${_("@@extension_id")}/`, "@@extension_id builtin message");
+
+ assertEq("Foo.", _("Foo"), "Simple message in selected locale.");
+
+ assertEq("(bar)", _("bar"), "Simple message fallback in default locale.");
+
+ assertEq("", _("some-unknown-locale-string"), "Unknown locale string.");
+
+ assertEq("", _("@@unknown_builtin_string"), "Unknown built-in string.");
+ assertEq("", _("@@bidi_unknown_builtin_string"), "Unknown built-in bidi string.");
+
+ assertEq("Føo.", _("Föo"), "Multi-byte message in selected locale.");
+
+ let substitutions = [];
+ substitutions[4] = "5";
+ substitutions[13] = "14";
+
+ assertEq("'$0' '14' '' '5' '$$$$' '$'.", _("basic_substitutions", substitutions),
+ "Basic numeric substitutions");
+
+ assertEq("'$0' '' 'just a string' '' '$$$$' '$'.", _("basic_substitutions", "just a string"),
+ "Basic numeric substitutions, with non-array value");
+
+ let values = _("named_placeholder_substitutions", ["(subst $1 $2)", "(2 $1 $2)"]).split("\n");
+
+ assertEq("_foo_ (subst $1 $2) _bar_", values[0], "Named and numeric substitution");
+
+ assertEq("(2 $1 $2)", values[1], "Numeric substitution amid named placeholders");
+
+ assertEq("$bad name$", values[2], "Named placeholder with invalid key");
+
+ assertEq("", values[3], "Named placeholder with an invalid value");
+
+ assertEq("Accepted, but shouldn't break.", values[4], "Named placeholder with a strange content value");
+
+ assertEq("$foo", values[5], "Non-placeholder token that should be ignored");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "default_locale": "jp",
+
+ content_scripts: [
+ {"matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content.js"]},
+ ],
+ },
+
+
+ files: {
+ "_locales/en_US/messages.json": {
+ "foo": {
+ "message": "Foo.",
+ "description": "foo",
+ },
+
+ "föo": {
+ "message": "Føo.",
+ "description": "foo",
+ },
+
+ "basic_substitutions": {
+ "message": "'$0' '$14' '$1' '$5' '$$$$$' '$$'.",
+ "description": "foo",
+ },
+
+ "Named_placeholder_substitutions": {
+ "message": "$Foo$\n$2\n$bad name$\n$bad_value$\n$bad_content_value$\n$foo",
+ "description": "foo",
+ "placeholders": {
+ "foO": {
+ "content": "_foo_ $1 _bar_",
+ "description": "foo",
+ },
+
+ "bad name": {
+ "content": "Nope.",
+ "description": "bad name",
+ },
+
+ "bad_value": "Nope.",
+
+ "bad_content_value": {
+ "content": ["Accepted, but shouldn't break."],
+ "description": "bad value",
+ },
+ },
+ },
+
+ "broken_placeholders": {
+ "message": "$broken$",
+ "description": "broken placeholders",
+ "placeholders": "foo.",
+ },
+ },
+
+ "_locales/jp/messages.json": {
+ "foo": {
+ "message": "(foo)",
+ "description": "foo",
+ },
+
+ "bar": {
+ "message": "(bar)",
+ "description": "bar",
+ },
+ },
+
+ "content.js": "new " + function(runTestsFn) {
+ runTestsFn((...args) => {
+ browser.runtime.sendMessage(["assertEq", ...args]);
+ });
+
+ browser.runtime.sendMessage(["content-script-finished"]);
+ } + `(${runTests})`,
+ },
+
+ background: "new " + function(runTestsFn) {
+ browser.runtime.onMessage.addListener(([msg, ...args]) => {
+ if (msg == "assertEq") {
+ browser.test.assertEq(...args);
+ } else {
+ browser.test.sendMessage(msg, ...args);
+ }
+ });
+
+ runTestsFn(browser.test.assertEq.bind(browser.test));
+ } + `(${runTests})`,
+ });
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitMessage("content-script-finished");
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* test_get_accept_languages() {
+ function background() {
+ function checkResults(source, results, expected) {
+ browser.test.assertEq(
+ expected.length,
+ results.length,
+ `got expected number of languages in ${source}`);
+ results.forEach((lang, index) => {
+ browser.test.assertEq(
+ expected[index],
+ lang,
+ `got expected language in ${source}`);
+ });
+ }
+
+ let tabId;
+
+ browser.tabs.query({currentWindow: true, active: true}, tabs => {
+ tabId = tabs[0].id;
+ browser.test.sendMessage("ready");
+ });
+
+ browser.test.onMessage.addListener(async ([msg, expected]) => {
+ let contentResults = await browser.tabs.sendMessage(tabId, "get-results");
+ let backgroundResults = await browser.i18n.getAcceptLanguages();
+
+ checkResults("contentScript", contentResults, expected);
+ checkResults("background", backgroundResults, expected);
+
+ browser.test.sendMessage("done");
+ });
+ }
+
+ function content() {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ browser.i18n.getAcceptLanguages(respond);
+ return true;
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": content,
+ },
+ });
+
+ let win = window.open("file_sample.html");
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ let expectedLangs = ["en-US", "en"];
+ extension.sendMessage(["expect-results", expectedLangs]);
+ yield extension.awaitMessage("done");
+
+ expectedLangs = ["en-US", "en", "fr-CA", "fr"];
+ SpecialPowers.setCharPref("intl.accept_languages", expectedLangs.toString());
+ extension.sendMessage(["expect-results", expectedLangs]);
+ yield extension.awaitMessage("done");
+ SpecialPowers.clearUserPref("intl.accept_languages");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* test_get_ui_language() {
+ function getResults() {
+ return {
+ getUILanguage: browser.i18n.getUILanguage(),
+ getMessage: browser.i18n.getMessage("@@ui_locale"),
+ };
+ }
+
+ function background(getResultsFn) {
+ function checkResults(source, results, expected) {
+ browser.test.assertEq(
+ expected,
+ results.getUILanguage,
+ `Got expected getUILanguage result in ${source}`
+ );
+ browser.test.assertEq(
+ expected,
+ results.getMessage,
+ `Got expected getMessage result in ${source}`
+ );
+ }
+
+ let tabId;
+
+ browser.test.onMessage.addListener(([msg, expected]) => {
+ browser.tabs.sendMessage(tabId, "get-results", result => {
+ checkResults("contentScript", result, expected);
+ checkResults("background", getResultsFn(), expected);
+
+ browser.test.sendMessage("done");
+ });
+ });
+
+ browser.tabs.query({currentWindow: true, active: true}, tabs => {
+ tabId = tabs[0].id;
+ browser.test.sendMessage("ready");
+ });
+ }
+
+ function content(getResultsFn) {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ respond(getResultsFn());
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ background: `(${background})(${getResults})`,
+
+ files: {
+ "content_script.js": `(${content})(${getResults})`,
+ },
+ });
+
+ let win = window.open("file_sample.html");
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage(["expect-results", "en_US"]);
+ yield extension.awaitMessage("done");
+
+ SpecialPowers.setCharPref("general.useragent.locale", "he");
+
+ extension.sendMessage(["expect-results", "he"]);
+ yield extension.awaitMessage("done");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+
+add_task(function* test_detect_language() {
+ const af_string = " aam skukuza die naam beteken hy wat skoonvee of hy wat alles onderstebo keer wysig " +
+ "bosveldkampe boskampe is kleiner afgeleë ruskampe wat oor min fasiliteite beskik daar is geen restaurante " +
+ "of winkels nie en slegs oornagbesoekers word toegelaat bateleur";
+ // String with intermixed French/English text
+ const fr_en_string = "France is the largest country in Western Europe and the third-largest in Europe as a whole. " +
+ "A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter " +
+ "Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, " +
+ "Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus." +
+ "Motoring events began soon after the construction of the first successful gasoline-fueled automobiles. The quick brown fox jumped over the lazy dog";
+
+ function background() {
+ function checkResult(source, result, expected) {
+ browser.test.assertEq(expected.isReliable, result.isReliable, "result.confident is true");
+ browser.test.assertEq(
+ expected.languages.length,
+ result.languages.length,
+ `result.languages contains the expected number of languages in ${source}`);
+ expected.languages.forEach((lang, index) => {
+ browser.test.assertEq(
+ lang.percentage,
+ result.languages[index].percentage,
+ `element ${index} of result.languages array has the expected percentage in ${source}`);
+ browser.test.assertEq(
+ lang.language,
+ result.languages[index].language,
+ `element ${index} of result.languages array has the expected language in ${source}`);
+ });
+ }
+
+ let tabId;
+
+ browser.tabs.query({currentWindow: true, active: true}, tabs => {
+ tabId = tabs[0].id;
+ browser.test.sendMessage("ready");
+ });
+
+ browser.test.onMessage.addListener(async ([msg, expected]) => {
+ let backgroundResults = await browser.i18n.detectLanguage(msg);
+ let contentResults = await browser.tabs.sendMessage(tabId, msg);
+
+ checkResult("background", backgroundResults, expected);
+ checkResult("contentScript", contentResults, expected);
+
+ browser.test.sendMessage("done");
+ });
+ }
+
+ function content() {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ browser.i18n.detectLanguage(msg, respond);
+ return true;
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": content,
+ },
+ });
+
+ let win = window.open("file_sample.html");
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ let expected = {
+ isReliable: true,
+ languages: [
+ {
+ language: "fr",
+ percentage: 67,
+ },
+ {
+ language: "en",
+ percentage: 32,
+ },
+ ],
+ };
+ extension.sendMessage([fr_en_string, expected]);
+ yield extension.awaitMessage("done");
+
+ expected = {
+ isReliable: true,
+ languages: [
+ {
+ language: "af",
+ percentage: 99,
+ },
+ ],
+ };
+ extension.sendMessage([af_string, expected]);
+ yield extension.awaitMessage("done");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html b/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html
new file mode 100644
index 000000000..7c6a8eeaa
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html
@@ -0,0 +1,116 @@
+<!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/SpawnTask.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(function* test_i18n_css() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ function backgroundFetch(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.onload = () => { resolve(xhr.responseText); };
+ xhr.onerror = reject;
+ xhr.send();
+ });
+ }
+
+ Promise.all([backgroundFetch("foo.css"), backgroundFetch("bar.CsS?x#y"), backgroundFetch("foo.txt")]).then(results => {
+ browser.test.assertEq("body { max-width: 42px; }", results[0], "CSS file localized");
+ browser.test.assertEq("body { max-width: 42px; }", results[1], "CSS file localized");
+
+ browser.test.assertEq("body { __MSG_foo__; }", results[2], "Text file not localized");
+
+ browser.test.notifyPass("i18n-css");
+ });
+
+ browser.test.sendMessage("ready", browser.runtime.getURL("foo.css"));
+ },
+
+ manifest: {
+ "web_accessible_resources": ["foo.css", "foo.txt", "locale.css"],
+
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "css": ["foo.css"],
+ }],
+
+ "default_locale": "en",
+ },
+
+ files: {
+ "_locales/en/messages.json": JSON.stringify({
+ "foo": {
+ "message": "max-width: 42px",
+ "description": "foo",
+ },
+ }),
+
+ "foo.css": "body { __MSG_foo__; }",
+ "bar.CsS": "body { __MSG_foo__; }",
+ "foo.txt": "body { __MSG_foo__; }",
+ "locale.css": '* { content: "__MSG_@@ui_locale__ __MSG_@@bidi_dir__ __MSG_@@bidi_reversed_dir__ __MSG_@@bidi_start_edge__ __MSG_@@bidi_end_edge__" }',
+ },
+ });
+
+ yield extension.startup();
+ let cssURL = yield extension.awaitMessage("ready");
+
+ function fetch(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.onload = () => { resolve(xhr.responseText); };
+ xhr.onerror = reject;
+ xhr.send();
+ });
+ }
+
+ let css = yield fetch(cssURL);
+
+ is(css, "body { max-width: 42px; }", "CSS file localized in mochitest scope");
+
+ let win = window.open("file_sample.html");
+ yield waitForLoad(win);
+
+ let style = win.getComputedStyle(win.document.body);
+ is(style.maxWidth, "42px", "stylesheet correctly applied");
+ win.close();
+
+ cssURL = cssURL.replace(/foo.css$/, "locale.css");
+
+ css = yield fetch(cssURL);
+ is(css, '* { content: "en_US ltr rtl left right" }', "CSS file localized in mochitest scope");
+
+ const LOCALE = "general.useragent.locale";
+ const DIR = "intl.uidirection.en";
+
+ // We don't wind up actually switching the chrome registry locale, since we
+ // don't have a chrome package for Hebrew. So just override it.
+ SpecialPowers.setCharPref(LOCALE, "he");
+ SpecialPowers.setCharPref(DIR, "rtl");
+
+ css = yield fetch(cssURL);
+ is(css, '* { content: "he rtl ltr right left" }', "CSS file localized in mochitest scope");
+
+ SpecialPowers.clearUserPref(LOCALE);
+ SpecialPowers.clearUserPref(DIR);
+
+ yield extension.awaitFinish("i18n-css");
+ yield 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 000000000..675cbb298
--- /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 type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(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>`,
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("inIncognitoContext");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html b/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html
new file mode 100644
index 000000000..da0c355e0
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.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";
+
+add_task(function* test_versioned_js() {
+ // We need to deal with escaping the close script tags.
+ // May as well consolidate it into one place.
+ let script = attrs => `<script ${attrs}><\/script>`;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "background": {"page": "background.html"},
+ },
+
+ files: {
+ "background.html": `
+ <meta charset="utf-8">
+ ${script('src="background.js" type="application/javascript"')}
+ ${script('src="background-1.js" type="application/javascript;version=1.8"')}
+ ${script('src="background-2.js" type="application/javascript;version=latest"')}
+ ${script('src="background-3.js" type="application/javascript"')}
+ `,
+
+ "background.js": function() {
+ window.reportResult = msg => {
+ browser.test.assertEq(
+ msg, "background-script-3",
+ "Expected a message only from the unversioned background script.");
+
+ browser.test.sendMessage("finished");
+ };
+ },
+
+ "background-1.js": function() {
+ window.reportResult("background-script-1");
+ },
+ "background-2.js": function() {
+ window.reportResult("background-script-2");
+ },
+ "background-3.js": function() {
+ window.reportResult("background-script-3");
+ },
+ },
+ });
+
+ let messages = [/Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/,
+ /Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/];
+
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, messages);
+ });
+
+ info("loading extension");
+
+ yield Promise.all([extension.startup(),
+ extension.awaitMessage("finished")]);
+
+ info("waiting for console");
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+
+ info("unloading extension");
+
+ yield extension.unload();
+
+ info("test complete");
+});
+
+</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 000000000..ca8db873e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html
@@ -0,0 +1,63 @@
+<!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/SpawnTask.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(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");
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("onchanged-listeners");
+
+ yield 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 000000000..d1b798cf9
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
@@ -0,0 +1,224 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for notifications</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" 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";
+
+// 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(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,
+ });
+ yield extension.startup();
+ let x = yield extension.awaitMessage("running");
+ is(x, "0", "got correct id from notifications.create");
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(function* test_notification_events() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ // 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(function() {});
+
+ // Test onClosed listener.
+ browser.notifications.onClosed.addListener(id => {
+ browser.test.sendMessage("closed", id);
+ browser.test.notifyPass("background test passed");
+ });
+
+ await browser.notifications.create("5", opts);
+ let id = await browser.notifications.create("5", opts);
+ browser.test.sendMessage("running", id);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ yield extension.startup();
+ let x = yield extension.awaitMessage("closed");
+ is(x, "5", "got correct id from onClosed listener");
+ x = yield extension.awaitMessage("running");
+ is(x, "5", "got correct id from notifications.create");
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(function* test_notification_clear() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ browser.notifications.onClosed.addListener(id => {
+ browser.test.sendMessage("closed", id);
+ });
+
+ let id = await browser.notifications.create("99", opts);
+
+ let wasCleared = await browser.notifications.clear(id);
+ browser.test.sendMessage("cleared", wasCleared);
+
+ browser.test.notifyPass("background test passed");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ yield extension.startup();
+ let x = yield extension.awaitMessage("closed");
+ is(x, "99", "got correct id from onClosed listener");
+ x = yield extension.awaitMessage("cleared");
+ is(x, true, "got correct boolean from notifications.clear");
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(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,
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("getAll empty");
+ yield extension.unload();
+});
+
+add_task(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,
+ },
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("getAll populated");
+ yield extension.unload();
+});
+
+add_task(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,
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("buttons-unsupported");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html b/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html
new file mode 100644
index 000000000..07967d5d0
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* test_simple() {
+ async function runTests(cx) {
+ function xhr(XMLHttpRequest) {
+ return (url) => {
+ return new Promise((resolve, reject) => {
+ let req = new XMLHttpRequest();
+ req.open("GET", url);
+ req.addEventListener("load", resolve);
+ req.addEventListener("error", reject);
+ req.send();
+ });
+ };
+ }
+
+ function run(shouldFail, fetch) {
+ function passListener() {
+ browser.test.succeed(`${cx}.${fetch.name} pass listener`);
+ }
+
+ function failListener() {
+ browser.test.fail(`${cx}.${fetch.name} fail listener`);
+ }
+
+ /* eslint-disable no-else-return */
+ if (shouldFail) {
+ return fetch("http://example.org/example.txt").then(failListener, passListener);
+ } else {
+ return fetch("http://example.com/example.txt").then(passListener, failListener);
+ }
+ /* eslint-enable no-else-return */
+ }
+
+ try {
+ await run(true, xhr(XMLHttpRequest));
+ await run(false, xhr(XMLHttpRequest));
+ await run(true, xhr(window.XMLHttpRequest));
+ await run(false, xhr(window.XMLHttpRequest));
+ await run(true, fetch);
+ await run(false, fetch);
+ await run(true, window.fetch);
+ await run(false, window.fetch);
+ } catch (err) {
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ browser.test.notifyFail("permission_xhr");
+ }
+ }
+
+ async function background(runTestsFn) {
+ await runTestsFn("bg");
+ browser.test.notifyPass("permission_xhr");
+ }
+
+ let extensionData = {
+ background: `(${background})(${runTests})`,
+ manifest: {
+ permissions: ["http://example.com/"],
+ content_scripts: [{
+ "matches": ["http://mochi.test/*/file_permission_xhr.html"],
+ "js": ["content.js"],
+ }],
+ },
+ files: {
+ "content.js": `(${async runTestsFn => {
+ await runTestsFn("content");
+
+ window.wrappedJSObject.privilegedFetch = fetch;
+ window.wrappedJSObject.privilegedXHR = XMLHttpRequest;
+
+ window.addEventListener("message", function rcv({data}) {
+ switch (data.msg) {
+ case "test":
+ break;
+
+ case "assertTrue":
+ browser.test.assertTrue(data.condition, data.description);
+ break;
+
+ case "finish":
+ window.removeEventListener("message", rcv, false);
+ browser.test.sendMessage("content-script-finished");
+ break;
+ }
+ }, false);
+ window.postMessage("test", "*");
+ }})(${runTests})`,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_permission_xhr.html");
+ yield extension.awaitMessage("content-script-finished");
+ win.close();
+
+ yield extension.awaitFinish("permission_xhr");
+ yield extension.unload();
+});
+</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 000000000..60351eaee
--- /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 type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+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");
+
+ 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(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish("runtime.connect")]);
+
+ win.close();
+
+ yield 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 000000000..dce12b21b
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+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(function* test_contentscript() {
+ let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
+ let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+ yield Promise.all([extension1.startup(), extension2.startup()]);
+
+ yield extension1.awaitMessage("background-ready");
+ yield extension2.awaitMessage("background-ready");
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win),
+ extension1.awaitFinish("sendmessage_reply"),
+ extension2.awaitFinish("sendmessage_reply")]);
+
+ win.close();
+
+ yield extension1.unload();
+ yield extension2.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 000000000..e84134eff
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html
@@ -0,0 +1,127 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+add_task(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);
+ yield extension.startup();
+ info("extension loaded");
+
+ yield extension.awaitMessage("ready");
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitFinish("ping pong done");
+ win.close();
+
+ yield 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 000000000..5764d0a3c
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+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(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield Promise.all([waitForLoad(win), extension.awaitMessage("connected")]);
+ win.close();
+ yield extension.awaitMessage("disconnected");
+
+ info("win.close() succeeded");
+
+ win = window.open("file_sample.html");
+ yield 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";
+ yield extension.awaitMessage("disconnected");
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
new file mode 100644
index 000000000..4cdefda41
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for browser.runtime.id</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_runtime_id() {
+ function background() {
+ browser.test.sendMessage("background-id", browser.runtime.id);
+ }
+
+ function content() {
+ browser.test.sendMessage("content-id", browser.runtime.id);
+ }
+
+ let uuidGenerator = SpecialPowers.Cc["@mozilla.org/uuid-generator;1"].getService(SpecialPowers.Ci.nsIUUIDGenerator);
+ let id = uuidGenerator.generateUUID().number;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: {gecko: {id}},
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": content,
+ },
+ });
+
+ yield extension.startup();
+
+ let backgroundId = yield extension.awaitMessage("background-id");
+ is(backgroundId, id, "runtime.id from background script is correct");
+ let win = window.open("file_sample.html");
+ let contentId = yield extension.awaitMessage("content-id");
+ is(contentId, id, "runtime.id from content script is correct");
+
+ win.close();
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html b/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html
new file mode 100644
index 000000000..426a71ac6
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html
@@ -0,0 +1,60 @@
+<!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/SpawnTask.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";
+
+function background() {
+ browser.runtime.onMessage.addListener(result => {
+ browser.test.assertEq(result, 12, "x is 12");
+ browser.test.notifyPass("background test passed");
+ });
+}
+
+function contentScript() {
+ window.x = 12;
+ browser.runtime.onMessage.addListener(function() {});
+ browser.runtime.sendMessage(window.x);
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish()]);
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_schema.html b/toolkit/components/extensions/test/mochitest/test_ext_schema.html
new file mode 100644
index 000000000..8a0e11c56
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for schema API creation</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.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";
+
+add_task(function* testEmptySchema() {
+ function background() {
+ browser.test.assertEq(undefined, browser.manifest, "browser.manifest is not defined");
+ browser.test.assertTrue("storage" in browser, "browser.storage should be defined");
+ browser.test.assertEq(undefined, browser.contextMenus, "browser.contextMenus should not be defined");
+ browser.test.notifyPass("schema");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["storage"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("schema");
+ yield extension.unload();
+});
+
+add_task(function* testUnknownProperties() {
+ function background() {
+ browser.test.notifyPass("loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["unknownPermission"],
+
+ unknown_property: {},
+ },
+
+ background,
+ });
+
+ let messages = [
+ {message: /processing permissions\.0: Unknown permission "unknownPermission"/},
+ {message: /processing unknown_property: An unexpected property was found in the WebExtension manifest/},
+ ];
+
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, messages);
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("loaded");
+
+ yield extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+});
+</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 000000000..a3ef37cad
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+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(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_doublereply")]);
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
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 000000000..96af6558e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+function loadContentScriptExtension(contentScript) {
+ let extensionData = {
+ manifest: {
+ "content_scripts": [{
+ "js": ["contentscript.js"],
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ },
+ };
+ return ExtensionTestUtils.loadExtension(extensionData);
+}
+
+add_task(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 = loadContentScriptExtension(contentScript);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitFinish("sendMessage callback was invoked");
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* test_content_script_chrome_sendMessage_without_listener() {
+ function contentScript() {
+ /* globals chrome */
+ 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");
+ // TODO(robwu): Fix the implementation and uncomment the next expectation.
+ // When content script APIs are schema-based (bugzil.la/1287007) this bug will be fixed for free.
+ // browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without callback");
+ browser.test.assertTrue(retval instanceof Promise, "TODO: chrome.runtime.sendMessage should return undefined, not a promise");
+
+ 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.notifyPass("finished chrome.runtime.sendMessage");
+ });
+ isAsyncCall = true;
+ }
+
+ let extension = loadContentScriptExtension(contentScript);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitFinish("finished chrome.runtime.sendMessage");
+ win.close();
+
+ yield extension.unload();
+});
+</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 000000000..a4ac708b2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+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(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_reply")]);
+
+ win.close();
+
+ yield 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 000000000..1ebc1b40f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+function backgroundScript(token) {
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == "done") {
+ browser.test.notifyPass("sendmessage_reply");
+ return;
+ }
+
+ let tabId = sender.tab.id;
+ browser.tabs.sendMessage(tabId, `${token}-tabMessage`);
+
+ browser.test.assertEq(msg, token, "token matches");
+ sendReply(`${token}-done`);
+ });
+}
+
+function contentScript(token) {
+ let gotTabMessage = false;
+ let badTabMessage = false;
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ if (msg == `${token}-tabMessage`) {
+ gotTabMessage = true;
+ } else {
+ badTabMessage = true;
+ }
+ });
+
+ browser.runtime.sendMessage(token, function(resp) {
+ if (resp != `${token}-done` || !gotTabMessage || badTabMessage) {
+ return; // test failed
+ }
+ 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_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": `(${contentScript})(${token})`,
+ },
+ };
+ return extensionData;
+}
+
+add_task(function* test_contentscript() {
+ let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
+ let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+
+ yield Promise.all([extension1.startup(), extension2.startup()]);
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win),
+ extension1.awaitFinish("sendmessage_reply"),
+ extension2.awaitFinish("sendmessage_reply")]);
+
+ win.close();
+
+ yield extension1.unload();
+ yield extension2.unload();
+ info("extensions unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html
new file mode 100644
index 000000000..09a33814a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html
@@ -0,0 +1,330 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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="application/javascript">
+"use strict";
+
+// Copied from toolkit/components/extensions/test/xpcshell/test_ext_storage.js.
+// The storage API in content scripts should behave identical to the storage API
+// in background pages.
+const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled";
+/**
+ * Utility function to ensure that all supported APIs for getting are
+ * tested.
+ *
+ * @param {string} areaName
+ * either "local" or "sync" according to what we want to test
+ * @param {string} prop
+ * "key" to look up using the storage API
+ * @param {Object} value
+ * "value" to compare against
+ */
+async function checkGetImpl(areaName, prop, value) {
+ let storage = browser.storage[areaName];
+
+ let data = await storage.get(null);
+ browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get(prop);
+ browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get([prop]);
+ browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get({[prop]: undefined});
+ browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`);
+}
+
+async function contentScript(checkGet) {
+ let globalChanges, gResolve;
+ function clearGlobalChanges() {
+ globalChanges = new Promise(resolve => { gResolve = resolve; });
+ }
+ clearGlobalChanges();
+ let expectedAreaName;
+
+ browser.storage.onChanged.addListener((changes, areaName) => {
+ browser.test.assertEq(expectedAreaName, areaName,
+ "Expected area name received by listener");
+ gResolve(changes);
+ });
+
+ async function checkChanges(areaName, changes, message) {
+ function checkSub(obj1, obj2) {
+ for (let prop in obj1) {
+ browser.test.assertTrue(obj1[prop] !== undefined,
+ `checkChanges ${areaName} ${prop} is missing (${message})`);
+ browser.test.assertTrue(obj2[prop] !== undefined,
+ `checkChanges ${areaName} ${prop} is missing (${message})`);
+ browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue,
+ `checkChanges ${areaName} ${prop} old (${message})`);
+ browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue,
+ `checkChanges ${areaName} ${prop} new (${message})`);
+ }
+ }
+
+ const recentChanges = await globalChanges;
+ checkSub(changes, recentChanges);
+ checkSub(recentChanges, changes);
+ clearGlobalChanges();
+ }
+
+ /* eslint-disable dot-notation */
+ async function runTests(areaName) {
+ expectedAreaName = areaName;
+ let storage = browser.storage[areaName];
+ // Set some data and then test getters.
+ try {
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+ await checkChanges(areaName,
+ {"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}},
+ "set (a)");
+
+ await checkGet(areaName, "test-prop1", "value1");
+ await checkGet(areaName, "test-prop2", "value2");
+
+ let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"});
+ browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)");
+ browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)");
+ browser.test.assertEq("default", data["other"], "other correct");
+
+ data = await storage.get(["test-prop1", "test-prop2", "other"]);
+ browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)");
+ browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)");
+ browser.test.assertFalse("other" in data, "other correct");
+
+ // Remove data in various ways.
+ await storage.remove("test-prop1");
+ await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)");
+ browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)");
+
+ await storage.set({"test-prop1": "value1"});
+ await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)");
+ browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)");
+
+ await storage.remove(["test-prop1", "test-prop2"]);
+ await checkChanges(areaName,
+ {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
+ "remove array");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)");
+ browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)");
+
+ // test storage.clear
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+ // Make sure that set() handler happened before we clear the
+ // promise again.
+ await globalChanges;
+
+ clearGlobalChanges();
+ await storage.clear();
+
+ await checkChanges(areaName,
+ {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
+ "clear");
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)");
+ browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)");
+
+ // Make sure we can store complex JSON data.
+ // known previous values
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+
+ // Make sure the set() handler landed.
+ await globalChanges;
+
+ clearGlobalChanges();
+ await storage.set({
+ "test-prop1": {
+ str: "hello",
+ bool: true,
+ null: null,
+ undef: undefined,
+ obj: {},
+ arr: [1, 2],
+ date: new Date(0),
+ regexp: /regexp/,
+ func: function func() {},
+ window,
+ },
+ });
+
+ await storage.set({"test-prop2": function func() {}});
+ const recentChanges = await globalChanges;
+
+ browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct");
+ browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj");
+ clearGlobalChanges();
+
+ data = await storage.get({"test-prop1": undefined, "test-prop2": undefined});
+ let obj = data["test-prop1"];
+
+ browser.test.assertEq("hello", obj.str, "string part correct");
+ browser.test.assertEq(true, obj.bool, "bool part correct");
+ browser.test.assertEq(null, obj.null, "null part correct");
+ browser.test.assertEq(undefined, obj.undef, "undefined part correct");
+ browser.test.assertEq(undefined, obj.func, "function part correct");
+ browser.test.assertEq(undefined, obj.window, "window part correct");
+ browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct");
+ browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct");
+ browser.test.assertEq("object", typeof(obj.obj), "object part correct");
+ browser.test.assertTrue(Array.isArray(obj.arr), "array part present");
+ browser.test.assertEq(1, obj.arr[0], "arr[0] part correct");
+ browser.test.assertEq(2, obj.arr[1], "arr[1] part correct");
+ browser.test.assertEq(2, obj.arr.length, "arr.length part correct");
+
+ obj = data["test-prop2"];
+
+ browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object");
+ browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("storage");
+ }
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ let promise;
+ if (msg === "test-local") {
+ promise = runTests("local");
+ } else if (msg === "test-sync") {
+ promise = runTests("sync");
+ }
+ promise.then(() => browser.test.sendMessage("test-finished"));
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+let extensionData = {
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+
+ permissions: ["storage"],
+ },
+
+ files: {
+ "content_script.js": `(${contentScript})(${checkGetImpl})`,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let win = window.open("file_sample.html");
+ yield waitForLoad(win);
+
+ yield SpecialPowers.pushPrefEnv({
+ set: [[STORAGE_SYNC_PREF, true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ extension.sendMessage("test-local");
+ yield extension.awaitMessage("test-finished");
+
+ extension.sendMessage("test-sync");
+ yield extension.awaitMessage("test-finished");
+
+ yield SpecialPowers.popPrefEnv();
+ yield extension.unload();
+
+ win.close();
+});
+
+add_task(function* test_local_cache_invalidation() {
+ let win = window.open("file_sample.html");
+
+ function background(checkGet) {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "set-initial") {
+ await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"});
+ browser.test.sendMessage("set-initial-done");
+ } else if (msg === "check") {
+ await checkGet("local", "test-prop1", "value1");
+ await checkGet("local", "test-prop2", "value2");
+ browser.test.sendMessage("check-done");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})(${checkGetImpl})`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("set-initial");
+ yield extension.awaitMessage("set-initial-done");
+
+ SpecialPowers.invalidateExtensionStorageCache();
+
+ extension.sendMessage("check");
+ yield extension.awaitMessage("check-done");
+
+ yield extension.unload();
+ win.close();
+});
+
+add_task(function* test_config_flag_needed() {
+ let win = window.open("file_sample.html");
+ yield waitForLoad(win);
+
+ function background() {
+ let promises = [];
+ let apiTests = [
+ {method: "get", args: ["foo"]},
+ {method: "set", args: [{foo: "bar"}]},
+ {method: "remove", args: ["foo"]},
+ {method: "clear", args: []},
+ ];
+ apiTests.forEach(testDef => {
+ promises.push(browser.test.assertRejects(
+ browser.storage.sync[testDef.method](...testDef.args),
+ "Please set webextensions.storage.sync.enabled to true in about:config",
+ `storage.sync.${testDef.method} is behind a flag`));
+ });
+
+ Promise.all(promises).then(() => browser.test.notifyPass("flag needed"));
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})(${checkGetImpl})`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("flag needed");
+ yield extension.unload();
+ win.close();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html
new file mode 100644
index 000000000..32d8e6af0
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_multiple_pages() {
+ async function background() {
+ let tabReady = new Promise(resolve => {
+ browser.runtime.onMessage.addListener(function listener(msg) {
+ browser.test.log("onMessage " + msg);
+ if (msg == "tab-ready") {
+ browser.runtime.onMessage.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+
+ let tabId;
+ let tabRemoved = new Promise(resolve => {
+ browser.tabs.onRemoved.addListener(function listener(removedId) {
+ if (removedId == tabId) {
+ browser.tabs.onRemoved.removeListener(listener);
+
+ // Delay long enough to be sure the inner window has been nuked.
+ setTimeout(resolve, 0);
+ }
+ });
+ });
+
+ try {
+ let storage = browser.storage.local;
+
+ browser.test.log("create");
+ let tab = await browser.tabs.create({url: "tab.html"});
+ tabId = tab.id;
+
+ await tabReady;
+
+ let result = await storage.get("key");
+ browser.test.assertEq(undefined, result.key, "Key should be undefined");
+
+ await browser.runtime.sendMessage("tab-set-key");
+
+ result = await storage.get("key");
+ browser.test.assertEq(JSON.stringify({foo: {bar: "baz"}}),
+ JSON.stringify(result.key),
+ "Key should be set to the value from the tab");
+
+ browser.test.log("Remove tab");
+
+ await Promise.all([
+ browser.tabs.remove(tabId),
+ tabRemoved,
+ ]);
+
+ result = await storage.get("key");
+ browser.test.assertEq(JSON.stringify({foo: {bar: "baz"}}),
+ JSON.stringify(result.key),
+ "Key should still be set to the value from the tab");
+
+ browser.test.notifyPass("storage-multiple");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("storage-multiple");
+ }
+ }
+
+ function tab() {
+ browser.test.log("tab");
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "tab-set-key") {
+ return browser.storage.local.set({key: {foo: {bar: "baz"}}});
+ }
+ });
+
+ browser.runtime.sendMessage("tab-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+
+ files: {
+ "tab.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="tab.js"><\/script>
+ </head>
+ </html>`,
+
+ "tab.js": tab,
+ },
+
+ manifest: {
+ permissions: ["storage"],
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("storage-multiple");
+ yield 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 000000000..1f3a9a3c9
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html
@@ -0,0 +1,202 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(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);
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("webext-tab-subframe-privileges");
+ yield extension.unload();
+});
+
+add_task(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);
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("webext-background-subframe-privileges");
+ yield extension.unload();
+});
+
+add_task(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": ["http://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);
+
+ yield extension.startup();
+
+ let win = window.open("http://example.com");
+
+ yield extension.awaitFinish("webext-contentscript-subframe-privileges");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html
new file mode 100644
index 000000000..dc351e48a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for extension tab teardown</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+// Test for tabs opened using tabs.create and window.open
+function* runTabReloadAndCloseTest(extension) {
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_teardown_test.js"));
+ yield chromeScript.promiseOneMessage("chromescript-startup");
+ function* getContextEvents() {
+ chromeScript.sendAsyncMessage("get-context-events");
+ let contextEvents = yield chromeScript.promiseOneMessage("context-events");
+ dump(JSON.stringify(contextEvents));
+ return contextEvents.filter(event => event.extensionId == extension.id);
+ }
+
+ extension.sendMessage("open extension page");
+ let extensionPageUrl = yield extension.awaitMessage("extension page loaded");
+
+ let contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1, "ExtensionContext change for opening a tab");
+ is(contextEvents[0].eventType, "load", "create ExtensionContext for tab");
+ is(contextEvents[0].url, extensionPageUrl,
+ "ExtensionContext URL after tab creation should be tab URL");
+
+ extension.sendMessage("reload extension page");
+ let extensionPageUrl2 = yield extension.awaitMessage("extension page loaded");
+
+ is(extensionPageUrl, extensionPageUrl2,
+ "The tab's URL is expected to not change after a page reload");
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 2, "ExtensionContext change after tab reload");
+ is(contextEvents[0].eventType, "unload", "unload old ExtensionContext");
+ is(contextEvents[0].url, extensionPageUrl,
+ "ExtensionContext URL before reload should be tab URL");
+ is(contextEvents[1].eventType, "load", "create new ExtensionContext for tab");
+ is(contextEvents[1].url, extensionPageUrl2,
+ "ExtensionContext URL after reload should be tab URL");
+
+ extension.sendMessage("close extension page");
+ yield extension.awaitMessage("closed extension page");
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1, "ExtensionContext after closing tab");
+ is(contextEvents[0].eventType, "unload", "unload tab's ExtensionContext");
+ is(contextEvents[0].url, extensionPageUrl2,
+ "ExtensionContext URL at closing tab should be tab URL");
+
+ chromeScript.sendAsyncMessage("cleanup");
+ chromeScript.destroy();
+ yield extension.unload();
+}
+
+add_task(function* test_extension_page_tabs_create_reload_and_close() {
+ function background() {
+ let tabId;
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "open extension page") {
+ chrome.tabs.create({url: "page.html"}, tab => {
+ tabId = tab.id;
+ });
+ } else if (msg === "reload extension page") {
+ chrome.tabs.reload(tabId);
+ } else if (msg === "close extension page") {
+ chrome.tabs.remove(tabId, () => {
+ browser.test.sendMessage("closed extension page");
+ });
+ }
+ });
+ }
+
+ function pageScript() {
+ browser.test.sendMessage("extension page loaded", document.URL);
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`,
+ "page.js": pageScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield* runTabReloadAndCloseTest(extension);
+});
+
+add_task(function* test_extension_page_window_open_reload_and_close() {
+ // This tests whether a context that is opened via window.open is properly
+ // disposed when the tab closes.
+ // The background page cannot use window.open (bugzil.la/1282021), so we open
+ // another extension page that manages the window.open-tab for testing.
+ function background() {
+ chrome.tabs.create({url: "window.open.html"});
+ }
+
+ function windowOpenScript() {
+ let win;
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "open extension page") {
+ win = window.open("page.html");
+ } else if (msg === "reload extension page") {
+ win.location.reload();
+ } else if (msg === "close extension page") {
+ browser.tabs.onRemoved.addListener(function listener() {
+ browser.tabs.onRemoved.removeListener(listener);
+ browser.test.sendMessage("closed extension page");
+ });
+ win.close();
+ }
+ });
+ browser.test.sendMessage("setup-intermediate-tab");
+ }
+
+ function pageScript() {
+ browser.test.sendMessage("extension page loaded", document.URL);
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`,
+ "page.js": pageScript,
+ "window.open.html": `<!DOCTYPE html><meta charset="utf-8"><script src="window.open.js"><\/script>`,
+ "window.open.js": windowOpenScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitMessage("setup-intermediate-tab");
+ yield* runTabReloadAndCloseTest(extension);
+});
+</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 000000000..fef31e0e2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html
@@ -0,0 +1,191 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Testing test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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;
+}
+
+function testScript() {
+ // Note: The result of these browser.test calls are intercepted by the test.
+ // See verifyTestResults for the expectations of each browser.test call.
+ 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 = [];
+ let dom = document.createElement("body");
+ browser.test.assertTrue(obj, "Object truthy");
+ browser.test.assertTrue(arr, "Array truthy");
+ browser.test.assertTrue(dom, "Element 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.assertTrue(false, document.createElement("html"));
+
+ browser.test.assertFalse(obj, "Object falsey");
+ browser.test.assertFalse(arr, "Array falsey");
+ browser.test.assertFalse(dom, "Element 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.assertFalse(true, document.createElement("head"));
+
+ browser.test.assertEq(obj, obj, "Object equality");
+ browser.test.assertEq(arr, arr, "Array equality");
+ browser.test.assertEq(dom, dom, "Element equality");
+ browser.test.assertEq(null, null, "Null equality");
+ browser.test.assertEq(undefined, undefined, "Void equality");
+
+ browser.test.assertEq({}, {}, "Object reference ineqality");
+ browser.test.assertEq([], [], "Array reference ineqality");
+ browser.test.assertEq(dom, document.createElement("body"), "Element ineqality");
+ browser.test.assertEq(null, undefined, "Null and void ineqality");
+ browser.test.assertEq(true, false, document.createElement("div"));
+
+ obj = {
+ toString() {
+ return "Dynamic toString forbidden";
+ },
+ };
+ browser.test.assertEq(obj, obj, "obj with dynamic toString()");
+ browser.test.sendMessage("Ran test at", location.protocol);
+ browser.test.sendMessage("This is the last browser.test call");
+}
+
+function verifyTestResults(results, shortName, expectedProtocol) {
+ 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, "Element 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 HTMLHtmlElement]"],
+
+ ["test-result", false, "Object falsey"],
+ ["test-result", false, "Array falsey"],
+ ["test-result", false, "Element falsey"],
+ ["test-result", false, "True falsey"],
+ ["test-result", true, "False falsey"],
+ ["test-result", true, "Null falsey"],
+ ["test-result", true, "Void falsey"],
+ ["test-result", false, "[object HTMLHeadElement]"],
+
+ ["test-eq", true, "Object equality", "[object Object]", "[object Object]"],
+ ["test-eq", true, "Array equality", "", ""],
+ ["test-eq", true, "Element equality", "[object HTMLBodyElement]", "[object HTMLBodyElement]"],
+ ["test-eq", true, "Null equality", "null", "null"],
+ ["test-eq", true, "Void equality", "undefined", "undefined"],
+
+ ["test-eq", false, "Object reference ineqality", "[object Object]", "[object Object] (different)"],
+ ["test-eq", false, "Array reference ineqality", "", " (different)"],
+ ["test-eq", false, "Element ineqality", "[object HTMLBodyElement]", "[object HTMLBodyElement] (different)"],
+ ["test-eq", false, "Null and void ineqality", "null", "undefined"],
+ ["test-eq", false, "[object HTMLDivElement]", "true", "false"],
+
+ ["test-eq", true, "obj with dynamic toString()", "[object Object]", "[object Object]"],
+
+ ["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(function* test_test_in_background() {
+ let extensionData = {
+ background: `(${testScript})()`,
+ };
+
+ let extension = loadExtensionAndInterceptTest(extensionData);
+ yield extension.startup();
+ let results = yield extension.awaitResults();
+ verifyTestResults(results, "background page", "moz-extension:");
+ yield extension.unload();
+});
+
+add_task(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);
+ yield extension.startup();
+ let win = window.open("file_sample.html");
+ let results = yield extension.awaitResults();
+ win.close();
+ verifyTestResults(results, "content script", "http:");
+ yield extension.unload();
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html b/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
new file mode 100644
index 000000000..5572de281
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
@@ -0,0 +1,170 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtensions test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+/* globals delayedNotifyPass */ // Available in the background page of the test extensions.
+
+// Background and content script for testSendMessage_*
+function sendMessage_background() {
+ browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
+ browser.test.assertEq("from frame", msg, "Expected message from frame");
+ sendResponse("msg from back"); // Should not throw or anything like that.
+ delayedNotifyPass("Received sendMessage from closing frame");
+ });
+}
+function sendMessage_contentScript(testType) {
+ browser.runtime.sendMessage("from frame", reply => {
+ // The frame has been removed, so we should not get this callback!
+ browser.test.fail(`Unexpected reply: ${reply}`);
+ });
+ if (testType == "frame") {
+ frameElement.remove();
+ } else {
+ window.close();
+ }
+}
+
+// Background and content script for testConnect_*
+function connect_background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq("port from frame", port.name);
+
+ let disconnected = false;
+ let hasMessage = false;
+ port.onDisconnect.addListener(() => {
+ browser.test.assertFalse(disconnected, "onDisconnect should fire once");
+ disconnected = true;
+ browser.test.assertTrue(hasMessage, "Expected onMessage before onDisconnect");
+ browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads");
+ delayedNotifyPass("Received onDisconnect from closing frame");
+ });
+ port.onMessage.addListener(msg => {
+ browser.test.assertFalse(hasMessage, "onMessage should fire once");
+ hasMessage = true;
+ browser.test.assertFalse(disconnected, "Should get message before disconnect");
+ browser.test.assertEq("from frame", msg, "Expected message from frame");
+ });
+
+ port.postMessage("reply to closing frame");
+ });
+}
+function connect_contentScript(testType) {
+ let isUnloading = false;
+ addEventListener("pagehide", () => { isUnloading = true; }, {once: true});
+
+ let port = browser.runtime.connect({name: "port from frame"});
+ port.onMessage.addListener(msg => {
+ // The background page sends a reply as soon as we call runtime.connect().
+ // It is possible that the reply reaches this frame before the
+ // window.close() request has been processed.
+ if (!isUnloading) {
+ browser.test.log(`Ignorting unexpected reply ("${msg}") because the page is not being unloaded.`);
+ return;
+ }
+
+ // The frame has been removed, so we should not get a reply.
+ browser.test.fail(`Unexpected reply: ${msg}`);
+ });
+ port.postMessage("from frame");
+
+ // Removing the frame or window should disconnect the port.
+ if (testType == "frame") {
+ frameElement.remove();
+ } else {
+ window.close();
+ }
+}
+
+// `testType` is "window" or "frame".
+function createTestExtension(testType, backgroundScript, contentScript) {
+ // Make a roundtrip between the background page and the test runner (which is
+ // in the same process as the content script) to make sure that we record a
+ // failure in case the content script's sendMessage or onMessage handlers are
+ // called even after the frame or window was removed.
+ function delayedNotifyPass(msg) {
+ browser.test.onMessage.addListener((type, echoMsg) => {
+ if (type == "pong") {
+ browser.test.assertEq(msg, echoMsg, "Echoed reply should be the same");
+ browser.test.notifyPass(msg);
+ }
+ });
+ browser.test.log("Starting ping-pong to flush messages...");
+ browser.test.sendMessage("ping", msg);
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `${delayedNotifyPass};(${backgroundScript})();`,
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ all_frames: testType == "frame",
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": `(${contentScript})("${testType}");`,
+ },
+ });
+ extension.awaitMessage("ping").then(msg => {
+ extension.sendMessage("pong", msg);
+ });
+ return extension;
+}
+
+add_task(function* testSendMessage_and_remove_frame() {
+ let extension = createTestExtension("frame", sendMessage_background, sendMessage_contentScript);
+ yield extension.startup();
+
+ let frame = document.createElement("iframe");
+ frame.src = "file_sample.html";
+ document.body.appendChild(frame);
+
+ yield extension.awaitFinish("Received sendMessage from closing frame");
+ yield extension.unload();
+});
+
+add_task(function* testConnect_and_remove_frame() {
+ let extension = createTestExtension("frame", connect_background, connect_contentScript);
+ yield extension.startup();
+
+ let frame = document.createElement("iframe");
+ frame.src = "file_sample.html";
+ document.body.appendChild(frame);
+
+ yield extension.awaitFinish("Received onDisconnect from closing frame");
+ yield extension.unload();
+});
+
+add_task(function* testSendMessage_and_remove_window() {
+ let extension = createTestExtension("window", sendMessage_background, sendMessage_contentScript);
+ yield extension.startup();
+
+ window.open("file_sample.html");
+
+ yield extension.awaitFinish("Received sendMessage from closing frame");
+ yield extension.unload();
+});
+
+add_task(function* testConnect_and_remove_window() {
+ let extension = createTestExtension("window", connect_background, connect_contentScript);
+ yield extension.startup();
+
+ window.open("file_sample.html");
+
+ yield extension.awaitFinish("Received onDisconnect from closing frame");
+ yield extension.unload();
+});
+
+</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 000000000..fa3228739
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
@@ -0,0 +1,353 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the web_accessible_resources manifest directive</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("security.mixed_content.block_display_content");
+});
+
+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 = document.createElement("img");
+ testImage.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});
+}
+
+add_task(function* test_web_accessible_resources() {
+ function background() {
+ let gotURL;
+ let tabId;
+
+ function loadFrame(url) {
+ return new Promise(resolve => {
+ browser.tabs.sendMessage(tabId, ["load-iframe", url], reply => {
+ resolve(reply);
+ });
+ });
+ }
+
+ let urls = [
+ [browser.extension.getURL("accessible.html"), true],
+ [browser.extension.getURL("accessible.html") + "?foo=bar", true],
+ [browser.extension.getURL("accessible.html") + "#!foo=bar", true],
+ [browser.extension.getURL("forbidden.html"), false],
+ [browser.extension.getURL("wild1.html"), true],
+ [browser.extension.getURL("wild2.htm"), false],
+ ];
+
+ async function runTests() {
+ for (let [url, shouldLoad] of urls) {
+ let success = await loadFrame(url);
+
+ browser.test.assertEq(shouldLoad, success, "Load was successful");
+ if (shouldLoad) {
+ 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], 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");
+ gotURL = url;
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ function contentScript() {
+ browser.runtime.onMessage.addListener(([msg, url], sender, respond) => {
+ if (msg == "load-iframe") {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("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({
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://example.com/"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+
+ "web_accessible_resources": [
+ "/accessible.html",
+ "wild*.html",
+ ],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": contentScript,
+
+ "accessible.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="accessible.js"><\/script>
+ </head></html>`,
+
+ "accessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
+
+ "inaccessible.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="inaccessible.js"><\/script>
+ </head></html>`,
+
+ "inaccessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
+
+ "wild1.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="wild.js"><\/script>
+ </head></html>`,
+
+ "wild2.htm": `<html><head>
+ <meta charset="utf-8">
+ <script src="wild.js"><\/script>
+ </head></html>`,
+
+ "wild.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitMessage("ready");
+
+ let win = window.open("http://example.com/");
+
+ yield extension.awaitFinish("web-accessible-resources");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* test_web_accessible_resources_csp() {
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ 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);
+ }
+ });
+
+ browser.test.sendMessage("background-ready");
+ }
+
+ function content() {
+ window.addEventListener("message", function rcv(event) {
+ browser.runtime.sendMessage("script-ran");
+ window.removeEventListener("message", rcv, false);
+ }, false);
+
+ testImageLoading(browser.extension.getURL("image.png"), "loaded");
+
+ let testScriptElement = document.createElement("script");
+ testScriptElement.setAttribute("src", browser.extension.getURL("test_script.js"));
+ document.head.appendChild(testScriptElement);
+ browser.runtime.sendMessage("script-loaded");
+ }
+
+ function testScript() {
+ window.postMessage("test-script-loaded", "*");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://example.com/*/file_csp.html"],
+ "run_at": "document_start",
+ "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,
+ },
+ });
+
+ // This is used to watch the blocked data bounce off CSP.
+ function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
+ }
+
+ let cspEventCount = 0;
+
+ examiner.prototype = {
+ observe: function(subject, topic, data) {
+ cspEventCount++;
+ let spec = SpecialPowers.wrap(subject).QueryInterface(SpecialPowers.Ci.nsIURI).spec;
+ ok(spec.includes("file_image_bad.png") || spec.includes("file_script_bad.js"),
+ `Expected file: ${spec} rejected by CSP`);
+ },
+
+ // We must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove: function() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ },
+ };
+
+ let observer = new examiner();
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
+
+ let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_csp.html");
+
+ yield Promise.all([
+ extension.awaitMessage("image-loaded"),
+ extension.awaitMessage("script-loaded"),
+ extension.awaitMessage("script-ran"),
+ ]);
+ is(cspEventCount, 2, "Two items were rejected by CSP");
+ win.close();
+
+ observer.remove();
+ yield extension.unload();
+});
+
+add_task(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");
+ }
+
+ function content() {
+ testImageLoading("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", "blocked");
+ testImageLoading(browser.extension.getURL("image.png"), "loaded");
+
+ let testScriptElement = document.createElement("script");
+ testScriptElement.setAttribute("src", browser.extension.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_start",
+ "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,
+ },
+ });
+
+ SpecialPowers.setBoolPref("security.mixed_content.block_display_content", true);
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
+
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_mixed.html");
+
+ yield Promise.all([
+ extension.awaitMessage("image-blocked"),
+ extension.awaitMessage("image-loaded"),
+ extension.awaitMessage("accessible-script-loaded"),
+ ]);
+ yield extension.awaitFinish("mixed-test");
+ win.close();
+
+ yield extension.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 000000000..2287fd9b1
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -0,0 +1,559 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" 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";
+
+/* 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(details.frameId, 0, "root frame ID correct");
+ browser.test.assertEq(details.parentFrameId, -1, "root parent frame ID correct");
+ } else {
+ browser.test.assertEq(details.parentFrameId, 0, "parent frame ID correct");
+ browser.test.assertTrue(details.frameId != 0, "frame ID probably okay");
+ }
+
+ browser.test.assertTrue(details.frameId !== undefined);
+ browser.test.assertTrue(details.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 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(function* webnav_transitions_props() {
+ function backgroundScriptTransitions() {
+ const EVENTS = [
+ "onCommitted",
+ "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();
+ }
+ });
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ yield loadAndWait(win, "onCompleted", URL, () => { win.location = URL; });
+
+ // transitionType: reload
+ received = [];
+ yield 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 = [];
+ yield loadAndWait(win, "onCompleted", URL, () => {
+ win.document.querySelector("form").submit();
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == 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 = [];
+ yield 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 = [];
+ yield loadAndWait(win, "onCompleted", URL, () => { win.history.back(); });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == 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 = [];
+ yield 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 = [];
+ yield loadAndWait(win, "onCompleted", REDIRECTED, () => {
+ win.location = CLIENT_REDIRECT_HTTPHEADER;
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" &&
+ data.url == CLIENT_REDIRECT_HTTPHEADER));
+
+ 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 = [];
+ yield 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 = [];
+ yield 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");
+ // 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 = [];
+ yield 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 = [];
+ yield 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) {
+ is(found.details.transitionType, "manual_subframe",
+ "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+ }
+
+ // cleanup phase
+ win.close();
+
+ yield extension.unload();
+ info("webnavigation extension unloaded");
+});
+
+add_task(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();
+ }
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ yield 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});
+ }
+
+ yield 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}`);
+ yield 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}`);
+ yield 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}`);
+ yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); });
+ info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+ }
+
+ win.close();
+
+ yield extension.unload();
+ info("webnavigation extension unloaded");
+});
+
+add_task(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();
+ }
+ });
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ received = [];
+ yield 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();
+
+ yield 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 000000000..a0de5e9e5
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html
@@ -0,0 +1,308 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_webnav_unresolved_uri_on_expected_URI_scheme() {
+ function background() {
+ let lastTest;
+
+ function cleanupTestListeners() {
+ if (lastTest) {
+ let {event, okListener, failListener} = lastTest;
+ lastTest = null;
+ browser.test.log(`Cleanup previous test event listeners`);
+ browser.webNavigation[event].removeListener(okListener);
+ browser.webNavigation[event].removeListener(failListener);
+ }
+ }
+
+ function createTestListener(event, fail, urlFilter) {
+ 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}`);
+ }
+
+ cleanupTestListeners();
+ browser.test.sendMessage("test-filter-next");
+ }
+
+ browser.webNavigation[event].addListener(listener, urlFilter);
+
+ return listener;
+ }
+
+ browser.test.onMessage.addListener((msg, event, okFilter, failFilter) => {
+ if (msg !== "test-filter") {
+ return;
+ }
+
+ lastTest = {
+ event,
+ // Register the failListener first, which should not be called
+ // and if it is called the test scenario is marked as a failure.
+ failListener: createTestListener(event, true, failFilter),
+ okListener: createTestListener(event, false, okFilter),
+ };
+
+ browser.test.sendMessage("test-filter-ready");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ yield extension.awaitMessage("ready");
+
+ let win = window.open();
+
+ let testFilterScenarios = [
+ {
+ url: "http://example.net/browser",
+ filters: [
+ // schemes
+ {
+ okFilter: [{schemes: ["http"]}],
+ failFilter: [{schemes: ["https"]}],
+ },
+ // ports
+ {
+ okFilter: [{ports: [80, 22, 443]}],
+ failFilter: [{ports: [81, 82, 83]}],
+ },
+ {
+ okFilter: [{ports: [22, 443, [10, 80]]}],
+ failFilter: [{ports: [22, 23, [81, 100]]}],
+ },
+ ],
+ },
+ {
+ url: "http://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"}],
+ },
+ ],
+ },
+ {
+ url: "http://example.net/browser",
+ filters: [
+ // multiple criteria in a single filter:
+ // if one of the critera is not verified, the event should not be received.
+ {
+ okFilter: [{schemes: ["http"], ports: [80, 22, 443]}],
+ failFilter: [{schemes: ["http"], ports: [81, 82, 83]}],
+ },
+ // multiple urlFilters on the same listener
+ // if at least one of the critera is verified, the event should be received.
+ {
+ okFilter: [{schemes: ["https"]}, {ports: [80, 22, 443]}],
+ failFilter: [{schemes: ["https"]}, {ports: [81, 82, 83]}],
+ },
+ ],
+ },
+ ];
+
+ function* runTestScenario(event, {url, filters}) {
+ for (let testFilters of filters) {
+ let {okFilter, failFilter} = testFilters;
+
+ info(`Prepare the new test scenario: ${event} ${url} ${JSON.stringify(testFilters)}`);
+ win.location = "about:blank";
+
+ extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter});
+ yield extension.awaitMessage("test-filter-ready");
+
+ info(`Loading the test url: ${url}`);
+ win.location = url;
+
+ yield extension.awaitMessage("test-filter-next");
+
+ info("Test scenario completed. Moving to the next test scenario.");
+ }
+ }
+
+ const BASE_WEBNAV_EVENTS = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ ];
+
+ info("WebNavigation event filters test scenarios starting...");
+
+ for (let filterScenario of testFilterScenarios) {
+ for (let event of BASE_WEBNAV_EVENTS) {
+ yield runTestScenario(event, filterScenario);
+ }
+ }
+
+ 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 event = "onCompleted";
+
+ info(`Loading the initial test url: ${url}`);
+ extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter});
+
+ yield extension.awaitMessage("test-filter-ready");
+ win.location = url;
+ yield extension.awaitMessage("test-filter-next");
+
+ event = "onReferenceFragmentUpdated";
+ extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter});
+
+ yield extension.awaitMessage("test-filter-ready");
+ win.location = url + "#ref1";
+ yield extension.awaitMessage("test-filter-next");
+
+ info("WebNavigation event filters test onHistoryStateUpdated scenario starting...");
+
+ event = "onHistoryStateUpdated";
+ extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter});
+ yield extension.awaitMessage("test-filter-ready");
+
+ win.history.pushState({}, "", BASE + "/pushState_page3.html");
+ yield 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.");
+
+ yield extension.unload();
+
+ win.close();
+});
+
+add_task(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,
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("webNav.emptyFilterValidationError");
+
+ yield extension.unload();
+});
+
+</script>
+
+</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 000000000..78efeab35
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_webRequest_serviceworker_events() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.serviceWorkers.testing.enabled", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ],
+ });
+
+ 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), `recieved ${name}`);
+ eventNames.delete(name);
+ 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/*"]}
+ );
+ }
+ },
+ });
+
+ yield extension.startup();
+ let registration = yield navigator.serviceWorker.register("webrequest_worker.js", {scope: "."});
+ yield extension.awaitMessage("done");
+ yield registration.unregister();
+ yield extension.unload();
+});
+
+add_task(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), `recieved ${name}`);
+ eventNames.delete(name);
+
+ if (eventNames.size === 0) {
+ browser.test.assertEq(0, eventNames.size, "messages recieved");
+ 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.pass("Fetch succeeded.");
+ }, () => {
+ browser.test.fail("fetch recieved");
+ browser.test.sendMessage("done");
+ });
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield 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 000000000..ef77fee3b
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
@@ -0,0 +1,327 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" 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";
+
+let extension;
+add_task(function* setup() {
+ // SelfSupport has a tendency to fire when running this test alone, without
+ // a good way to turn it off we just set the url to ""
+ yield SpecialPowers.pushPrefEnv({
+ set: [["browser.selfsupport.url", ""]],
+ });
+
+ // 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);
+
+ extension = makeExtension();
+ yield 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
+
+add_task(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});
+ yield extension.awaitMessage("continue");
+ addStylesheet("file_style_bad.css");
+ yield extension.awaitMessage("cancelled");
+ // we redirect to style_good which completes the test
+ addStylesheet("file_style_redirect.css");
+ yield extension.awaitMessage("done");
+
+ let style = window.getComputedStyle(document.getElementById("test"), null);
+ is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded");
+});
+
+add_task(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});
+ yield extension.awaitMessage("continue");
+ addImage("file_image_bad.png");
+ yield extension.awaitMessage("cancelled");
+ // we redirect to image_good which completes the test
+ addImage("file_image_redirect.png");
+ yield extension.awaitMessage("done");
+});
+
+add_task(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});
+ yield extension.awaitMessage("continue");
+ addScript("file_script_bad.js");
+ yield extension.awaitMessage("cancelled");
+ // we redirect to script_good which completes the test
+ addScript("file_script_redirect.js");
+ yield extension.awaitMessage("done");
+
+ is(window.success, 1, "Good script ran");
+ is(window.failure, undefined, "Failure script didn't run");
+});
+
+add_task(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});
+ yield extension.awaitMessage("continue");
+ addScript("file_script_xhr.js");
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_nonexistent() {
+ let expect = {
+ "nonexistent_script_url.js": {
+ status: 404,
+ type: "script",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addScript("nonexistent_script_url.js");
+ yield extension.awaitMessage("done");
+});
+
+add_task(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: true,
+ },
+ "nonexistent_script_url.js": {
+ status: 404,
+ type: "script",
+ cached: false,
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addImage("file_image_good.png");
+ addScript("file_script_good.js");
+ addStylesheet("file_style_good.css");
+ addScript("nonexistent_script_url.js");
+ yield extension.awaitMessage("done");
+
+ is(window.success, 2, "Good script ran");
+ is(window.failure, undefined, "Failure script didn't run");
+});
+
+add_task(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});
+ yield extension.awaitMessage("continue");
+ addScript("file_script_nonexistent.js");
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_tabId() {
+ let expect = {
+ "file_WebRequest_page3.html": {
+ type: "main_frame",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ let a = addLink("file_WebRequest_page3.html?trigger=a");
+ a.click();
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_tabId_browser() {
+ async function background(url) {
+ 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 tabExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "tabs",
+ ],
+ },
+ background: `(${background})('${SimpleTest.getTestFileURL("file_sample.html")}?nocache=${Math.random()}')`,
+ });
+
+ let expect = {
+ "file_sample.html": {
+ type: "main_frame",
+ },
+ };
+ // expecting origin == undefined
+ extension.sendMessage("set-expected", {expect});
+ yield extension.awaitMessage("continue");
+
+ // open a tab from a system principal
+ yield tabExt.startup();
+
+ yield extension.awaitMessage("done");
+ tabExt.sendMessage("done");
+ yield tabExt.awaitMessage("done");
+ yield tabExt.unload();
+});
+
+add_task(function* test_webRequest_frames() {
+ let expect = {
+ "text/plain,webRequestTest": {
+ type: "sub_frame",
+ events: ["onBeforeRequest", "onCompleted"],
+ },
+ "text/plain,webRequestTest_bad": {
+ type: "sub_frame",
+ events: ["onBeforeRequest", "onCompleted"],
+ cancel: "onBeforeRequest",
+ },
+ "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"],
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addFrame("data:text/plain,webRequestTest");
+ addFrame("data:text/plain,webRequestTest_bad");
+ yield extension.awaitMessage("cancelled");
+ addFrame("redirection.sjs");
+ addFrame("https://invalid.localhost/badrobot");
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* teardown() {
+ yield extension.unload();
+});
+</script>
+</head>
+<body>
+<div id="test">Sample text</div>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html
new file mode 100644
index 000000000..c8423ec7c
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html
@@ -0,0 +1,216 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_suspend() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ ],
+ },
+
+ background() {
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ details => {
+ // Make sure that returning undefined or a promise that resolves to
+ // undefined does not break later handlers.
+ },
+ {urls: ["<all_urls>"]},
+ ["blocking", "requestHeaders"]);
+
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ details => {
+ return Promise.resolve();
+ },
+ {urls: ["<all_urls>"]},
+ ["blocking", "requestHeaders"]);
+
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ details => {
+ let requestHeaders = details.requestHeaders.concat({name: "Foo", value: "Bar"});
+
+ return new Promise(resolve => {
+ setTimeout(resolve, 500);
+ }).then(() => {
+ return {requestHeaders};
+ });
+ },
+ {urls: ["<all_urls>"]},
+ ["blocking", "requestHeaders"]);
+ },
+ });
+
+ yield extension.startup();
+
+ let result = yield fetch(SimpleTest.getTestFileURL("return_headers.sjs"));
+
+ let headers = JSON.parse(yield result.text());
+
+ is(headers.foo, "Bar", "Request header was correctly set on suspended request");
+
+ yield extension.unload();
+});
+
+
+// Test that requests that were canceled while suspended for a blocking
+// listener are correctly resumed.
+add_task(function* test_error_resume() {
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let observer = channel => {
+ if (channel instanceof Ci.nsIHttpChannel && channel.URI.spec === "http://example.com/") {
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+
+ // Wait until the next tick to make sure this runs after WebRequest observers.
+ Promise.resolve().then(() => {
+ channel.cancel(Cr.NS_BINDING_ABORTED);
+ });
+ }
+ };
+
+ Services.obs.addObserver(observer, "http-on-modify-request", false);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ ],
+ },
+
+ background() {
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ details => {
+ browser.test.log(`onBeforeSendHeaders({url: ${details.url}})`);
+
+ if (details.url === "http://example.com/") {
+ browser.test.sendMessage("got-before-send-headers");
+ }
+ },
+ {urls: ["<all_urls>"]},
+ ["blocking"]);
+
+ browser.webRequest.onErrorOccurred.addListener(
+ details => {
+ browser.test.log(`onErrorOccurred({url: ${details.url}})`);
+
+ if (details.url === "http://example.com/") {
+ browser.test.sendMessage("got-error-occurred");
+ }
+ },
+ {urls: ["<all_urls>"]});
+ },
+ });
+
+ yield extension.startup();
+
+ try {
+ yield fetch("http://example.com/");
+ ok(false, "Fetch should have failed.");
+ } catch (e) {
+ ok(true, "Got expected error.");
+ }
+
+ yield extension.awaitMessage("got-before-send-headers");
+ yield extension.awaitMessage("got-error-occurred");
+
+ yield extension.unload();
+ chromeScript.destroy();
+});
+
+
+// Test that response header modifications take effect before onStartRequest fires.
+add_task(function* test_set_responseHeaders() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "http://example.com/",
+ ],
+ },
+
+ background() {
+ browser.webRequest.onHeadersReceived.addListener(
+ details => {
+ browser.test.log(`onHeadersReceived({url: ${details.url}})`);
+
+ details.responseHeaders.push({name: "foo", value: "bar"});
+
+ return {responseHeaders: details.responseHeaders};
+ },
+ {urls: ["http://example.com/?modify_headers"]},
+ ["blocking", "responseHeaders"]);
+ },
+ });
+
+ yield extension.startup();
+
+ yield new Promise(resolve => setTimeout(resolve, 0));
+
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+ Cu.import("resource://gre/modules/NetUtil.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ let ssm = Services.scriptSecurityManager;
+
+ let channel = NetUtil.newChannel({
+ uri: "http://example.com/?modify_headers",
+ loadingPrincipal: ssm.createCodebasePrincipalFromOrigin("http://example.com"),
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ });
+
+ channel.asyncOpen2({
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]),
+
+ onStartRequest(request, context) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+
+ try {
+ sendAsyncMessage("response-header-foo", request.getResponseHeader("foo"));
+ } catch (e) {
+ sendAsyncMessage("response-header-foo", null);
+ }
+ request.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onStopRequest() {
+ },
+
+ onDataAvailable() {
+ throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
+ },
+ });
+ });
+
+ let headerValue = yield chromeScript.promiseOneMessage("response-header-foo");
+ is(headerValue, "bar", "Expected Foo header value");
+
+ yield extension.unload();
+ chromeScript.destroy();
+});
+
+
+</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 000000000..998ab9800
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html
@@ -0,0 +1,199 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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>
+
+<form method="post"
+ action="file_WebRequest_page3.html?trigger=form"
+ target="_blank"
+ enctype="multipart/form-data"
+ >
+<input type="text" name="&quot;special&quot; 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(function* test_setup() {
+ files = yield 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,
+ };
+});
+
+function background() {
+ const FILTERS = {urls: ["<all_urls>"]};
+
+ let requestBodySupported = true;
+
+ function onUpload(details) {
+ let url = new URL(details.url);
+ let upload = url.searchParams.get("upload");
+ if (!upload || !requestBodySupported) {
+ 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}`);
+
+ browser.test.sendMessage("done");
+ },
+ FILTERS);
+
+ let onBeforeRequest = details => {
+ browser.test.log(`${name} ${details.requestId} ${details.url}`);
+
+ onUpload(details);
+ };
+
+ try {
+ browser.webRequest.onBeforeRequest.addListener(
+ onBeforeRequest, FILTERS, ["requestBody"]);
+ } 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;
+ }
+
+ browser.webRequest.onBeforeRequest.addListener(
+ onBeforeRequest, FILTERS);
+ }
+}
+
+add_task(function* test_xhr_forms() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ ],
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ 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();
+ yield extension.awaitMessage("done");
+
+ 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 extension.awaitMessage("done");
+ };
+
+ formData.append(blob.name, blob.content, blob.fileName);
+ formData.append("formDataField", "some value");
+ updateActionURL();
+ yield post(formData);
+
+ action.searchParams.set("upload", JSON.stringify([{file: "<file>"}]));
+ yield post(testFile);
+
+ action.searchParams.set("upload", `${blob.content.size} bytes`);
+ yield post(blob.content);
+
+ let byteLength = 16;
+ action.searchParams.set("upload", `${byteLength} bytes`);
+ yield post(new ArrayBuffer(byteLength));
+ }
+
+ yield extension.unload();
+});
+</script>
+</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 000000000..7d49d55ba
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html
@@ -0,0 +1,105 @@
+<!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/SpawnTask.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";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(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);
+ yield extension.startup();
+
+ let iframeURL = yield extension.awaitMessage("iframe-url");
+ let testURL = SimpleTest.getTestFileURL("file_sample.html");
+
+ for (let url of [iframeURL, testURL]) {
+ info(`Testing URL ${url}`);
+
+ let iframe = yield createIframe(url);
+
+ iframe.contentWindow.postMessage(
+ "ping", url);
+
+ let pong = yield awaitMessage();
+ is(pong.pong, url, "Got expected pong");
+
+ iframe.remove();
+ }
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html b/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html
new file mode 100644
index 000000000..1afdadb9f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test XHR capabilities</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(function* test_xhr_capabilities() {
+ function backgroundScript() {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", browser.extension.getURL("bad.xml"));
+
+ browser.test.sendMessage("result",
+ {name: "Background script XHRs should not be privileged",
+ result: xhr.channel === undefined});
+
+ xhr.onload = () => {
+ browser.test.sendMessage("result",
+ {name: "Background script XHRs should not yield <parsererrors>",
+ result: xhr.responseXML === null});
+ };
+ xhr.send();
+ }
+
+ function contentScript() {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", browser.extension.getURL("bad.xml"));
+
+ browser.test.sendMessage("result",
+ {name: "Content script XHRs should not be privileged",
+ result: xhr.channel === undefined});
+
+ xhr.onload = () => {
+ browser.test.sendMessage("result",
+ {name: "Content script XHRs should not yield <parsererrors>",
+ result: xhr.responseXML === null});
+ };
+ xhr.send();
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ background: {
+ "scripts": ["background.js"],
+ },
+ content_scripts: [{
+ "matches": ["http://example.com/"],
+ "js": ["content_script.js"],
+ }],
+ web_accessible_resources: [
+ "bad.xml",
+ ],
+ },
+
+ files: {
+ "bad.xml": "<xml",
+ "background.js": `(${backgroundScript})()`,
+ "content_script.js": `(${contentScript})()`,
+ },
+ });
+
+ yield extension.startup();
+
+ let win = window.open("http://example.com/");
+
+ // We expect four test results from the content/background scripts.
+ for (let i = 0; i < 4; ++i) {
+ let result = yield extension.awaitMessage("result");
+ ok(result.result, result.name);
+ }
+
+ win.close();
+ yield extension.unload();
+});
+</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 000000000..ccfb2ac1f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js
@@ -0,0 +1,8 @@
+"use strict";
+
+onmessage = function(event) {
+ fetch("https://example.com/example.txt").then(() => {
+ postMessage("Done!");
+ });
+};
+
diff --git a/toolkit/components/extensions/test/mochitest/webrequest_test.jsm b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm
new file mode 100644
index 000000000..bfb148301
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm
@@ -0,0 +1,22 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["webrequest_test"];
+
+Components.utils.importGlobalProperties(["fetch", "XMLHttpRequest"]);
+
+this.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 000000000..dcffd0857
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/webrequest_worker.js
@@ -0,0 +1,3 @@
+"use strict";
+
+fetch("https://example.com/example.txt");
diff --git a/toolkit/components/extensions/test/xpcshell/.eslintrc.js b/toolkit/components/extensions/test/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..3758537ef
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": "../../../../../testing/xpcshell/xpcshell.eslintrc.js",
+
+ "globals": {
+ "browser": false,
+ },
+};
diff --git a/toolkit/components/extensions/test/xpcshell/data/file_download.html b/toolkit/components/extensions/test/xpcshell/data/file_download.html
new file mode 100644
index 000000000..d970c6325
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/data/file_download.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div>Download HTML File</div>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/xpcshell/data/file_download.txt b/toolkit/components/extensions/test/xpcshell/data/file_download.txt
new file mode 100644
index 000000000..6293c7af7
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/data/file_download.txt
@@ -0,0 +1 @@
+This is a sample file used in download tests.
diff --git a/toolkit/components/extensions/test/xpcshell/head.js b/toolkit/components/extensions/test/xpcshell/head.js
new file mode 100644
index 000000000..9e22be6da
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/head.js
@@ -0,0 +1,111 @@
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* exported createHttpServer, promiseConsoleOutput, cleanupDir */
+
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Timer.jsm");
+Components.utils.import("resource://testing-common/AddonTestUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+ "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
+ "resource://testing-common/ExtensionXPCShellUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+ExtensionTestUtils.init(this);
+
+/**
+ * Creates a new HttpServer for testing, and begins listening on the
+ * specified port. Automatically shuts down the server when the test
+ * unit ends.
+ *
+ * @param {integer} [port]
+ * The port to listen on. If omitted, listen on a random
+ * port. The latter is the preferred behavior.
+ *
+ * @returns {HttpServer}
+ */
+function createHttpServer(port = -1) {
+ let server = new HttpServer();
+ server.start(port);
+
+ do_register_cleanup(() => {
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
+
+ return server;
+}
+
+var promiseConsoleOutput = Task.async(function* (task) {
+ const DONE = `=== console listener ${Math.random()} done ===`;
+
+ let listener;
+ let messages = [];
+ let awaitListener = new Promise(resolve => {
+ listener = msg => {
+ if (msg == DONE) {
+ resolve();
+ } else {
+ void (msg instanceof Ci.nsIConsoleMessage);
+ messages.push(msg);
+ }
+ };
+ });
+
+ Services.console.registerListener(listener);
+ try {
+ let result = yield task();
+
+ Services.console.logStringMessage(DONE);
+ yield awaitListener;
+
+ return {messages, result};
+ } finally {
+ Services.console.unregisterListener(listener);
+ }
+});
+
+// Attempt to remove a directory. If the Windows OS is still using the
+// file sometimes remove() will fail. So try repeatedly until we can
+// remove it or we give up.
+function cleanupDir(dir) {
+ let count = 0;
+ return new Promise((resolve, reject) => {
+ function tryToRemoveDir() {
+ count += 1;
+ try {
+ dir.remove(true);
+ } catch (e) {
+ // ignore
+ }
+ if (!dir.exists()) {
+ return resolve();
+ }
+ if (count >= 25) {
+ return reject(`Failed to cleanup directory: ${dir}`);
+ }
+ setTimeout(tryToRemoveDir, 100);
+ }
+ tryToRemoveDir();
+ });
+}
diff --git a/toolkit/components/extensions/test/xpcshell/head_native_messaging.js b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js
new file mode 100644
index 000000000..f7c619b76
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js
@@ -0,0 +1,131 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals AppConstants, FileUtils */
+/* exported getSubprocessCount, setupHosts, waitForSubprocessExit */
+
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
+ "resource://testing-common/MockRegistry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+
+let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
+
+
+// It's important that we use a space in this directory name to make sure we
+// correctly handle executing batch files with spaces in their path.
+let tmpDir = FileUtils.getDir("TmpD", ["Native Messaging"]);
+tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+do_register_cleanup(() => {
+ tmpDir.remove(true);
+});
+
+function getPath(filename) {
+ return OS.Path.join(tmpDir.path, filename);
+}
+
+const ID = "native@tests.mozilla.org";
+
+
+function* setupHosts(scripts) {
+ const PERMS = {unixMode: 0o755};
+
+ const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ const pythonPath = yield Subprocess.pathSearch(env.get("PYTHON"));
+
+ function* writeManifest(script, scriptPath, path) {
+ let body = `#!${pythonPath} -u\n${script.script}`;
+
+ yield OS.File.writeAtomic(scriptPath, body);
+ yield OS.File.setPermissions(scriptPath, PERMS);
+
+ let manifest = {
+ name: script.name,
+ description: script.description,
+ path,
+ type: "stdio",
+ allowed_extensions: [ID],
+ };
+
+ let manifestPath = getPath(`${script.name}.json`);
+ yield OS.File.writeAtomic(manifestPath, JSON.stringify(manifest));
+
+ return manifestPath;
+ }
+
+ switch (AppConstants.platform) {
+ case "macosx":
+ case "linux":
+ let dirProvider = {
+ getFile(property) {
+ if (property == "XREUserNativeMessaging") {
+ return tmpDir.clone();
+ } else if (property == "XRESysNativeMessaging") {
+ return tmpDir.clone();
+ }
+ return null;
+ },
+ };
+
+ Services.dirsvc.registerProvider(dirProvider);
+ do_register_cleanup(() => {
+ Services.dirsvc.unregisterProvider(dirProvider);
+ });
+
+ for (let script of scripts) {
+ let path = getPath(`${script.name}.py`);
+
+ yield writeManifest(script, path, path);
+ }
+ break;
+
+ case "win":
+ const REGKEY = String.raw`Software\Mozilla\NativeMessagingHosts`;
+
+ let registry = new MockRegistry();
+ do_register_cleanup(() => {
+ registry.shutdown();
+ });
+
+ for (let script of scripts) {
+ // It's important that we use a space in this filename. See directory
+ // name comment above.
+ let batPath = getPath(`batch ${script.name}.bat`);
+ let scriptPath = getPath(`${script.name}.py`);
+
+ let batBody = `@ECHO OFF\n${pythonPath} -u "${scriptPath}" %*\n`;
+ yield OS.File.writeAtomic(batPath, batBody);
+
+ // Create absolute and relative path versions of the entry.
+ for (let [name, path] of [[script.name, batPath],
+ [`relative.${script.name}`, OS.Path.basename(batPath)]]) {
+ script.name = name;
+ let manifestPath = yield writeManifest(script, scriptPath, path);
+
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGKEY}\\${script.name}`, "", manifestPath);
+ }
+ }
+ break;
+
+ default:
+ ok(false, `Native messaging is not supported on ${AppConstants.platform}`);
+ }
+}
+
+
+function getSubprocessCount() {
+ return SubprocessImpl.Process.getWorker().call("getProcesses", [])
+ .then(result => result.size);
+}
+function waitForSubprocessExit() {
+ return SubprocessImpl.Process.getWorker().call("waitForNoProcesses", []).then(() => {
+ // Return to the main event loop to give IO handlers enough time to consume
+ // their remaining buffered input.
+ return new Promise(resolve => setTimeout(resolve, 0));
+ });
+}
diff --git a/toolkit/components/extensions/test/xpcshell/head_sync.js b/toolkit/components/extensions/test/xpcshell/head_sync.js
new file mode 100644
index 000000000..9b66b78e7
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/head_sync.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported withSyncContext */
+
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm", this);
+
+var {
+ BaseContext,
+} = ExtensionCommon;
+
+class Context extends BaseContext {
+ constructor(principal) {
+ super();
+ Object.defineProperty(this, "principal", {
+ value: principal,
+ configurable: true,
+ });
+ this.sandbox = Components.utils.Sandbox(principal, {wantXrays: false});
+ this.extension = {id: "test@web.extension"};
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+}
+
+/**
+ * Call the given function with a newly-constructed context.
+ * Unload the context on the way out.
+ *
+ * @param {function} f the function to call
+ */
+function* withContext(f) {
+ const ssm = Services.scriptSecurityManager;
+ const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org");
+ const context = new Context(PRINCIPAL1);
+ try {
+ yield* f(context);
+ } finally {
+ yield context.unload();
+ }
+}
+
+/**
+ * Like withContext(), but also turn on the "storage.sync" pref for
+ * the duration of the function.
+ * Calls to this function can be replaced with calls to withContext
+ * once the pref becomes on by default.
+ *
+ * @param {function} f the function to call
+ */
+function* withSyncContext(f) {
+ const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled";
+ let prefs = Services.prefs;
+
+ try {
+ prefs.setBoolPref(STORAGE_SYNC_PREF, true);
+ yield* withContext(f);
+ } finally {
+ prefs.clearUserPref(STORAGE_SYNC_PREF);
+ }
+}
diff --git a/toolkit/components/extensions/test/xpcshell/native_messaging.ini b/toolkit/components/extensions/test/xpcshell/native_messaging.ini
new file mode 100644
index 000000000..d0e1da163
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/native_messaging.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+head = head.js head_native_messaging.js
+tail =
+firefox-appdir = browser
+skip-if = appname == "thunderbird" || os == "android"
+subprocess = true
+support-files =
+ data/**
+tags = webextensions
+
+[test_ext_native_messaging.js]
+[test_ext_native_messaging_perf.js]
+[test_ext_native_messaging_unresponsive.js]
diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js
new file mode 100644
index 000000000..b6213baac
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js
@@ -0,0 +1,38 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+const ADDON_ID = "test@web.extension";
+
+const aps = Cc["@mozilla.org/addons/policy-service;1"]
+ .getService(Ci.nsIAddonPolicyService).wrappedJSObject;
+
+do_register_cleanup(() => {
+ aps.setAddonCSP(ADDON_ID, null);
+});
+
+add_task(function* test_addon_csp() {
+ equal(aps.baseCSP, Preferences.get("extensions.webextensions.base-content-security-policy"),
+ "Expected base CSP value");
+
+ equal(aps.defaultCSP, Preferences.get("extensions.webextensions.default-content-security-policy"),
+ "Expected default CSP value");
+
+ equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
+ "CSP for unknown add-on ID should be the default CSP");
+
+
+ const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'";
+
+ aps.setAddonCSP(ADDON_ID, CUSTOM_POLICY);
+
+ equal(aps.getAddonCSP(ADDON_ID), CUSTOM_POLICY, "CSP should point to add-on's custom policy");
+
+
+ aps.setAddonCSP(ADDON_ID, null);
+
+ equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
+ "CSP should revert to default when set to null");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_validator.js b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js
new file mode 100644
index 000000000..59a7322bc
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js
@@ -0,0 +1,85 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const cps = Cc["@mozilla.org/addons/content-policy;1"].getService(Ci.nsIAddonContentPolicy);
+
+add_task(function* test_csp_validator() {
+ let checkPolicy = (policy, expectedResult, message = null) => {
+ do_print(`Checking policy: ${policy}`);
+
+ let result = cps.validateAddonCSP(policy);
+ equal(result, expectedResult);
+ };
+
+ checkPolicy("script-src 'self'; object-src 'self';",
+ null);
+
+ let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='";
+
+ checkPolicy(`script-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash} 'unsafe-eval'; ` +
+ `object-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash}`,
+ null);
+
+ checkPolicy("",
+ "Policy is missing a required \u2018script-src\u2019 directive");
+
+ checkPolicy("object-src 'none';",
+ "Policy is missing a required \u2018script-src\u2019 directive");
+
+
+ checkPolicy("default-src 'self'", null,
+ "A valid default-src should count as a valid script-src or object-src");
+
+ checkPolicy("default-src 'self'; script-src 'self'", null,
+ "A valid default-src should count as a valid script-src or object-src");
+
+ checkPolicy("default-src 'self'; object-src 'self'", null,
+ "A valid default-src should count as a valid script-src or object-src");
+
+
+ checkPolicy("default-src 'self'; script-src http://example.com",
+ "\u2018script-src\u2019 directive contains a forbidden http: protocol source",
+ "A valid default-src should not allow an invalid script-src directive");
+
+ checkPolicy("default-src 'self'; object-src http://example.com",
+ "\u2018object-src\u2019 directive contains a forbidden http: protocol source",
+ "A valid default-src should not allow an invalid object-src directive");
+
+
+ checkPolicy("script-src 'self';",
+ "Policy is missing a required \u2018object-src\u2019 directive");
+
+ checkPolicy("script-src 'none'; object-src 'none'",
+ "\u2018script-src\u2019 must include the source 'self'");
+
+ checkPolicy("script-src 'self'; object-src 'none';",
+ null);
+
+ checkPolicy("script-src 'self' 'unsafe-inline'; object-src 'self';",
+ "\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword");
+
+
+ let directives = ["script-src", "object-src"];
+
+ for (let [directive, other] of [directives, directives.slice().reverse()]) {
+ for (let src of ["https://*", "https://*.blogspot.com", "https://*"]) {
+ checkPolicy(`${directive} 'self' ${src}; ${other} 'self';`,
+ `https: wildcard sources in \u2018${directive}\u2019 directives must include at least one non-generic sub-domain (e.g., *.example.com rather than *.com)`);
+ }
+
+ checkPolicy(`${directive} 'self' https:; ${other} 'self';`,
+ `https: protocol requires a host in \u2018${directive}\u2019 directives`);
+
+ checkPolicy(`${directive} 'self' http://example.com; ${other} 'self';`,
+ `\u2018${directive}\u2019 directive contains a forbidden http: protocol source`);
+
+ for (let protocol of ["http", "ftp", "meh"]) {
+ checkPolicy(`${directive} 'self' ${protocol}:; ${other} 'self';`,
+ `\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source`);
+ }
+
+ checkPolicy(`${directive} 'self' 'nonce-01234'; ${other} 'self';`,
+ `\u2018${directive}\u2019 directive contains a forbidden 'nonce-*' keyword`);
+ }
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js
new file mode 100644
index 000000000..936c984c6
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js
@@ -0,0 +1,210 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_alarm_without_permissions() {
+ function backgroundScript() {
+ browser.test.assertTrue(!browser.alarms,
+ "alarm API is not available when the alarm permission is not required");
+ browser.test.notifyPass("alarms_permission");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: [],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarms_permission");
+ yield extension.unload();
+});
+
+
+add_task(function* test_alarm_fires() {
+ function backgroundScript() {
+ let ALARM_NAME = "test_ext_alarms";
+ let timer;
+
+ browser.alarms.onAlarm.addListener(alarm => {
+ browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the correct name");
+ clearTimeout(timer);
+ browser.test.notifyPass("alarm-fires");
+ });
+
+ browser.alarms.create(ALARM_NAME, {delayInMinutes: 0.02});
+
+ timer = setTimeout(async () => {
+ browser.test.fail("alarm fired within expected time");
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+ browser.test.notifyFail("alarm-fires");
+ }, 10000);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-fires");
+ yield extension.unload();
+});
+
+
+add_task(function* test_alarm_fires_with_when() {
+ function backgroundScript() {
+ let ALARM_NAME = "test_ext_alarms";
+ let timer;
+
+ browser.alarms.onAlarm.addListener(alarm => {
+ browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the expected name");
+ clearTimeout(timer);
+ browser.test.notifyPass("alarm-when");
+ });
+
+ browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
+
+ timer = setTimeout(async () => {
+ browser.test.fail("alarm fired within expected time");
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+ browser.test.notifyFail("alarm-when");
+ }, 10000);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-when");
+ yield extension.unload();
+});
+
+
+add_task(function* test_alarm_clear_non_matching_name() {
+ async function backgroundScript() {
+ let ALARM_NAME = "test_ext_alarms";
+
+ browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000});
+
+ let wasCleared = await browser.alarms.clear(ALARM_NAME + "1");
+ browser.test.assertFalse(wasCleared, "alarm was not cleared");
+
+ let alarms = await browser.alarms.getAll();
+ browser.test.assertEq(1, alarms.length, "alarm was not removed");
+ browser.test.notifyPass("alarm-clear");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-clear");
+ yield extension.unload();
+});
+
+
+add_task(function* test_alarm_get_and_clear_single_argument() {
+ async function backgroundScript() {
+ browser.alarms.create({when: Date.now() + 2000});
+
+ let alarm = await browser.alarms.get();
+ browser.test.assertEq("", alarm.name, "expected alarm returned");
+
+ let wasCleared = await browser.alarms.clear();
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ let alarms = await browser.alarms.getAll();
+ browser.test.assertEq(0, alarms.length, "alarm was removed");
+
+ browser.test.notifyPass("alarm-single-arg");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-single-arg");
+ yield extension.unload();
+});
+
+
+add_task(function* test_get_get_all_clear_all_alarms() {
+ async function backgroundScript() {
+ const ALARM_NAME = "test_alarm";
+
+ let suffixes = [0, 1, 2];
+
+ for (let suffix of suffixes) {
+ browser.alarms.create(ALARM_NAME + suffix, {when: Date.now() + (suffix + 1) * 10000});
+ }
+
+ let alarms = await browser.alarms.getAll();
+ browser.test.assertEq(suffixes.length, alarms.length, "expected number of alarms were found");
+ alarms.forEach((alarm, index) => {
+ browser.test.assertEq(ALARM_NAME + index, alarm.name, "alarm has the expected name");
+ });
+
+
+ for (let suffix of suffixes) {
+ let alarm = await browser.alarms.get(ALARM_NAME + suffix);
+ browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "alarm has the expected name");
+ browser.test.sendMessage(`get-${suffix}`);
+ }
+
+ let wasCleared = await browser.alarms.clear(ALARM_NAME + suffixes[0]);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ alarms = await browser.alarms.getAll();
+ browser.test.assertEq(2, alarms.length, "alarm was removed");
+
+ let alarm = await browser.alarms.get(ALARM_NAME + suffixes[0]);
+ browser.test.assertEq(undefined, alarm, "non-existent alarm is undefined");
+ browser.test.sendMessage(`get-invalid`);
+
+ wasCleared = await browser.alarms.clearAll();
+ browser.test.assertTrue(wasCleared, "alarms were cleared");
+
+ alarms = await browser.alarms.getAll();
+ browser.test.assertEq(0, alarms.length, "no alarms exist");
+ browser.test.sendMessage("clearAll");
+ browser.test.sendMessage("clear");
+ browser.test.sendMessage("getAll");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield Promise.all([
+ extension.startup(),
+ extension.awaitMessage("getAll"),
+ extension.awaitMessage("get-0"),
+ extension.awaitMessage("get-1"),
+ extension.awaitMessage("get-2"),
+ extension.awaitMessage("clear"),
+ extension.awaitMessage("get-invalid"),
+ extension.awaitMessage("clearAll"),
+ ]);
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js
new file mode 100644
index 000000000..11407b108
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js
@@ -0,0 +1,33 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_cleared_alarm_does_not_fire() {
+ async function backgroundScript() {
+ let ALARM_NAME = "test_ext_alarms";
+
+ browser.alarms.onAlarm.addListener(alarm => {
+ browser.test.fail("cleared alarm does not fire");
+ browser.test.notifyFail("alarm-cleared");
+ });
+ browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
+
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ browser.test.notifyPass("alarm-cleared");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-cleared");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js
new file mode 100644
index 000000000..6bcdf4e33
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_periodic_alarm_fires() {
+ function backgroundScript() {
+ const ALARM_NAME = "test_ext_alarms";
+ let count = 0;
+ let timer;
+
+ browser.alarms.onAlarm.addListener(async alarm => {
+ browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name");
+ if (count++ === 3) {
+ clearTimeout(timer);
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ browser.test.notifyPass("alarm-periodic");
+ }
+ });
+
+ browser.alarms.create(ALARM_NAME, {periodInMinutes: 0.02});
+
+ timer = setTimeout(async () => {
+ browser.test.fail("alarm fired expected number of times");
+
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ browser.test.notifyFail("alarm-periodic");
+ }, 30000);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-periodic");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js
new file mode 100644
index 000000000..96f61acb5
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+add_task(function* test_duplicate_alarm_name_replaces_alarm() {
+ function backgroundScript() {
+ let count = 0;
+
+ browser.alarms.onAlarm.addListener(async alarm => {
+ if (alarm.name === "master alarm") {
+ browser.alarms.create("child alarm", {delayInMinutes: 0.05});
+ let results = await browser.alarms.getAll();
+
+ browser.test.assertEq(2, results.length, "exactly two alarms exist");
+ browser.test.assertEq("master alarm", results[0].name, "first alarm has the expected name");
+ browser.test.assertEq("child alarm", results[1].name, "second alarm has the expected name");
+
+ if (count++ === 3) {
+ await browser.alarms.clear("master alarm");
+ await browser.alarms.clear("child alarm");
+
+ browser.test.notifyPass("alarm-duplicate");
+ }
+ } else {
+ browser.test.fail("duplicate named alarm replaced existing alarm");
+ browser.test.notifyFail("alarm-duplicate");
+ }
+ });
+
+ browser.alarms.create("master alarm", {delayInMinutes: 0.025, periodInMinutes: 0.025});
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-duplicate");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js
new file mode 100644
index 000000000..d653d0e7a
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js
@@ -0,0 +1,64 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
+function getNextContext() {
+ return new Promise(resolve => {
+ Management.on("proxy-context-load", function listener(type, context) {
+ Management.off("proxy-context-load", listener);
+ resolve(context);
+ });
+ });
+}
+
+add_task(function* test_storage_api_without_permissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ // Force API initialization.
+ void browser.storage;
+ },
+
+ manifest: {
+ permissions: [],
+ },
+ });
+
+ let contextPromise = getNextContext();
+ yield extension.startup();
+
+ let context = yield contextPromise;
+
+ // Force API initialization.
+ void context.apiObj;
+
+ ok(!("storage" in context.apiObj),
+ "The storage API should not be initialized");
+
+ yield extension.unload();
+});
+
+add_task(function* test_storage_api_with_permissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ void browser.storage;
+ },
+
+ manifest: {
+ permissions: ["storage"],
+ },
+ });
+
+ let contextPromise = getNextContext();
+ yield extension.startup();
+
+ let context = yield contextPromise;
+
+ // Force API initialization.
+ void context.apiObj;
+
+ equal(typeof context.apiObj.storage, "object",
+ "The storage API should be initialized");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js b/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
new file mode 100644
index 000000000..3f6672a11
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+
+const {
+ SchemaAPIManager,
+} = ExtensionCommon;
+
+this.unknownvar = "Some module-global var";
+
+var gUniqueId = 0;
+
+// SchemaAPIManager's loadScript uses loadSubScript to load a script. This
+// requires a local (resource://) URL. So create such a temporary URL for
+// testing.
+function toLocalURI(code) {
+ let dataUrl = `data:charset=utf-8,${encodeURIComponent(code)}`;
+ let uniqueResPart = `need-a-local-uri-for-subscript-loading-${++gUniqueId}`;
+ Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler)
+ .setSubstitution(uniqueResPart, Services.io.newURI(dataUrl, null, null));
+ return `resource://${uniqueResPart}`;
+}
+
+add_task(function* test_global_isolation() {
+ let manA = new SchemaAPIManager("procA");
+ let manB = new SchemaAPIManager("procB");
+
+ // The "global" variable should be persistent and shared.
+ manA.loadScript(toLocalURI`global.globalVar = 1;`);
+ do_check_eq(manA.global.globalVar, 1);
+ do_check_eq(manA.global.unknownvar, undefined);
+ manA.loadScript(toLocalURI`global.canSeeGlobal = global.globalVar;`);
+ do_check_eq(manA.global.canSeeGlobal, 1);
+
+ // Each loadScript call should have their own scope, and global is shared.
+ manA.loadScript(toLocalURI`this.aVar = 1; global.thisScopeVar = aVar`);
+ do_check_eq(manA.global.aVar, undefined);
+ do_check_eq(manA.global.thisScopeVar, 1);
+ manA.loadScript(toLocalURI`global.differentScopeVar = this.aVar;`);
+ do_check_eq(manA.global.differentScopeVar, undefined);
+ manA.loadScript(toLocalURI`global.cantSeeOtherScope = typeof aVar;`);
+ do_check_eq(manA.global.cantSeeOtherScope, "undefined");
+
+ manB.loadScript(toLocalURI`global.tryReadOtherGlobal = global.tryagain;`);
+ do_check_eq(manA.global.tryReadOtherGlobal, undefined);
+
+ // Cu.import without second argument exports to the caller's global. Let's
+ // verify that it does not leak to the SchemaAPIManager's global.
+ do_check_eq(typeof ExtensionUtils, "undefined"); // Sanity check #1.
+ manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
+ do_check_eq(manA.global.hasExtUtils, "undefined"); // Sanity check #2
+
+ Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+ do_check_eq(typeof ExtensionUtils, "object"); // Sanity check #3.
+
+ manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
+ do_check_eq(manA.global.hasExtUtils, "undefined");
+ manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
+ do_check_eq(manB.global.hasExtUtils, "undefined");
+
+ // Confirm that Cu.import does not leak between SchemaAPIManager globals.
+ manA.loadScript(toLocalURI`
+ Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+ global.hasExtUtils = typeof ExtensionUtils;
+ `);
+ do_check_eq(manA.global.hasExtUtils, "object"); // Sanity check.
+ manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
+ do_check_eq(manB.global.hasExtUtils, "undefined");
+
+ // Prototype modifications should be isolated.
+ manA.loadScript(toLocalURI`
+ Object.prototype.modifiedByA = "Prrft";
+ global.fromA = {};
+ `);
+ manA.loadScript(toLocalURI`
+ global.fromAagain = {};
+ `);
+ manB.loadScript(toLocalURI`
+ global.fromB = {};
+ `);
+ do_check_eq(manA.global.modifiedByA, "Prrft");
+ do_check_eq(manA.global.fromA.modifiedByA, "Prrft");
+ do_check_eq(manA.global.fromAagain.modifiedByA, "Prrft");
+ do_check_eq(manB.global.modifiedByA, undefined);
+ do_check_eq(manB.global.fromB.modifiedByA, undefined);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js
new file mode 100644
index 000000000..26282fcb9
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js
@@ -0,0 +1,23 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* test_DOMContentLoaded_in_generated_background_page() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ function reportListener(event) {
+ browser.test.sendMessage("eventname", event.type);
+ }
+ document.addEventListener("DOMContentLoaded", reportListener);
+ window.addEventListener("load", reportListener);
+ },
+ });
+
+ yield extension.startup();
+ equal("DOMContentLoaded", yield extension.awaitMessage("eventname"));
+ equal("load", yield extension.awaitMessage("eventname"));
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js
new file mode 100644
index 000000000..4bf59b798
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js
@@ -0,0 +1,24 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_reload_generated_background_page() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ if (location.hash !== "#firstrun") {
+ browser.test.sendMessage("first run");
+ location.hash = "#firstrun";
+ browser.test.assertEq("#firstrun", location.hash);
+ location.reload();
+ } else {
+ browser.test.notifyPass("second run");
+ }
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("first run");
+ yield extension.awaitFinish("second run");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js
new file mode 100644
index 000000000..092a9f5b3
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js
@@ -0,0 +1,22 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://testing-common/PlacesTestUtils.jsm");
+
+add_task(function* test_global_history() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("background-loaded", location.href);
+ },
+ });
+
+ yield extension.startup();
+
+ let backgroundURL = yield extension.awaitMessage("background-loaded");
+
+ yield extension.unload();
+
+ let exists = yield PlacesTestUtils.isPageInDB(backgroundURL);
+ ok(!exists, "Background URL should not be in history database");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js
new file mode 100644
index 000000000..8e8b5e0b0
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js
@@ -0,0 +1,40 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+function* testBackgroundPage(expected) {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ browser.test.assertEq(window, browser.extension.getBackgroundPage(),
+ "Caller should be able to access itself as a background page");
+ browser.test.assertEq(window, await browser.runtime.getBackgroundPage(),
+ "Caller should be able to access itself as a background page");
+
+ browser.test.sendMessage("incognito", browser.extension.inIncognitoContext);
+ },
+ });
+
+ yield extension.startup();
+
+ let incognito = yield extension.awaitMessage("incognito");
+ equal(incognito, expected.incognito, "Expected incognito value");
+
+ yield extension.unload();
+}
+
+add_task(function* test_background_incognito() {
+ do_print("Test background page incognito value with permanent private browsing disabled");
+
+ yield testBackgroundPage({incognito: false});
+
+ do_print("Test background page incognito value with permanent private browsing enabled");
+
+ Preferences.set("browser.privatebrowsing.autostart", true);
+ do_register_cleanup(() => {
+ Preferences.reset("browser.privatebrowsing.autostart");
+ });
+
+ yield testBackgroundPage({incognito: true});
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js
new file mode 100644
index 000000000..426833edd
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js
@@ -0,0 +1,72 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function backgroundScript() {
+ let received_ports_number = 0;
+
+ const expected_received_ports_number = 1;
+
+ function countReceivedPorts(port) {
+ received_ports_number++;
+
+ if (port.name == "check-results") {
+ browser.runtime.onConnect.removeListener(countReceivedPorts);
+
+ browser.test.assertEq(expected_received_ports_number, received_ports_number, "invalid connect should not create a port");
+
+ browser.test.notifyPass("runtime.connect invalid params");
+ }
+ }
+
+ browser.runtime.onConnect.addListener(countReceivedPorts);
+
+ let childFrame = document.createElement("iframe");
+ childFrame.src = "extensionpage.html";
+ document.body.appendChild(childFrame);
+}
+
+function senderScript() {
+ let detected_invalid_connect_params = 0;
+
+ const invalid_connect_params = [
+ // too many params
+ ["fake-extensions-id", {name: "fake-conn-name"}, "unexpected third params"],
+ // invalid params format
+ [{}, {}],
+ ["fake-extensions-id", "invalid-connect-info-format"],
+ ];
+ const expected_detected_invalid_connect_params = invalid_connect_params.length;
+
+ function assertInvalidConnectParamsException(params) {
+ try {
+ browser.runtime.connect(...params);
+ } catch (e) {
+ detected_invalid_connect_params++;
+ browser.test.assertTrue(e.toString().indexOf("Incorrect argument types for runtime.connect.") >= 0, "exception message is correct");
+ }
+ }
+ for (let params of invalid_connect_params) {
+ assertInvalidConnectParamsException(params);
+ }
+ browser.test.assertEq(expected_detected_invalid_connect_params, detected_invalid_connect_params, "all invalid runtime.connect params detected");
+
+ browser.runtime.connect(browser.runtime.id, {name: "check-results"});
+}
+
+let extensionData = {
+ background: backgroundScript,
+ files: {
+ "senderScript.js": senderScript,
+ "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`,
+ },
+};
+
+add_task(function* test_backgroundRuntimeConnectParams() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("runtime.connect invalid params");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js
new file mode 100644
index 000000000..c5f2f1332
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.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";
+
+add_task(function* testBackgroundWindow() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.log("background script executed");
+
+ browser.test.sendMessage("background-script-load");
+
+ let img = document.createElement("img");
+ img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
+ document.body.appendChild(img);
+
+ img.onload = () => {
+ browser.test.log("image loaded");
+
+ let iframe = document.createElement("iframe");
+ iframe.src = "about:blank?1";
+
+ iframe.onload = () => {
+ browser.test.log("iframe loaded");
+ setTimeout(() => {
+ browser.test.notifyPass("background sub-window test done");
+ }, 0);
+ };
+ document.body.appendChild(iframe);
+ };
+ },
+ });
+
+ let loadCount = 0;
+ extension.onMessage("background-script-load", () => {
+ loadCount++;
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("background sub-window test done");
+
+ equal(loadCount, 1, "background script loaded only once");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js
new file mode 100644
index 000000000..948e2913e
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js
@@ -0,0 +1,34 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* testBackgroundWindowProperties() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ let expectedValues = {
+ screenX: 0,
+ screenY: 0,
+ outerWidth: 0,
+ outerHeight: 0,
+ };
+
+ for (let k in window) {
+ try {
+ if (k in expectedValues) {
+ browser.test.assertEq(expectedValues[k], window[k],
+ `should return the expected value for window property: ${k}`);
+ } else {
+ void window[k];
+ }
+ } catch (e) {
+ browser.test.assertEq(null, e, `unexpected exception accessing window property: ${k}`);
+ }
+ }
+
+ browser.test.notifyPass("background.testWindowProperties.done");
+ },
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("background.testWindowProperties.done");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
new file mode 100644
index 000000000..56a14e189
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
@@ -0,0 +1,190 @@
+"use strict";
+
+const global = this;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+ BaseContext,
+} = ExtensionCommon;
+
+var {
+ EventManager,
+ SingletonEventManager,
+} = ExtensionUtils;
+
+class StubContext extends BaseContext {
+ constructor() {
+ let fakeExtension = {id: "test@web.extension"};
+ super("testEnv", fakeExtension);
+ this.sandbox = Cu.Sandbox(global);
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+}
+
+
+add_task(function* test_post_unload_promises() {
+ let context = new StubContext();
+
+ let fail = result => {
+ ok(false, `Unexpected callback: ${result}`);
+ };
+
+ // Make sure promises resolve normally prior to unload.
+ let promises = [
+ context.wrapPromise(Promise.resolve()),
+ context.wrapPromise(Promise.reject({message: ""})).catch(() => {}),
+ ];
+
+ yield Promise.all(promises);
+
+ // Make sure promises that resolve after unload do not trigger
+ // resolution handlers.
+
+ context.wrapPromise(Promise.resolve("resolved"))
+ .then(fail);
+
+ context.wrapPromise(Promise.reject({message: "rejected"}))
+ .then(fail, fail);
+
+ context.unload();
+
+ // The `setTimeout` ensures that we return to the event loop after
+ // promise resolution, which means we're guaranteed to return after
+ // any micro-tasks that get enqueued by the resolution handlers above.
+ yield new Promise(resolve => setTimeout(resolve, 0));
+});
+
+
+add_task(function* test_post_unload_listeners() {
+ let context = new StubContext();
+
+ let fireEvent;
+ let onEvent = new EventManager(context, "onEvent", fire => {
+ fireEvent = fire;
+ return () => {};
+ });
+
+ let fireSingleton;
+ let onSingleton = new SingletonEventManager(context, "onSingleton", callback => {
+ fireSingleton = () => {
+ Promise.resolve().then(callback);
+ };
+ return () => {};
+ });
+
+ let fail = event => {
+ ok(false, `Unexpected event: ${event}`);
+ };
+
+ // Check that event listeners aren't called after they've been removed.
+ onEvent.addListener(fail);
+ onSingleton.addListener(fail);
+
+ let promises = [
+ new Promise(resolve => onEvent.addListener(resolve)),
+ new Promise(resolve => onSingleton.addListener(resolve)),
+ ];
+
+ fireEvent("onEvent");
+ fireSingleton("onSingleton");
+
+ // Both `fireEvent` calls are dispatched asynchronously, so they won't
+ // have fired by this point. The `fail` listeners that we remove now
+ // should not be called, even though the events have already been
+ // enqueued.
+ onEvent.removeListener(fail);
+ onSingleton.removeListener(fail);
+
+ // Wait for the remaining listeners to be called, which should always
+ // happen after the `fail` listeners would normally be called.
+ yield Promise.all(promises);
+
+ // Check that event listeners aren't called after the context has
+ // unloaded.
+ onEvent.addListener(fail);
+ onSingleton.addListener(fail);
+
+ // The EventManager `fire` callback always dispatches events
+ // asynchronously, so we need to test that any pending event callbacks
+ // aren't fired after the context unloads. We also need to test that
+ // any `fire` calls that happen *after* the context is unloaded also
+ // do not trigger callbacks.
+ fireEvent("onEvent");
+ Promise.resolve("onEvent").then(fireEvent);
+
+ fireSingleton("onSingleton");
+ Promise.resolve("onSingleton").then(fireSingleton);
+
+ context.unload();
+
+ // The `setTimeout` ensures that we return to the event loop after
+ // promise resolution, which means we're guaranteed to return after
+ // any micro-tasks that get enqueued by the resolution handlers above.
+ yield new Promise(resolve => setTimeout(resolve, 0));
+});
+
+class Context extends BaseContext {
+ constructor(principal) {
+ let fakeExtension = {id: "test@web.extension"};
+ super("testEnv", fakeExtension);
+ Object.defineProperty(this, "principal", {
+ value: principal,
+ configurable: true,
+ });
+ this.sandbox = Cu.Sandbox(principal, {wantXrays: false});
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+}
+
+let ssm = Services.scriptSecurityManager;
+const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org");
+const PRINCIPAL2 = ssm.createCodebasePrincipalFromOrigin("http://www.somethingelse.org");
+
+// Test that toJSON() works in the json sandbox
+add_task(function* test_stringify_toJSON() {
+ let context = new Context(PRINCIPAL1);
+ let obj = Cu.evalInSandbox("({hidden: true, toJSON() { return {visible: true}; } })", context.sandbox);
+
+ let stringified = context.jsonStringify(obj);
+ let expected = JSON.stringify({visible: true});
+ equal(stringified, expected, "Stringified object with toJSON() method is as expected");
+});
+
+// Test that stringifying in inaccessible property throws
+add_task(function* test_stringify_inaccessible() {
+ let context = new Context(PRINCIPAL1);
+ let sandbox = context.sandbox;
+ let sandbox2 = Cu.Sandbox(PRINCIPAL2);
+
+ Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2);
+ let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox);
+ Assert.throws(() => {
+ context.jsonStringify(obj);
+ });
+});
+
+add_task(function* test_stringify_accessible() {
+ // Test that an accessible property from another global is included
+ let principal = Cu.getObjectPrincipal(Cu.Sandbox([PRINCIPAL1, PRINCIPAL2]));
+ let context = new Context(principal);
+ let sandbox = context.sandbox;
+ let sandbox2 = Cu.Sandbox(PRINCIPAL2);
+
+ Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2);
+ let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox);
+ let stringified = context.jsonStringify(obj);
+
+ let expected = JSON.stringify({local: true, nested: {subobject: true}});
+ equal(stringified, expected, "Stringified object with accessible property is as expected");
+});
+
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js
new file mode 100644
index 000000000..058b9b18c
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js
@@ -0,0 +1,76 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_downloads_api_namespace_and_permissions() {
+ function backgroundScript() {
+ browser.test.assertTrue(!!browser.downloads, "`downloads` API is present.");
+ browser.test.assertTrue(!!browser.downloads.FilenameConflictAction,
+ "`downloads.FilenameConflictAction` enum is present.");
+ browser.test.assertTrue(!!browser.downloads.InterruptReason,
+ "`downloads.InterruptReason` enum is present.");
+ browser.test.assertTrue(!!browser.downloads.DangerType,
+ "`downloads.DangerType` enum is present.");
+ browser.test.assertTrue(!!browser.downloads.State,
+ "`downloads.State` enum is present.");
+ browser.test.notifyPass("downloads tests");
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads", "downloads.open", "downloads.shelf"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("downloads tests");
+ yield extension.unload();
+});
+
+add_task(function* test_downloads_open_permission() {
+ function backgroundScript() {
+ browser.test.assertFalse("open" in browser.downloads,
+ "`downloads.open` permission is required.");
+ browser.test.notifyPass("downloads tests");
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("downloads tests");
+ yield extension.unload();
+});
+
+add_task(function* test_downloads_open() {
+ async function backgroundScript() {
+ await browser.test.assertRejects(
+ browser.downloads.open(10),
+ "Invalid download id 10",
+ "The error is informative.");
+
+ browser.test.notifyPass("downloads tests");
+
+ // TODO: Once downloads.{pause,cancel,resume} lands (bug 1245602) test that this gives a good
+ // error when called with an incompleted download.
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads", "downloads.open"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("downloads tests");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js
new file mode 100644
index 000000000..37ddd4d7c
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js
@@ -0,0 +1,354 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* global OS */
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Downloads.jsm");
+
+const gServer = createHttpServer();
+gServer.registerDirectory("/data/", do_get_file("data"));
+
+const WINDOWS = AppConstants.platform == "win";
+
+const BASE = `http://localhost:${gServer.identity.primaryPort}/data`;
+const FILE_NAME = "file_download.txt";
+const FILE_URL = BASE + "/" + FILE_NAME;
+const FILE_NAME_UNIQUE = "file_download(1).txt";
+const FILE_LEN = 46;
+
+let downloadDir;
+
+function setup() {
+ downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_print(`Using download directory ${downloadDir.path}`);
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir);
+
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+
+ let entries = downloadDir.directoryEntries;
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsIFile);
+ ok(false, `Leftover file ${entry.path} in download directory`);
+ entry.remove(false);
+ }
+
+ downloadDir.remove(false);
+ });
+}
+
+function backgroundScript() {
+ let blobUrl;
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg == "download.request") {
+ let options = args[0];
+
+ if (options.blobme) {
+ let blob = new Blob(options.blobme);
+ delete options.blobme;
+ blobUrl = options.url = window.URL.createObjectURL(blob);
+ }
+
+ try {
+ let id = await browser.downloads.download(options);
+ browser.test.sendMessage("download.done", {status: "success", id});
+ } catch (error) {
+ browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
+ }
+ } else if (msg == "killTheBlob") {
+ window.URL.revokeObjectURL(blobUrl);
+ blobUrl = null;
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+// This function is a bit of a sledgehammer, it looks at every download
+// the browser knows about and waits for all active downloads to complete.
+// But we only start one at a time and only do a handful in total, so
+// this lets us test download() without depending on anything else.
+async function waitForDownloads() {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ let inprogress = downloads.filter(dl => !dl.stopped);
+ return Promise.all(inprogress.map(dl => dl.whenSucceeded()));
+}
+
+// Create a file in the downloads directory.
+function touch(filename) {
+ let file = downloadDir.clone();
+ file.append(filename);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+}
+
+// Remove a file in the downloads directory.
+function remove(filename, recursive = false) {
+ let file = downloadDir.clone();
+ file.append(filename);
+ file.remove(recursive);
+}
+
+add_task(function* test_downloads() {
+ setup();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ });
+
+ function download(options) {
+ extension.sendMessage("download.request", options);
+ return extension.awaitMessage("download.done");
+ }
+
+ async function testDownload(options, localFile, expectedSize, description) {
+ let msg = await download(options);
+ equal(msg.status, "success", `downloads.download() works with ${description}`);
+
+ await waitForDownloads();
+
+ let localPath = downloadDir.clone();
+ let parts = Array.isArray(localFile) ? localFile : [localFile];
+
+ parts.map(p => localPath.append(p));
+ equal(localPath.fileSize, expectedSize, "Downloaded file has expected size");
+ localPath.remove(false);
+ }
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+ do_print("extension started");
+
+ // Call download() with just the url property.
+ yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source");
+
+ // Call download() with a filename property.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "newpath.txt",
+ }, "newpath.txt", FILE_LEN, "source and filename");
+
+ // Call download() with a filename with subdirs.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "sub/dir/file",
+ }, ["sub", "dir", "file"], FILE_LEN, "source and filename with subdirs");
+
+ // Call download() with a filename with existing subdirs.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "sub/dir/file2",
+ }, ["sub", "dir", "file2"], FILE_LEN, "source and filename with existing subdirs");
+
+ // Only run Windows path separator test on Windows.
+ if (WINDOWS) {
+ // Call download() with a filename with Windows path separator.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "sub\\dir\\file3",
+ }, ["sub", "dir", "file3"], FILE_LEN, "filename with Windows path separator");
+ }
+ remove("sub", true);
+
+ // Call download(), filename with subdir, skipping parts.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "skip//part",
+ }, ["skip", "part"], FILE_LEN, "source, filename, with subdir, skipping parts");
+ remove("skip", true);
+
+ // Check conflictAction of "uniquify".
+ touch(FILE_NAME);
+ yield testDownload({
+ url: FILE_URL,
+ conflictAction: "uniquify",
+ }, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify");
+ // todo check that preexisting file was not modified?
+ remove(FILE_NAME);
+
+ // Check conflictAction of "overwrite".
+ touch(FILE_NAME);
+ yield testDownload({
+ url: FILE_URL,
+ conflictAction: "overwrite",
+ }, FILE_NAME, FILE_LEN, "conflictAction=overwrite");
+
+ // Try to download in invalid url
+ yield download({url: "this is not a valid URL"}).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with invalid url");
+ ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct");
+ });
+
+ // Try to download to an empty path.
+ yield download({
+ url: FILE_URL,
+ filename: "",
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with empty filename");
+ equal(msg.errmsg, "filename must not be empty", "error message for empty filename is correct");
+ });
+
+ // Try to download to an absolute path.
+ const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt");
+ yield download({
+ url: FILE_URL,
+ filename: absolutePath,
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with absolute filename");
+ equal(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`);
+ });
+
+ if (WINDOWS) {
+ yield download({
+ url: FILE_URL,
+ filename: "C:\\file_download.txt",
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with absolute filename");
+ equal(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct");
+ });
+ }
+
+ // Try to download to a relative path containing ..
+ yield download({
+ url: FILE_URL,
+ filename: OS.Path.join("..", "file_download.txt"),
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with back-references");
+ equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
+ });
+
+ // Try to download to a long relative path containing ..
+ yield download({
+ url: FILE_URL,
+ filename: OS.Path.join("foo", "..", "..", "file_download.txt"),
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with back-references");
+ equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
+ });
+
+ // Try to download a blob url
+ const BLOB_STRING = "Hello, world";
+ yield testDownload({
+ blobme: [BLOB_STRING],
+ filename: FILE_NAME,
+ }, FILE_NAME, BLOB_STRING.length, "blob url");
+ extension.sendMessage("killTheBlob");
+
+ // Try to download a blob url without a given filename
+ yield testDownload({
+ blobme: [BLOB_STRING],
+ }, "download", BLOB_STRING.length, "blob url with no filename");
+ extension.sendMessage("killTheBlob");
+
+ yield extension.unload();
+});
+
+add_task(function* test_download_post() {
+ const server = createHttpServer();
+ const url = `http://localhost:${server.identity.primaryPort}/post-log`;
+
+ let received;
+ server.registerPathHandler("/post-log", request => {
+ received = request;
+ });
+
+ // Confirm received vs. expected values.
+ function confirm(method, headers = {}, body) {
+ equal(received.method, method, "method is correct");
+
+ for (let name in headers) {
+ ok(received.hasHeader(name), `header ${name} received`);
+ equal(received.getHeader(name), headers[name], `header ${name} is correct`);
+ }
+
+ if (body) {
+ const str = NetUtil.readInputStreamToString(received.bodyInputStream,
+ received.bodyInputStream.available());
+ equal(str, body, "body is correct");
+ }
+ }
+
+ function background() {
+ browser.test.onMessage.addListener(async options => {
+ try {
+ await browser.downloads.download(options);
+ } catch (err) {
+ browser.test.sendMessage("done", {err: err.message});
+ }
+ });
+ browser.downloads.onChanged.addListener(({state}) => {
+ if (state && state.current === "complete") {
+ browser.test.sendMessage("done", {ok: true});
+ }
+ });
+ }
+
+ const manifest = {permissions: ["downloads"]};
+ const extension = ExtensionTestUtils.loadExtension({background, manifest});
+ yield extension.startup();
+
+ function download(options) {
+ options.url = url;
+ options.conflictAction = "overwrite";
+
+ extension.sendMessage(options);
+ return extension.awaitMessage("done");
+ }
+
+ // Test method option.
+ let result = yield download({});
+ ok(result.ok, "download works without the method option, defaults to GET");
+ confirm("GET");
+
+ result = yield download({method: "PUT"});
+ ok(!result.ok, "download rejected with PUT method");
+ ok(/method: Invalid enumeration/.test(result.err), "descriptive error message");
+
+ result = yield download({method: "POST"});
+ ok(result.ok, "download works with POST method");
+ confirm("POST");
+
+ // Test body option values.
+ result = yield download({body: []});
+ ok(!result.ok, "download rejected because of non-string body");
+ ok(/body: Expected string/.test(result.err), "descriptive error message");
+
+ result = yield download({method: "POST", body: "of work"});
+ ok(result.ok, "download works with POST method and body");
+ confirm("POST", {"Content-Length": 7}, "of work");
+
+ // Test custom headers.
+ result = yield download({headers: [{name: "X-Custom"}]});
+ ok(!result.ok, "download rejected because of missing header value");
+ ok(/"value" is required/.test(result.err), "descriptive error message");
+
+ result = yield download({headers: [{name: "X-Custom", value: "13"}]});
+ ok(result.ok, "download works with a custom header");
+ confirm("GET", {"X-Custom": "13"});
+
+ // Test forbidden headers.
+ result = yield download({headers: [{name: "DNT", value: "1"}]});
+ ok(!result.ok, "download rejected because of forbidden header name DNT");
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ result = yield download({headers: [{name: "Proxy-Connection", value: "keep"}]});
+ ok(!result.ok, "download rejected because of forbidden header name prefix Proxy-");
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ result = yield download({headers: [{name: "Sec-ret", value: "13"}]});
+ ok(!result.ok, "download rejected because of forbidden header name prefix Sec-");
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ remove("post-log");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
new file mode 100644
index 000000000..d08aab666
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
@@ -0,0 +1,862 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Downloads.jsm");
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const ROOT = `http://localhost:${server.identity.primaryPort}`;
+const BASE = `${ROOT}/data`;
+const TXT_FILE = "file_download.txt";
+const TXT_URL = BASE + "/" + TXT_FILE;
+
+// Keep these in sync with code in interruptible.sjs
+const INT_PARTIAL_LEN = 15;
+const INT_TOTAL_LEN = 31;
+
+const TEST_DATA = "This is 31 bytes of sample data";
+const TOTAL_LEN = TEST_DATA.length;
+const PARTIAL_LEN = 15;
+
+// A handler to let us systematically test pausing/resuming/canceling
+// of downloads. This target represents a small text file but a simple
+// GET will stall after sending part of the data, to give the test code
+// a chance to pause or do other operations on an in-progress download.
+// A resumed download (ie, a GET with a Range: header) will allow the
+// download to complete.
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+
+ if (request.hasHeader("Range")) {
+ let start, end;
+ let matches = request.getHeader("Range")
+ .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
+ if (matches != null) {
+ start = matches[1] ? parseInt(matches[1], 10) : 0;
+ end = matches[2] ? parseInt(matches[2], 10) : (TOTAL_LEN - 1);
+ }
+
+ if (end == undefined || end >= TOTAL_LEN) {
+ response.setStatusLine(request.httpVersion, 416, "Requested Range Not Satisfiable");
+ response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false);
+ response.finish();
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false);
+ response.write(TEST_DATA.slice(start, end + 1));
+ } else {
+ response.processAsync();
+ response.setHeader("Content-Length", `${TOTAL_LEN}`, false);
+ response.write(TEST_DATA.slice(0, PARTIAL_LEN));
+ }
+
+ do_register_cleanup(() => {
+ try {
+ response.finish();
+ } catch (e) {
+ // This will throw, but we don't care at this point.
+ }
+ });
+}
+
+server.registerPathHandler("/interruptible.html", handleRequest);
+
+let interruptibleCount = 0;
+function getInterruptibleUrl() {
+ let n = interruptibleCount++;
+ return `${ROOT}/interruptible.html?count=${n}`;
+}
+
+function backgroundScript() {
+ let events = new Set();
+ let eventWaiter = null;
+
+ browser.downloads.onCreated.addListener(data => {
+ events.add({type: "onCreated", data});
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ browser.downloads.onChanged.addListener(data => {
+ events.add({type: "onChanged", data});
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ browser.downloads.onErased.addListener(data => {
+ events.add({type: "onErased", data});
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ // Returns a promise that will resolve when the given list of expected
+ // events have all been seen. By default, succeeds only if the exact list
+ // of expected events is seen in the given order. options.exact can be
+ // set to false to allow other events and options.inorder can be set to
+ // false to allow the events to arrive in any order.
+ function waitForEvents(expected, options = {}) {
+ function compare(a, b) {
+ if (typeof b == "object" && b != null) {
+ if (typeof a != "object") {
+ return false;
+ }
+ return Object.keys(b).every(fld => compare(a[fld], b[fld]));
+ }
+ return (a == b);
+ }
+
+ const exact = ("exact" in options) ? options.exact : true;
+ const inorder = ("inorder" in options) ? options.inorder : true;
+ return new Promise((resolve, reject) => {
+ function check() {
+ function fail(msg) {
+ browser.test.fail(msg);
+ reject(new Error(msg));
+ }
+ if (events.size < expected.length) {
+ return;
+ }
+ if (exact && expected.length < events.size) {
+ fail(`Got ${events.size} events but only expected ${expected.length}`);
+ return;
+ }
+
+ let remaining = new Set(events);
+ if (inorder) {
+ for (let event of events) {
+ if (compare(event, expected[0])) {
+ expected.shift();
+ remaining.delete(event);
+ }
+ }
+ } else {
+ expected = expected.filter(val => {
+ for (let remainingEvent of remaining) {
+ if (compare(remainingEvent, val)) {
+ remaining.delete(remainingEvent);
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+
+ // Events that did occur have been removed from expected so if
+ // expected is empty, we're done. If we didn't see all the
+ // expected events and we're not looking for an exact match,
+ // then we just may not have seen the event yet, so return without
+ // failing and check() will be called again when a new event arrives.
+ if (expected.length == 0) {
+ events = remaining;
+ eventWaiter = null;
+ resolve();
+ } else if (exact) {
+ fail(`Mismatched event: expecting ${JSON.stringify(expected[0])} but got ${JSON.stringify(Array.from(remaining)[0])}`);
+ }
+ }
+ eventWaiter = check;
+ check();
+ });
+ }
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ let match = msg.match(/(\w+).request$/);
+ if (!match) {
+ return;
+ }
+
+ let what = match[1];
+ if (what == "waitForEvents") {
+ try {
+ await waitForEvents(...args);
+ browser.test.sendMessage("waitForEvents.done", {status: "success"});
+ } catch (error) {
+ browser.test.sendMessage("waitForEvents.done", {status: "error", errmsg: error.message});
+ }
+ } else if (what == "clearEvents") {
+ events = new Set();
+ browser.test.sendMessage("clearEvents.done", {status: "success"});
+ } else {
+ try {
+ let result = await browser.downloads[what](...args);
+ browser.test.sendMessage(`${what}.done`, {status: "success", result});
+ } catch (error) {
+ browser.test.sendMessage(`${what}.done`, {status: "error", errmsg: error.message});
+ }
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+let downloadDir;
+let extension;
+
+async function clearDownloads(callback) {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ await Promise.all(downloads.map(download => list.remove(download)));
+
+ return downloads;
+}
+
+function runInExtension(what, ...args) {
+ extension.sendMessage(`${what}.request`, ...args);
+ return extension.awaitMessage(`${what}.done`);
+}
+
+// This is pretty simplistic, it looks for a progress update for a
+// download of the given url in which the total bytes are exactly equal
+// to the given value. Unless you know exactly how data will arrive from
+// the server (eg see interruptible.sjs), it probably isn't very useful.
+async function waitForProgress(url, bytes) {
+ let list = await Downloads.getList(Downloads.ALL);
+
+ return new Promise(resolve => {
+ const view = {
+ onDownloadChanged(download) {
+ if (download.source.url == url && download.currentBytes == bytes) {
+ list.removeView(view);
+ resolve();
+ }
+ },
+ };
+ list.addView(view);
+ });
+}
+
+add_task(function* setup() {
+ const nsIFile = Ci.nsIFile;
+ downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_print(`downloadDir ${downloadDir.path}`);
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
+
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ downloadDir.remove(true);
+
+ return clearDownloads();
+ });
+
+ yield clearDownloads().then(downloads => {
+ do_print(`removed ${downloads.length} pre-existing downloads from history`);
+ });
+
+ extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+});
+
+add_task(function* test_events() {
+ let msg = yield runInExtension("download", {url: TXT_URL});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id, url: TXT_URL}},
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onCreated and onChanged events");
+});
+
+add_task(function* test_cancel() {
+ let url = getInterruptibleUrl();
+ do_print(url);
+ let msg = yield runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id}},
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ yield progressPromise;
+ do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = yield runInExtension("cancel", id);
+ equal(msg.status, "success", "cancel() succeeded");
+
+ // This sequence of events is bogus (bug 1256243)
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ paused: {
+ previous: true,
+ current: false,
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged events corresponding to cancel()");
+
+ msg = yield runInExtension("search", {error: "USER_CANCELED"});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause a canceled download");
+
+ msg = yield runInExtension("resume", id);
+ equal(msg.status, "error", "cannot resume a canceled download");
+});
+
+add_task(function* test_pauseresume() {
+ let url = getInterruptibleUrl();
+ let msg = yield runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id}},
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ yield progressPromise;
+ do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ }]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = yield runInExtension("search", {paused: true});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, true, "download.paused is correct");
+ equal(msg.result[0].canResume, true, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = yield runInExtension("search", {error: "USER_CANCELED"});
+ equal(msg.status, "success", "search() succeeded");
+ let found = msg.result.filter(item => item.id == id);
+ equal(found.length, 1, "search() by error found the paused download");
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause an already paused download");
+
+ msg = yield runInExtension("resume", id);
+ equal(msg.status, "success", "resume() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "interrupted",
+ current: "in_progress",
+ },
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ error: {
+ previous: "USER_CANCELED",
+ current: null,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged events for resume and complete");
+
+ msg = yield runInExtension("search", {id});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].state, "complete", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, null, "download.error is correct");
+ equal(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, true, "download.exists is correct");
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause a completed download");
+
+ msg = yield runInExtension("resume", id);
+ equal(msg.status, "error", "cannot resume a completed download");
+});
+
+add_task(function* test_pausecancel() {
+ let url = getInterruptibleUrl();
+ let msg = yield runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id}},
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ yield progressPromise;
+ do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ }]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = yield runInExtension("search", {paused: true});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, true, "download.paused is correct");
+ equal(msg.result[0].canResume, true, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = yield runInExtension("search", {error: "USER_CANCELED"});
+ equal(msg.status, "success", "search() succeeded");
+ let found = msg.result.filter(item => item.id == id);
+ equal(found.length, 1, "search() by error found the paused download");
+
+ msg = yield runInExtension("cancel", id);
+ equal(msg.status, "success", "cancel() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged event for cancel");
+
+ msg = yield runInExtension("search", {id});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, false, "download.exists is correct");
+});
+
+add_task(function* test_pause_resume_cancel_badargs() {
+ let BAD_ID = 1000;
+
+ let msg = yield runInExtension("pause", BAD_ID);
+ equal(msg.status, "error", "pause() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+
+ msg = yield runInExtension("resume", BAD_ID);
+ equal(msg.status, "error", "resume() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+
+ msg = yield runInExtension("cancel", BAD_ID);
+ equal(msg.status, "error", "cancel() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+});
+
+add_task(function* test_file_removal() {
+ let msg = yield runInExtension("download", {url: TXT_URL});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id, url: TXT_URL}},
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+
+ equal(msg.status, "success", "got onCreated and onChanged events");
+
+ msg = yield runInExtension("removeFile", id);
+ equal(msg.status, "success", "removeFile() succeeded");
+
+ msg = yield runInExtension("removeFile", id);
+ equal(msg.status, "error", "removeFile() fails since the file was already removed.");
+ ok(/file doesn't exist/.test(msg.errmsg), "removeFile() failed on removed file.");
+
+ msg = yield runInExtension("removeFile", 1000);
+ ok(/Invalid download id/.test(msg.errmsg), "removeFile() failed due to non-existent id");
+});
+
+add_task(function* test_removal_of_incomplete_download() {
+ let url = getInterruptibleUrl();
+ let msg = yield runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id}},
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ yield progressPromise;
+ do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ }]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = yield runInExtension("removeFile", id);
+ equal(msg.status, "error", "removeFile() on paused download failed");
+
+ ok(/Cannot remove incomplete download/.test(msg.errmsg), "removeFile() failed due to download being incomplete");
+
+ msg = yield runInExtension("resume", id);
+ equal(msg.status, "success", "resume() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "interrupted",
+ current: "in_progress",
+ },
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ error: {
+ previous: "USER_CANCELED",
+ current: null,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged events for resume and complete");
+
+ msg = yield runInExtension("removeFile", id);
+ equal(msg.status, "success", "removeFile() succeeded following completion of resumed download.");
+});
+
+// Test erase(). We don't do elaborate testing of the query handling
+// since it uses the exact same engine as search() which is tested
+// more thoroughly in test_chrome_ext_downloads_search.html
+add_task(function* test_erase() {
+ yield clearDownloads();
+
+ yield runInExtension("clearEvents");
+
+ function* download() {
+ let msg = yield runInExtension("download", {url: TXT_URL});
+ equal(msg.status, "success", "download succeeded");
+ let id = msg.result;
+
+ msg = yield runInExtension("waitForEvents", [{
+ type: "onChanged", data: {id, state: {current: "complete"}},
+ }], {exact: false});
+ equal(msg.status, "success", "download finished");
+
+ return id;
+ }
+
+ let ids = {};
+ ids.dl1 = yield download();
+ ids.dl2 = yield download();
+ ids.dl3 = yield download();
+
+ let msg = yield runInExtension("search", {});
+ equal(msg.status, "success", "search succeded");
+ equal(msg.result.length, 3, "search found 3 downloads");
+
+ msg = yield runInExtension("clearEvents");
+
+ msg = yield runInExtension("erase", {id: ids.dl1});
+ equal(msg.status, "success", "erase by id succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onErased", data: ids.dl1},
+ ]);
+ equal(msg.status, "success", "received onErased event");
+
+ msg = yield runInExtension("search", {});
+ equal(msg.status, "success", "search succeded");
+ equal(msg.result.length, 2, "search found 2 downloads");
+
+ msg = yield runInExtension("erase", {});
+ equal(msg.status, "success", "erase everything succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onErased", data: ids.dl2},
+ {type: "onErased", data: ids.dl3},
+ ], {inorder: false});
+ equal(msg.status, "success", "received 2 onErased events");
+
+ msg = yield runInExtension("search", {});
+ equal(msg.status, "success", "search succeded");
+ equal(msg.result.length, 0, "search found 0 downloads");
+});
+
+function loadImage(img, data) {
+ return new Promise((resolve) => {
+ img.src = data;
+ img.onload = resolve;
+ });
+}
+
+add_task(function* test_getFileIcon() {
+ let webNav = Services.appShell.createWindowlessBrowser(false);
+ let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ let system = Services.scriptSecurityManager.getSystemPrincipal();
+ docShell.createAboutBlankContentViewer(system);
+
+ let img = webNav.document.createElement("img");
+
+ let msg = yield runInExtension("download", {url: TXT_URL});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = yield runInExtension("getFileIcon", id);
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ yield loadImage(img, msg.result);
+ equal(img.height, 32, "returns an icon with the right height");
+ equal(img.width, 32, "returns an icon with the right width");
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id, url: TXT_URL}},
+ {type: "onChanged"},
+ ]);
+ equal(msg.status, "success", "got events");
+
+ msg = yield runInExtension("getFileIcon", id);
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ yield loadImage(img, msg.result);
+ equal(img.height, 32, "returns an icon with the right height after download");
+ equal(img.width, 32, "returns an icon with the right width after download");
+
+ msg = yield runInExtension("getFileIcon", id + 100);
+ equal(msg.status, "error", "getFileIcon() failed");
+ ok(msg.errmsg.includes("Invalid download id"), "download id is invalid");
+
+ msg = yield runInExtension("getFileIcon", id, {size: 127});
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ yield loadImage(img, msg.result);
+ equal(img.height, 127, "returns an icon with the right custom height");
+ equal(img.width, 127, "returns an icon with the right custom width");
+
+ msg = yield runInExtension("getFileIcon", id, {size: 1});
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ yield loadImage(img, msg.result);
+ equal(img.height, 1, "returns an icon with the right custom height");
+ equal(img.width, 1, "returns an icon with the right custom width");
+
+ msg = yield runInExtension("getFileIcon", id, {size: "foo"});
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is not a number");
+
+ msg = yield runInExtension("getFileIcon", id, {size: 0});
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is too small");
+
+ msg = yield runInExtension("getFileIcon", id, {size: 128});
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is too big");
+
+ webNav.close();
+});
+
+add_task(function* cleanup() {
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js
new file mode 100644
index 000000000..4caa82456
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js
@@ -0,0 +1,402 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Downloads.jsm");
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const BASE = `http://localhost:${server.identity.primaryPort}/data`;
+const TXT_FILE = "file_download.txt";
+const TXT_URL = BASE + "/" + TXT_FILE;
+const TXT_LEN = 46;
+const HTML_FILE = "file_download.html";
+const HTML_URL = BASE + "/" + HTML_FILE;
+const HTML_LEN = 117;
+const BIG_LEN = 1000; // something bigger both TXT_LEN and HTML_LEN
+
+function backgroundScript() {
+ let complete = new Map();
+
+ function waitForComplete(id) {
+ if (complete.has(id)) {
+ return complete.get(id).promise;
+ }
+
+ let promise = new Promise(resolve => {
+ complete.set(id, {resolve});
+ });
+ complete.get(id).promise = promise;
+ return promise;
+ }
+
+ browser.downloads.onChanged.addListener(change => {
+ if (change.state && change.state.current == "complete") {
+ // Make sure we have a promise.
+ waitForComplete(change.id);
+ complete.get(change.id).resolve();
+ }
+ });
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg == "download.request") {
+ try {
+ let id = await browser.downloads.download(args[0]);
+ browser.test.sendMessage("download.done", {status: "success", id});
+ } catch (error) {
+ browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
+ }
+ } else if (msg == "search.request") {
+ try {
+ let downloads = await browser.downloads.search(args[0]);
+ browser.test.sendMessage("search.done", {status: "success", downloads});
+ } catch (error) {
+ browser.test.sendMessage("search.done", {status: "error", errmsg: error.message});
+ }
+ } else if (msg == "waitForComplete.request") {
+ await waitForComplete(args[0]);
+ browser.test.sendMessage("waitForComplete.done");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+async function clearDownloads(callback) {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ await Promise.all(downloads.map(download => list.remove(download)));
+
+ return downloads;
+}
+
+add_task(function* test_search() {
+ const nsIFile = Ci.nsIFile;
+ let downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_print(`downloadDir ${downloadDir.path}`);
+
+ function downloadPath(filename) {
+ let path = downloadDir.clone();
+ path.append(filename);
+ return path.path;
+ }
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
+
+ do_register_cleanup(async () => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ await cleanupDir(downloadDir);
+ await clearDownloads();
+ });
+
+ yield clearDownloads().then(downloads => {
+ do_print(`removed ${downloads.length} pre-existing downloads from history`);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ });
+
+ async function download(options) {
+ extension.sendMessage("download.request", options);
+ let result = await extension.awaitMessage("download.done");
+
+ if (result.status == "success") {
+ do_print(`wait for onChanged event to indicate ${result.id} is complete`);
+ extension.sendMessage("waitForComplete.request", result.id);
+
+ await extension.awaitMessage("waitForComplete.done");
+ }
+
+ return result;
+ }
+
+ function search(query) {
+ extension.sendMessage("search.request", query);
+ return extension.awaitMessage("search.done");
+ }
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ // Do some downloads...
+ const time1 = new Date();
+
+ let downloadIds = {};
+ let msg = yield download({url: TXT_URL});
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.txt1 = msg.id;
+
+ const TXT_FILE2 = "NewFile.txt";
+ msg = yield download({url: TXT_URL, filename: TXT_FILE2});
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.txt2 = msg.id;
+
+ const time2 = new Date();
+
+ msg = yield download({url: HTML_URL});
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.html1 = msg.id;
+
+ const HTML_FILE2 = "renamed.html";
+ msg = yield download({url: HTML_URL, filename: HTML_FILE2});
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.html2 = msg.id;
+
+ const time3 = new Date();
+
+ // Search for each individual download and check
+ // the corresponding DownloadItem.
+ function* checkDownloadItem(id, expect) {
+ let item = yield search({id});
+ equal(item.status, "success", "search() succeeded");
+ equal(item.downloads.length, 1, "search() found exactly 1 download");
+
+ Object.keys(expect).forEach(function(field) {
+ equal(item.downloads[0][field], expect[field], `DownloadItem.${field} is correct"`);
+ });
+ }
+ yield checkDownloadItem(downloadIds.txt1, {
+ url: TXT_URL,
+ filename: downloadPath(TXT_FILE),
+ mime: "text/plain",
+ state: "complete",
+ bytesReceived: TXT_LEN,
+ totalBytes: TXT_LEN,
+ fileSize: TXT_LEN,
+ exists: true,
+ });
+
+ yield checkDownloadItem(downloadIds.txt2, {
+ url: TXT_URL,
+ filename: downloadPath(TXT_FILE2),
+ mime: "text/plain",
+ state: "complete",
+ bytesReceived: TXT_LEN,
+ totalBytes: TXT_LEN,
+ fileSize: TXT_LEN,
+ exists: true,
+ });
+
+ yield checkDownloadItem(downloadIds.html1, {
+ url: HTML_URL,
+ filename: downloadPath(HTML_FILE),
+ mime: "text/html",
+ state: "complete",
+ bytesReceived: HTML_LEN,
+ totalBytes: HTML_LEN,
+ fileSize: HTML_LEN,
+ exists: true,
+ });
+
+ yield checkDownloadItem(downloadIds.html2, {
+ url: HTML_URL,
+ filename: downloadPath(HTML_FILE2),
+ mime: "text/html",
+ state: "complete",
+ bytesReceived: HTML_LEN,
+ totalBytes: HTML_LEN,
+ fileSize: HTML_LEN,
+ exists: true,
+ });
+
+ function* checkSearch(query, expected, description, exact) {
+ let item = yield search(query);
+ equal(item.status, "success", "search() succeeded");
+ equal(item.downloads.length, expected.length, `search() for ${description} found exactly ${expected.length} downloads`);
+
+ let receivedIds = item.downloads.map(i => i.id);
+ if (exact) {
+ receivedIds.forEach((id, idx) => {
+ equal(id, downloadIds[expected[idx]], `search() for ${description} returned ${expected[idx]} in position ${idx}`);
+ });
+ } else {
+ Object.keys(downloadIds).forEach(key => {
+ const id = downloadIds[key];
+ const thisExpected = expected.includes(key);
+ equal(receivedIds.includes(id), thisExpected,
+ `search() for ${description} ${thisExpected ? "includes" : "does not include"} ${key}`);
+ });
+ }
+ }
+
+ // Check that search with an invalid id returns nothing.
+ // NB: for now ids are not persistent and we start numbering them at 1
+ // so a sufficiently large number will be unused.
+ const INVALID_ID = 1000;
+ yield checkSearch({id: INVALID_ID}, [], "invalid id");
+
+ // Check that search on url works.
+ yield checkSearch({url: TXT_URL}, ["txt1", "txt2"], "url");
+
+ // Check that regexp on url works.
+ const HTML_REGEX = "[downlad]{8}\.html+$";
+ yield checkSearch({urlRegex: HTML_REGEX}, ["html1", "html2"], "url regexp");
+
+ // Check that compatible url+regexp works
+ yield checkSearch({url: HTML_URL, urlRegex: HTML_REGEX}, ["html1", "html2"], "compatible url+urlRegex");
+
+ // Check that incompatible url+regexp works
+ yield checkSearch({url: TXT_URL, urlRegex: HTML_REGEX}, [], "incompatible url+urlRegex");
+
+ // Check that search on filename works.
+ yield checkSearch({filename: downloadPath(TXT_FILE)}, ["txt1"], "filename");
+
+ // Check that regexp on filename works.
+ yield checkSearch({filenameRegex: HTML_REGEX}, ["html1"], "filename regex");
+
+ // Check that compatible filename+regexp works
+ yield checkSearch({filename: downloadPath(HTML_FILE), filenameRegex: HTML_REGEX}, ["html1"], "compatible filename+filename regex");
+
+ // Check that incompatible filename+regexp works
+ yield checkSearch({filename: downloadPath(TXT_FILE), filenameRegex: HTML_REGEX}, [], "incompatible filename+filename regex");
+
+ // Check that simple positive search terms work.
+ yield checkSearch({query: ["file_download"]}, ["txt1", "txt2", "html1", "html2"],
+ "term file_download");
+ yield checkSearch({query: ["NewFile"]}, ["txt2"], "term NewFile");
+
+ // Check that positive search terms work case-insensitive.
+ yield checkSearch({query: ["nEwfILe"]}, ["txt2"], "term nEwfiLe");
+
+ // Check that negative search terms work.
+ yield checkSearch({query: ["-txt"]}, ["html1", "html2"], "term -txt");
+
+ // Check that positive and negative search terms together work.
+ yield checkSearch({query: ["html", "-renamed"]}, ["html1"], "postive and negative terms");
+
+ function* checkSearchWithDate(query, expected, description) {
+ const fields = Object.keys(query);
+ if (fields.length != 1 || !(query[fields[0]] instanceof Date)) {
+ throw new Error("checkSearchWithDate expects exactly one Date field");
+ }
+ const field = fields[0];
+ const date = query[field];
+
+ let newquery = {};
+
+ // Check as a Date
+ newquery[field] = date;
+ yield checkSearch(newquery, expected, `${description} as Date`);
+
+ // Check as numeric milliseconds
+ newquery[field] = date.valueOf();
+ yield checkSearch(newquery, expected, `${description} as numeric ms`);
+
+ // Check as stringified milliseconds
+ newquery[field] = date.valueOf().toString();
+ yield checkSearch(newquery, expected, `${description} as string ms`);
+
+ // Check as ISO string
+ newquery[field] = date.toISOString();
+ yield checkSearch(newquery, expected, `${description} as iso string`);
+ }
+
+ // Check startedBefore
+ yield checkSearchWithDate({startedBefore: time1}, [], "before time1");
+ yield checkSearchWithDate({startedBefore: time2}, ["txt1", "txt2"], "before time2");
+ yield checkSearchWithDate({startedBefore: time3}, ["txt1", "txt2", "html1", "html2"], "before time3");
+
+ // Check startedAfter
+ yield checkSearchWithDate({startedAfter: time1}, ["txt1", "txt2", "html1", "html2"], "after time1");
+ yield checkSearchWithDate({startedAfter: time2}, ["html1", "html2"], "after time2");
+ yield checkSearchWithDate({startedAfter: time3}, [], "after time3");
+
+ // Check simple search on totalBytes
+ yield checkSearch({totalBytes: TXT_LEN}, ["txt1", "txt2"], "totalBytes");
+ yield checkSearch({totalBytes: HTML_LEN}, ["html1", "html2"], "totalBytes");
+
+ // Check simple test on totalBytes{Greater,Less}
+ // (NB: TXT_LEN < HTML_LEN < BIG_LEN)
+ yield checkSearch({totalBytesGreater: 0}, ["txt1", "txt2", "html1", "html2"], "totalBytesGreater than 0");
+ yield checkSearch({totalBytesGreater: TXT_LEN}, ["html1", "html2"], `totalBytesGreater than ${TXT_LEN}`);
+ yield checkSearch({totalBytesGreater: HTML_LEN}, [], `totalBytesGreater than ${HTML_LEN}`);
+ yield checkSearch({totalBytesLess: TXT_LEN}, [], `totalBytesLess than ${TXT_LEN}`);
+ yield checkSearch({totalBytesLess: HTML_LEN}, ["txt1", "txt2"], `totalBytesLess than ${HTML_LEN}`);
+ yield checkSearch({totalBytesLess: BIG_LEN}, ["txt1", "txt2", "html1", "html2"], `totalBytesLess than ${BIG_LEN}`);
+
+ // Check good combinations of totalBytes*.
+ yield checkSearch({totalBytes: HTML_LEN, totalBytesGreater: TXT_LEN}, ["html1", "html2"], "totalBytes and totalBytesGreater");
+ yield checkSearch({totalBytes: TXT_LEN, totalBytesLess: HTML_LEN}, ["txt1", "txt2"], "totalBytes and totalBytesGreater");
+ yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: BIG_LEN, totalBytesGreater: 0}, ["html1", "html2"], "totalBytes and totalBytesLess and totalBytesGreater");
+
+ // Check bad combination of totalBytes*.
+ yield checkSearch({totalBytesLess: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytesLess, totalBytesGreater combination");
+ yield checkSearch({totalBytes: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytes, totalBytesGreater combination");
+ yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: TXT_LEN}, [], "bad totalBytes, totalBytesLess combination");
+
+ // Check mime.
+ yield checkSearch({mime: "text/plain"}, ["txt1", "txt2"], "mime text/plain");
+ yield checkSearch({mime: "text/html"}, ["html1", "html2"], "mime text/htmlplain");
+ yield checkSearch({mime: "video/webm"}, [], "mime video/webm");
+
+ // Check fileSize.
+ yield checkSearch({fileSize: TXT_LEN}, ["txt1", "txt2"], "fileSize");
+ yield checkSearch({fileSize: HTML_LEN}, ["html1", "html2"], "fileSize");
+
+ // Fields like bytesReceived, paused, state, exists are meaningful
+ // for downloads that are in progress but have not yet completed.
+ // todo: add tests for these when we have better support for in-progress
+ // downloads (e.g., after pause(), resume() and cancel() are implemented)
+
+ // Check multiple query properties.
+ // We could make this testing arbitrarily complicated...
+ // We already tested combining fields with obvious interactions above
+ // (e.g., filename and filenameRegex or startTime and startedBefore/After)
+ // so now just throw as many fields as we can at a single search and
+ // make sure a simple case still works.
+ yield checkSearch({
+ url: TXT_URL,
+ urlRegex: "download",
+ filename: downloadPath(TXT_FILE),
+ filenameRegex: "download",
+ query: ["download"],
+ startedAfter: time1.valueOf().toString(),
+ startedBefore: time2.valueOf().toString(),
+ totalBytes: TXT_LEN,
+ totalBytesGreater: 0,
+ totalBytesLess: BIG_LEN,
+ mime: "text/plain",
+ fileSize: TXT_LEN,
+ }, ["txt1"], "many properties");
+
+ // Check simple orderBy (forward and backward).
+ yield checkSearch({orderBy: ["startTime"]}, ["txt1", "txt2", "html1", "html2"], "orderBy startTime", true);
+ yield checkSearch({orderBy: ["-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy -startTime", true);
+
+ // Check orderBy with multiple fields.
+ // NB: TXT_URL and HTML_URL differ only in extension and .html precedes .txt
+ yield checkSearch({orderBy: ["url", "-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy with multiple fields", true);
+
+ // Check orderBy with limit.
+ yield checkSearch({orderBy: ["url"], limit: 1}, ["html1"], "orderBy with limit", true);
+
+ // Check bad arguments.
+ function* checkBadSearch(query, pattern, description) {
+ let item = yield search(query);
+ equal(item.status, "error", "search() failed");
+ ok(pattern.test(item.errmsg), `error message for ${description} was correct (${item.errmsg}).`);
+ }
+
+ yield checkBadSearch("myquery", /Incorrect argument type/, "query is not an object");
+ yield checkBadSearch({bogus: "boo"}, /Unexpected property/, "query contains an unknown field");
+ yield checkBadSearch({query: "query string"}, /Expected array/, "query.query is a string");
+ yield checkBadSearch({startedBefore: "i am not a time"}, /Type error/, "query.startedBefore is not a valid time");
+ yield checkBadSearch({startedAfter: "i am not a time"}, /Type error/, "query.startedAfter is not a valid time");
+ yield checkBadSearch({endedBefore: "i am not a time"}, /Type error/, "query.endedBefore is not a valid time");
+ yield checkBadSearch({endedAfter: "i am not a time"}, /Type error/, "query.endedAfter is not a valid time");
+ yield checkBadSearch({urlRegex: "["}, /Invalid urlRegex/, "query.urlRegexp is not a valid regular expression");
+ yield checkBadSearch({filenameRegex: "["}, /Invalid filenameRegex/, "query.filenameRegexp is not a valid regular expression");
+ yield checkBadSearch({orderBy: "startTime"}, /Expected array/, "query.orderBy is not an array");
+ yield checkBadSearch({orderBy: ["bogus"]}, /Invalid orderBy field/, "query.orderBy references a non-existent field");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
new file mode 100644
index 000000000..bc6bfcd68
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
@@ -0,0 +1,175 @@
+"use strict";
+
+/* globals browser */
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+function promiseAddonStartup() {
+ const {Management} = Cu.import("resource://gre/modules/Extension.jsm");
+
+ return new Promise(resolve => {
+ let listener = (evt, extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
+add_task(function* setup() {
+ yield ExtensionTestUtils.startAddonManager();
+});
+
+add_task(function* test_experiments_api() {
+ let apiAddonFile = Extension.generateZipFile({
+ "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
+ <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest"
+ em:id="meh@experiments.addons.mozilla.org"
+ em:name="Meh Experiment"
+ em:type="256"
+ em:version="0.1"
+ em:description="Meh experiment"
+ em:creator="Mozilla">
+
+ <em:targetApplication>
+ <Description
+ em:id="xpcshell@tests.mozilla.org"
+ em:minVersion="48"
+ em:maxVersion="*"/>
+ </em:targetApplication>
+ </Description>
+ </RDF>
+ `,
+
+ "api.js": String.raw`
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ Services.obs.notifyObservers(null, "webext-api-loaded", "");
+
+ class API extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ meh: {
+ hello(text) {
+ Services.obs.notifyObservers(null, "webext-api-hello", text);
+ }
+ }
+ }
+ }
+ }
+ `,
+
+ "schema.json": [
+ {
+ "namespace": "meh",
+ "description": "All full of meh.",
+ "permissions": ["experiments.meh"],
+ "functions": [
+ {
+ "name": "hello",
+ "type": "function",
+ "description": "Hates you. This is all.",
+ "parameters": [
+ {"type": "string", "name": "text"},
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ let addonFile = Extension.generateXPI({
+ manifest: {
+ applications: {gecko: {id: "meh@web.extension"}},
+ permissions: ["experiments.meh"],
+ },
+
+ background() {
+ // The test code below checks that hello() is called at the right
+ // time with the string "Here I am". Verify that the api schema is
+ // being correctly interpreted by calling hello() with bad arguments
+ // and only calling hello() with the magic string if the call with
+ // bad arguments throws.
+ try {
+ browser.meh.hello("I should not see this", "since two arguments are bad");
+ } catch (err) {
+ browser.meh.hello("Here I am");
+ }
+ },
+ });
+
+ let boringAddonFile = Extension.generateXPI({
+ manifest: {
+ applications: {gecko: {id: "boring@web.extension"}},
+ },
+ background() {
+ if (browser.meh) {
+ browser.meh.hello("Here I should not be");
+ }
+ },
+ });
+
+ do_register_cleanup(() => {
+ for (let file of [apiAddonFile, addonFile, boringAddonFile]) {
+ Services.obs.notifyObservers(file, "flush-cache-entry", null);
+ file.remove(false);
+ }
+ });
+
+
+ let resolveHello;
+ let observer = (subject, topic, data) => {
+ if (topic == "webext-api-loaded") {
+ ok(!!resolveHello, "Should not see API loaded until dependent extension loads");
+ } else if (topic == "webext-api-hello") {
+ resolveHello(data);
+ }
+ };
+
+ Services.obs.addObserver(observer, "webext-api-loaded", false);
+ Services.obs.addObserver(observer, "webext-api-hello", false);
+ do_register_cleanup(() => {
+ Services.obs.removeObserver(observer, "webext-api-loaded");
+ Services.obs.removeObserver(observer, "webext-api-hello");
+ });
+
+
+ // Install API add-on.
+ let apiAddon = yield AddonManager.installTemporaryAddon(apiAddonFile);
+
+ let {APIs} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
+ ok(APIs.apis.has("meh"), "Should have meh API.");
+
+
+ // Install boring WebExtension add-on.
+ let boringAddon = yield AddonManager.installTemporaryAddon(boringAddonFile);
+ yield promiseAddonStartup();
+
+
+ // Install interesting WebExtension add-on.
+ let promise = new Promise(resolve => {
+ resolveHello = resolve;
+ });
+
+ let addon = yield AddonManager.installTemporaryAddon(addonFile);
+ yield promiseAddonStartup();
+
+ let hello = yield promise;
+ equal(hello, "Here I am", "Should get hello from add-on");
+
+ // Cleanup.
+ apiAddon.uninstall();
+
+ boringAddon.userDisabled = true;
+ yield new Promise(do_execute_soon);
+
+ equal(addon.appDisabled, true, "Add-on should be app-disabled after its dependency is removed.");
+
+ addon.uninstall();
+ boringAddon.uninstall();
+});
+
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_extension.js b/toolkit/components/extensions/test/xpcshell/test_ext_extension.js
new file mode 100644
index 000000000..f18845f6a
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extension.js
@@ -0,0 +1,55 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_is_allowed_incognito_access() {
+ async function background() {
+ let allowed = await browser.extension.isAllowedIncognitoAccess();
+
+ browser.test.assertEq(true, allowed, "isAllowedIncognitoAccess is true");
+ browser.test.notifyPass("isAllowedIncognitoAccess");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("isAllowedIncognitoAccess");
+ yield extension.unload();
+});
+
+add_task(function* test_in_incognito_context_false() {
+ function background() {
+ browser.test.assertEq(false, browser.extension.inIncognitoContext, "inIncognitoContext returned false");
+ browser.test.notifyPass("inIncognitoContext");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("inIncognitoContext");
+ yield extension.unload();
+});
+
+add_task(function* test_is_allowed_file_scheme_access() {
+ async function background() {
+ let allowed = await browser.extension.isAllowedFileSchemeAccess();
+
+ browser.test.assertEq(false, allowed, "isAllowedFileSchemeAccess is false");
+ browser.test.notifyPass("isAllowedFileSchemeAccess");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("isAllowedFileSchemeAccess");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_idle.js b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js
new file mode 100644
index 000000000..89bcac217
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js
@@ -0,0 +1,202 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+let idleService = {
+ _observers: new Set(),
+ _activity: {
+ addCalls: [],
+ removeCalls: [],
+ observerFires: [],
+ },
+ _reset: function() {
+ this._observers.clear();
+ this._activity.addCalls = [];
+ this._activity.removeCalls = [];
+ this._activity.observerFires = [];
+ },
+ _fireObservers: function(state) {
+ for (let observer of this._observers.values()) {
+ observer.observe(observer, state, null);
+ this._activity.observerFires.push(state);
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIIdleService]),
+ idleTime: 19999,
+ addIdleObserver: function(observer, time) {
+ this._observers.add(observer);
+ this._activity.addCalls.push(time);
+ },
+ removeIdleObserver: function(observer, time) {
+ this._observers.delete(observer);
+ this._activity.removeCalls.push(time);
+ },
+};
+
+function checkActivity(expectedActivity) {
+ let {expectedAdd, expectedRemove, expectedFires} = expectedActivity;
+ let {addCalls, removeCalls, observerFires} = idleService._activity;
+ equal(expectedAdd.length, addCalls.length, "idleService.addIdleObserver was called the expected number of times");
+ equal(expectedRemove.length, removeCalls.length, "idleService.removeIdleObserver was called the expected number of times");
+ equal(expectedFires.length, observerFires.length, "idle observer was fired the expected number of times");
+ deepEqual(addCalls, expectedAdd, "expected interval passed to idleService.addIdleObserver");
+ deepEqual(removeCalls, expectedRemove, "expected interval passed to idleService.removeIdleObserver");
+ deepEqual(observerFires, expectedFires, "expected topic passed to idle observer");
+}
+
+add_task(function* setup() {
+ let fakeIdleService = MockRegistrar.register("@mozilla.org/widget/idleservice;1", idleService);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(fakeIdleService);
+ });
+});
+
+add_task(function* testQueryStateActive() {
+ function background() {
+ browser.idle.queryState(20).then(status => {
+ browser.test.assertEq("active", status, "Idle status is active");
+ browser.test.notifyPass("idle");
+ },
+ err => {
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ browser.test.notifyFail("idle");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("idle");
+ yield extension.unload();
+});
+
+add_task(function* testQueryStateIdle() {
+ function background() {
+ browser.idle.queryState(15).then(status => {
+ browser.test.assertEq("idle", status, "Idle status is idle");
+ browser.test.notifyPass("idle");
+ },
+ err => {
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ browser.test.notifyFail("idle");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("idle");
+ yield extension.unload();
+});
+
+add_task(function* testOnlySetDetectionInterval() {
+ function background() {
+ browser.idle.setDetectionInterval(99);
+ browser.test.sendMessage("detectionIntervalSet");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("detectionIntervalSet");
+ idleService._fireObservers("idle");
+ checkActivity({expectedAdd: [], expectedRemove: [], expectedFires: []});
+ yield extension.unload();
+});
+
+add_task(function* testSetDetectionIntervalBeforeAddingListener() {
+ function background() {
+ browser.idle.setDetectionInterval(99);
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.test.sendMessage("listenerAdded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("listenerAdded");
+ idleService._fireObservers("idle");
+ yield extension.awaitMessage("listenerFired");
+ checkActivity({expectedAdd: [99], expectedRemove: [], expectedFires: ["idle"]});
+ yield extension.unload();
+});
+
+add_task(function* testSetDetectionIntervalAfterAddingListener() {
+ function background() {
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.idle.setDetectionInterval(99);
+ browser.test.sendMessage("detectionIntervalSet");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("detectionIntervalSet");
+ idleService._fireObservers("idle");
+ yield extension.awaitMessage("listenerFired");
+ checkActivity({expectedAdd: [60, 99], expectedRemove: [60], expectedFires: ["idle"]});
+ yield extension.unload();
+});
+
+add_task(function* testOnlyAddingListener() {
+ function background() {
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("active", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.test.sendMessage("listenerAdded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("listenerAdded");
+ idleService._fireObservers("active");
+ yield extension.awaitMessage("listenerFired");
+ // check that "idle-daily" topic does not cause a listener to fire
+ idleService._fireObservers("idle-daily");
+ checkActivity({expectedAdd: [60], expectedRemove: [], expectedFires: ["active", "idle-daily"]});
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js b/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
new file mode 100644
index 000000000..652f41315
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
@@ -0,0 +1,37 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_json_parser() {
+ const ID = "json@test.web.extension";
+
+ let xpi = Extension.generateXPI({
+ files: {
+ "manifest.json": String.raw`{
+ // This is a manifest.
+ "applications": {"gecko": {"id": "${ID}"}},
+ "name": "This \" is // not a comment",
+ "version": "0.1\\" // , "description": "This is not a description"
+ }`,
+ },
+ });
+
+ let expectedManifest = {
+ "applications": {"gecko": {"id": ID}},
+ "name": "This \" is // not a comment",
+ "version": "0.1\\",
+ };
+
+ let fileURI = Services.io.newFileURI(xpi);
+ let uri = NetUtil.newURI(`jar:${fileURI.spec}!/`);
+
+ let extension = new ExtensionData(uri);
+
+ yield extension.readManifest();
+
+ Assert.deepEqual(extension.rawManifest, expectedManifest,
+ "Manifest with correctly-filtered comments");
+
+ Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
+ xpi.remove(false);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js
new file mode 100644
index 000000000..63d5361a1
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js
@@ -0,0 +1,168 @@
+"use strict";
+
+/* globals browser */
+
+Cu.import("resource://gre/modules/Extension.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {LegacyExtensionContext} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
+
+/**
+ * This test case ensures that LegacyExtensionContext instances:
+ * - expose the expected API object and can join the messaging
+ * of a webextension given its addon id
+ * - the exposed API object can receive a port related to a `runtime.connect`
+ * Port created in the webextension's background page
+ * - the received Port instance can exchange messages with the background page
+ * - the received Port receive a disconnect event when the webextension is
+ * shutting down
+ */
+add_task(function* test_legacy_extension_context() {
+ function background() {
+ let bgURL = window.location.href;
+
+ let extensionInfo = {
+ bgURL,
+ // Extract the assigned uuid from the background page url.
+ uuid: window.location.hostname,
+ };
+
+ browser.test.sendMessage("webextension-ready", extensionInfo);
+
+ let port;
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "do-send-message") {
+ let reply = await browser.runtime.sendMessage("webextension -> legacy_extension message");
+
+ browser.test.assertEq("legacy_extension -> webextension reply", reply,
+ "Got the expected message from the LegacyExtensionContext");
+ browser.test.sendMessage("got-reply-message");
+ } else if (msg == "do-connect") {
+ port = browser.runtime.connect();
+
+ port.onMessage.addListener(portMsg => {
+ browser.test.assertEq("legacy_extension -> webextension port message", portMsg,
+ "Got the expected message from the LegacyExtensionContext");
+ port.postMessage("webextension -> legacy_extension port message");
+ });
+ } else if (msg == "do-disconnect") {
+ port.disconnect();
+ }
+ });
+ }
+
+ let extensionData = {
+ background,
+ };
+
+ let extension = Extension.generate(extensionData);
+
+ let waitForExtensionInfo = new Promise((resolve, reject) => {
+ extension.on("test-message", function testMessageListener(kind, msg, ...args) {
+ if (msg != "webextension-ready") {
+ reject(new Error(`Got an unexpected test-message: ${msg}`));
+ } else {
+ extension.off("test-message", testMessageListener);
+ resolve(args[0]);
+ }
+ });
+ });
+
+ // Connect to the target extension as an external context
+ // using the given custom sender info.
+ let legacyContext;
+
+ extension.on("startup", function onStartup() {
+ extension.off("startup", onStartup);
+ legacyContext = new LegacyExtensionContext(extension);
+ extension.callOnClose({
+ close: () => legacyContext.unload(),
+ });
+ });
+
+ yield extension.startup();
+
+ let extensionInfo = yield waitForExtensionInfo;
+
+ equal(legacyContext.envType, "legacy_extension",
+ "LegacyExtensionContext instance has the expected type");
+
+ ok(legacyContext.api, "Got the expected API object");
+ ok(legacyContext.api.browser, "Got the expected browser property");
+
+ let waitMessage = new Promise(resolve => {
+ const {browser} = legacyContext.api;
+ browser.runtime.onMessage.addListener((singleMsg, msgSender) => {
+ resolve({singleMsg, msgSender});
+
+ // Send a reply to the sender.
+ return Promise.resolve("legacy_extension -> webextension reply");
+ });
+ });
+
+ extension.testMessage("do-send-message");
+
+ let {singleMsg, msgSender} = yield waitMessage;
+ equal(singleMsg, "webextension -> legacy_extension message",
+ "Got the expected message");
+ ok(msgSender, "Got a message sender object");
+
+ equal(msgSender.id, extensionInfo.uuid, "The sender has the expected id property");
+ equal(msgSender.url, extensionInfo.bgURL, "The sender has the expected url property");
+
+ // Wait confirmation that the reply has been received.
+ yield new Promise((resolve, reject) => {
+ extension.on("test-message", function testMessageListener(kind, msg, ...args) {
+ if (msg != "got-reply-message") {
+ reject(new Error(`Got an unexpected test-message: ${msg}`));
+ } else {
+ extension.off("test-message", testMessageListener);
+ resolve();
+ }
+ });
+ });
+
+ let waitConnectPort = new Promise(resolve => {
+ let {browser} = legacyContext.api;
+ browser.runtime.onConnect.addListener(port => {
+ resolve(port);
+ });
+ });
+
+ extension.testMessage("do-connect");
+
+ let port = yield waitConnectPort;
+
+ ok(port, "Got the Port API object");
+ ok(port.sender, "The port has a sender property");
+ equal(port.sender.id, extensionInfo.uuid,
+ "The port sender has the expected id property");
+ equal(port.sender.url, extensionInfo.bgURL,
+ "The port sender has the expected url property");
+
+ let waitPortMessage = new Promise(resolve => {
+ port.onMessage.addListener((msg) => {
+ resolve(msg);
+ });
+ });
+
+ port.postMessage("legacy_extension -> webextension port message");
+
+ let msg = yield waitPortMessage;
+
+ equal(msg, "webextension -> legacy_extension port message",
+ "LegacyExtensionContext received the expected message from the webextension");
+
+ let waitForDisconnect = new Promise(resolve => {
+ port.onDisconnect.addListener(resolve);
+ });
+
+ extension.testMessage("do-disconnect");
+
+ yield waitForDisconnect;
+
+ do_print("Got the disconnect event on unload");
+
+ yield extension.shutdown();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js
new file mode 100644
index 000000000..ea5d78524
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js
@@ -0,0 +1,188 @@
+"use strict";
+
+/* globals browser */
+
+Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
+
+// Import EmbeddedExtensionManager to be able to check that the
+// tacked instances are cleared after the embedded extension shutdown.
+const {
+ EmbeddedExtensionManager,
+} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm", {});
+
+/**
+ * This test case ensures that the LegacyExtensionsUtils.EmbeddedExtension:
+ * - load the embedded webextension resources from a "/webextension/" dir
+ * inside the XPI.
+ * - EmbeddedExtension.prototype.api returns an API object which exposes
+ * a working `runtime.onConnect` event object (e.g. the API can receive a port
+ * when the embedded webextension is started and it can exchange messages
+ * with the background page).
+ * - EmbeddedExtension.prototype.startup/shutdown methods manage the embedded
+ * webextension lifecycle as expected.
+ */
+add_task(function* test_embedded_webextension_utils() {
+ function backgroundScript() {
+ let port = browser.runtime.connect();
+
+ port.onMessage.addListener((msg) => {
+ if (msg == "legacy_extension -> webextension") {
+ port.postMessage("webextension -> legacy_extension");
+ port.disconnect();
+ }
+ });
+ }
+
+ const id = "@test.embedded.web.extension";
+
+ // Extensions.generateXPI is used here (and in the other hybrid addons tests in this same
+ // test dir) to be able to generate an xpi with the directory layout that we expect from
+ // an hybrid legacy+webextension addon (where all the embedded webextension resources are
+ // loaded from a 'webextension/' directory).
+ let fakeHybridAddonFile = Extension.generateZipFile({
+ "webextension/manifest.json": {
+ applications: {gecko: {id}},
+ name: "embedded webextension name",
+ manifest_version: 2,
+ version: "1.0",
+ background: {
+ scripts: ["bg.js"],
+ },
+ },
+ "webextension/bg.js": `new ${backgroundScript}`,
+ });
+
+ // Remove the generated xpi file and flush the its jar cache
+ // on cleanup.
+ do_register_cleanup(() => {
+ Services.obs.notifyObservers(fakeHybridAddonFile, "flush-cache-entry", null);
+ fakeHybridAddonFile.remove(false);
+ });
+
+ let fileURI = Services.io.newFileURI(fakeHybridAddonFile);
+ let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null);
+
+ let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
+ id, resourceURI,
+ });
+
+ ok(embeddedExtension, "Got the embeddedExtension object");
+
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+ "Got the expected number of tracked embedded extension instances");
+
+ do_print("waiting embeddedExtension.startup");
+ let embeddedExtensionAPI = yield embeddedExtension.startup();
+ ok(embeddedExtensionAPI, "Got the embeddedExtensionAPI object");
+
+ let waitConnectPort = new Promise(resolve => {
+ let {browser} = embeddedExtensionAPI;
+ browser.runtime.onConnect.addListener(port => {
+ resolve(port);
+ });
+ });
+
+ let port = yield waitConnectPort;
+
+ ok(port, "Got the Port API object");
+
+ let waitPortMessage = new Promise(resolve => {
+ port.onMessage.addListener((msg) => {
+ resolve(msg);
+ });
+ });
+
+ port.postMessage("legacy_extension -> webextension");
+
+ let msg = yield waitPortMessage;
+
+ equal(msg, "webextension -> legacy_extension",
+ "LegacyExtensionContext received the expected message from the webextension");
+
+ let waitForDisconnect = new Promise(resolve => {
+ port.onDisconnect.addListener(resolve);
+ });
+
+ do_print("Wait for the disconnect port event");
+ yield waitForDisconnect;
+ do_print("Got the disconnect port event");
+
+ yield embeddedExtension.shutdown();
+
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+ "EmbeddedExtension instances has been untracked from the EmbeddedExtensionManager");
+});
+
+function* createManifestErrorTestCase(id, xpi, expectedError) {
+ // Remove the generated xpi file and flush the its jar cache
+ // on cleanup.
+ do_register_cleanup(() => {
+ Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
+ xpi.remove(false);
+ });
+
+ let fileURI = Services.io.newFileURI(xpi);
+ let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null);
+
+ let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
+ id, resourceURI,
+ });
+
+ yield Assert.rejects(embeddedExtension.startup(), expectedError,
+ "embedded extension startup rejected");
+
+ // Shutdown a "never-started" addon with an embedded webextension should not
+ // raise any exception, and if it does this test will fail.
+ yield embeddedExtension.shutdown();
+}
+
+add_task(function* test_startup_error_empty_manifest() {
+ const id = "empty-manifest@test.embedded.web.extension";
+ const files = {
+ "webextension/manifest.json": ``,
+ };
+ const expectedError = "(NS_BASE_STREAM_CLOSED)";
+
+ let fakeHybridAddonFile = Extension.generateZipFile(files);
+
+ yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
+});
+
+add_task(function* test_startup_error_invalid_json_manifest() {
+ const id = "invalid-json-manifest@test.embedded.web.extension";
+ const files = {
+ "webextension/manifest.json": `{ "name": }`,
+ };
+ const expectedError = "JSON.parse:";
+
+ let fakeHybridAddonFile = Extension.generateZipFile(files);
+
+ yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
+});
+
+add_task(function* test_startup_error_blocking_validation_errors() {
+ const id = "blocking-manifest-validation-error@test.embedded.web.extension";
+ const files = {
+ "webextension/manifest.json": {
+ name: "embedded webextension name",
+ manifest_version: 2,
+ version: "1.0",
+ background: {
+ scripts: {},
+ },
+ },
+ };
+
+ function expectedError(actual) {
+ if (actual.errors && actual.errors.length == 1 &&
+ actual.errors[0].startsWith("Reading manifest:")) {
+ return true;
+ }
+
+ return false;
+ }
+
+ let fakeHybridAddonFile = Extension.generateZipFile(files);
+
+ yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js b/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js
new file mode 100644
index 000000000..0f0b41085
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js
@@ -0,0 +1,50 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function backgroundScript() {
+ let hasRun = localStorage.getItem("has-run");
+ let result;
+ if (!hasRun) {
+ localStorage.setItem("has-run", "yup");
+ localStorage.setItem("test-item", "item1");
+ result = "item1";
+ } else {
+ let data = localStorage.getItem("test-item");
+ if (data == "item1") {
+ localStorage.setItem("test-item", "item2");
+ result = "item2";
+ } else if (data == "item2") {
+ localStorage.removeItem("test-item");
+ result = "deleted";
+ } else if (!data) {
+ localStorage.clear();
+ result = "cleared";
+ }
+ }
+ browser.test.sendMessage("result", result);
+ browser.test.notifyPass("localStorage");
+}
+
+const ID = "test-webextension@mozilla.com";
+let extensionData = {
+ manifest: {applications: {gecko: {id: ID}}},
+ background: backgroundScript,
+};
+
+add_task(function* test_localStorage() {
+ const RESULTS = ["item1", "item2", "deleted", "cleared", "item1"];
+
+ for (let expected of RESULTS) {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ let actual = yield extension.awaitMessage("result");
+
+ yield extension.awaitFinish("localStorage");
+ yield extension.unload();
+
+ equal(actual, expected, "got expected localStorage data");
+ }
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_management.js b/toolkit/components/extensions/test/xpcshell/test_ext_management.js
new file mode 100644
index 000000000..b19554a57
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_management.js
@@ -0,0 +1,20 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_management_schema() {
+ function background() {
+ browser.test.assertTrue(browser.management, "browser.management API exists");
+ browser.test.notifyPass("management-schema");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["management"],
+ },
+ background: `(${background})()`,
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("management-schema");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js b/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js
new file mode 100644
index 000000000..7d80a9c23
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js
@@ -0,0 +1,135 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://testing-common/AddonTestUtils.jsm");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+const {promiseAddonByID} = AddonTestUtils;
+const id = "uninstall_self_test@tests.mozilla.com";
+
+const manifest = {
+ applications: {
+ gecko: {
+ id,
+ },
+ },
+ name: "test extension name",
+ version: "1.0",
+};
+
+const waitForUninstalled = () => new Promise(resolve => {
+ const listener = {
+ onUninstalled: (addon) => {
+ equal(addon.id, id, "The expected add-on has been uninstalled");
+ AddonManager.getAddonByID(addon.id, checkedAddon => {
+ equal(checkedAddon, null, "Add-on no longer exists");
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ });
+ },
+ };
+ AddonManager.addAddonListener(listener);
+});
+
+let promptService = {
+ _response: null,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]),
+ confirmEx: function(...args) {
+ this._confirmExArgs = args;
+ return this._response;
+ },
+};
+
+add_task(function* setup() {
+ let fakePromptService = MockRegistrar.register("@mozilla.org/embedcomp/prompt-service;1", promptService);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(fakePromptService);
+ });
+ yield ExtensionTestUtils.startAddonManager();
+});
+
+add_task(function* test_management_uninstall_no_prompt() {
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.management.uninstallSelf();
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield waitForUninstalled();
+ yield extension.markUnloaded();
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
+
+add_task(function* test_management_uninstall_prompt_uninstall() {
+ promptService._response = 0;
+
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.management.uninstallSelf({showConfirmDialog: true});
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield waitForUninstalled();
+ yield extension.markUnloaded();
+
+ // Test localization strings
+ equal(promptService._confirmExArgs[1], `Uninstall ${manifest.name}`);
+ equal(promptService._confirmExArgs[2],
+ `The extension “${manifest.name}†is requesting to be uninstalled. What would you like to do?`);
+ equal(promptService._confirmExArgs[4], "Uninstall");
+ equal(promptService._confirmExArgs[5], "Keep Installed");
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
+
+add_task(function* test_management_uninstall_prompt_keep() {
+ promptService._response = 1;
+
+ function background() {
+ browser.test.onMessage.addListener(async msg => {
+ await browser.test.assertRejects(
+ browser.management.uninstallSelf({showConfirmDialog: true}),
+ "User cancelled uninstall of extension",
+ "Expected rejection when user declines uninstall");
+
+ browser.test.sendMessage("uninstall-rejected");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield extension.awaitMessage("uninstall-rejected");
+ addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on remains installed");
+ yield extension.unload();
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js
new file mode 100644
index 000000000..2b0084980
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js
@@ -0,0 +1,30 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+add_task(function* test_manifest_csp() {
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
+ "content_security_policy": "script-src 'self'; object-src 'none'",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+ equal(normalized.value.content_security_policy,
+ "script-src 'self'; object-src 'none'",
+ "Should have the expected poilcy string");
+
+
+ normalized = yield ExtensionTestUtils.normalizeManifest({
+ "content_security_policy": "object-src 'none'",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+
+ Assert.deepEqual(normalized.errors,
+ ["Error processing content_security_policy: SyntaxError: Policy is missing a required \u2018script-src\u2019 directive"],
+ "Should have the expected warning");
+
+ equal(normalized.value.content_security_policy, null,
+ "Invalid policy string should be omitted");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
new file mode 100644
index 000000000..94649692e
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
@@ -0,0 +1,27 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+add_task(function* test_manifest_incognito() {
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
+ "incognito": "spanning",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+ equal(normalized.value.incognito,
+ "spanning",
+ "Should have the expected incognito string");
+
+ normalized = yield ExtensionTestUtils.normalizeManifest({
+ "incognito": "split",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+ Assert.deepEqual(normalized.errors,
+ ['Error processing incognito: Invalid enumeration value "split"'],
+ "Should have the expected warning");
+ equal(normalized.value.incognito, null,
+ "Invalid incognito string should be omitted");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js
new file mode 100644
index 000000000..fad5661bb
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js
@@ -0,0 +1,13 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+add_task(function* test_manifest_minimum_chrome_version() {
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
+ "minimum_chrome_version": "42",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
new file mode 100644
index 000000000..5a6b628f5
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
@@ -0,0 +1,514 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals chrome */
+
+const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
+const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
+
+const ECHO_BODY = String.raw`
+ import struct
+ import sys
+
+ while True:
+ rawlen = sys.stdin.read(4)
+ if len(rawlen) == 0:
+ sys.exit(0)
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = sys.stdin.read(msglen)
+
+ sys.stdout.write(struct.pack('@I', msglen))
+ sys.stdout.write(msg)
+`;
+
+const INFO_BODY = String.raw`
+ import json
+ import os
+ import struct
+ import sys
+
+ msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()})
+ sys.stdout.write(struct.pack('@I', len(msg)))
+ sys.stdout.write(msg)
+ sys.exit(0)
+`;
+
+const STDERR_LINES = ["hello stderr", "this should be a separate line"];
+let STDERR_MSG = STDERR_LINES.join("\\n");
+
+const STDERR_BODY = String.raw`
+ import sys
+ sys.stderr.write("${STDERR_MSG}")
+`;
+
+const SCRIPTS = [
+ {
+ name: "echo",
+ description: "a native app that echoes back messages it receives",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ },
+ {
+ name: "info",
+ description: "a native app that gives some info about how it was started",
+ script: INFO_BODY.replace(/^ {2}/gm, ""),
+ },
+ {
+ name: "stderr",
+ description: "a native app that writes to stderr and then exits",
+ script: STDERR_BODY.replace(/^ {2}/gm, ""),
+ },
+];
+
+add_task(function* setup() {
+ yield setupHosts(SCRIPTS);
+});
+
+// Test the basic operation of native messaging with a simple
+// script that echoes back whatever message is sent to it.
+add_task(function* test_happy_path() {
+ function background() {
+ let port = browser.runtime.connectNative("echo");
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("message", msg);
+ });
+ browser.test.onMessage.addListener((what, payload) => {
+ if (what == "send") {
+ if (payload._json) {
+ let json = payload._json;
+ payload.toJSON = () => json;
+ delete payload._json;
+ }
+ port.postMessage(payload);
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+ const tests = [
+ {
+ data: "this is a string",
+ what: "simple string",
+ },
+ {
+ data: "Это юникода",
+ what: "unicode string",
+ },
+ {
+ data: {test: "hello"},
+ what: "simple object",
+ },
+ {
+ data: {
+ what: "An object with a few properties",
+ number: 123,
+ bool: true,
+ nested: {what: "another object"},
+ },
+ what: "object with several properties",
+ },
+
+ {
+ data: {
+ ignoreme: true,
+ _json: {data: "i have a tojson method"},
+ },
+ expected: {data: "i have a tojson method"},
+ what: "object with toJSON() method",
+ },
+ ];
+ for (let test of tests) {
+ extension.sendMessage("send", test.data);
+ let response = yield extension.awaitMessage("message");
+ let expected = test.expected || test.data;
+ deepEqual(response, expected, `Echoed a message of type ${test.what}`);
+ }
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 1, "subprocess is still running");
+ let exitPromise = waitForSubprocessExit();
+ yield extension.unload();
+ yield exitPromise;
+});
+
+if (AppConstants.platform == "win") {
+ // "relative.echo" has a relative path in the host manifest.
+ add_task(function* test_relative_path() {
+ function background() {
+ let port = browser.runtime.connectNative("relative.echo");
+ let MSG = "test relative echo path";
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(MSG, msg, "Got expected message back");
+ browser.test.sendMessage("done");
+ });
+ port.postMessage(MSG);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 1, "subprocess is still running");
+ let exitPromise = waitForSubprocessExit();
+ yield extension.unload();
+ yield exitPromise;
+ });
+}
+
+// Test sendNativeMessage()
+add_task(function* test_sendNativeMessage() {
+ async function background() {
+ let MSG = {test: "hello world"};
+
+ // Check error handling
+ await browser.test.assertRejects(
+ browser.runtime.sendNativeMessage("nonexistent", MSG),
+ /Attempt to postMessage on disconnected port/,
+ "sendNativeMessage() to a nonexistent app failed");
+
+ // Check regular message exchange
+ let reply = await browser.runtime.sendNativeMessage("echo", MSG);
+
+ let expected = JSON.stringify(MSG);
+ let received = JSON.stringify(reply);
+ browser.test.assertEq(expected, received, "Received echoed native message");
+
+ browser.test.sendMessage("finished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+
+ // With sendNativeMessage(), the subprocess should be disconnected
+ // after exchanging a single message.
+ yield waitForSubprocessExit();
+
+ yield extension.unload();
+});
+
+// Test calling Port.disconnect()
+add_task(function* test_disconnect() {
+ function background() {
+ let port = browser.runtime.connectNative("echo");
+ port.onMessage.addListener((msg, msgPort) => {
+ browser.test.assertEq(port, msgPort, "onMessage handler should receive the port as the second argument");
+ browser.test.sendMessage("message", msg);
+ });
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.fail("onDisconnect should not be called for disconnect()");
+ });
+ browser.test.onMessage.addListener((what, payload) => {
+ if (what == "send") {
+ if (payload._json) {
+ let json = payload._json;
+ payload.toJSON = () => json;
+ delete payload._json;
+ }
+ port.postMessage(payload);
+ } else if (what == "disconnect") {
+ try {
+ port.disconnect();
+ browser.test.sendMessage("disconnect-result", {success: true});
+ } catch (err) {
+ browser.test.sendMessage("disconnect-result", {
+ success: false,
+ errmsg: err.message,
+ });
+ }
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("send", "test");
+ let response = yield extension.awaitMessage("message");
+ equal(response, "test", "Echoed a string");
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 1, "subprocess is running");
+
+ extension.sendMessage("disconnect");
+ response = yield extension.awaitMessage("disconnect-result");
+ equal(response.success, true, "disconnect succeeded");
+
+ do_print("waiting for subprocess to exit");
+ yield waitForSubprocessExit();
+ procCount = yield getSubprocessCount();
+ equal(procCount, 0, "subprocess is no longer running");
+
+ extension.sendMessage("disconnect");
+ response = yield extension.awaitMessage("disconnect-result");
+ equal(response.success, true, "second call to disconnect silently ignored");
+
+ yield extension.unload();
+});
+
+// Test the limit on message size for writing
+add_task(function* test_write_limit() {
+ Services.prefs.setIntPref(PREF_MAX_WRITE, 10);
+ function clearPref() {
+ Services.prefs.clearUserPref(PREF_MAX_WRITE);
+ }
+ do_register_cleanup(clearPref);
+
+ function background() {
+ const PAYLOAD = "0123456789A";
+ let port = browser.runtime.connectNative("echo");
+ try {
+ port.postMessage(PAYLOAD);
+ browser.test.sendMessage("result", null);
+ } catch (ex) {
+ browser.test.sendMessage("result", ex.message);
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+
+ let errmsg = yield extension.awaitMessage("result");
+ notEqual(errmsg, null, "native postMessage() failed for overly large message");
+
+ yield extension.unload();
+ yield waitForSubprocessExit();
+
+ clearPref();
+});
+
+// Test the limit on message size for reading
+add_task(function* test_read_limit() {
+ Services.prefs.setIntPref(PREF_MAX_READ, 10);
+ function clearPref() {
+ Services.prefs.clearUserPref(PREF_MAX_READ);
+ }
+ do_register_cleanup(clearPref);
+
+ function background() {
+ const PAYLOAD = "0123456789A";
+ let port = browser.runtime.connectNative("echo");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
+ browser.test.assertEq("Native application tried to send a message of 13 bytes, which exceeds the limit of 10 bytes.", port.error && port.error.message);
+ browser.test.sendMessage("result", "disconnected");
+ });
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", "message");
+ });
+ port.postMessage(PAYLOAD);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+
+ let result = yield extension.awaitMessage("result");
+ equal(result, "disconnected", "native port disconnected on receiving large message");
+
+ yield extension.unload();
+ yield waitForSubprocessExit();
+
+ clearPref();
+});
+
+// Test that an extension without the nativeMessaging permission cannot
+// use native messaging.
+add_task(function* test_ext_permission() {
+ function background() {
+ browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission");
+ browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission");
+ browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission");
+ browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission");
+ browser.test.sendMessage("finished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+ yield extension.unload();
+});
+
+// Test that an extension that is not listed in allowed_extensions for
+// a native application cannot use that application.
+add_task(function* test_app_permission() {
+ function background() {
+ let port = browser.runtime.connectNative("echo");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
+ browser.test.assertEq("This extension does not have permission to use native application echo (or the application is not installed)", port.error && port.error.message);
+ browser.test.sendMessage("result", "disconnected");
+ });
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", "message");
+ });
+ port.postMessage({test: "test"});
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["nativeMessaging"],
+ },
+ }, "somethingelse@tests.mozilla.org");
+
+ yield extension.startup();
+
+ let result = yield extension.awaitMessage("result");
+ equal(result, "disconnected", "connectNative() failed without native app permission");
+
+ yield extension.unload();
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 0, "No child process was started");
+});
+
+// Test that the command-line arguments and working directory for the
+// native application are as expected.
+add_task(function* test_child_process() {
+ function background() {
+ let port = browser.runtime.connectNative("info");
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", msg);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+
+ let msg = yield extension.awaitMessage("result");
+ equal(msg.args.length, 2, "Received one command line argument");
+ equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest");
+ equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path,
+ "Working directory is the directory containing the native appliation");
+
+ let exitPromise = waitForSubprocessExit();
+ yield extension.unload();
+ yield exitPromise;
+});
+
+add_task(function* test_stderr() {
+ function background() {
+ let port = browser.runtime.connectNative("stderr");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
+ browser.test.assertEq(null, port.error, "Normal application exit is not an error");
+ browser.test.sendMessage("finished");
+ });
+ }
+
+ let {messages} = yield promiseConsoleOutput(function* () {
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+ yield extension.unload();
+
+ yield waitForSubprocessExit();
+ });
+
+ let lines = STDERR_LINES.map(line => messages.findIndex(msg => msg.message.includes(line)));
+ notEqual(lines[0], -1, "Saw first line of stderr output on the console");
+ notEqual(lines[1], -1, "Saw second line of stderr output on the console");
+ notEqual(lines[0], lines[1], "Stderr output lines are separated in the console");
+});
+
+// Test that calling connectNative() multiple times works
+// (bug 1313980 was a previous regression in this area)
+add_task(function* test_multiple_connects() {
+ async function background() {
+ function once() {
+ return new Promise(resolve => {
+ let MSG = "hello";
+ let port = browser.runtime.connectNative("echo");
+
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(MSG, msg, "Got expected message back");
+ port.disconnect();
+ resolve();
+ });
+ port.postMessage(MSG);
+ });
+ }
+
+ await once();
+ await once();
+ browser.test.notifyPass("multiple-connect");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("multiple-connect");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js
new file mode 100644
index 000000000..693f67dde
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js
@@ -0,0 +1,128 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
+ "resource://testing-common/MockRegistry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+Cu.import("resource://gre/modules/Subprocess.jsm");
+
+const MAX_ROUND_TRIP_TIME_MS = AppConstants.DEBUG || AppConstants.ASAN ? 36 : 18;
+const MAX_RETRIES = 5;
+
+
+const ECHO_BODY = String.raw`
+ import struct
+ import sys
+
+ while True:
+ rawlen = sys.stdin.read(4)
+ if len(rawlen) == 0:
+ sys.exit(0)
+
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = sys.stdin.read(msglen)
+
+ sys.stdout.write(struct.pack('@I', msglen))
+ sys.stdout.write(msg)
+`;
+
+const SCRIPTS = [
+ {
+ name: "echo",
+ description: "A native app that echoes back messages it receives",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ },
+];
+
+add_task(function* setup() {
+ yield setupHosts(SCRIPTS);
+});
+
+add_task(function* test_round_trip_perf() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.onMessage.addListener(msg => {
+ if (msg != "run-tests") {
+ return;
+ }
+
+ let port = browser.runtime.connectNative("echo");
+
+ function next() {
+ port.postMessage({
+ "Lorem": {
+ "ipsum": {
+ "dolor": [
+ "sit amet",
+ "consectetur adipiscing elit",
+ "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ ],
+ "Ut enim": [
+ "ad minim veniam",
+ "quis nostrud exercitation ullamco",
+ "laboris nisi ut aliquip ex ea commodo consequat.",
+ ],
+ "Duis": [
+ "aute irure dolor in reprehenderit in",
+ "voluptate velit esse cillum dolore eu",
+ "fugiat nulla pariatur.",
+ ],
+ "Excepteur": [
+ "sint occaecat cupidatat non proident",
+ "sunt in culpa qui officia deserunt",
+ "mollit anim id est laborum.",
+ ],
+ },
+ },
+ });
+ }
+
+ const COUNT = 1000;
+ let now;
+ function finish() {
+ let roundTripTime = (Date.now() - now) / COUNT;
+
+ port.disconnect();
+ browser.test.sendMessage("result", roundTripTime);
+ }
+
+ let count = 0;
+ port.onMessage.addListener(() => {
+ if (count == 0) {
+ // Skip the first round, since it includes the time it takes
+ // the app to start up.
+ now = Date.now();
+ }
+
+ if (count++ <= COUNT) {
+ next();
+ } else {
+ finish();
+ }
+ });
+
+ next();
+ });
+ },
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+
+ let roundTripTime = Infinity;
+ for (let i = 0; i < MAX_RETRIES && roundTripTime > MAX_ROUND_TRIP_TIME_MS; i++) {
+ extension.sendMessage("run-tests");
+ roundTripTime = yield extension.awaitMessage("result");
+ }
+
+ yield extension.unload();
+
+ ok(roundTripTime <= MAX_ROUND_TRIP_TIME_MS,
+ `Expected round trip time (${roundTripTime}ms) to be less than ${MAX_ROUND_TRIP_TIME_MS}ms`);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js
new file mode 100644
index 000000000..a75a1d49d
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js
@@ -0,0 +1,82 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const WONTDIE_BODY = String.raw`
+ import signal
+ import struct
+ import sys
+ import time
+
+ signal.signal(signal.SIGTERM, signal.SIG_IGN)
+
+ def spin():
+ while True:
+ try:
+ signal.pause()
+ except AttributeError:
+ time.sleep(5)
+
+ while True:
+ rawlen = sys.stdin.read(4)
+ if len(rawlen) == 0:
+ spin()
+
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = sys.stdin.read(msglen)
+
+ sys.stdout.write(struct.pack('@I', msglen))
+ sys.stdout.write(msg)
+`;
+
+const SCRIPTS = [
+ {
+ name: "wontdie",
+ description: "a native app that does not exit when stdin closes or on SIGTERM",
+ script: WONTDIE_BODY.replace(/^ {2}/gm, ""),
+ },
+];
+
+add_task(function* setup() {
+ yield setupHosts(SCRIPTS);
+});
+
+
+// Test that an unresponsive native application still gets killed eventually
+add_task(function* test_unresponsive_native_app() {
+ // XXX expose GRACEFUL_SHUTDOWN_TIME as a pref and reduce it
+ // just for this test?
+
+ function background() {
+ let port = browser.runtime.connectNative("wontdie");
+
+ const MSG = "echo me";
+ // bounce a message to make sure the process actually starts
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, MSG, "Received echoed message");
+ browser.test.sendMessage("ready");
+ });
+ port.postMessage(MSG);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 1, "subprocess is running");
+
+ let exitPromise = waitForSubprocessExit();
+ yield extension.unload();
+ yield exitPromise;
+
+ procCount = yield getSubprocessCount();
+ equal(procCount, 0, "subprocess was succesfully killed");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js b/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js
new file mode 100644
index 000000000..6f8b553fc
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js
@@ -0,0 +1,30 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function backgroundScript() {
+ function listener() {
+ browser.test.notifyFail("listener should not be invoked");
+ }
+
+ browser.runtime.onMessage.addListener(listener);
+ browser.runtime.onMessage.removeListener(listener);
+ browser.runtime.sendMessage("hello");
+
+ // Make sure that, if we somehow fail to remove the listener, then we'll run
+ // the listener before the test is marked as passing.
+ setTimeout(function() {
+ browser.test.notifyPass("onmessage_removelistener");
+ }, 0);
+}
+
+let extensionData = {
+ background: backgroundScript,
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("onmessage_removelistener");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js
new file mode 100644
index 000000000..2a1342cde
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js
@@ -0,0 +1,23 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_connect_without_listener() {
+ function background() {
+ let port = browser.runtime.connect();
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq("Could not establish connection. Receiving end does not exist.", port.error && port.error.message);
+ browser.test.notifyPass("port.onDisconnect was called");
+ });
+ }
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("port.onDisconnect was called");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js
new file mode 100644
index 000000000..a280206fa
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(function* setup() {
+ ExtensionTestUtils.mockAppInfo();
+});
+
+add_task(function* test_getBrowserInfo() {
+ async function background() {
+ let info = await browser.runtime.getBrowserInfo();
+
+ browser.test.assertEq(info.name, "XPCShell", "name is valid");
+ browser.test.assertEq(info.vendor, "Mozilla", "vendor is Mozilla");
+ browser.test.assertEq(info.version, "48", "version is correct");
+ browser.test.assertEq(info.buildID, "20160315", "buildID is correct");
+
+ browser.test.notifyPass("runtime.getBrowserInfo");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({background});
+ yield extension.startup();
+ yield extension.awaitFinish("runtime.getBrowserInfo");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js
new file mode 100644
index 000000000..29bad0c10
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js
@@ -0,0 +1,25 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function backgroundScript() {
+ browser.runtime.getPlatformInfo(info => {
+ let validOSs = ["mac", "win", "android", "cros", "linux", "openbsd"];
+ let validArchs = ["arm", "x86-32", "x86-64"];
+
+ browser.test.assertTrue(validOSs.indexOf(info.os) != -1, "OS is valid");
+ browser.test.assertTrue(validArchs.indexOf(info.arch) != -1, "Architecture is valid");
+ browser.test.notifyPass("runtime.getPlatformInfo");
+ });
+}
+
+let extensionData = {
+ background: backgroundScript,
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("runtime.getPlatformInfo");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js
new file mode 100644
index 000000000..fa6461412
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js
@@ -0,0 +1,337 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "Management", () => {
+ const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ return Management;
+});
+
+const {
+ createAppInfo,
+ createTempWebExtensionFile,
+ promiseAddonByID,
+ promiseAddonEvent,
+ promiseCompleteAllInstalls,
+ promiseFindAddonUpdates,
+ promiseRestartManager,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+// Allow for unsigned addons.
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+function awaitEvent(eventName) {
+ return new Promise(resolve => {
+ let listener = (_eventName, ...args) => {
+ if (_eventName === eventName) {
+ Management.off(eventName, listener);
+ resolve(...args);
+ }
+ };
+
+ Management.on(eventName, listener);
+ });
+}
+
+function background() {
+ let onInstalledDetails = null;
+ let onStartupFired = false;
+
+ browser.runtime.onInstalled.addListener(details => {
+ onInstalledDetails = details;
+ });
+
+ browser.runtime.onStartup.addListener(() => {
+ onStartupFired = true;
+ });
+
+ browser.test.onMessage.addListener(message => {
+ if (message === "get-on-installed-details") {
+ onInstalledDetails = onInstalledDetails || {fired: false};
+ browser.test.sendMessage("on-installed-details", onInstalledDetails);
+ } else if (message === "did-on-startup-fire") {
+ browser.test.sendMessage("on-startup-fired", onStartupFired);
+ } else if (message === "reload-extension") {
+ browser.runtime.reload();
+ }
+ });
+
+ browser.runtime.onUpdateAvailable.addListener(details => {
+ browser.test.sendMessage("reloading");
+ browser.runtime.reload();
+ });
+}
+
+function* expectEvents(extension, {onStartupFired, onInstalledFired, onInstalledReason}) {
+ extension.sendMessage("get-on-installed-details");
+ let details = yield extension.awaitMessage("on-installed-details");
+ if (onInstalledFired) {
+ equal(details.reason, onInstalledReason, "runtime.onInstalled fired with the correct reason");
+ } else {
+ equal(details.fired, onInstalledFired, "runtime.onInstalled should not have fired");
+ }
+
+ extension.sendMessage("did-on-startup-fire");
+ let fired = yield extension.awaitMessage("on-startup-fired");
+ equal(fired, onStartupFired, `Expected runtime.onStartup to ${onStartupFired ? "" : "not "} fire`);
+}
+
+add_task(function* test_should_fire_on_addon_update() {
+ const EXTENSION_ID = "test_runtime_on_installed_addon_update@tests.mozilla.org";
+
+ const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+
+ // The test extension uses an insecure update url.
+ Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+ const testServer = createHttpServer();
+ const port = testServer.identity.primaryPort;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ "update_url": `http://localhost:${port}/test_update.json`,
+ },
+ },
+ },
+ background,
+ });
+
+ testServer.registerPathHandler("/test_update.json", (request, response) => {
+ response.write(`{
+ "addons": {
+ "${EXTENSION_ID}": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:${port}/addons/test_runtime_on_installed-2.0.xpi"
+ }
+ ]
+ }
+ }
+ }`);
+ });
+
+ let webExtensionFile = createTempWebExtensionFile({
+ manifest: {
+ version: "2.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ },
+ background,
+ });
+
+ testServer.registerFile("/addons/test_runtime_on_installed-2.0.xpi", webExtensionFile);
+
+ yield promiseStartupManager();
+
+ yield extension.startup();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "install",
+ });
+
+ let addon = yield promiseAddonByID(EXTENSION_ID);
+ equal(addon.version, "1.0", "The installed addon has the correct version");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ let promiseInstalled = promiseAddonEvent("onInstalled");
+ yield promiseCompleteAllInstalls([install]);
+
+ yield extension.awaitMessage("reloading");
+
+ let startupPromise = awaitEvent("ready");
+
+ let [updated_addon] = yield promiseInstalled;
+ equal(updated_addon.version, "2.0", "The updated addon has the correct version");
+
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "update",
+ });
+
+ yield extension.unload();
+
+ yield updated_addon.uninstall();
+ yield promiseShutdownManager();
+});
+
+add_task(function* test_should_fire_on_browser_update() {
+ const EXTENSION_ID = "test_runtime_on_installed_browser_update@tests.mozilla.org";
+
+ yield promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ },
+ },
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "install",
+ });
+
+ let startupPromise = awaitEvent("ready");
+ yield promiseRestartManager("1");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: true,
+ onInstalledFired: false,
+ });
+
+ // Update the browser.
+ startupPromise = awaitEvent("ready");
+ yield promiseRestartManager("2");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: true,
+ onInstalledFired: true,
+ onInstalledReason: "browser_update",
+ });
+
+ // Restart the browser.
+ startupPromise = awaitEvent("ready");
+ yield promiseRestartManager("2");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: true,
+ onInstalledFired: false,
+ });
+
+ // Update the browser again.
+ startupPromise = awaitEvent("ready");
+ yield promiseRestartManager("3");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: true,
+ onInstalledFired: true,
+ onInstalledReason: "browser_update",
+ });
+
+ yield extension.unload();
+
+ yield promiseShutdownManager();
+});
+
+add_task(function* test_should_not_fire_on_reload() {
+ const EXTENSION_ID = "test_runtime_on_installed_reload@tests.mozilla.org";
+
+ yield promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ },
+ },
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "install",
+ });
+
+ let startupPromise = awaitEvent("ready");
+ extension.sendMessage("reload-extension");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: false,
+ });
+
+ yield extension.unload();
+ yield promiseShutdownManager();
+});
+
+add_task(function* test_should_not_fire_on_restart() {
+ const EXTENSION_ID = "test_runtime_on_installed_restart@tests.mozilla.org";
+
+ yield promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ },
+ },
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "install",
+ });
+
+ let addon = yield promiseAddonByID(EXTENSION_ID);
+ addon.userDisabled = true;
+
+ let startupPromise = awaitEvent("ready");
+ addon.userDisabled = false;
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: false,
+ });
+
+ yield extension.markUnloaded();
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js
new file mode 100644
index 000000000..fec8e13dd
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js
@@ -0,0 +1,79 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* tabsSendMessageReply() {
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond(msg);
+ } else if (msg == "respond-soon") {
+ setTimeout(() => { respond(msg); }, 0);
+ return true;
+ } else if (msg == "respond-promise") {
+ return Promise.resolve(msg);
+ } else if (msg == "respond-never") {
+ return;
+ } else if (msg == "respond-error") {
+ return Promise.reject(new Error(msg));
+ } else if (msg == "throw-error") {
+ throw new Error(msg);
+ }
+ });
+
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond("hello");
+ } else if (msg == "respond-now-2") {
+ respond(msg);
+ }
+ });
+
+ let childFrame = document.createElement("iframe");
+ childFrame.src = "extensionpage.html";
+ document.body.appendChild(childFrame);
+ }
+
+ function senderScript() {
+ Promise.all([
+ browser.runtime.sendMessage("respond-now"),
+ browser.runtime.sendMessage("respond-now-2"),
+ new Promise(resolve => browser.runtime.sendMessage("respond-soon", resolve)),
+ browser.runtime.sendMessage("respond-promise"),
+ browser.runtime.sendMessage("respond-never"),
+ new Promise(resolve => {
+ browser.runtime.sendMessage("respond-never", response => { resolve(response); });
+ }),
+
+ browser.runtime.sendMessage("respond-error").catch(error => Promise.resolve({error})),
+ browser.runtime.sendMessage("throw-error").catch(error => Promise.resolve({error})),
+ ]).then(([respondNow, respondNow2, respondSoon, respondPromise, respondNever, respondNever2, respondError, throwError]) => {
+ browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response");
+ browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener");
+ browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response");
+ browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response");
+ browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution");
+ browser.test.assertEq(undefined, respondNever2, "Got the expected no-response resolution");
+
+ browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response");
+ browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response");
+
+ browser.test.notifyPass("sendMessage");
+ }).catch(e => {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("sendMessage");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ files: {
+ "senderScript.js": senderScript,
+ "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`,
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("sendMessage");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js
new file mode 100644
index 000000000..f1a8d5a36
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js
@@ -0,0 +1,59 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_sendMessage_error() {
+ async function background() {
+ let circ = {};
+ circ.circ = circ;
+ let testCases = [
+ // [arguments, expected error string],
+ [[], "runtime.sendMessage's message argument is missing"],
+ [[null, null, null, null], "runtime.sendMessage's last argument is not a function"],
+ [[null, null, 1], "runtime.sendMessage's options argument is invalid"],
+ [[1, null, null], "runtime.sendMessage's extensionId argument is invalid"],
+ [[null, null, null, null, null], "runtime.sendMessage received too many arguments"],
+
+ // Even when the parameters are accepted, we still expect an error
+ // because there is no onMessage listener.
+ [[null, null, null], "Could not establish connection. Receiving end does not exist."],
+
+ // Structural cloning doesn't work with DOM but we fall back
+ // JSON serialization, so we don't expect another error.
+ [[null, location, null], "Could not establish connection. Receiving end does not exist."],
+
+ // Structured cloning supports cyclic self-references.
+ [[null, [circ, location], null], "cyclic object value"],
+ // JSON serialization does not support cyclic references.
+ [[null, circ, null], "Could not establish connection. Receiving end does not exist."],
+ // (the last two tests shows whether sendMessage is implemented as structured cloning).
+ ];
+
+ // Repeat all tests with the undefined value instead of null.
+ for (let [args, expectedError] of testCases.slice()) {
+ args = args.map(arg => arg === null ? undefined : arg);
+ testCases.push([args, expectedError]);
+ }
+
+ for (let [args, expectedError] of testCases) {
+ let description = `runtime.sendMessage(${args.map(String).join(", ")})`;
+
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage(...args),
+ expectedError,
+ `expected error message for ${description}`);
+ }
+
+ browser.test.notifyPass("sendMessage parameter validation");
+ }
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("sendMessage parameter validation");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js
new file mode 100644
index 000000000..f906333d2
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js
@@ -0,0 +1,54 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_sendMessage_without_listener() {
+ async function background() {
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage("msg"),
+ "Could not establish connection. Receiving end does not exist.",
+ "sendMessage callback was invoked");
+
+ browser.test.notifyPass("sendMessage callback was invoked");
+ }
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("sendMessage callback was invoked");
+
+ yield extension.unload();
+});
+
+add_task(function* test_chrome_sendMessage_without_listener() {
+ function background() {
+ /* globals chrome */
+ 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");
+ browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without 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.notifyPass("finished chrome.runtime.sendMessage");
+ });
+ isAsyncCall = true;
+ }
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("finished chrome.runtime.sendMessage");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js
new file mode 100644
index 000000000..e4f5e951f
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js
@@ -0,0 +1,51 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+add_task(function* test_sendMessage_to_self_should_not_trigger_onMessage() {
+ async function background() {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq("msg from child", msg);
+ browser.test.notifyPass("sendMessage did not call same-frame onMessage");
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("sendMessage with a listener in another frame", msg);
+ browser.runtime.sendMessage("should only reach another frame");
+ });
+
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage("should not trigger same-frame onMessage"),
+ "Could not establish connection. Receiving end does not exist.");
+
+ let anotherFrame = document.createElement("iframe");
+ anotherFrame.src = browser.extension.getURL("extensionpage.html");
+ document.body.appendChild(anotherFrame);
+ }
+
+ function lastScript() {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq("should only reach another frame", msg);
+ browser.runtime.sendMessage("msg from child");
+ });
+ browser.test.sendMessage("sendMessage callback called");
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "lastScript.js": lastScript,
+ "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="lastScript.js"></script>`,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitMessage("sendMessage callback called");
+ extension.sendMessage("sendMessage with a listener in another frame");
+ yield extension.awaitFinish("sendMessage did not call same-frame onMessage");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
new file mode 100644
index 000000000..d838be5b5
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -0,0 +1,1427 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
+
+let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionCommon;
+
+let json = [
+ {namespace: "testing",
+
+ properties: {
+ PROP1: {value: 20},
+ prop2: {type: "string"},
+ prop3: {
+ $ref: "submodule",
+ },
+ prop4: {
+ $ref: "submodule",
+ unsupported: true,
+ },
+ },
+
+ types: [
+ {
+ id: "type1",
+ type: "string",
+ "enum": ["value1", "value2", "value3"],
+ },
+
+ {
+ id: "type2",
+ type: "object",
+ properties: {
+ prop1: {type: "integer"},
+ prop2: {type: "array", items: {"$ref": "type1"}},
+ },
+ },
+
+ {
+ id: "basetype1",
+ type: "object",
+ properties: {
+ prop1: {type: "string"},
+ },
+ },
+
+ {
+ id: "basetype2",
+ choices: [
+ {type: "integer"},
+ ],
+ },
+
+ {
+ $extend: "basetype1",
+ properties: {
+ prop2: {type: "string"},
+ },
+ },
+
+ {
+ $extend: "basetype2",
+ choices: [
+ {type: "string"},
+ ],
+ },
+
+ {
+ id: "submodule",
+ type: "object",
+ functions: [
+ {
+ name: "sub_foo",
+ type: "function",
+ parameters: [],
+ returns: "integer",
+ },
+ ],
+ },
+ ],
+
+ functions: [
+ {
+ name: "foo",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "integer", optional: true, default: 99},
+ {name: "arg2", type: "boolean", optional: true},
+ ],
+ },
+
+ {
+ name: "bar",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "integer", optional: true},
+ {name: "arg2", type: "boolean"},
+ ],
+ },
+
+ {
+ name: "baz",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "object", properties: {
+ prop1: {type: "string"},
+ prop2: {type: "integer", optional: true},
+ prop3: {type: "integer", unsupported: true},
+ }},
+ ],
+ },
+
+ {
+ name: "qux",
+ type: "function",
+ parameters: [
+ {name: "arg1", "$ref": "type1"},
+ ],
+ },
+
+ {
+ name: "quack",
+ type: "function",
+ parameters: [
+ {name: "arg1", "$ref": "type2"},
+ ],
+ },
+
+ {
+ name: "quora",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "function"},
+ ],
+ },
+
+ {
+ name: "quileute",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "integer", optional: true},
+ {name: "arg2", type: "integer"},
+ ],
+ },
+
+ {
+ name: "queets",
+ type: "function",
+ unsupported: true,
+ parameters: [],
+ },
+
+ {
+ name: "quintuplets",
+ type: "function",
+ parameters: [
+ {name: "obj", type: "object", properties: [], additionalProperties: {type: "integer"}},
+ ],
+ },
+
+ {
+ name: "quasar",
+ type: "function",
+ parameters: [
+ {name: "abc", type: "object", properties: {
+ func: {type: "function", parameters: [
+ {name: "x", type: "integer"},
+ ]},
+ }},
+ ],
+ },
+
+ {
+ name: "quosimodo",
+ type: "function",
+ parameters: [
+ {name: "xyz", type: "object", additionalProperties: {type: "any"}},
+ ],
+ },
+
+ {
+ name: "patternprop",
+ type: "function",
+ parameters: [
+ {
+ name: "obj",
+ type: "object",
+ properties: {"prop1": {type: "string", pattern: "^\\d+$"}},
+ patternProperties: {
+ "(?i)^prop\\d+$": {type: "string"},
+ "^foo\\d+$": {type: "string"},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "pattern",
+ type: "function",
+ parameters: [
+ {name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$"},
+ ],
+ },
+
+ {
+ name: "format",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ url: {type: "string", "format": "url", "optional": true},
+ relativeUrl: {type: "string", "format": "relativeUrl", "optional": true},
+ strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "optional": true},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "formatDate",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ date: {type: "string", format: "date", optional: true},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "deep",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ foo: {
+ type: "object",
+ properties: {
+ bar: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ baz: {
+ type: "object",
+ properties: {
+ required: {type: "integer"},
+ optional: {type: "string", optional: true},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+
+ {
+ name: "errors",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ warn: {
+ type: "string",
+ pattern: "^\\d+$",
+ optional: true,
+ onError: "warn",
+ },
+ ignore: {
+ type: "string",
+ pattern: "^\\d+$",
+ optional: true,
+ onError: "ignore",
+ },
+ default: {
+ type: "string",
+ pattern: "^\\d+$",
+ optional: true,
+ },
+ },
+ },
+ ],
+ },
+
+ {
+ name: "localize",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ foo: {type: "string", "preprocess": "localize", "optional": true},
+ bar: {type: "string", "optional": true},
+ url: {type: "string", "preprocess": "localize", "format": "url", "optional": true},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "extended1",
+ type: "function",
+ parameters: [
+ {name: "val", $ref: "basetype1"},
+ ],
+ },
+
+ {
+ name: "extended2",
+ type: "function",
+ parameters: [
+ {name: "val", $ref: "basetype2"},
+ ],
+ },
+ ],
+
+ events: [
+ {
+ name: "onFoo",
+ type: "function",
+ },
+
+ {
+ name: "onBar",
+ type: "function",
+ extraParameters: [{
+ name: "filter",
+ type: "integer",
+ optional: true,
+ default: 1,
+ }],
+ },
+ ],
+ },
+ {
+ namespace: "foreign",
+ properties: {
+ foreignRef: {$ref: "testing.submodule"},
+ },
+ },
+ {
+ namespace: "inject",
+ properties: {
+ PROP1: {value: "should inject"},
+ },
+ },
+ {
+ namespace: "do-not-inject",
+ properties: {
+ PROP1: {value: "should not inject"},
+ },
+ },
+];
+
+let tallied = null;
+
+function tally(kind, ns, name, args) {
+ tallied = [kind, ns, name, args];
+}
+
+function verify(...args) {
+ do_check_eq(JSON.stringify(tallied), JSON.stringify(args));
+ tallied = null;
+}
+
+let talliedErrors = [];
+
+function checkErrors(errors) {
+ do_check_eq(talliedErrors.length, errors.length, "Got expected number of errors");
+ for (let [i, error] of errors.entries()) {
+ do_check_true(i in talliedErrors && talliedErrors[i].includes(error),
+ `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`);
+ }
+
+ talliedErrors.length = 0;
+}
+
+let permissions = new Set();
+
+class TallyingAPIImplementation extends SchemaAPIInterface {
+ constructor(namespace, name) {
+ super();
+ this.namespace = namespace;
+ this.name = name;
+ }
+
+ callFunction(args) {
+ tally("call", this.namespace, this.name, args);
+ }
+
+ callFunctionNoReturn(args) {
+ tally("call", this.namespace, this.name, args);
+ }
+
+ getProperty() {
+ tally("get", this.namespace, this.name);
+ }
+
+ setProperty(value) {
+ tally("set", this.namespace, this.name, value);
+ }
+
+ addListener(listener, args) {
+ tally("addListener", this.namespace, this.name, [listener, args]);
+ }
+
+ removeListener(listener) {
+ tally("removeListener", this.namespace, this.name, [listener]);
+ }
+
+ hasListener(listener) {
+ tally("hasListener", this.namespace, this.name, [listener]);
+ }
+}
+
+let wrapper = {
+ url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
+
+ checkLoadURL(url) {
+ return !url.startsWith("chrome:");
+ },
+
+ preprocessors: {
+ localize(value, context) {
+ return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`);
+ },
+ },
+
+ logError(message) {
+ talliedErrors.push(message);
+ },
+
+ hasPermission(permission) {
+ return permissions.has(permission);
+ },
+
+ shouldInject(ns) {
+ return ns != "do-not-inject";
+ },
+
+ getImplementation(namespace, name) {
+ return new TallyingAPIImplementation(namespace, name);
+ },
+};
+
+add_task(function* () {
+ let url = "data:," + JSON.stringify(json);
+ yield Schemas.load(url);
+
+ let root = {};
+ tallied = null;
+ Schemas.inject(root, wrapper);
+ do_check_eq(tallied, null);
+
+ do_check_eq(root.testing.PROP1, 20, "simple value property");
+ do_check_eq(root.testing.type1.VALUE1, "value1", "enum type");
+ do_check_eq(root.testing.type1.VALUE2, "value2", "enum type");
+
+ do_check_eq("inject" in root, true, "namespace 'inject' should be injected");
+ do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected");
+
+ root.testing.foo(11, true);
+ verify("call", "testing", "foo", [11, true]);
+
+ root.testing.foo(true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(null, true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(undefined, true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(11);
+ verify("call", "testing", "foo", [11, null]);
+
+ Assert.throws(() => root.testing.bar(11),
+ /Incorrect argument types/,
+ "should throw without required arg");
+
+ Assert.throws(() => root.testing.bar(11, true, 10),
+ /Incorrect argument types/,
+ "should throw with too many arguments");
+
+ root.testing.bar(true);
+ verify("call", "testing", "bar", [null, true]);
+
+ root.testing.baz({prop1: "hello", prop2: 22});
+ verify("call", "testing", "baz", [{prop1: "hello", prop2: 22}]);
+
+ root.testing.baz({prop1: "hello"});
+ verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]);
+
+ root.testing.baz({prop1: "hello", prop2: null});
+ verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]);
+
+ Assert.throws(() => root.testing.baz({prop2: 12}),
+ /Property "prop1" is required/,
+ "should throw without required property");
+
+ Assert.throws(() => root.testing.baz({prop1: "hi", prop3: 12}),
+ /Property "prop3" is unsupported by Firefox/,
+ "should throw with unsupported property");
+
+ Assert.throws(() => root.testing.baz({prop1: "hi", prop4: 12}),
+ /Unexpected property "prop4"/,
+ "should throw with unexpected property");
+
+ Assert.throws(() => root.testing.baz({prop1: 12}),
+ /Expected string instead of 12/,
+ "should throw with wrong type");
+
+ root.testing.qux("value2");
+ verify("call", "testing", "qux", ["value2"]);
+
+ Assert.throws(() => root.testing.qux("value4"),
+ /Invalid enumeration value "value4"/,
+ "should throw for invalid enum value");
+
+ root.testing.quack({prop1: 12, prop2: ["value1", "value3"]});
+ verify("call", "testing", "quack", [{prop1: 12, prop2: ["value1", "value3"]}]);
+
+ Assert.throws(() => root.testing.quack({prop1: 12, prop2: ["value1", "value3", "value4"]}),
+ /Invalid enumeration value "value4"/,
+ "should throw for invalid array type");
+
+ function f() {}
+ root.testing.quora(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ let g = () => 0;
+ root.testing.quora(g);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"]));
+ do_check_eq(tallied[3][0], g);
+ tallied = null;
+
+ root.testing.quileute(10);
+ verify("call", "testing", "quileute", [null, 10]);
+
+ Assert.throws(() => root.testing.queets(),
+ /queets is not a function/,
+ "should throw for unsupported functions");
+
+ root.testing.quintuplets({a: 10, b: 20, c: 30});
+ verify("call", "testing", "quintuplets", [{a: 10, b: 20, c: 30}]);
+
+ Assert.throws(() => root.testing.quintuplets({a: 10, b: 20, c: 30, d: "hi"}),
+ /Expected integer instead of "hi"/,
+ "should throw for wrong additionalProperties type");
+
+ root.testing.quasar({func: f});
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quasar"]));
+ do_check_eq(tallied[3][0].func, f);
+ tallied = null;
+
+ root.testing.quosimodo({a: 10, b: 20, c: 30});
+ verify("call", "testing", "quosimodo", [{a: 10, b: 20, c: 30}]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.quosimodo(10),
+ /Incorrect argument types/,
+ "should throw for wrong type");
+
+ root.testing.patternprop({prop1: "12", prop2: "42", Prop3: "43", foo1: "x"});
+ verify("call", "testing", "patternprop", [{prop1: "12", prop2: "42", Prop3: "43", foo1: "x"}]);
+ tallied = null;
+
+ root.testing.patternprop({prop1: "12"});
+ verify("call", "testing", "patternprop", [{prop1: "12"}]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", foo1: null}),
+ /Expected string instead of null/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "xx", prop2: "yy"}),
+ /String "xx" must match \/\^\\d\+\$\//,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: 42}),
+ /Expected string instead of 42/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: null}),
+ /Expected string instead of null/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", propx: "42"}),
+ /Unexpected property "propx"/,
+ "should throw for unexpected property");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", Foo1: "x"}),
+ /Unexpected property "Foo1"/,
+ "should throw for unexpected property");
+
+ root.testing.pattern("DEADbeef");
+ verify("call", "testing", "pattern", ["DEADbeef"]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.pattern("DEADcow"),
+ /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
+ "should throw for non-match");
+
+ root.testing.format({url: "http://foo/bar",
+ relativeUrl: "http://foo/bar"});
+ verify("call", "testing", "format", [{url: "http://foo/bar",
+ relativeUrl: "http://foo/bar",
+ strictRelativeUrl: null}]);
+ tallied = null;
+
+ root.testing.format({relativeUrl: "foo.html", strictRelativeUrl: "foo.html"});
+ verify("call", "testing", "format", [{url: null,
+ relativeUrl: `${wrapper.url}foo.html`,
+ strictRelativeUrl: `${wrapper.url}foo.html`}]);
+ tallied = null;
+
+ for (let format of ["url", "relativeUrl"]) {
+ Assert.throws(() => root.testing.format({[format]: "chrome://foo/content/"}),
+ /Access denied/,
+ "should throw for access denied");
+ }
+
+ for (let urlString of ["//foo.html", "http://foo/bar.html"]) {
+ Assert.throws(() => root.testing.format({strictRelativeUrl: urlString}),
+ /must be a relative URL/,
+ "should throw for non-relative URL");
+ }
+
+ const dates = [
+ "2016-03-04",
+ "2016-03-04T08:00:00Z",
+ "2016-03-04T08:00:00.000Z",
+ "2016-03-04T08:00:00-08:00",
+ "2016-03-04T08:00:00.000-08:00",
+ "2016-03-04T08:00:00+08:00",
+ "2016-03-04T08:00:00.000+08:00",
+ "2016-03-04T08:00:00+0800",
+ "2016-03-04T08:00:00-0800",
+ ];
+ dates.forEach(str => {
+ root.testing.formatDate({date: str});
+ verify("call", "testing", "formatDate", [{date: str}]);
+ });
+
+ // Make sure that a trivial change to a valid date invalidates it.
+ dates.forEach(str => {
+ Assert.throws(() => root.testing.formatDate({date: "0" + str}),
+ /Invalid date string/,
+ "should throw for invalid iso date string");
+ Assert.throws(() => root.testing.formatDate({date: str + "0"}),
+ /Invalid date string/,
+ "should throw for invalid iso date string");
+ });
+
+ const badDates = [
+ "I do not look anything like a date string",
+ "2016-99-99",
+ "2016-03-04T25:00:00Z",
+ ];
+ badDates.forEach(str => {
+ Assert.throws(() => root.testing.formatDate({date: str}),
+ /Invalid date string/,
+ "should throw for invalid iso date string");
+ });
+
+ root.testing.deep({foo: {bar: [{baz: {required: 12, optional: "42"}}]}});
+ verify("call", "testing", "deep", [{foo: {bar: [{baz: {required: 12, optional: "42"}}]}}]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {optional: "42"}}]}}),
+ /Type error for parameter arg \(Error processing foo\.bar\.0\.baz: Property "required" is required\) for testing\.deep/,
+ "should throw with the correct object path");
+
+ Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {required: 12, optional: 42}}]}}),
+ /Type error for parameter arg \(Error processing foo\.bar\.0\.baz\.optional: Expected string instead of 42\) for testing\.deep/,
+ "should throw with the correct object path");
+
+
+ talliedErrors.length = 0;
+
+ root.testing.errors({warn: "0123", ignore: "0123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: "0123", ignore: "0123", default: "0123"}]);
+ checkErrors([]);
+
+ root.testing.errors({warn: "0123", ignore: "x123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: "0123", ignore: null, default: "0123"}]);
+ checkErrors([]);
+
+ root.testing.errors({warn: "x123", ignore: "0123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: null, ignore: "0123", default: "0123"}]);
+ checkErrors([
+ 'String "x123" must match /^\\d+$/',
+ ]);
+
+
+ root.testing.onFoo.addListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([]));
+ tallied = null;
+
+ root.testing.onFoo.removeListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["removeListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ root.testing.onFoo.hasListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["hasListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ Assert.throws(() => root.testing.onFoo.addListener(10),
+ /Invalid listener/,
+ "addListener with non-function should throw");
+
+ root.testing.onBar.addListener(f, 10);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([10]));
+ tallied = null;
+
+ root.testing.onBar.addListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([1]));
+ tallied = null;
+
+ Assert.throws(() => root.testing.onBar.addListener(f, "hi"),
+ /Incorrect argument types/,
+ "addListener with wrong extra parameter should throw");
+
+ let target = {prop1: 12, prop2: ["value1", "value3"]};
+ let proxy = new Proxy(target, {});
+ Assert.throws(() => root.testing.quack(proxy),
+ /Expected a plain JavaScript object, got a Proxy/,
+ "should throw when passing a Proxy");
+
+ if (Symbol.toStringTag) {
+ let stringTarget = {prop1: 12, prop2: ["value1", "value3"]};
+ stringTarget[Symbol.toStringTag] = () => "[object Object]";
+ let stringProxy = new Proxy(stringTarget, {});
+ Assert.throws(() => root.testing.quack(stringProxy),
+ /Expected a plain JavaScript object, got a Proxy/,
+ "should throw when passing a Proxy");
+ }
+
+
+ root.testing.localize({foo: "__MSG_foo__", bar: "__MSG_foo__", url: "__MSG_http://example.com/__"});
+ verify("call", "testing", "localize", [{foo: "FOO", bar: "__MSG_foo__", url: "http://example.com/"}]);
+ tallied = null;
+
+
+ Assert.throws(() => root.testing.localize({url: "__MSG_/foo/bar__"}),
+ /\/FOO\/BAR is not a valid URL\./,
+ "should throw for invalid URL");
+
+
+ root.testing.extended1({prop1: "foo", prop2: "bar"});
+ verify("call", "testing", "extended1", [{prop1: "foo", prop2: "bar"}]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: 12}),
+ /Expected string instead of 12/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.extended1({prop1: "foo"}),
+ /Property "prop2" is required/,
+ "should throw for missing property");
+
+ Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: "bar", prop3: "xxx"}),
+ /Unexpected property "prop3"/,
+ "should throw for extra property");
+
+
+ root.testing.extended2("foo");
+ verify("call", "testing", "extended2", ["foo"]);
+ tallied = null;
+
+ root.testing.extended2(12);
+ verify("call", "testing", "extended2", [12]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.extended2(true),
+ /Incorrect argument types/,
+ "should throw for wrong argument type");
+
+ root.testing.prop3.sub_foo();
+ verify("call", "testing.prop3", "sub_foo", []);
+ tallied = null;
+
+ Assert.throws(() => root.testing.prop4.sub_foo(),
+ /root.testing.prop4 is undefined/,
+ "should throw for unsupported submodule");
+
+ root.foreign.foreignRef.sub_foo();
+ verify("call", "foreign.foreignRef", "sub_foo", []);
+ tallied = null;
+});
+
+let deprecatedJson = [
+ {namespace: "deprecated",
+
+ properties: {
+ accessor: {
+ type: "string",
+ writable: true,
+ deprecated: "This is not the property you are looking for",
+ },
+ },
+
+ types: [
+ {
+ "id": "Type",
+ "type": "string",
+ },
+ ],
+
+ functions: [
+ {
+ name: "property",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ foo: {
+ type: "string",
+ },
+ },
+ additionalProperties: {
+ type: "any",
+ deprecated: "Unknown property",
+ },
+ },
+ ],
+ },
+
+ {
+ name: "value",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ type: "integer",
+ },
+ {
+ type: "string",
+ deprecated: "Please use an integer, not ${value}",
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "choices",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ deprecated: "You have no choices",
+ choices: [
+ {
+ type: "integer",
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "ref",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ $ref: "Type",
+ deprecated: "Deprecated alias",
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "method",
+ type: "function",
+ deprecated: "Do not call this method",
+ parameters: [
+ ],
+ },
+ ],
+
+ events: [
+ {
+ name: "onDeprecated",
+ type: "function",
+ deprecated: "This event does not work",
+ },
+ ],
+ },
+];
+
+add_task(function* testDeprecation() {
+ let url = "data:," + JSON.stringify(deprecatedJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+
+ root.deprecated.property({foo: "bar", xxx: "any", yyy: "property"});
+ verify("call", "deprecated", "property", [{foo: "bar", xxx: "any", yyy: "property"}]);
+ checkErrors([
+ "Error processing xxx: Unknown property",
+ "Error processing yyy: Unknown property",
+ ]);
+
+ root.deprecated.value(12);
+ verify("call", "deprecated", "value", [12]);
+ checkErrors([]);
+
+ root.deprecated.value("12");
+ verify("call", "deprecated", "value", ["12"]);
+ checkErrors(["Please use an integer, not \"12\""]);
+
+ root.deprecated.choices(12);
+ verify("call", "deprecated", "choices", [12]);
+ checkErrors(["You have no choices"]);
+
+ root.deprecated.ref("12");
+ verify("call", "deprecated", "ref", ["12"]);
+ checkErrors(["Deprecated alias"]);
+
+ root.deprecated.method();
+ verify("call", "deprecated", "method", []);
+ checkErrors(["Do not call this method"]);
+
+
+ void root.deprecated.accessor;
+ verify("get", "deprecated", "accessor", null);
+ checkErrors(["This is not the property you are looking for"]);
+
+ root.deprecated.accessor = "x";
+ verify("set", "deprecated", "accessor", "x");
+ checkErrors(["This is not the property you are looking for"]);
+
+
+ root.deprecated.onDeprecated.addListener(() => {});
+ checkErrors(["This event does not work"]);
+
+ root.deprecated.onDeprecated.removeListener(() => {});
+ checkErrors(["This event does not work"]);
+
+ root.deprecated.onDeprecated.hasListener(() => {});
+ checkErrors(["This event does not work"]);
+});
+
+
+let choicesJson = [
+ {namespace: "choices",
+
+ types: [
+ ],
+
+ functions: [
+ {
+ name: "meh",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ type: "string",
+ enum: ["foo", "bar", "baz"],
+ },
+ {
+ type: "string",
+ pattern: "florg.*meh",
+ },
+ {
+ type: "integer",
+ minimum: 12,
+ maximum: 42,
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "foo",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ type: "object",
+ properties: {
+ blurg: {
+ type: "string",
+ unsupported: true,
+ optional: true,
+ },
+ },
+ additionalProperties: {
+ type: "string",
+ },
+ },
+ {
+ type: "string",
+ },
+ {
+ type: "array",
+ minItems: 2,
+ maxItems: 3,
+ items: {
+ type: "integer",
+ },
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "bar",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ type: "object",
+ properties: {
+ baz: {
+ type: "string",
+ },
+ },
+ },
+ {
+ type: "array",
+ items: {
+ type: "integer",
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ]},
+];
+
+add_task(function* testChoices() {
+ let url = "data:," + JSON.stringify(choicesJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+ Assert.throws(() => root.choices.meh("frog"),
+ /Value must either: be one of \["foo", "bar", "baz"\], match the pattern \/florg\.\*meh\/, or be an integer value/);
+
+ Assert.throws(() => root.choices.meh(4),
+ /be a string value, or be at least 12/);
+
+ Assert.throws(() => root.choices.meh(43),
+ /be a string value, or be no greater than 42/);
+
+
+ Assert.throws(() => root.choices.foo([]),
+ /be an object value, be a string value, or have at least 2 items/);
+
+ Assert.throws(() => root.choices.foo([1, 2, 3, 4]),
+ /be an object value, be a string value, or have at most 3 items/);
+
+ Assert.throws(() => root.choices.foo({foo: 12}),
+ /.foo must be a string value, be a string value, or be an array value/);
+
+ Assert.throws(() => root.choices.foo({blurg: "foo"}),
+ /not contain an unsupported "blurg" property, be a string value, or be an array value/);
+
+
+ Assert.throws(() => root.choices.bar({}),
+ /contain the required "baz" property, or be an array value/);
+
+ Assert.throws(() => root.choices.bar({baz: "x", quux: "y"}),
+ /not contain an unexpected "quux" property, or be an array value/);
+
+ Assert.throws(() => root.choices.bar({baz: "x", quux: "y", foo: "z"}),
+ /not contain the unexpected properties \[foo, quux\], or be an array value/);
+});
+
+
+let permissionsJson = [
+ {namespace: "noPerms",
+
+ types: [],
+
+ functions: [
+ {
+ name: "noPerms",
+ type: "function",
+ parameters: [],
+ },
+
+ {
+ name: "fooPerm",
+ type: "function",
+ permissions: ["foo"],
+ parameters: [],
+ },
+ ]},
+
+ {namespace: "fooPerm",
+
+ permissions: ["foo"],
+
+ types: [],
+
+ functions: [
+ {
+ name: "noPerms",
+ type: "function",
+ parameters: [],
+ },
+
+ {
+ name: "fooBarPerm",
+ type: "function",
+ permissions: ["foo.bar"],
+ parameters: [],
+ },
+ ]},
+];
+
+add_task(function* testPermissions() {
+ let url = "data:," + JSON.stringify(permissionsJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+
+ ok(!("fooPerm" in root.noPerms), "noPerms.fooPerm should not method exist");
+
+ ok(!("fooPerm" in root), "fooPerm namespace should not exist");
+
+
+ do_print('Add "foo" permission');
+ permissions.add("foo");
+
+ root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist");
+
+ equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
+ equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist");
+
+ ok(!("fooBarPerm" in root.fooPerm), "fooPerm.fooBarPerm method should not exist");
+
+
+ do_print('Add "foo.bar" permission');
+ permissions.add("foo.bar");
+
+ root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist");
+
+ equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
+ equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.fooPerm.fooBarPerm, "function", "noPerms.fooBarPerm method should exist");
+});
+
+let nestedNamespaceJson = [
+ {
+ "namespace": "nested.namespace",
+ "types": [
+ {
+ "id": "CustomType",
+ "type": "object",
+ "events": [
+ {
+ "name": "onEvent",
+ },
+ ],
+ "properties": {
+ "url": {
+ "type": "string",
+ },
+ },
+ "functions": [
+ {
+ "name": "functionOnCustomType",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ "properties": {
+ "instanceOfCustomType": {
+ "$ref": "CustomType",
+ },
+ },
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ },
+ ],
+ },
+ ],
+ },
+];
+
+add_task(function* testNestedNamespace() {
+ let url = "data:," + JSON.stringify(nestedNamespaceJson);
+
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+ ok(root.nested, "The root object contains the first namespace level");
+ ok(root.nested.namespace, "The first level object contains the second namespace level");
+
+ ok(root.nested.namespace.create, "Got the expected function in the nested namespace");
+ do_check_eq(typeof root.nested.namespace.create, "function",
+ "The property is a function as expected");
+
+ let {instanceOfCustomType} = root.nested.namespace;
+
+ ok(instanceOfCustomType,
+ "Got the expected instance of the CustomType defined in the schema");
+ ok(instanceOfCustomType.functionOnCustomType,
+ "Got the expected method in the CustomType instance");
+
+ // TODO: test support events and properties in a SubModuleType defined in the schema,
+ // once implemented, e.g.:
+ //
+ // ok(instanceOfCustomType.url,
+ // "Got the expected property defined in the CustomType instance)
+ //
+ // ok(instanceOfCustomType.onEvent &&
+ // instanceOfCustomType.onEvent.addListener &&
+ // typeof instanceOfCustomType.onEvent.addListener == "function",
+ // "Got the expected event defined in the CustomType instance");
+});
+
+add_task(function* testLocalAPIImplementation() {
+ let countGet2 = 0;
+ let countProp3 = 0;
+ let countProp3SubFoo = 0;
+
+ let testingApiObj = {
+ get PROP1() {
+ // PROP1 is a schema-defined constant.
+ throw new Error("Unexpected get PROP1");
+ },
+ get prop2() {
+ ++countGet2;
+ return "prop2 val";
+ },
+ get prop3() {
+ throw new Error("Unexpected get prop3");
+ },
+ set prop3(v) {
+ // prop3 is a submodule, defined as a function, so the API should not pass
+ // through assignment to prop3.
+ throw new Error("Unexpected set prop3");
+ },
+ };
+ let submoduleApiObj = {
+ get sub_foo() {
+ ++countProp3;
+ return () => {
+ return ++countProp3SubFoo;
+ };
+ },
+ };
+
+ let localWrapper = {
+ shouldInject(ns) {
+ return ns == "testing" || ns == "testing.prop3";
+ },
+ getImplementation(ns, name) {
+ do_check_true(ns == "testing" || ns == "testing.prop3");
+ if (ns == "testing.prop3" && name == "sub_foo") {
+ // It is fine to use `null` here because we don't call async functions.
+ return new LocalAPIImplementation(submoduleApiObj, name, null);
+ }
+ // It is fine to use `null` here because we don't call async functions.
+ return new LocalAPIImplementation(testingApiObj, name, null);
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+ do_check_eq(countGet2, 0);
+ do_check_eq(countProp3, 0);
+ do_check_eq(countProp3SubFoo, 0);
+
+ do_check_eq(root.testing.PROP1, 20);
+
+ do_check_eq(root.testing.prop2, "prop2 val");
+ do_check_eq(countGet2, 1);
+
+ do_check_eq(root.testing.prop2, "prop2 val");
+ do_check_eq(countGet2, 2);
+
+ do_print(JSON.stringify(root.testing));
+ do_check_eq(root.testing.prop3.sub_foo(), 1);
+ do_check_eq(countProp3, 1);
+ do_check_eq(countProp3SubFoo, 1);
+
+ do_check_eq(root.testing.prop3.sub_foo(), 2);
+ do_check_eq(countProp3, 2);
+ do_check_eq(countProp3SubFoo, 2);
+
+ root.testing.prop3.sub_foo = () => { return "overwritten"; };
+ do_check_eq(root.testing.prop3.sub_foo(), "overwritten");
+
+ root.testing.prop3 = {sub_foo() { return "overwritten again"; }};
+ do_check_eq(root.testing.prop3.sub_foo(), "overwritten again");
+ do_check_eq(countProp3SubFoo, 2);
+});
+
+
+let defaultsJson = [
+ {namespace: "defaultsJson",
+
+ types: [],
+
+ functions: [
+ {
+ name: "defaultFoo",
+ type: "function",
+ parameters: [
+ {name: "arg", type: "object", optional: true, properties: {
+ prop1: {type: "integer", optional: true},
+ }, default: {prop1: 1}},
+ ],
+ returns: {
+ type: "object",
+ },
+ },
+ ]},
+];
+
+add_task(function* testDefaults() {
+ let url = "data:," + JSON.stringify(defaultsJson);
+ yield Schemas.load(url);
+
+ let testingApiObj = {
+ defaultFoo: function(arg) {
+ if (Object.keys(arg) != "prop1") {
+ throw new Error(`Received the expected default object, default: ${JSON.stringify(arg)}`);
+ }
+ arg.newProp = 1;
+ return arg;
+ },
+ };
+
+ let localWrapper = {
+ shouldInject(ns) {
+ return true;
+ },
+ getImplementation(ns, name) {
+ return new LocalAPIImplementation(testingApiObj, name, null);
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+
+ deepEqual(root.defaultsJson.defaultFoo(), {prop1: 1, newProp: 1});
+ deepEqual(root.defaultsJson.defaultFoo({prop1: 2}), {prop1: 2, newProp: 1});
+ deepEqual(root.defaultsJson.defaultFoo(), {prop1: 1, newProp: 1});
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js
new file mode 100644
index 000000000..606459764
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js
@@ -0,0 +1,147 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+let schemaJson = [
+ {
+ namespace: "noAllowedContexts",
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", allowedContexts: ["test_zero", "test_one"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, allowedContexts: ["numeric_one"]},
+ },
+ },
+ {
+ namespace: "defaultContexts",
+ defaultContexts: ["test_two"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", allowedContexts: ["test_three"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, allowedContexts: ["numeric_two"]},
+ },
+ },
+ {
+ namespace: "withAllowedContexts",
+ allowedContexts: ["test_four"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", allowedContexts: ["test_five"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, allowedContexts: ["numeric_three"]},
+ },
+ },
+ {
+ namespace: "withAllowedContextsAndDefault",
+ allowedContexts: ["test_six"],
+ defaultContexts: ["test_seven"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", allowedContexts: ["test_eight"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, allowedContexts: ["numeric_four"]},
+ },
+ },
+ {
+ namespace: "with_submodule",
+ defaultContexts: ["test_nine"],
+ types: [{
+ id: "subtype",
+ type: "object",
+ functions: [{
+ name: "noAllowedContexts",
+ type: "function",
+ parameters: [],
+ }, {
+ name: "allowedContexts",
+ allowedContexts: ["test_ten"],
+ type: "function",
+ parameters: [],
+ }],
+ }],
+ properties: {
+ prop1: {$ref: "subtype"},
+ prop2: {$ref: "subtype", allowedContexts: ["test_eleven"]},
+ },
+ },
+];
+add_task(function* testRestrictions() {
+ let url = "data:," + JSON.stringify(schemaJson);
+ yield Schemas.load(url);
+ let results = {};
+ let localWrapper = {
+ shouldInject(ns, name, allowedContexts) {
+ name = name === null ? ns : ns + "." + name;
+ results[name] = allowedContexts.join(",");
+ return true;
+ },
+ getImplementation() {
+ // The actual implementation is not significant for this test.
+ // Let's take this opportunity to see if schema generation is free of
+ // exceptions even when somehow getImplementation does not return an
+ // implementation.
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+
+ function verify(path, expected) {
+ let obj = root;
+ for (let thing of path.split(".")) {
+ try {
+ obj = obj[thing];
+ } catch (e) {
+ // Blech.
+ }
+ }
+
+ let result = results[path];
+ equal(result, expected);
+ }
+
+ verify("noAllowedContexts", "");
+ verify("noAllowedContexts.prop1", "");
+ verify("noAllowedContexts.prop2", "test_zero,test_one");
+ verify("noAllowedContexts.prop3", "");
+ verify("noAllowedContexts.prop4", "numeric_one");
+
+ verify("defaultContexts", "");
+ verify("defaultContexts.prop1", "test_two");
+ verify("defaultContexts.prop2", "test_three");
+ verify("defaultContexts.prop3", "test_two");
+ verify("defaultContexts.prop4", "numeric_two");
+
+ verify("withAllowedContexts", "test_four");
+ verify("withAllowedContexts.prop1", "");
+ verify("withAllowedContexts.prop2", "test_five");
+ verify("withAllowedContexts.prop3", "");
+ verify("withAllowedContexts.prop4", "numeric_three");
+
+ verify("withAllowedContextsAndDefault", "test_six");
+ verify("withAllowedContextsAndDefault.prop1", "test_seven");
+ verify("withAllowedContextsAndDefault.prop2", "test_eight");
+ verify("withAllowedContextsAndDefault.prop3", "test_seven");
+ verify("withAllowedContextsAndDefault.prop4", "numeric_four");
+
+ verify("with_submodule", "");
+ verify("with_submodule.prop1", "test_nine");
+ verify("with_submodule.prop1.noAllowedContexts", "test_nine");
+ verify("with_submodule.prop1.allowedContexts", "test_ten");
+ verify("with_submodule.prop2", "test_eleven");
+ // Note: test_nine inherits allowed contexts from the namespace, not from
+ // submodule. There is no "defaultContexts" for submodule types to not
+ // complicate things.
+ verify("with_submodule.prop1.noAllowedContexts", "test_nine");
+ verify("with_submodule.prop1.allowedContexts", "test_ten");
+
+ // This is a constant, so it does not matter that getImplementation does not
+ // return an implementation since the API injector should take care of it.
+ equal(root.noAllowedContexts.prop3, 1);
+
+ Assert.throws(() => root.noAllowedContexts.prop1,
+ /undefined/,
+ "Should throw when the implementation is absent.");
+});
+
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
new file mode 100644
index 000000000..36d88d722
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
@@ -0,0 +1,102 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+let {
+ BaseContext,
+ SchemaAPIManager,
+} = ExtensionCommon;
+
+let nestedNamespaceJson = [
+ {
+ "namespace": "backgroundAPI.testnamespace",
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ },
+ ],
+ "returns": {
+ "type": "string",
+ },
+ },
+ ],
+ },
+ {
+ "namespace": "noBackgroundAPI.testnamespace",
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ },
+ ],
+ },
+ ],
+ },
+];
+
+let global = this;
+class StubContext extends BaseContext {
+ constructor() {
+ let fakeExtension = {id: "test@web.extension"};
+ super("addon_child", fakeExtension);
+ this.sandbox = Cu.Sandbox(global);
+ this.viewType = "background";
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+}
+
+add_task(function* testSchemaAPIInjection() {
+ let url = "data:," + JSON.stringify(nestedNamespaceJson);
+
+ // Load the schema of the fake APIs.
+ yield Schemas.load(url);
+
+ let apiManager = new SchemaAPIManager("addon");
+
+ // Register an API that will skip the background page.
+ apiManager.registerSchemaAPI("noBackgroundAPI.testnamespace", "addon_child", context => {
+ // This API should not be available in this context, return null so that
+ // the schema wrapper is removed as well.
+ return null;
+ });
+
+ // Register an API that will skip any but the background page.
+ apiManager.registerSchemaAPI("backgroundAPI.testnamespace", "addon_child", context => {
+ if (context.viewType === "background") {
+ return {
+ backgroundAPI: {
+ testnamespace: {
+ create(title) {
+ return title;
+ },
+ },
+ },
+ };
+ }
+
+ // This API should not be available in this context, return null so that
+ // the schema wrapper is removed as well.
+ return null;
+ });
+
+ let context = new StubContext();
+ let browserObj = {};
+ apiManager.generateAPIs(context, browserObj);
+
+ do_check_eq(browserObj.noBackgroundAPI, undefined);
+ const res = browserObj.backgroundAPI.testnamespace.create("param-value");
+ do_check_eq(res, "param-value");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
new file mode 100644
index 000000000..6397d1f96
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
@@ -0,0 +1,232 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+let {BaseContext, LocalAPIImplementation} = ExtensionCommon;
+
+let schemaJson = [
+ {
+ namespace: "testnamespace",
+ functions: [{
+ name: "one_required",
+ type: "function",
+ parameters: [{
+ name: "first",
+ type: "function",
+ parameters: [],
+ }],
+ }, {
+ name: "one_optional",
+ type: "function",
+ parameters: [{
+ name: "first",
+ type: "function",
+ parameters: [],
+ optional: true,
+ }],
+ }, {
+ name: "async_required",
+ type: "function",
+ async: "first",
+ parameters: [{
+ name: "first",
+ type: "function",
+ parameters: [],
+ }],
+ }, {
+ name: "async_optional",
+ type: "function",
+ async: "first",
+ parameters: [{
+ name: "first",
+ type: "function",
+ parameters: [],
+ optional: true,
+ }],
+ }],
+ },
+];
+
+const global = this;
+class StubContext extends BaseContext {
+ constructor() {
+ let fakeExtension = {id: "test@web.extension"};
+ super("testEnv", fakeExtension);
+ this.sandbox = Cu.Sandbox(global);
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+
+ get principal() {
+ return Cu.getObjectPrincipal(this.sandbox);
+ }
+}
+
+let context;
+
+function generateAPIs(extraWrapper, apiObj) {
+ context = new StubContext();
+ let localWrapper = {
+ shouldInject() {
+ return true;
+ },
+ getImplementation(namespace, name) {
+ return new LocalAPIImplementation(apiObj, name, context);
+ },
+ };
+ Object.assign(localWrapper, extraWrapper);
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+ return root.testnamespace;
+}
+
+add_task(function* testParameterValidation() {
+ yield Schemas.load("data:," + JSON.stringify(schemaJson));
+
+ let testnamespace;
+ function assertThrows(name, ...args) {
+ Assert.throws(() => testnamespace[name](...args),
+ /Incorrect argument types/,
+ `Expected testnamespace.${name}(${args.map(String).join(", ")}) to throw.`);
+ }
+ function assertNoThrows(name, ...args) {
+ try {
+ testnamespace[name](...args);
+ } catch (e) {
+ do_print(`testnamespace.${name}(${args.map(String).join(", ")}) unexpectedly threw.`);
+ throw new Error(e);
+ }
+ }
+ let cb = () => {};
+
+ for (let isChromeCompat of [true, false]) {
+ do_print(`Testing API validation with isChromeCompat=${isChromeCompat}`);
+ testnamespace = generateAPIs({
+ isChromeCompat,
+ }, {
+ one_required() {},
+ one_optional() {},
+ async_required() {},
+ async_optional() {},
+ });
+
+ assertThrows("one_required");
+ assertThrows("one_required", null);
+ assertNoThrows("one_required", cb);
+ assertThrows("one_required", cb, null);
+ assertThrows("one_required", cb, cb);
+
+ assertNoThrows("one_optional");
+ assertNoThrows("one_optional", null);
+ assertNoThrows("one_optional", cb);
+ assertThrows("one_optional", cb, null);
+ assertThrows("one_optional", cb, cb);
+
+ // Schema-based validation happens before an async method is called, so
+ // errors should be thrown synchronously.
+
+ // The parameter was declared as required, but there was also an "async"
+ // attribute with the same value as the parameter name, so the callback
+ // parameter is actually optional.
+ assertNoThrows("async_required");
+ assertNoThrows("async_required", null);
+ assertNoThrows("async_required", cb);
+ assertThrows("async_required", cb, null);
+ assertThrows("async_required", cb, cb);
+
+ assertNoThrows("async_optional");
+ assertNoThrows("async_optional", null);
+ assertNoThrows("async_optional", cb);
+ assertThrows("async_optional", cb, null);
+ assertThrows("async_optional", cb, cb);
+ }
+});
+
+add_task(function* testAsyncResults() {
+ yield Schemas.load("data:," + JSON.stringify(schemaJson));
+ function* runWithCallback(func) {
+ do_print(`Calling testnamespace.${func.name}, expecting callback with result`);
+ return yield new Promise(resolve => {
+ let result = "uninitialized value";
+ let returnValue = func(reply => {
+ result = reply;
+ resolve(result);
+ });
+ // When a callback is given, the return value must be missing.
+ do_check_eq(returnValue, undefined);
+ // Callback must be called asynchronously.
+ do_check_eq(result, "uninitialized value");
+ });
+ }
+
+ function* runFailCallback(func) {
+ do_print(`Calling testnamespace.${func.name}, expecting callback with error`);
+ return yield new Promise(resolve => {
+ func(reply => {
+ do_check_eq(reply, undefined);
+ resolve(context.lastError.message); // eslint-disable-line no-undef
+ });
+ });
+ }
+
+ for (let isChromeCompat of [true, false]) {
+ do_print(`Testing API invocation with isChromeCompat=${isChromeCompat}`);
+ let testnamespace = generateAPIs({
+ isChromeCompat,
+ }, {
+ async_required(cb) {
+ do_check_eq(cb, undefined);
+ return Promise.resolve(1);
+ },
+ async_optional(cb) {
+ do_check_eq(cb, undefined);
+ return Promise.resolve(2);
+ },
+ });
+ if (!isChromeCompat) { // No promises for chrome.
+ do_print("testnamespace.async_required should be a Promise");
+ let promise = testnamespace.async_required();
+ do_check_true(promise instanceof context.cloneScope.Promise);
+ do_check_eq(yield promise, 1);
+
+ do_print("testnamespace.async_optional should be a Promise");
+ promise = testnamespace.async_optional();
+ do_check_true(promise instanceof context.cloneScope.Promise);
+ do_check_eq(yield promise, 2);
+ }
+
+ do_check_eq(yield* runWithCallback(testnamespace.async_required), 1);
+ do_check_eq(yield* runWithCallback(testnamespace.async_optional), 2);
+
+ let otherSandbox = Cu.Sandbox(null, {});
+ let errorFactories = [
+ msg => { throw new context.cloneScope.Error(msg); },
+ msg => context.cloneScope.Promise.reject({message: msg}),
+ msg => Cu.evalInSandbox(`throw new Error("${msg}")`, otherSandbox),
+ msg => Cu.evalInSandbox(`Promise.reject({message: "${msg}"})`, otherSandbox),
+ ];
+ for (let makeError of errorFactories) {
+ do_print(`Testing callback/promise with error caused by: ${makeError}`);
+ testnamespace = generateAPIs({
+ isChromeCompat,
+ }, {
+ async_required() { return makeError("ONE"); },
+ async_optional() { return makeError("TWO"); },
+ });
+
+ if (!isChromeCompat) { // No promises for chrome.
+ yield Assert.rejects(testnamespace.async_required(), /ONE/,
+ "should reject testnamespace.async_required()").catch(() => {});
+ yield Assert.rejects(testnamespace.async_optional(), /TWO/,
+ "should reject testnamespace.async_optional()").catch(() => {});
+ }
+
+ do_check_eq(yield* runFailCallback(testnamespace.async_required), "ONE");
+ do_check_eq(yield* runFailCallback(testnamespace.async_optional), "TWO");
+ }
+ }
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_simple.js b/toolkit/components/extensions/test/xpcshell/test_ext_simple.js
new file mode 100644
index 000000000..91b10354c
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_simple.js
@@ -0,0 +1,69 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_simple() {
+ let extensionData = {
+ manifest: {
+ "name": "Simple extension test",
+ "version": "1.0",
+ "manifest_version": 2,
+ "description": "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.unload();
+});
+
+add_task(function* test_background() {
+ 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,
+ manifest: {
+ "name": "Simple extension test",
+ "version": "1.0",
+ "manifest_version": 2,
+ "description": "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ equal(x, 1, "got correct value from extension");
+
+ extension.sendMessage(10, 20);
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(function* test_extensionTypes() {
+ let extensionData = {
+ background: function() {
+ browser.test.assertEq(typeof browser.extensionTypes, "object", "browser.extensionTypes exists");
+ browser.test.assertEq(typeof browser.extensionTypes.RunAt, "object", "browser.extensionTypes.RunAt exists");
+ browser.test.notifyPass("extentionTypes test passed");
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage.js
new file mode 100644
index 000000000..df46dfb63
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage.js
@@ -0,0 +1,334 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled";
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+/**
+ * Utility function to ensure that all supported APIs for getting are
+ * tested.
+ *
+ * @param {string} areaName
+ * either "local" or "sync" according to what we want to test
+ * @param {string} prop
+ * "key" to look up using the storage API
+ * @param {Object} value
+ * "value" to compare against
+ */
+async function checkGetImpl(areaName, prop, value) {
+ let storage = browser.storage[areaName];
+
+ let data = await storage.get(null);
+ browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get(prop);
+ browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get([prop]);
+ browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get({[prop]: undefined});
+ browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`);
+}
+
+add_task(function* test_local_cache_invalidation() {
+ function background(checkGet) {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "set-initial") {
+ await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"});
+ browser.test.sendMessage("set-initial-done");
+ } else if (msg === "check") {
+ await checkGet("local", "test-prop1", "value1");
+ await checkGet("local", "test-prop2", "value2");
+ browser.test.sendMessage("check-done");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})(${checkGetImpl})`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("set-initial");
+ yield extension.awaitMessage("set-initial-done");
+
+ Services.obs.notifyObservers(null, "extension-invalidate-storage-cache", "");
+
+ extension.sendMessage("check");
+ yield extension.awaitMessage("check-done");
+
+ yield extension.unload();
+});
+
+add_task(function* test_config_flag_needed() {
+ function background() {
+ let promises = [];
+ let apiTests = [
+ {method: "get", args: ["foo"]},
+ {method: "set", args: [{foo: "bar"}]},
+ {method: "remove", args: ["foo"]},
+ {method: "clear", args: []},
+ ];
+ apiTests.forEach(testDef => {
+ promises.push(browser.test.assertRejects(
+ browser.storage.sync[testDef.method](...testDef.args),
+ "Please set webextensions.storage.sync.enabled to true in about:config",
+ `storage.sync.${testDef.method} is behind a flag`));
+ });
+
+ Promise.all(promises).then(() => browser.test.notifyPass("flag needed"));
+ }
+
+ ok(!Preferences.get(STORAGE_SYNC_PREF));
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})(${checkGetImpl})`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("flag needed");
+ yield extension.unload();
+});
+
+add_task(function* test_reloading_extensions_works() {
+ // Just some random extension ID that we can re-use
+ const extensionId = "my-extension-id@1";
+
+ function loadExtension() {
+ function background() {
+ browser.storage.sync.set({"a": "b"}).then(() => {
+ browser.test.notifyPass("set-works");
+ });
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})()`,
+ }, extensionId);
+ }
+
+ Preferences.set(STORAGE_SYNC_PREF, true);
+
+ let extension1 = loadExtension();
+
+ yield extension1.startup();
+ yield extension1.awaitFinish("set-works");
+ yield extension1.unload();
+
+ let extension2 = loadExtension();
+
+ yield extension2.startup();
+ yield extension2.awaitFinish("set-works");
+ yield extension2.unload();
+
+ Preferences.reset(STORAGE_SYNC_PREF);
+});
+
+do_register_cleanup(() => {
+ Preferences.reset(STORAGE_SYNC_PREF);
+});
+
+add_task(function* test_backgroundScript() {
+ async function backgroundScript(checkGet) {
+ let globalChanges, gResolve;
+ function clearGlobalChanges() {
+ globalChanges = new Promise(resolve => { gResolve = resolve; });
+ }
+ clearGlobalChanges();
+ let expectedAreaName;
+
+ browser.storage.onChanged.addListener((changes, areaName) => {
+ browser.test.assertEq(expectedAreaName, areaName,
+ "Expected area name received by listener");
+ gResolve(changes);
+ });
+
+ async function checkChanges(areaName, changes, message) {
+ function checkSub(obj1, obj2) {
+ for (let prop in obj1) {
+ browser.test.assertTrue(obj1[prop] !== undefined,
+ `checkChanges ${areaName} ${prop} is missing (${message})`);
+ browser.test.assertTrue(obj2[prop] !== undefined,
+ `checkChanges ${areaName} ${prop} is missing (${message})`);
+ browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue,
+ `checkChanges ${areaName} ${prop} old (${message})`);
+ browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue,
+ `checkChanges ${areaName} ${prop} new (${message})`);
+ }
+ }
+
+ const recentChanges = await globalChanges;
+ checkSub(changes, recentChanges);
+ checkSub(recentChanges, changes);
+ clearGlobalChanges();
+ }
+
+ /* eslint-disable dot-notation */
+ async function runTests(areaName) {
+ expectedAreaName = areaName;
+ let storage = browser.storage[areaName];
+ // Set some data and then test getters.
+ try {
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+ await checkChanges(areaName,
+ {"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}},
+ "set (a)");
+
+ await checkGet(areaName, "test-prop1", "value1");
+ await checkGet(areaName, "test-prop2", "value2");
+
+ let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"});
+ browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)");
+ browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)");
+ browser.test.assertEq("default", data["other"], "other correct");
+
+ data = await storage.get(["test-prop1", "test-prop2", "other"]);
+ browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)");
+ browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)");
+ browser.test.assertFalse("other" in data, "other correct");
+
+ // Remove data in various ways.
+ await storage.remove("test-prop1");
+ await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)");
+ browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)");
+
+ await storage.set({"test-prop1": "value1"});
+ await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)");
+ browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)");
+
+ await storage.remove(["test-prop1", "test-prop2"]);
+ await checkChanges(areaName,
+ {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
+ "remove array");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)");
+ browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)");
+
+ // test storage.clear
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+ // Make sure that set() handler happened before we clear the
+ // promise again.
+ await globalChanges;
+
+ clearGlobalChanges();
+ await storage.clear();
+
+ await checkChanges(areaName,
+ {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
+ "clear");
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)");
+ browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)");
+
+ // Make sure we can store complex JSON data.
+ // known previous values
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+
+ // Make sure the set() handler landed.
+ await globalChanges;
+
+ clearGlobalChanges();
+ await storage.set({
+ "test-prop1": {
+ str: "hello",
+ bool: true,
+ null: null,
+ undef: undefined,
+ obj: {},
+ arr: [1, 2],
+ date: new Date(0),
+ regexp: /regexp/,
+ func: function func() {},
+ window,
+ },
+ });
+
+ await storage.set({"test-prop2": function func() {}});
+ const recentChanges = await globalChanges;
+
+ browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct");
+ browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj");
+ clearGlobalChanges();
+
+ data = await storage.get({"test-prop1": undefined, "test-prop2": undefined});
+ let obj = data["test-prop1"];
+
+ browser.test.assertEq("hello", obj.str, "string part correct");
+ browser.test.assertEq(true, obj.bool, "bool part correct");
+ browser.test.assertEq(null, obj.null, "null part correct");
+ browser.test.assertEq(undefined, obj.undef, "undefined part correct");
+ browser.test.assertEq(undefined, obj.func, "function part correct");
+ browser.test.assertEq(undefined, obj.window, "window part correct");
+ browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct");
+ browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct");
+ browser.test.assertEq("object", typeof(obj.obj), "object part correct");
+ browser.test.assertTrue(Array.isArray(obj.arr), "array part present");
+ browser.test.assertEq(1, obj.arr[0], "arr[0] part correct");
+ browser.test.assertEq(2, obj.arr[1], "arr[1] part correct");
+ browser.test.assertEq(2, obj.arr.length, "arr.length part correct");
+
+ obj = data["test-prop2"];
+
+ browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object");
+ browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("storage");
+ }
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ let promise;
+ if (msg === "test-local") {
+ promise = runTests("local");
+ } else if (msg === "test-sync") {
+ promise = runTests("sync");
+ }
+ promise.then(() => browser.test.sendMessage("test-finished"));
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ background: `(${backgroundScript})(${checkGetImpl})`,
+ manifest: {
+ permissions: ["storage"],
+ },
+ };
+
+ Preferences.set(STORAGE_SYNC_PREF, true);
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("test-local");
+ yield extension.awaitMessage("test-finished");
+
+ extension.sendMessage("test-sync");
+ yield extension.awaitMessage("test-finished");
+
+ Preferences.reset(STORAGE_SYNC_PREF);
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
new file mode 100644
index 000000000..4258289e3
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
@@ -0,0 +1,1073 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+do_get_profile(); // so we can use FxAccounts
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://gre/modules/ExtensionStorageSync.jsm");
+const {
+ CollectionKeyEncryptionRemoteTransformer,
+ cryptoCollection,
+ idToKey,
+ extensionIdToCollectionId,
+ keyToId,
+} = Cu.import("resource://gre/modules/ExtensionStorageSync.jsm");
+Cu.import("resource://services-sync/engines/extension-storage.js");
+Cu.import("resource://services-sync/keys.js");
+Cu.import("resource://services-sync/util.js");
+
+/* globals BulkKeyBundle, CommonUtils, EncryptionRemoteTransformer */
+/* globals KeyRingEncryptionRemoteTransformer */
+/* globals Utils */
+
+function handleCannedResponse(cannedResponse, request, response) {
+ response.setStatusLine(null, cannedResponse.status.status,
+ cannedResponse.status.statusText);
+ // send the headers
+ for (let headerLine of cannedResponse.sampleHeaders) {
+ let headerElements = headerLine.split(":");
+ response.setHeader(headerElements[0], headerElements[1].trimLeft());
+ }
+ response.setHeader("Date", (new Date()).toUTCString());
+
+ response.write(cannedResponse.responseBody);
+}
+
+function collectionRecordsPath(collectionId) {
+ return `/buckets/default/collections/${collectionId}/records`;
+}
+
+class KintoServer {
+ constructor() {
+ // Set up an HTTP Server
+ this.httpServer = new HttpServer();
+ this.httpServer.start(-1);
+
+ // Map<CollectionId, Set<Object>> corresponding to the data in the
+ // Kinto server
+ this.collections = new Map();
+
+ // ETag to serve with responses
+ this.etag = 1;
+
+ this.port = this.httpServer.identity.primaryPort;
+ // POST requests we receive from the client go here
+ this.posts = [];
+ // DELETEd buckets will go here.
+ this.deletedBuckets = [];
+ // Anything in here will force the next POST to generate a conflict
+ this.conflicts = [];
+
+ this.installConfigPath();
+ this.installBatchPath();
+ this.installCatchAll();
+ }
+
+ clearPosts() {
+ this.posts = [];
+ }
+
+ getPosts() {
+ return this.posts;
+ }
+
+ getDeletedBuckets() {
+ return this.deletedBuckets;
+ }
+
+ installConfigPath() {
+ const configPath = "/v1/";
+ const responseBody = JSON.stringify({
+ "settings": {"batch_max_requests": 25},
+ "url": `http://localhost:${this.port}/v1/`,
+ "documentation": "https://kinto.readthedocs.org/",
+ "version": "1.5.1",
+ "commit": "cbc6f58",
+ "hello": "kinto",
+ });
+ const configResponse = {
+ "sampleHeaders": [
+ "Access-Control-Allow-Origin: *",
+ "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+ "Content-Type: application/json; charset=UTF-8",
+ "Server: waitress",
+ ],
+ "status": {status: 200, statusText: "OK"},
+ "responseBody": responseBody,
+ };
+
+ function handleGetConfig(request, response) {
+ if (request.method != "GET") {
+ dump(`ARGH, got ${request.method}\n`);
+ }
+ return handleCannedResponse(configResponse, request, response);
+ }
+
+ this.httpServer.registerPathHandler(configPath, handleGetConfig);
+ }
+
+ installBatchPath() {
+ const batchPath = "/v1/batch";
+
+ function handlePost(request, response) {
+ let bodyStr = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ let body = JSON.parse(bodyStr);
+ let defaults = body.defaults;
+ for (let req of body.requests) {
+ let headers = Object.assign({}, defaults && defaults.headers || {}, req.headers);
+ // FIXME: assert auth is "Bearer ...token..."
+ this.posts.push(Object.assign({}, req, {headers}));
+ }
+
+ response.setStatusLine(null, 200, "OK");
+ response.setHeader("Content-Type", "application/json; charset=UTF-8");
+ response.setHeader("Date", (new Date()).toUTCString());
+
+ let postResponse = {
+ responses: body.requests.map(req => {
+ let oneBody;
+ if (req.method == "DELETE") {
+ let id = req.path.match(/^\/buckets\/default\/collections\/.+\/records\/(.+)$/)[1];
+ oneBody = {
+ "data": {
+ "deleted": true,
+ "id": id,
+ "last_modified": this.etag,
+ },
+ };
+ } else {
+ oneBody = {"data": Object.assign({}, req.body.data, {last_modified: this.etag}),
+ "permissions": []};
+ }
+
+ return {
+ path: req.path,
+ status: 201, // FIXME -- only for new posts??
+ headers: {"ETag": 3000}, // FIXME???
+ body: oneBody,
+ };
+ }),
+ };
+
+ if (this.conflicts.length > 0) {
+ const {collectionId, encrypted} = this.conflicts.shift();
+ this.collections.get(collectionId).add(encrypted);
+ dump(`responding with etag ${this.etag}\n`);
+ postResponse = {
+ responses: body.requests.map(req => {
+ return {
+ path: req.path,
+ status: 412,
+ headers: {"ETag": this.etag}, // is this correct??
+ body: {
+ details: {
+ existing: encrypted,
+ },
+ },
+ };
+ }),
+ };
+ }
+
+ response.write(JSON.stringify(postResponse));
+
+ // "sampleHeaders": [
+ // "Access-Control-Allow-Origin: *",
+ // "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+ // "Server: waitress",
+ // "Etag: \"4000\""
+ // ],
+ }
+
+ this.httpServer.registerPathHandler(batchPath, handlePost.bind(this));
+ }
+
+ installCatchAll() {
+ this.httpServer.registerPathHandler("/", (request, response) => {
+ dump(`got request: ${request.method}:${request.path}?${request.queryString}\n`);
+ dump(`${CommonUtils.readBytesFromInputStream(request.bodyInputStream)}\n`);
+ });
+ }
+
+ installCollection(collectionId) {
+ this.collections.set(collectionId, new Set());
+
+ const remoteRecordsPath = "/v1" + collectionRecordsPath(encodeURIComponent(collectionId));
+
+ function handleGetRecords(request, response) {
+ if (request.method != "GET") {
+ do_throw(`only GET is supported on ${remoteRecordsPath}`);
+ }
+
+ response.setStatusLine(null, 200, "OK");
+ response.setHeader("Content-Type", "application/json; charset=UTF-8");
+ response.setHeader("Date", (new Date()).toUTCString());
+ response.setHeader("ETag", this.etag.toString());
+
+ const records = this.collections.get(collectionId);
+ // Can't JSON a Set directly, so convert to Array
+ let data = Array.from(records);
+ if (request.queryString.includes("_since=")) {
+ data = data.filter(r => !(r._inPast || false));
+ }
+
+ // Remove records that we only needed to serve once.
+ // FIXME: come up with a more coherent idea of time here.
+ // See bug 1321570.
+ for (const record of records) {
+ if (record._onlyOnce) {
+ records.delete(record);
+ }
+ }
+
+ const body = JSON.stringify({
+ "data": data,
+ });
+ response.write(body);
+ }
+
+ this.httpServer.registerPathHandler(remoteRecordsPath, handleGetRecords.bind(this));
+ }
+
+ installDeleteBucket() {
+ this.httpServer.registerPrefixHandler("/v1/buckets/", (request, response) => {
+ if (request.method != "DELETE") {
+ dump(`got a non-delete action on bucket: ${request.method} ${request.path}\n`);
+ return;
+ }
+
+ const noPrefix = request.path.slice("/v1/buckets/".length);
+ const [bucket, afterBucket] = noPrefix.split("/", 1);
+ if (afterBucket && afterBucket != "") {
+ dump(`got a delete for a non-bucket: ${request.method} ${request.path}\n`);
+ }
+
+ this.deletedBuckets.push(bucket);
+ // Fake like this actually deletes the records.
+ for (const [, set] of this.collections) {
+ set.clear();
+ }
+
+ response.write(JSON.stringify({
+ data: {
+ deleted: true,
+ last_modified: 1475161309026,
+ id: "b09f1618-d789-302d-696e-74ec53ee18a8", // FIXME
+ },
+ }));
+ });
+ }
+
+ // Utility function to install a keyring at the start of a test.
+ installKeyRing(keysData, etag, {conflict = false} = {}) {
+ this.installCollection("storage-sync-crypto");
+ const keysRecord = {
+ "id": "keys",
+ "keys": keysData,
+ "last_modified": etag,
+ };
+ this.etag = etag;
+ const methodName = conflict ? "encryptAndAddRecordWithConflict" : "encryptAndAddRecord";
+ this[methodName](new KeyRingEncryptionRemoteTransformer(),
+ "storage-sync-crypto", keysRecord);
+ }
+
+ // Add an already-encrypted record.
+ addRecord(collectionId, record) {
+ this.collections.get(collectionId).add(record);
+ }
+
+ // Add a record that is only served if no `_since` is present.
+ //
+ // Since in real life, Kinto only serves a record as part of a
+ // changes feed if `_since` is before the record's modification
+ // time, this can be helpful to test certain kinds of syncing logic.
+ //
+ // FIXME: tracking of "time" in this mock server really needs to be
+ // implemented correctly rather than these hacks. See bug 1321570.
+ addRecordInPast(collectionId, record) {
+ record._inPast = true;
+ this.addRecord(collectionId, record);
+ }
+
+ encryptAndAddRecord(transformer, collectionId, record) {
+ return transformer.encode(record).then(encrypted => {
+ this.addRecord(collectionId, encrypted);
+ });
+ }
+
+ // Like encryptAndAddRecord, but add a flag that will only serve
+ // this record once.
+ //
+ // Since in real life, Kinto only serves a record as part of a changes feed
+ // once, this can be useful for testing complicated syncing logic.
+ //
+ // FIXME: This kind of logic really needs to be subsumed into some
+ // more-realistic tracking of "time" (simulated by etags). See bug 1321570.
+ encryptAndAddRecordOnlyOnce(transformer, collectionId, record) {
+ return transformer.encode(record).then(encrypted => {
+ encrypted._onlyOnce = true;
+ this.addRecord(collectionId, encrypted);
+ });
+ }
+
+ // Conflicts block the next push and then appear in the collection specified.
+ encryptAndAddRecordWithConflict(transformer, collectionId, record) {
+ return transformer.encode(record).then(encrypted => {
+ this.conflicts.push({collectionId, encrypted});
+ });
+ }
+
+ clearCollection(collectionId) {
+ this.collections.get(collectionId).clear();
+ }
+
+ stop() {
+ this.httpServer.stop(() => { });
+ }
+}
+
+// Run a block of code with access to a KintoServer.
+function* withServer(f) {
+ let server = new KintoServer();
+ // Point the sync.storage client to use the test server we've just started.
+ Services.prefs.setCharPref("webextensions.storage.sync.serverURL",
+ `http://localhost:${server.port}/v1`);
+ try {
+ yield* f(server);
+ } finally {
+ server.stop();
+ }
+}
+
+// Run a block of code with access to both a sync context and a
+// KintoServer. This is meant as a workaround for eslint's refusal to
+// let me have 5 nested callbacks.
+function* withContextAndServer(f) {
+ yield* withSyncContext(function* (context) {
+ yield* withServer(function* (server) {
+ yield* f(context, server);
+ });
+ });
+}
+
+// Run a block of code with fxa mocked out to return a specific user.
+function* withSignedInUser(user, f) {
+ const oldESSFxAccounts = ExtensionStorageSync._fxaService;
+ const oldERTFxAccounts = EncryptionRemoteTransformer.prototype._fxaService;
+ ExtensionStorageSync._fxaService = EncryptionRemoteTransformer.prototype._fxaService = {
+ getSignedInUser() {
+ return Promise.resolve(user);
+ },
+ getOAuthToken() {
+ return Promise.resolve("some-access-token");
+ },
+ sessionStatus() {
+ return Promise.resolve(true);
+ },
+ };
+
+ try {
+ yield* f();
+ } finally {
+ ExtensionStorageSync._fxaService = oldESSFxAccounts;
+ EncryptionRemoteTransformer.prototype._fxaService = oldERTFxAccounts;
+ }
+}
+
+// Some assertions that make it easier to write tests about what was
+// posted and when.
+
+// Assert that the request was made with the correct access token.
+// This should be true of all requests, so this is usually called from
+// another assertion.
+function assertAuthenticatedRequest(post) {
+ equal(post.headers.Authorization, "Bearer some-access-token");
+}
+
+// Assert that this post was made with the correct request headers to
+// create a new resource while protecting against someone else
+// creating it at the same time (in other words, "If-None-Match: *").
+// Also calls assertAuthenticatedRequest(post).
+function assertPostedNewRecord(post) {
+ assertAuthenticatedRequest(post);
+ equal(post.headers["If-None-Match"], "*");
+}
+
+// Assert that this post was made with the correct request headers to
+// update an existing resource while protecting against concurrent
+// modification (in other words, `If-Match: "${etag}"`).
+// Also calls assertAuthenticatedRequest(post).
+function assertPostedUpdatedRecord(post, since) {
+ assertAuthenticatedRequest(post);
+ equal(post.headers["If-Match"], `"${since}"`);
+}
+
+// Assert that this post was an encrypted keyring, and produce the
+// decrypted body. Sanity check the body while we're here.
+const assertPostedEncryptedKeys = Task.async(function* (post) {
+ equal(post.path, collectionRecordsPath("storage-sync-crypto") + "/keys");
+
+ let body = yield new KeyRingEncryptionRemoteTransformer().decode(post.body.data);
+ ok(body.keys, `keys object should be present in decoded body`);
+ ok(body.keys.default, `keys object should have a default key`);
+ return body;
+});
+
+// assertEqual, but for keyring[extensionId] == key.
+function assertKeyRingKey(keyRing, extensionId, expectedKey, message) {
+ if (!message) {
+ message = `expected keyring's key for ${extensionId} to match ${expectedKey.keyPairB64}`;
+ }
+ ok(keyRing.hasKeysFor([extensionId]),
+ `expected keyring to have a key for ${extensionId}\n`);
+ deepEqual(keyRing.keyForCollection(extensionId).keyPairB64, expectedKey.keyPairB64,
+ message);
+}
+
+// Tests using this ID will share keys in local storage, so be careful.
+const defaultExtensionId = "{13bdde76-4dc7-11e6-9bdc-54ee758d6342}";
+const defaultExtension = {id: defaultExtensionId};
+
+const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+const ANOTHER_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde0";
+const loggedInUser = {
+ uid: "0123456789abcdef0123456789abcdef",
+ kB: BORING_KB,
+ oauthTokens: {
+ "sync:addon-storage": {
+ token: "some-access-token",
+ },
+ },
+};
+const defaultCollectionId = extensionIdToCollectionId(loggedInUser, defaultExtensionId);
+
+function uuid() {
+ const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ return uuidgen.generateUUID().toString();
+}
+
+add_task(function* test_key_to_id() {
+ equal(keyToId("foo"), "key-foo");
+ equal(keyToId("my-new-key"), "key-my_2D_new_2D_key");
+ equal(keyToId(""), "key-");
+ equal(keyToId("â„¢"), "key-_2122_");
+ equal(keyToId("\b"), "key-_8_");
+ equal(keyToId("abc\ndef"), "key-abc_A_def");
+ equal(keyToId("Kinto's fancy_string"), "key-Kinto_27_s_20_fancy_5F_string");
+
+ const KEYS = ["foo", "my-new-key", "", "Kinto's fancy_string", "â„¢", "\b"];
+ for (let key of KEYS) {
+ equal(idToKey(keyToId(key)), key);
+ }
+
+ equal(idToKey("hi"), null);
+ equal(idToKey("-key-hi"), null);
+ equal(idToKey("key--abcd"), null);
+ equal(idToKey("key-%"), null);
+ equal(idToKey("key-_HI"), null);
+ equal(idToKey("key-_HI_"), null);
+ equal(idToKey("key-"), "");
+ equal(idToKey("key-1"), "1");
+ equal(idToKey("key-_2D_"), "-");
+});
+
+add_task(function* test_extension_id_to_collection_id() {
+ const newKBUser = Object.assign(loggedInUser, {kB: ANOTHER_KB});
+ const extensionId = "{9419cce6-5435-11e6-84bf-54ee758d6342}";
+ const extensionId2 = "{9419cce6-5435-11e6-84bf-54ee758d6343}";
+
+ // "random" 32-char hex userid
+ equal(extensionIdToCollectionId(loggedInUser, extensionId),
+ "abf4e257dad0c89027f8f25bd196d4d69c100df375655a0c49f4cea7b791ea7d");
+ equal(extensionIdToCollectionId(loggedInUser, extensionId),
+ extensionIdToCollectionId(newKBUser, extensionId));
+ equal(extensionIdToCollectionId(loggedInUser, extensionId2),
+ "6584b0153336fb274912b31a3225c15a92b703cdc3adfe1917c1aa43122a52b8");
+});
+
+add_task(function* ensureKeysFor_posts_new_keys() {
+ const extensionId = uuid();
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ server.installCollection("storage-sync-crypto");
+ server.etag = 1000;
+
+ let newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ ok(newKeys.hasKeysFor([extensionId]), `key isn't present for ${extensionId}`);
+
+ let posts = server.getPosts();
+ equal(posts.length, 1);
+ const post = posts[0];
+ assertPostedNewRecord(post);
+ const body = yield assertPostedEncryptedKeys(post);
+ ok(body.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
+
+ // Try adding another key to make sure that the first post was
+ // OK, even on a new profile.
+ yield cryptoCollection._clear();
+ server.clearPosts();
+ // Restore the first posted keyring
+ server.addRecordInPast("storage-sync-crypto", post.body.data);
+ const extensionId2 = uuid();
+ newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId2]);
+ ok(newKeys.hasKeysFor([extensionId]), `didn't forget key for ${extensionId}`);
+ ok(newKeys.hasKeysFor([extensionId2]), `new key generated for ${extensionId2}`);
+
+ posts = server.getPosts();
+ // FIXME: some kind of bug where we try to repush the
+ // server_wins version multiple times in a single sync. We
+ // actually push 5 times as of this writing.
+ // See bug 1321571.
+ // equal(posts.length, 1);
+ const newPost = posts[posts.length - 1];
+ const newBody = yield assertPostedEncryptedKeys(newPost);
+ ok(newBody.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
+ ok(newBody.keys.collections[extensionId2], `keys object should have a key for ${extensionId2}`);
+
+ });
+ });
+});
+
+add_task(function* ensureKeysFor_pulls_key() {
+ // ensureKeysFor is implemented by adding a key to our local record
+ // and doing a sync. This means that if the same key exists
+ // remotely, we get a "conflict". Ensure that we handle this
+ // correctly -- we keep the server key (since presumably it's
+ // already been used to encrypt records) and we don't wipe out other
+ // collections' keys.
+ const extensionId = uuid();
+ const extensionId2 = uuid();
+ const DEFAULT_KEY = new BulkKeyBundle("[default]");
+ DEFAULT_KEY.generateRandom();
+ const RANDOM_KEY = new BulkKeyBundle(extensionId);
+ RANDOM_KEY.generateRandom();
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ const keysData = {
+ "default": DEFAULT_KEY.keyPairB64,
+ "collections": {
+ [extensionId]: RANDOM_KEY.keyPairB64,
+ },
+ };
+ server.installKeyRing(keysData, 999);
+
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY);
+
+ let posts = server.getPosts();
+ equal(posts.length, 0,
+ "ensureKeysFor shouldn't push when the server keyring has the right key");
+
+ // Another client generates a key for extensionId2
+ const newKey = new BulkKeyBundle(extensionId2);
+ newKey.generateRandom();
+ keysData.collections[extensionId2] = newKey.keyPairB64;
+ server.clearCollection("storage-sync-crypto");
+ server.installKeyRing(keysData, 1000);
+
+ let newCollectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId, extensionId2]);
+ assertKeyRingKey(newCollectionKeys, extensionId2, newKey);
+ assertKeyRingKey(newCollectionKeys, extensionId, RANDOM_KEY,
+ `ensureKeysFor shouldn't lose the old key for ${extensionId}`);
+
+ posts = server.getPosts();
+ equal(posts.length, 0, "ensureKeysFor shouldn't push when updating keys");
+ });
+ });
+});
+
+add_task(function* ensureKeysFor_handles_conflicts() {
+ // Syncing is done through a pull followed by a push of any merged
+ // changes. Accordingly, the only way to have a "true" conflict --
+ // i.e. with the server rejecting a change -- is if
+ // someone pushes changes between our pull and our push. Ensure that
+ // if this happens, we still behave sensibly (keep the remote key).
+ const extensionId = uuid();
+ const DEFAULT_KEY = new BulkKeyBundle("[default]");
+ DEFAULT_KEY.generateRandom();
+ const RANDOM_KEY = new BulkKeyBundle(extensionId);
+ RANDOM_KEY.generateRandom();
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ const keysData = {
+ "default": DEFAULT_KEY.keyPairB64,
+ "collections": {
+ [extensionId]: RANDOM_KEY.keyPairB64,
+ },
+ };
+ server.installKeyRing(keysData, 765, {conflict: true});
+
+ yield cryptoCollection._clear();
+
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY,
+ `syncing keyring should keep the server key for ${extensionId}`);
+
+ let posts = server.getPosts();
+ equal(posts.length, 1,
+ "syncing keyring should have tried to post a keyring");
+ const failedPost = posts[0];
+ assertPostedNewRecord(failedPost);
+ let body = yield assertPostedEncryptedKeys(failedPost);
+ // This key will be the one the client generated locally, so
+ // we don't know what its value will be
+ ok(body.keys.collections[extensionId],
+ `decrypted failed post should have a key for ${extensionId}`);
+ notEqual(body.keys.collections[extensionId], RANDOM_KEY.keyPairB64,
+ `decrypted failed post should have a randomly-generated key for ${extensionId}`);
+ });
+ });
+});
+
+add_task(function* checkSyncKeyRing_reuploads_keys() {
+ // Verify that when keys are present, they are reuploaded with the
+ // new kB when we call touchKeys().
+ const extensionId = uuid();
+ let extensionKey;
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ server.installCollection("storage-sync-crypto");
+ server.etag = 765;
+
+ yield cryptoCollection._clear();
+
+ // Do an `ensureKeysFor` to generate some keys.
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ ok(collectionKeys.hasKeysFor([extensionId]),
+ `ensureKeysFor should return a keyring that has a key for ${extensionId}`);
+ extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
+ equal(server.getPosts().length, 1,
+ "generating a key that doesn't exist on the server should post it");
+ });
+
+ // The user changes their password. This is their new kB, with
+ // the last f changed to an e.
+ const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
+ const newUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
+ let postedKeys;
+ yield* withSignedInUser(newUser, function* () {
+ yield ExtensionStorageSync.checkSyncKeyRing();
+
+ let posts = server.getPosts();
+ equal(posts.length, 2,
+ "when kB changes, checkSyncKeyRing should post the keyring reencrypted with the new kB");
+ postedKeys = posts[1];
+ assertPostedUpdatedRecord(postedKeys, 765);
+
+ let body = yield assertPostedEncryptedKeys(postedKeys);
+ deepEqual(body.keys.collections[extensionId], extensionKey,
+ `the posted keyring should have the same key for ${extensionId} as the old one`);
+ });
+
+ // Verify that with the old kB, we can't decrypt the record.
+ yield* withSignedInUser(loggedInUser, function* () {
+ let error;
+ try {
+ yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
+ } catch (e) {
+ error = e;
+ }
+ ok(error, "decrypting the keyring with the old kB should fail");
+ ok(Utils.isHMACMismatch(error) || KeyRingEncryptionRemoteTransformer.isOutdatedKB(error),
+ "decrypting the keyring with the old kB should throw an HMAC mismatch");
+ });
+ });
+});
+
+add_task(function* checkSyncKeyRing_overwrites_on_conflict() {
+ // If there is already a record on the server that was encrypted
+ // with a different kB, we wipe the server, clear sync state, and
+ // overwrite it with our keys.
+ const extensionId = uuid();
+ const transformer = new KeyRingEncryptionRemoteTransformer();
+ let extensionKey;
+ yield* withSyncContext(function* (context) {
+ yield* withServer(function* (server) {
+ // The old device has this kB, which is very similar to the
+ // current kB but with the last f changed to an e.
+ const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
+ const oldUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
+ server.installCollection("storage-sync-crypto");
+ server.installDeleteBucket();
+ server.etag = 765;
+ yield* withSignedInUser(oldUser, function* () {
+ const FAKE_KEYRING = {
+ id: "keys",
+ keys: {},
+ uuid: "abcd",
+ kbHash: "abcd",
+ };
+ yield server.encryptAndAddRecord(transformer, "storage-sync-crypto", FAKE_KEYRING);
+ });
+
+ // Now we have this new user with a different kB.
+ yield* withSignedInUser(loggedInUser, function* () {
+ yield cryptoCollection._clear();
+
+ // Do an `ensureKeysFor` to generate some keys.
+ // This will try to sync, notice that the record is
+ // undecryptable, and clear the server.
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ ok(collectionKeys.hasKeysFor([extensionId]),
+ `ensureKeysFor should always return a keyring with a key for ${extensionId}`);
+ extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
+
+ deepEqual(server.getDeletedBuckets(), ["default"],
+ "Kinto server should have been wiped when keyring was thrown away");
+
+ let posts = server.getPosts();
+ equal(posts.length, 1,
+ "new keyring should have been uploaded");
+ const postedKeys = posts[0];
+ // The POST was to an empty server, so etag shouldn't be respected
+ equal(postedKeys.headers.Authorization, "Bearer some-access-token",
+ "keyring upload should be authorized");
+ equal(postedKeys.headers["If-None-Match"], "*",
+ "keyring upload should be to empty Kinto server");
+ equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
+ "keyring upload should be to keyring path");
+
+ let body = yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
+ ok(body.uuid, "new keyring should have a UUID");
+ equal(typeof body.uuid, "string", "keyring UUIDs should be strings");
+ notEqual(body.uuid, "abcd",
+ "new keyring should not have the same UUID as previous keyring");
+ ok(body.keys,
+ "new keyring should have a keys attribute");
+ ok(body.keys.default, "new keyring should have a default key");
+ // We should keep the extension key that was in our uploaded version.
+ deepEqual(extensionKey, body.keys.collections[extensionId],
+ "ensureKeysFor should have returned keyring with the same key that was uploaded");
+
+ // This should be a no-op; the keys were uploaded as part of ensurekeysfor
+ yield ExtensionStorageSync.checkSyncKeyRing();
+ equal(server.getPosts().length, 1,
+ "checkSyncKeyRing should not need to post keys after they were reuploaded");
+ });
+ });
+ });
+});
+
+add_task(function* checkSyncKeyRing_flushes_on_uuid_change() {
+ // If we can decrypt the record, but the UUID has changed, that
+ // means another client has wiped the server and reuploaded a
+ // keyring, so reset sync state and reupload everything.
+ const extensionId = uuid();
+ const extension = {id: extensionId};
+ const collectionId = extensionIdToCollectionId(loggedInUser, extensionId);
+ const transformer = new KeyRingEncryptionRemoteTransformer();
+ yield* withSyncContext(function* (context) {
+ yield* withServer(function* (server) {
+ server.installCollection("storage-sync-crypto");
+ server.installCollection(collectionId);
+ server.installDeleteBucket();
+ yield* withSignedInUser(loggedInUser, function* () {
+ yield cryptoCollection._clear();
+
+ // Do an `ensureKeysFor` to get access to keys.
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ ok(collectionKeys.hasKeysFor([extensionId]),
+ `ensureKeysFor should always return a keyring that has a key for ${extensionId}`);
+ const extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
+
+ // Set something to make sure that it gets re-uploaded when
+ // uuid changes.
+ yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+ yield ExtensionStorageSync.syncAll();
+
+ let posts = server.getPosts();
+ equal(posts.length, 2,
+ "should have posted a new keyring and an extension datum");
+ const postedKeys = posts[0];
+ equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
+ "should have posted keyring to /keys");
+
+ let body = yield transformer.decode(postedKeys.body.data);
+ ok(body.uuid,
+ "keyring should have a UUID");
+ ok(body.keys,
+ "keyring should have a keys attribute");
+ ok(body.keys.default,
+ "keyring should have a default key");
+ deepEqual(extensionKey, body.keys.collections[extensionId],
+ "new keyring should have the same key that we uploaded");
+
+ // Another client comes along and replaces the UUID.
+ // In real life, this would mean changing the keys too, but
+ // this test verifies that just changing the UUID is enough.
+ const newKeyRingData = Object.assign({}, body, {
+ uuid: "abcd",
+ // Technically, last_modified should be served outside the
+ // object, but the transformer will pass it through in
+ // either direction, so this is OK.
+ last_modified: 765,
+ });
+ server.clearCollection("storage-sync-crypto");
+ server.etag = 765;
+ yield server.encryptAndAddRecordOnlyOnce(transformer, "storage-sync-crypto", newKeyRingData);
+
+ // Fake adding another extension just so that the keyring will
+ // really get synced.
+ const newExtension = uuid();
+ const newKeyRing = yield ExtensionStorageSync.ensureKeysFor([newExtension]);
+
+ // This should have detected the UUID change and flushed everything.
+ // The keyring should, however, be the same, since we just
+ // changed the UUID of the previously POSTed one.
+ deepEqual(newKeyRing.keyForCollection(extensionId).keyPairB64, extensionKey,
+ "ensureKeysFor should have pulled down a new keyring with the same keys");
+
+ // Syncing should reupload the data for the extension.
+ yield ExtensionStorageSync.syncAll();
+ posts = server.getPosts();
+ equal(posts.length, 4,
+ "should have posted keyring for new extension and reuploaded extension data");
+
+ const finalKeyRingPost = posts[2];
+ const reuploadedPost = posts[3];
+
+ equal(finalKeyRingPost.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
+ "keyring for new extension should have been posted to /keys");
+ let finalKeyRing = yield transformer.decode(finalKeyRingPost.body.data);
+ equal(finalKeyRing.uuid, "abcd",
+ "newly uploaded keyring should preserve UUID from replacement keyring");
+
+ // Confirm that the data got reuploaded
+ equal(reuploadedPost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
+ "extension data should be posted to path corresponding to its key");
+ let reuploadedData = yield new CollectionKeyEncryptionRemoteTransformer(extensionId).decode(reuploadedPost.body.data);
+ equal(reuploadedData.key, "my-key",
+ "extension data should have a key attribute corresponding to the extension data key");
+ equal(reuploadedData.data, 5,
+ "extension data should have a data attribute corresponding to the extension data value");
+ });
+ });
+ });
+});
+
+add_task(function* test_storage_sync_pulls_changes() {
+ const extensionId = defaultExtensionId;
+ const collectionId = defaultCollectionId;
+ const extension = defaultExtension;
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
+ server.installCollection(collectionId);
+ server.installCollection("storage-sync-crypto");
+
+ let calls = [];
+ yield ExtensionStorageSync.addOnChangedListener(extension, function() {
+ calls.push(arguments);
+ }, context);
+
+ yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ yield server.encryptAndAddRecord(transformer, collectionId, {
+ "id": "key-remote_2D_key",
+ "key": "remote-key",
+ "data": 6,
+ });
+
+ yield ExtensionStorageSync.syncAll();
+ const remoteValue = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];
+ equal(remoteValue, 6,
+ "ExtensionStorageSync.get() returns value retrieved from sync");
+
+ equal(calls.length, 1,
+ "syncing calls on-changed listener");
+ deepEqual(calls[0][0], {"remote-key": {newValue: 6}});
+ calls = [];
+
+ // Syncing again doesn't do anything
+ yield ExtensionStorageSync.syncAll();
+
+ equal(calls.length, 0,
+ "syncing again shouldn't call on-changed listener");
+
+ // Updating the server causes us to pull down the new value
+ server.etag = 1000;
+ server.clearCollection(collectionId);
+ yield server.encryptAndAddRecord(transformer, collectionId, {
+ "id": "key-remote_2D_key",
+ "key": "remote-key",
+ "data": 7,
+ });
+
+ yield ExtensionStorageSync.syncAll();
+ const remoteValue2 = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];
+ equal(remoteValue2, 7,
+ "ExtensionStorageSync.get() returns value updated from sync");
+
+ equal(calls.length, 1,
+ "syncing calls on-changed listener on update");
+ deepEqual(calls[0][0], {"remote-key": {oldValue: 6, newValue: 7}});
+ });
+ });
+});
+
+add_task(function* test_storage_sync_pushes_changes() {
+ const extensionId = defaultExtensionId;
+ const collectionId = defaultCollectionId;
+ const extension = defaultExtension;
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
+ server.installCollection(collectionId);
+ server.installCollection("storage-sync-crypto");
+ server.etag = 1000;
+
+ yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+
+ // install this AFTER we set the key to 5...
+ let calls = [];
+ ExtensionStorageSync.addOnChangedListener(extension, function() {
+ calls.push(arguments);
+ }, context);
+
+ yield ExtensionStorageSync.syncAll();
+ const localValue = (yield ExtensionStorageSync.get(extension, "my-key", context))["my-key"];
+ equal(localValue, 5,
+ "pushing an ExtensionStorageSync value shouldn't change local value");
+
+ let posts = server.getPosts();
+ equal(posts.length, 1,
+ "pushing a value should cause a post to the server");
+ const post = posts[0];
+ assertPostedNewRecord(post);
+ equal(post.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
+ "pushing a value should have a path corresponding to its id");
+
+ const encrypted = post.body.data;
+ ok(encrypted.ciphertext,
+ "pushing a value should post an encrypted record");
+ ok(!encrypted.data,
+ "pushing a value should not have any plaintext data");
+ equal(encrypted.id, "key-my_2D_key",
+ "pushing a value should use a kinto-friendly record ID");
+
+ const record = yield transformer.decode(encrypted);
+ equal(record.key, "my-key",
+ "when decrypted, a pushed value should have a key field corresponding to its storage.sync key");
+ equal(record.data, 5,
+ "when decrypted, a pushed value should have a data field corresponding to its storage.sync value");
+ equal(record.id, "key-my_2D_key",
+ "when decrypted, a pushed value should have an id field corresponding to its record ID");
+
+ equal(calls.length, 0,
+ "pushing a value shouldn't call the on-changed listener");
+
+ yield ExtensionStorageSync.set(extension, {"my-key": 6}, context);
+ yield ExtensionStorageSync.syncAll();
+
+ // Doesn't push keys because keys were pushed by a previous test.
+ posts = server.getPosts();
+ equal(posts.length, 2,
+ "updating a value should trigger another push");
+ const updatePost = posts[1];
+ assertPostedUpdatedRecord(updatePost, 1000);
+ equal(updatePost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
+ "pushing an updated value should go to the same path");
+
+ const updateEncrypted = updatePost.body.data;
+ ok(updateEncrypted.ciphertext,
+ "pushing an updated value should still be encrypted");
+ ok(!updateEncrypted.data,
+ "pushing an updated value should not have any plaintext visible");
+ equal(updateEncrypted.id, "key-my_2D_key",
+ "pushing an updated value should maintain the same ID");
+ });
+ });
+});
+
+add_task(function* test_storage_sync_pulls_deletes() {
+ const collectionId = defaultCollectionId;
+ const extension = defaultExtension;
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ server.installCollection(collectionId);
+ server.installCollection("storage-sync-crypto");
+
+ yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+ yield ExtensionStorageSync.syncAll();
+ server.clearPosts();
+
+ let calls = [];
+ yield ExtensionStorageSync.addOnChangedListener(extension, function() {
+ calls.push(arguments);
+ }, context);
+
+ yield server.addRecord(collectionId, {
+ "id": "key-my_2D_key",
+ "deleted": true,
+ });
+
+ yield ExtensionStorageSync.syncAll();
+ const remoteValues = (yield ExtensionStorageSync.get(extension, "my-key", context));
+ ok(!remoteValues["my-key"],
+ "ExtensionStorageSync.get() shows value was deleted by sync");
+
+ equal(server.getPosts().length, 0,
+ "pulling the delete shouldn't cause posts");
+
+ equal(calls.length, 1,
+ "syncing calls on-changed listener");
+ deepEqual(calls[0][0], {"my-key": {oldValue: 5}});
+ calls = [];
+
+ // Syncing again doesn't do anything
+ yield ExtensionStorageSync.syncAll();
+
+ equal(calls.length, 0,
+ "syncing again shouldn't call on-changed listener");
+ });
+ });
+});
+
+add_task(function* test_storage_sync_pushes_deletes() {
+ const extensionId = uuid();
+ const collectionId = extensionIdToCollectionId(loggedInUser, extensionId);
+ const extension = {id: extensionId};
+ yield cryptoCollection._clear();
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ server.installCollection(collectionId);
+ server.installCollection("storage-sync-crypto");
+ server.etag = 1000;
+
+ yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+
+ let calls = [];
+ ExtensionStorageSync.addOnChangedListener(extension, function() {
+ calls.push(arguments);
+ }, context);
+
+ yield ExtensionStorageSync.syncAll();
+ let posts = server.getPosts();
+ equal(posts.length, 2,
+ "pushing a non-deleted value should post keys and post the value to the server");
+
+ yield ExtensionStorageSync.remove(extension, ["my-key"], context);
+ equal(calls.length, 1,
+ "deleting a value should call the on-changed listener");
+
+ yield ExtensionStorageSync.syncAll();
+ equal(calls.length, 1,
+ "pushing a deleted value shouldn't call the on-changed listener");
+
+ // Doesn't push keys because keys were pushed by a previous test.
+ posts = server.getPosts();
+ equal(posts.length, 3,
+ "deleting a value should trigger another push");
+ const post = posts[2];
+ assertPostedUpdatedRecord(post, 1000);
+ equal(post.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
+ "pushing a deleted value should go to the same path");
+ ok(post.method, "DELETE");
+ ok(!post.body,
+ "deleting a value shouldn't have a body");
+ });
+ });
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js b/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js
new file mode 100644
index 000000000..eb3f552ed
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js
@@ -0,0 +1,85 @@
+"use strict";
+
+Cu.import("resource://gre/modules/NewTabUtils.jsm");
+
+
+function TestProvider(getLinksFn) {
+ this.getLinks = getLinksFn;
+ this._observers = new Set();
+}
+
+TestProvider.prototype = {
+ addObserver: function(observer) {
+ this._observers.add(observer);
+ },
+ notifyLinkChanged: function(link, index = -1, deleted = false) {
+ this._notifyObservers("onLinkChanged", link, index, deleted);
+ },
+ notifyManyLinksChanged: function() {
+ this._notifyObservers("onManyLinksChanged");
+ },
+ _notifyObservers: function(observerMethodName, ...args) {
+ args.unshift(this);
+ for (let obs of this._observers) {
+ if (obs[observerMethodName]) {
+ obs[observerMethodName].apply(NewTabUtils.links, args);
+ }
+ }
+ },
+};
+
+function makeLinks(links) {
+ // Important: To avoid test failures due to clock jitter on Windows XP, call
+ // Date.now() once here, not each time through the loop.
+ let frecency = 0;
+ let now = Date.now() * 1000;
+ let places = [];
+ links.map((link, i) => {
+ places.push({
+ url: link.url,
+ title: link.title,
+ lastVisitDate: now - i,
+ frecency: frecency++,
+ });
+ });
+ return places;
+}
+
+add_task(function* test_topSites() {
+ let expect = [{url: "http://example.com/", title: "site#-1"},
+ {url: "http://example0.com/", title: "site#0"},
+ {url: "http://example1.com/", title: "site#1"},
+ {url: "http://example2.com/", title: "site#2"},
+ {url: "http://example3.com/", title: "site#3"}];
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": [
+ "topSites",
+ ],
+ },
+ background() {
+ browser.topSites.get(result => {
+ browser.test.sendMessage("done", result);
+ });
+ },
+ });
+
+
+ let expectedLinks = makeLinks(expect);
+ let provider = new TestProvider(done => done(expectedLinks));
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider);
+
+ yield NewTabUtils.links.populateCache();
+
+ yield extension.startup();
+
+ let result = yield extension.awaitMessage("done");
+ Assert.deepEqual(expect, result, "got topSites");
+
+ yield extension.unload();
+
+ NewTabUtils.links.removeProvider(provider);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js b/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js
new file mode 100644
index 000000000..68741a6cc
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js
@@ -0,0 +1,55 @@
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function createWindowWithAddonId(addonId) {
+ let baseURI = Services.io.newURI("about:blank", null, null);
+ let originAttributes = {addonId};
+ let principal = Services.scriptSecurityManager
+ .createCodebasePrincipal(baseURI, originAttributes);
+ let chromeNav = Services.appShell.createWindowlessBrowser(true);
+ let interfaceRequestor = chromeNav.QueryInterface(Ci.nsIInterfaceRequestor);
+ let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+ docShell.createAboutBlankContentViewer(principal);
+
+ return {chromeNav, window: docShell.contentViewer.DOMDocument.defaultView};
+}
+
+add_task(function* test_eventpages() {
+ const {getAPILevelForWindow, getAddonIdForWindow} = ExtensionManagement;
+ const {NO_PRIVILEGES, FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS;
+ const FAKE_ADDON_ID = "fakeAddonId";
+ const OTHER_ADDON_ID = "otherFakeAddonId";
+ const EMPTY_ADDON_ID = "";
+
+ let fakeAddonId = createWindowWithAddonId(FAKE_ADDON_ID);
+ equal(getAddonIdForWindow(fakeAddonId.window), FAKE_ADDON_ID,
+ "the window has the expected addonId");
+
+ let apiLevel = getAPILevelForWindow(fakeAddonId.window, FAKE_ADDON_ID);
+ equal(apiLevel, FULL_PRIVILEGES,
+ "apiLevel for the window with the right addonId should be FULL_PRIVILEGES");
+
+ apiLevel = getAPILevelForWindow(fakeAddonId.window, OTHER_ADDON_ID);
+ equal(apiLevel, NO_PRIVILEGES,
+ "apiLevel for the window with a different addonId should be NO_PRIVILEGES");
+
+ fakeAddonId.chromeNav.close();
+
+ // NOTE: check that window with an empty addon Id (which are window that are
+ // not Extensions pages) always get no WebExtensions APIs.
+ let emptyAddonId = createWindowWithAddonId(EMPTY_ADDON_ID);
+ equal(getAddonIdForWindow(emptyAddonId.window), EMPTY_ADDON_ID,
+ "the window has the expected addonId");
+
+ apiLevel = getAPILevelForWindow(emptyAddonId.window, EMPTY_ADDON_ID);
+ equal(apiLevel, NO_PRIVILEGES,
+ "apiLevel for empty addonId should be NO_PRIVILEGES");
+
+ apiLevel = getAPILevelForWindow(emptyAddonId.window, OTHER_ADDON_ID);
+ equal(apiLevel, NO_PRIVILEGES,
+ "apiLevel for an 'empty addonId' window should be always NO_PRIVILEGES");
+
+ emptyAddonId.chromeNav.close();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_locale_converter.js b/toolkit/components/extensions/test/xpcshell/test_locale_converter.js
new file mode 100644
index 000000000..c8b1ee92b
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_locale_converter.js
@@ -0,0 +1,133 @@
+"use strict";
+
+const convService = Cc["@mozilla.org/streamConverters;1"]
+ .getService(Ci.nsIStreamConverterService);
+
+const UUID = "72b61ee3-aceb-476c-be1b-0822b036c9f1";
+const ADDON_ID = "test@web.extension";
+const URI = NetUtil.newURI(`moz-extension://${UUID}/file.css`);
+
+const FROM_TYPE = "application/vnd.mozilla.webext.unlocalized";
+const TO_TYPE = "text/css";
+
+
+function StringStream(string) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+
+ stream.data = string;
+ return stream;
+}
+
+
+// Initialize the policy service with a stub localizer for our
+// add-on ID.
+add_task(function* init() {
+ const aps = Cc["@mozilla.org/addons/policy-service;1"]
+ .getService(Ci.nsIAddonPolicyService).wrappedJSObject;
+
+ let oldCallback = aps.setExtensionURIToAddonIdCallback(uri => {
+ if (uri.host == UUID) {
+ return ADDON_ID;
+ }
+ });
+
+ aps.setAddonLocalizeCallback(ADDON_ID, string => {
+ return string.replace(/__MSG_(.*?)__/g, "<localized-$1>");
+ });
+
+ do_register_cleanup(() => {
+ aps.setExtensionURIToAddonIdCallback(oldCallback);
+ aps.setAddonLocalizeCallback(ADDON_ID, null);
+ });
+});
+
+
+// Test that the synchronous converter works as expected with a
+// simple string.
+add_task(function* testSynchronousConvert() {
+ let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz");
+
+ let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI);
+
+ let result = NetUtil.readInputStreamToString(resultStream, resultStream.available());
+
+ equal(result, "Foo <localized-xxx> bar <localized-yyy> baz");
+});
+
+
+// Test that the asynchronous converter works as expected with input
+// split into multiple chunks, and a boundary in the middle of a
+// replacement token.
+add_task(function* testAsyncConvert() {
+ let listener;
+ let awaitResult = new Promise((resolve, reject) => {
+ listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]),
+
+ onDataAvailable(request, context, inputStream, offset, count) {
+ this.resultParts.push(NetUtil.readInputStreamToString(inputStream, count));
+ },
+
+ onStartRequest() {
+ ok(!("resultParts" in this));
+ this.resultParts = [];
+ },
+
+ onStopRequest(request, context, statusCode) {
+ if (!Components.isSuccessCode(statusCode)) {
+ reject(new Error(statusCode));
+ }
+
+ resolve(this.resultParts.join("\n"));
+ },
+ };
+ });
+
+ let parts = ["Foo __MSG_x", "xx__ bar __MSG_yyy__ baz"];
+
+ let converter = convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, URI);
+ converter.onStartRequest(null, null);
+
+ for (let part of parts) {
+ converter.onDataAvailable(null, null, StringStream(part), 0, part.length);
+ }
+
+ converter.onStopRequest(null, null, Cr.NS_OK);
+
+
+ let result = yield awaitResult;
+ equal(result, "Foo <localized-xxx> bar <localized-yyy> baz");
+});
+
+
+// Test that attempting to initialize a converter with the URI of a
+// nonexistent WebExtension fails.
+add_task(function* testInvalidUUID() {
+ let uri = NetUtil.newURI("moz-extension://eb4f3be8-41c9-4970-aa6d-b84d1ecc02b2/file.css");
+ let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz");
+
+ // Assert.throws raise a TypeError exception when the expected param
+ // is an arrow function. (See Bug 1237961 for rationale)
+ let expectInvalidContextException = function(e) {
+ return e.result === Cr.NS_ERROR_INVALID_ARG && /Invalid context/.test(e);
+ };
+
+ Assert.throws(() => {
+ convService.convert(stream, FROM_TYPE, TO_TYPE, uri);
+ }, expectInvalidContextException);
+
+ Assert.throws(() => {
+ let listener = {QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])};
+
+ convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, uri);
+ }, expectInvalidContextException);
+});
+
+
+// Test that an empty stream does not throw an NS_ERROR_ILLEGAL_VALUE.
+add_task(function* testEmptyStream() {
+ let stream = StringStream("");
+ let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI);
+ equal(resultStream.data, "");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_locale_data.js b/toolkit/components/extensions/test/xpcshell/test_locale_data.js
new file mode 100644
index 000000000..c3cd44e57
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_locale_data.js
@@ -0,0 +1,130 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Extension.jsm");
+
+/* globals ExtensionData */
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+function* generateAddon(data) {
+ let id = uuidGenerator.generateUUID().number;
+
+ data = Object.assign({embedded: true}, data);
+ data.manifest = Object.assign({applications: {gecko: {id}}}, data.manifest);
+
+ let xpi = Extension.generateXPI(data);
+ do_register_cleanup(() => {
+ Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
+ xpi.remove(false);
+ });
+
+ let fileURI = Services.io.newFileURI(xpi);
+ let jarURI = NetUtil.newURI(`jar:${fileURI.spec}!/webextension/`);
+
+ let extension = new ExtensionData(jarURI);
+ yield extension.readManifest();
+
+ return extension;
+}
+
+add_task(function* testMissingDefaultLocale() {
+ let extension = yield generateAddon({
+ "files": {
+ "_locales/en_US/messages.json": {},
+ },
+ });
+
+ equal(extension.errors.length, 0, "No errors reported");
+
+ yield extension.initAllLocales();
+
+ equal(extension.errors.length, 1, "One error reported");
+
+ do_print(`Got error: ${extension.errors[0]}`);
+
+ ok(extension.errors[0].includes('"default_locale" property is required'),
+ "Got missing default_locale error");
+});
+
+
+add_task(function* testInvalidDefaultLocale() {
+ let extension = yield generateAddon({
+ "manifest": {
+ "default_locale": "en",
+ },
+
+ "files": {
+ "_locales/en_US/messages.json": {},
+ },
+ });
+
+ equal(extension.errors.length, 1, "One error reported");
+
+ do_print(`Got error: ${extension.errors[0]}`);
+
+ ok(extension.errors[0].includes("Loading locale file _locales/en/messages.json"),
+ "Got invalid default_locale error");
+
+ yield extension.initAllLocales();
+
+ equal(extension.errors.length, 2, "Two errors reported");
+
+ do_print(`Got error: ${extension.errors[1]}`);
+
+ ok(extension.errors[1].includes('"default_locale" property must correspond'),
+ "Got invalid default_locale error");
+});
+
+
+add_task(function* testUnexpectedDefaultLocale() {
+ let extension = yield generateAddon({
+ "manifest": {
+ "default_locale": "en_US",
+ },
+ });
+
+ equal(extension.errors.length, 1, "One error reported");
+
+ do_print(`Got error: ${extension.errors[0]}`);
+
+ ok(extension.errors[0].includes("Loading locale file _locales/en-US/messages.json"),
+ "Got invalid default_locale error");
+
+ yield extension.initAllLocales();
+
+ equal(extension.errors.length, 2, "One error reported");
+
+ do_print(`Got error: ${extension.errors[1]}`);
+
+ ok(extension.errors[1].includes('"default_locale" property must correspond'),
+ "Got unexpected default_locale error");
+});
+
+
+add_task(function* testInvalidSyntax() {
+ let extension = yield generateAddon({
+ "manifest": {
+ "default_locale": "en_US",
+ },
+
+ "files": {
+ "_locales/en_US/messages.json": '{foo: {message: "bar", description: "baz"}}',
+ },
+ });
+
+ equal(extension.errors.length, 1, "No errors reported");
+
+ do_print(`Got error: ${extension.errors[0]}`);
+
+ ok(extension.errors[0].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"),
+ "Got syntax error");
+
+ yield extension.initAllLocales();
+
+ equal(extension.errors.length, 2, "One error reported");
+
+ do_print(`Got error: ${extension.errors[1]}`);
+
+ ok(extension.errors[1].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"),
+ "Got syntax error");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_native_messaging.js b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
new file mode 100644
index 000000000..1fcb7799e
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
@@ -0,0 +1,302 @@
+"use strict";
+
+/* global OS, HostManifestManager, NativeApp */
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Schemas.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
+Cu.import("resource://gre/modules/NativeMessaging.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+let registry = null;
+if (AppConstants.platform == "win") {
+ Cu.import("resource://testing-common/MockRegistry.jsm");
+ registry = new MockRegistry();
+ do_register_cleanup(() => {
+ registry.shutdown();
+ });
+}
+
+const REGPATH = "Software\\Mozilla\\NativeMessagingHosts";
+
+const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
+
+let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
+dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+let userDir = dir.clone();
+userDir.append("user");
+userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+let globalDir = dir.clone();
+globalDir.append("global");
+globalDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+let dirProvider = {
+ getFile(property) {
+ if (property == "XREUserNativeMessaging") {
+ return userDir.clone();
+ } else if (property == "XRESysNativeMessaging") {
+ return globalDir.clone();
+ }
+ return null;
+ },
+};
+
+Services.dirsvc.registerProvider(dirProvider);
+
+do_register_cleanup(() => {
+ Services.dirsvc.unregisterProvider(dirProvider);
+ dir.remove(true);
+});
+
+function writeManifest(path, manifest) {
+ if (typeof manifest != "string") {
+ manifest = JSON.stringify(manifest);
+ }
+ return OS.File.writeAtomic(path, manifest);
+}
+
+let PYTHON;
+add_task(function* setup() {
+ yield Schemas.load(BASE_SCHEMA);
+
+ PYTHON = yield Subprocess.pathSearch("python2.7");
+ if (PYTHON == null) {
+ PYTHON = yield Subprocess.pathSearch("python");
+ }
+ notEqual(PYTHON, null, "Found a suitable python interpreter");
+});
+
+let global = this;
+
+// Test of HostManifestManager.lookupApplication() begin here...
+let context = {
+ url: null,
+ jsonStringify(...args) { return JSON.stringify(...args); },
+ cloneScope: global,
+ logError() {},
+ preprocessors: {},
+ callOnClose: () => {},
+ forgetOnClose: () => {},
+};
+
+class MockContext extends ExtensionCommon.BaseContext {
+ constructor(extensionId) {
+ let fakeExtension = {id: extensionId};
+ super("testEnv", fakeExtension);
+ this.sandbox = Cu.Sandbox(global);
+ }
+
+ get cloneScope() {
+ return global;
+ }
+
+ get principal() {
+ return Cu.getObjectPrincipal(this.sandbox);
+ }
+}
+
+let templateManifest = {
+ name: "test",
+ description: "this is only a test",
+ path: "/bin/cat",
+ type: "stdio",
+ allowed_extensions: ["extension@tests.mozilla.org"],
+};
+
+add_task(function* test_nonexistent_manifest() {
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication returns null for non-existent application");
+});
+
+const USER_TEST_JSON = OS.Path.join(userDir.path, "test.json");
+
+add_task(function* test_good_manifest() {
+ yield writeManifest(USER_TEST_JSON, templateManifest);
+ if (registry) {
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGPATH}\\test`, "", USER_TEST_JSON);
+ }
+
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ notEqual(result, null, "lookupApplication finds a good manifest");
+ equal(result.path, USER_TEST_JSON, "lookupApplication returns the correct path");
+ deepEqual(result.manifest, templateManifest, "lookupApplication returns the manifest contents");
+});
+
+add_task(function* test_invalid_json() {
+ yield writeManifest(USER_TEST_JSON, "this is not valid json");
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication ignores bad json");
+});
+
+add_task(function* test_invalid_name() {
+ let manifest = Object.assign({}, templateManifest);
+ manifest.name = "../test";
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication ignores an invalid name");
+});
+
+add_task(function* test_name_mismatch() {
+ let manifest = Object.assign({}, templateManifest);
+ manifest.name = "not test";
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ let what = (AppConstants.platform == "win") ? "registry key" : "json filename";
+ equal(result, null, `lookupApplication ignores mistmatch between ${what} and name property`);
+});
+
+add_task(function* test_missing_props() {
+ const PROPS = [
+ "name",
+ "description",
+ "path",
+ "type",
+ "allowed_extensions",
+ ];
+ for (let prop of PROPS) {
+ let manifest = Object.assign({}, templateManifest);
+ delete manifest[prop];
+
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, `lookupApplication ignores missing ${prop}`);
+ }
+});
+
+add_task(function* test_invalid_type() {
+ let manifest = Object.assign({}, templateManifest);
+ manifest.type = "bogus";
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication ignores invalid type");
+});
+
+add_task(function* test_no_allowed_extensions() {
+ let manifest = Object.assign({}, templateManifest);
+ manifest.allowed_extensions = [];
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication ignores manifest with no allowed_extensions");
+});
+
+const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, "test.json");
+let globalManifest = Object.assign({}, templateManifest);
+globalManifest.description = "This manifest is from the systemwide directory";
+
+add_task(function* good_manifest_system_dir() {
+ yield OS.File.remove(USER_TEST_JSON);
+ yield writeManifest(GLOBAL_TEST_JSON, globalManifest);
+ if (registry) {
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGPATH}\\test`, "", null);
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ `${REGPATH}\\test`, "", GLOBAL_TEST_JSON);
+ }
+
+ let where = (AppConstants.platform == "win") ? "registry location" : "directory";
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ notEqual(result, null, `lookupApplication finds a manifest in the system-wide ${where}`);
+ equal(result.path, GLOBAL_TEST_JSON, `lookupApplication returns path in the system-wide ${where}`);
+ deepEqual(result.manifest, globalManifest, `lookupApplication returns manifest contents from the system-wide ${where}`);
+});
+
+add_task(function* test_user_dir_precedence() {
+ yield writeManifest(USER_TEST_JSON, templateManifest);
+ if (registry) {
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGPATH}\\test`, "", USER_TEST_JSON);
+ }
+ // global test.json and LOCAL_MACHINE registry key on windows are
+ // still present from the previous test
+
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ notEqual(result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide locations");
+ equal(result.path, USER_TEST_JSON, "lookupApplication returns the user-specific path when user-specific and system-wide entries both exist");
+ deepEqual(result.manifest, templateManifest, "lookupApplication returns user-specific manifest contents with user-specific and system-wide entries both exist");
+});
+
+// Test shutdown handling in NativeApp
+add_task(function* test_native_app_shutdown() {
+ const SCRIPT = String.raw`
+import signal
+import struct
+import sys
+
+signal.signal(signal.SIGTERM, signal.SIG_IGN)
+
+while True:
+ rawlen = sys.stdin.read(4)
+ if len(rawlen) == 0:
+ signal.pause()
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = sys.stdin.read(msglen)
+
+ sys.stdout.write(struct.pack('@I', msglen))
+ sys.stdout.write(msg)
+ `;
+
+ let scriptPath = OS.Path.join(userDir.path, "wontdie.py");
+ let manifestPath = OS.Path.join(userDir.path, "wontdie.json");
+
+ const ID = "native@tests.mozilla.org";
+ let manifest = {
+ name: "wontdie",
+ description: "test async shutdown of native apps",
+ type: "stdio",
+ allowed_extensions: [ID],
+ };
+
+ if (AppConstants.platform == "win") {
+ yield OS.File.writeAtomic(scriptPath, SCRIPT);
+
+ let batPath = OS.Path.join(userDir.path, "wontdie.bat");
+ let batBody = `@ECHO OFF\n${PYTHON} -u "${scriptPath}" %*\n`;
+ yield OS.File.writeAtomic(batPath, batBody);
+ yield OS.File.setPermissions(batPath, {unixMode: 0o755});
+
+ manifest.path = batPath;
+ yield writeManifest(manifestPath, manifest);
+
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGPATH}\\wontdie`, "", manifestPath);
+ } else {
+ yield OS.File.writeAtomic(scriptPath, `#!${PYTHON} -u\n${SCRIPT}`);
+ yield OS.File.setPermissions(scriptPath, {unixMode: 0o755});
+ manifest.path = scriptPath;
+ yield writeManifest(manifestPath, manifest);
+ }
+
+ let mockContext = new MockContext(ID);
+ let app = new NativeApp(mockContext, "wontdie");
+
+ // send a message and wait for the reply to make sure the app is running
+ let MSG = "test";
+ let recvPromise = new Promise(resolve => {
+ let listener = (what, msg) => {
+ equal(msg, MSG, "Received test message");
+ app.off("message", listener);
+ resolve();
+ };
+ app.on("message", listener);
+ });
+
+ let buffer = NativeApp.encodeMessage(mockContext, MSG);
+ app.send(buffer);
+ yield recvPromise;
+
+ app._cleanup();
+
+ do_print("waiting for async shutdown");
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ AsyncShutdown.profileBeforeChange._trigger();
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+
+ let procs = yield SubprocessImpl.Process.getWorker().call("getProcesses", []);
+ equal(procs.size, 0, "native process exited");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell.ini b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..3d0198ee9
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -0,0 +1,72 @@
+[DEFAULT]
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = appname == "thunderbird"
+support-files =
+ data/** head_sync.js
+tags = webextensions
+
+[test_csp_custom_policies.js]
+[test_csp_validator.js]
+[test_ext_alarms.js]
+[test_ext_alarms_does_not_fire.js]
+[test_ext_alarms_periodic.js]
+[test_ext_alarms_replaces.js]
+[test_ext_apimanager.js]
+[test_ext_api_permissions.js]
+[test_ext_background_generated_load_events.js]
+[test_ext_background_generated_reload.js]
+[test_ext_background_global_history.js]
+skip-if = os == "android" # Android does not use Places for history.
+[test_ext_background_private_browsing.js]
+[test_ext_background_runtime_connect_params.js]
+[test_ext_background_sub_windows.js]
+[test_ext_background_window_properties.js]
+skip-if = os == "android"
+[test_ext_contexts.js]
+[test_ext_downloads.js]
+[test_ext_downloads_download.js]
+skip-if = os == "android"
+[test_ext_downloads_misc.js]
+skip-if = os == "android"
+[test_ext_downloads_search.js]
+skip-if = os == "android"
+[test_ext_experiments.js]
+skip-if = release_or_beta
+[test_ext_extension.js]
+[test_ext_idle.js]
+[test_ext_json_parser.js]
+[test_ext_localStorage.js]
+[test_ext_management.js]
+[test_ext_management_uninstall_self.js]
+[test_ext_manifest_content_security_policy.js]
+[test_ext_manifest_incognito.js]
+[test_ext_manifest_minimum_chrome_version.js]
+[test_ext_onmessage_removelistener.js]
+[test_ext_runtime_connect_no_receiver.js]
+[test_ext_runtime_getBrowserInfo.js]
+[test_ext_runtime_getPlatformInfo.js]
+[test_ext_runtime_onInstalled_and_onStartup.js]
+[test_ext_runtime_sendMessage.js]
+[test_ext_runtime_sendMessage_errors.js]
+[test_ext_runtime_sendMessage_no_receiver.js]
+[test_ext_runtime_sendMessage_self.js]
+[test_ext_schemas.js]
+[test_ext_schemas_api_injection.js]
+[test_ext_schemas_async.js]
+[test_ext_schemas_allowed_contexts.js]
+[test_ext_simple.js]
+[test_ext_storage.js]
+[test_ext_storage_sync.js]
+head = head.js head_sync.js
+skip-if = os == "android"
+[test_ext_topSites.js]
+skip-if = os == "android"
+[test_getAPILevelForWindow.js]
+[test_ext_legacy_extension_context.js]
+[test_ext_legacy_extension_embedding.js]
+[test_locale_converter.js]
+[test_locale_data.js]
+[test_native_messaging.js]
+skip-if = os == "android"