diff options
Diffstat (limited to 'toolkit/components/extensions/test/mochitest')
208 files changed, 26073 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/mochitest/.eslintrc.js b/toolkit/components/extensions/test/mochitest/.eslintrc.js new file mode 100644 index 0000000000..a776405c9d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/.eslintrc.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = { + env: { + browser: true, + webextensions: true, + }, + + rules: { + "no-shadow": 0, + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/chrome.ini b/toolkit/components/extensions/test/mochitest/chrome.ini new file mode 100644 index 0000000000..30781a8759 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome.ini @@ -0,0 +1,38 @@ +[DEFAULT] +support-files = + chrome_cleanup_script.js + head.js + head_cookies.js + file_image_good.png + file_image_great.png + file_sample.html + file_with_images.html + webrequest_chromeworker.js + webrequest_test.sys.mjs +prefs = + security.mixed_content.upgrade_display_content=false +tags = webextensions in-process-webextensions + +# NO NEW TESTS. mochitest-chrome does not run under e10s, avoid adding new +# tests here unless absolutely necessary. + +[test_chrome_ext_contentscript_data_uri.html] +[test_chrome_ext_contentscript_telemetry.html] +[test_chrome_ext_contentscript_unrecognizedprop_warning.html] +[test_chrome_ext_downloads_open.html] +[test_chrome_ext_downloads_saveAs.html] +skip-if = (verify && !debug && (os == 'win')) || (os == 'android') || (os == 'win' && os_version == '10.0') # Bug 1695612 +[test_chrome_ext_downloads_uniquify.html] +skip-if = os == 'win' && os_version == '10.0' # Bug 1695612 +[test_chrome_ext_permissions.html] +skip-if = os == 'android' # Bug 1350559 +[test_chrome_ext_svg_context_fill.html] +[test_chrome_ext_trackingprotection.html] +[test_chrome_ext_webnavigation_resolved_urls.html] +[test_chrome_ext_webrequest_background_events.html] +[test_chrome_ext_webrequest_host_permissions.html] +skip-if = verify +[test_chrome_ext_webrequest_mozextension.html] +skip-if = true # Bug 1404172 +[test_chrome_native_messaging_paths.html] +skip-if = os != "mac" && os != "linux" diff --git a/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js b/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js new file mode 100644 index 0000000000..9afa95f302 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js @@ -0,0 +1,65 @@ +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +let listener = msg => { + void (msg instanceof Ci.nsIConsoleMessage); + dump(`Console message: ${msg}\n`); +}; + +Services.console.registerListener(listener); + +let getBrowserApp, getTabBrowser; +if (AppConstants.MOZ_BUILD_APP === "mobile/android") { + getBrowserApp = win => win.BrowserApp; + getTabBrowser = tab => tab.browser; +} else { + getBrowserApp = win => win.gBrowser; + getTabBrowser = tab => tab.linkedBrowser; +} + +function* iterBrowserWindows() { + for (let win of Services.wm.getEnumerator("navigator:browser")) { + if (!win.closed && getBrowserApp(win)) { + yield win; + } + } +} + +let initialTabs = new Map(); +for (let win of iterBrowserWindows()) { + initialTabs.set(win, new Set(getBrowserApp(win).tabs)); +} + +addMessageListener("check-cleanup", extensionId => { + Services.console.unregisterListener(listener); + + let results = { + extraWindows: [], + extraTabs: [], + }; + + for (let win of iterBrowserWindows()) { + if (initialTabs.has(win)) { + let tabs = initialTabs.get(win); + + for (let tab of getBrowserApp(win).tabs) { + if (!tabs.has(tab)) { + results.extraTabs.push(getTabBrowser(tab).currentURI.spec); + } + } + } else { + results.extraWindows.push( + Array.from(win.gBrowser.tabs, tab => getTabBrowser(tab).currentURI.spec) + ); + } + } + + initialTabs = null; + + sendAsyncMessage("cleanup-results", results); +}); diff --git a/toolkit/components/extensions/test/mochitest/chrome_head.js b/toolkit/components/extensions/test/mochitest/chrome_head.js new file mode 100644 index 0000000000..3918c74e44 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome_head.js @@ -0,0 +1 @@ +"use strict"; diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html new file mode 100644 index 0000000000..663ebc6112 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="file_WebNavigation_page2.html" width="200" height="200"></iframe> + +<form> +</form> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html new file mode 100644 index 0000000000..cc1acc83d6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> + +<html> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html new file mode 100644 index 0000000000..a0a26a2e9d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<a id="elt" href="file_WebNavigation_page3.html#ref">click me</a> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html new file mode 100644 index 0000000000..24c7a42986 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +<script> +"use strict"; +</script> +</head> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contains_iframe.html b/toolkit/components/extensions/test/mochitest/file_contains_iframe.html new file mode 100644 index 0000000000..e905b5a224 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contains_iframe.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>file contains iframe</title> +</head> +<body> + +<iframe src="//example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_img.html"> +</iframe> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contains_img.html b/toolkit/components/extensions/test/mochitest/file_contains_img.html new file mode 100644 index 0000000000..2b0c3137d6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contains_img.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>file contains img</title> +</head> +<body> + +<img src="file_image_good.png"/> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html new file mode 100644 index 0000000000..6c1675cb47 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> + +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe id="emptyframe"></iframe> + <iframe id="regularframe" src="http://test1.example.com/"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html new file mode 100644 index 0000000000..3b102b3d67 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe srcdoc="<iframe src='http://test1.example.com/'></iframe>"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html b/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html new file mode 100644 index 0000000000..670bad1360 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe id="frame" src="https://test2.example.com/"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_green.html b/toolkit/components/extensions/test/mochitest/file_green.html new file mode 100644 index 0000000000..20755c5b56 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_green.html @@ -0,0 +1,3 @@ +<meta charset=utf-8> +<title>Super green test page</title> +<body style="background: #0f0"> diff --git a/toolkit/components/extensions/test/mochitest/file_green_blue.html b/toolkit/components/extensions/test/mochitest/file_green_blue.html new file mode 100644 index 0000000000..9266b637ba --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_green_blue.html @@ -0,0 +1,16 @@ +<meta charset=utf-8> +<title>Upper square green, rest blue</title> +<style> + div { + position: absolute; + width: 50vw; + height: 50vh; + top: 0; + left: 0; + background-color: lime; + } + :root { + background-color: blue; + } +</style> +<div></div> diff --git a/toolkit/components/extensions/test/mochitest/file_image_bad.png b/toolkit/components/extensions/test/mochitest/file_image_bad.png Binary files differnew file mode 100644 index 0000000000..4c3be50847 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_bad.png diff --git a/toolkit/components/extensions/test/mochitest/file_image_good.png b/toolkit/components/extensions/test/mochitest/file_image_good.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_good.png diff --git a/toolkit/components/extensions/test/mochitest/file_image_great.png b/toolkit/components/extensions/test/mochitest/file_image_great.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_great.png diff --git a/toolkit/components/extensions/test/mochitest/file_image_redirect.png b/toolkit/components/extensions/test/mochitest/file_image_redirect.png Binary files differnew file mode 100644 index 0000000000..4c3be50847 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_redirect.png diff --git a/toolkit/components/extensions/test/mochitest/file_indexedDB.html b/toolkit/components/extensions/test/mochitest/file_indexedDB.html new file mode 100644 index 0000000000..65b7e0ad2f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_indexedDB.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> +"use strict"; + +const objectStoreName = "Objects"; + +let test = {key: 0, value: "test"}; + +let request = indexedDB.open("WebExtensionTest", 1); +request.onupgradeneeded = event => { + let db = event.target.result; + let objectStore = db.createObjectStore(objectStoreName, + {autoIncrement: 0}); + request = objectStore.add(test.value, test.key); + request.onsuccess = event => { + db.close(); + window.postMessage("indexedDBCreated", "*"); + }; +}; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/toolkit/components/extensions/test/mochitest/file_mixed.html b/toolkit/components/extensions/test/mochitest/file_mixed.html new file mode 100644 index 0000000000..f3c7dda580 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_mixed.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<div id="test">Sample text</div> +<img id="bad-image" src="http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" /> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html b/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html new file mode 100644 index 0000000000..b8fda2369a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>1450965 Skip Cors Check for Early WebExtention Redirects</title> +</head> +<body> + <pre id="c"> + Fetching ... + </pre> + <script> + "use strict"; + let c = document.querySelector("#c"); + const channel = new BroadcastChannel("test_bus"); + function l(t) { c.innerText += `${t}\n`; } + + fetch("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_cors_blocked.txt") + .then(r => r.text()) + .then(t => { + // This Request should have been redirected to /file_sample.txt in + // onBeforeRequest. So the text should be 'Sample' + l(`Loaded: ${t}`); + channel.postMessage(t); + }).catch(e => { + // The Redirect Failed, most likly due to a CORS Error + l(`e`); + channel.postMessage(e.toString()); + }); + </script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html b/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html new file mode 100644 index 0000000000..fe8e5bea44 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1434357: Allow Web Request API to redirect to data: URI</title> +</head> +<body> + <div id="testdiv">foo</div> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_remote_frame.html b/toolkit/components/extensions/test/mochitest/file_remote_frame.html new file mode 100644 index 0000000000..f1b9240092 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_remote_frame.html @@ -0,0 +1,20 @@ +<!DOCTYPE> +<html> + <head> + <meta charset="utf-8"> + <script> + "use strict"; + var response = { + tabs: false, + cookie: document.cookie, + }; + try { + browser.tabs.create({url: "file_sample.html"}); + response.tabs = true; + } catch (e) { + // ok + } + window.parent.postMessage(response, "*"); + </script> + </head> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_sample.html b/toolkit/components/extensions/test/mochitest/file_sample.html new file mode 100644 index 0000000000..aa1ef6e6f4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +<title>file sample</title> +</head> +<body> + +<div id="test">Sample text</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_sample.txt b/toolkit/components/extensions/test/mochitest/file_sample.txt new file mode 100644 index 0000000000..c02cd532b1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.txt @@ -0,0 +1 @@ +Sample
\ No newline at end of file diff --git a/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ b/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ new file mode 100644 index 0000000000..cb762eff80 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/toolkit/components/extensions/test/mochitest/file_script_bad.js b/toolkit/components/extensions/test/mochitest/file_script_bad.js new file mode 100644 index 0000000000..c425122c71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_bad.js @@ -0,0 +1,3 @@ +"use strict"; + +window.failure = true; diff --git a/toolkit/components/extensions/test/mochitest/file_script_good.js b/toolkit/components/extensions/test/mochitest/file_script_good.js new file mode 100644 index 0000000000..14e959aa5c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_good.js @@ -0,0 +1,12 @@ +"use strict"; + +window.success = window.success ? window.success + 1 : 1; + +{ + let scripts = document.getElementsByTagName("script"); + let url = new URL(scripts[scripts.length - 1].src); + let flag = url.searchParams.get("q"); + if (flag) { + window.postMessage(flag, "*"); + } +} diff --git a/toolkit/components/extensions/test/mochitest/file_script_redirect.js b/toolkit/components/extensions/test/mochitest/file_script_redirect.js new file mode 100644 index 0000000000..c425122c71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_redirect.js @@ -0,0 +1,3 @@ +"use strict"; + +window.failure = true; diff --git a/toolkit/components/extensions/test/mochitest/file_script_xhr.js b/toolkit/components/extensions/test/mochitest/file_script_xhr.js new file mode 100644 index 0000000000..ad01f74253 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_xhr.js @@ -0,0 +1,9 @@ +"use strict"; + +var request = new XMLHttpRequest(); +request.open( + "get", + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/xhr_resource", + false +); +request.send(); diff --git a/toolkit/components/extensions/test/mochitest/file_serviceWorker.html b/toolkit/components/extensions/test/mochitest/file_serviceWorker.html new file mode 100644 index 0000000000..d2b99769cc --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_serviceWorker.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + "use strict"; + + navigator.serviceWorker.register("serviceWorker.js").then(() => { + window.postMessage("serviceWorkerRegistered", "*"); + }); + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_iframe_worker.html b/toolkit/components/extensions/test/mochitest/file_simple_iframe_worker.html new file mode 100644 index 0000000000..2ecc24e648 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_iframe_worker.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script type="application/javascript"> +"use strict"; + +fetch("file_simple_iframe.txt"); +const worker = new Worker("file_simple_worker.js?iniframe=true"); +worker.onmessage = (msg) => { + worker.postMessage("file_simple_iframe_worker.txt"); +} + +const sharedworker = new SharedWorker("file_simple_sharedworker.js?iniframe=true"); +sharedworker.port.onmessage = (msg) => { + sharedworker.port.postMessage("file_simple_iframe_sharedworker.txt"); +} +sharedworker.port.start(); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html new file mode 100644 index 0000000000..909a1f9e36 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +let req = new XMLHttpRequest(); +req.open("GET", "/xhr_sandboxed"); +req.send(); + +let sandbox = document.createElement("iframe"); +sandbox.setAttribute("sandbox", "allow-scripts"); +sandbox.setAttribute("src", "file_simple_sandboxed_subframe.html"); +document.documentElement.appendChild(sandbox); +</script> +<img src="file_image_great.png"/> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html new file mode 100644 index 0000000000..a0a437d0eb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_sharedworker.js b/toolkit/components/extensions/test/mochitest/file_simple_sharedworker.js new file mode 100644 index 0000000000..e8776216f1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_sharedworker.js @@ -0,0 +1,11 @@ +"use strict"; + +self.onconnect = async evt => { + const port = evt.ports[0]; + port.onmessage = async message => { + await fetch(message.data); + self.close(); + }; + port.start(); + port.postMessage("loaded"); +}; diff --git a/toolkit/components/extensions/test/mochitest/file_simple_webrequest_worker.html b/toolkit/components/extensions/test/mochitest/file_simple_webrequest_worker.html new file mode 100644 index 0000000000..a90c4509be --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_webrequest_worker.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script type="application/javascript"> +"use strict"; + +fetch("file_simple_toplevel.txt"); +const worker = new Worker("file_simple_worker.js"); +worker.onmessage = (msg) => { + worker.postMessage("file_simple_worker.txt"); +} + +const sharedworker = new SharedWorker("file_simple_sharedworker.js"); +sharedworker.port.onmessage = (msg) => { + dump(`postMessage to sharedworker\n`); + sharedworker.port.postMessage("file_simple_sharedworker.txt"); +} +sharedworker.port.start(); + +</script> +<iframe src="file_simple_iframe_worker.html"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_worker.js b/toolkit/components/extensions/test/mochitest/file_simple_worker.js new file mode 100644 index 0000000000..9638a8e9c2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_worker.js @@ -0,0 +1,8 @@ +"use strict"; + +self.onmessage = async message => { + await fetch(message.data); + self.close(); +}; + +self.postMessage("loaded"); diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr.html new file mode 100644 index 0000000000..1b43f804d9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +let req = new XMLHttpRequest(); +req.open("GET", "https://example.org/example.txt"); +req.send(); +</script> +<img src="file_image_good.png"/> +<iframe src="file_simple_xhr_frame.html"/> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html new file mode 100644 index 0000000000..7f38247ac0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +let req = new XMLHttpRequest(); +req.open("GET", "/xhr_resource"); +req.send(); +</script> +<img src="file_image_bad.png"/> +<iframe src="file_simple_xhr_frame2.html"/> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html new file mode 100644 index 0000000000..6174a0b402 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +let req = new XMLHttpRequest(); +req.open("GET", "/xhr_resource_2"); +req.send(); + +let sandbox = document.createElement("iframe"); +sandbox.setAttribute("sandbox", "allow-scripts"); +sandbox.setAttribute("src", "file_simple_sandboxed_frame.html"); +document.documentElement.appendChild(sandbox); +</script> +<img src="file_image_redirect.png"/> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_slowed_document.sjs b/toolkit/components/extensions/test/mochitest/file_slowed_document.sjs new file mode 100644 index 0000000000..8c42fcc966 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_slowed_document.sjs @@ -0,0 +1,49 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +// This script slows the load of an HTML document so that we can reliably test +// all phases of the load cycle supported by the extension API. + +/* eslint-disable no-unused-vars */ + +const URL = "file_slowed_document.sjs"; + +const DELAY = 2 * 1000; // Delay two seconds before completing the request. + +let nsTimer = Components.Constructor( + "@mozilla.org/timer;1", + "nsITimer", + "initWithCallback" +); + +let timer; + +function handleRequest(request, response) { + response.processAsync(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + response.write(`<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <title></title> + </head> + <body> + `); + + // Note: We need to store a reference to the timer to prevent it from being + // canceled when it's GCed. + timer = new nsTimer( + () => { + if (request.queryString.includes("with-iframe")) { + response.write(`<iframe src="${URL}?r=${Math.random()}"></iframe>`); + } + response.write(`</body></html>`); + response.finish(); + }, + DELAY, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/toolkit/components/extensions/test/mochitest/file_streamfilter.txt b/toolkit/components/extensions/test/mochitest/file_streamfilter.txt new file mode 100644 index 0000000000..56cdd85e1d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_streamfilter.txt @@ -0,0 +1 @@ +Middle diff --git a/toolkit/components/extensions/test/mochitest/file_style_bad.css b/toolkit/components/extensions/test/mochitest/file_style_bad.css new file mode 100644 index 0000000000..8dbc8dc7a4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_bad.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/components/extensions/test/mochitest/file_style_good.css b/toolkit/components/extensions/test/mochitest/file_style_good.css new file mode 100644 index 0000000000..46f9774b5f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_good.css @@ -0,0 +1,3 @@ +#test { + color: red; +} diff --git a/toolkit/components/extensions/test/mochitest/file_style_redirect.css b/toolkit/components/extensions/test/mochitest/file_style_redirect.css new file mode 100644 index 0000000000..8dbc8dc7a4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_redirect.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html new file mode 100644 index 0000000000..63f503ad3c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> + <title>The Title</title> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html new file mode 100644 index 0000000000..87ac7a2f64 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> + +<html> +<head> + <title>Another Title</title> + <link href="file_image_great.png" rel="icon" type="image/png" /> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_third_party.html b/toolkit/components/extensions/test/mochitest/file_third_party.html new file mode 100644 index 0000000000..fc5a326297 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_third_party.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> + +"use strict" + +let url = new URL(location); +let img = new Image(); +img.src = `http://${url.searchParams.get("domain")}/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png`; +document.body.appendChild(img); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html b/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html new file mode 100644 index 0000000000..6ebd54d9a3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body style="background: #ff9"> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html new file mode 100644 index 0000000000..cba3043f71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> + +<html> + <head> + <meta http-equiv="refresh" content="1;dummy_page.html"> + </head> + <body> + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html new file mode 100644 index 0000000000..c5b436979f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> + +<html> + <head> + </head> + <body> + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ new file mode 100644 index 0000000000..574a392a15 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ @@ -0,0 +1 @@ +Refresh: 1;url=dummy_page.html diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html new file mode 100644 index 0000000000..d360bcbb13 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="file_webNavigation_clientRedirect.html" width="200" height="200"></iframe> + +<form> +</form> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html new file mode 100644 index 0000000000..06dbd43741 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="redirection.sjs" width="200" height="200"></iframe> + +<form> +</form> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html new file mode 100644 index 0000000000..307990714b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="file_webNavigation_manualSubframe_page1.html" width="200" height="200"></iframe> + +<form> +</form> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html new file mode 100644 index 0000000000..55bb7aa6ae --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> + +<html> + <body> + <h1>page1</h1> + <a href="file_webNavigation_manualSubframe_page2.html">page2</a> + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html new file mode 100644 index 0000000000..8f589f8bbd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> + +<html> + <body> + <h1>page2</h1> + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_with_about_blank.html b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html new file mode 100644 index 0000000000..af51c2e52a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html @@ -0,0 +1,10 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe id="a_b" src="about:blank"></iframe> + <iframe srcdoc="galactica actual" src="adama"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_with_images.html b/toolkit/components/extensions/test/mochitest/file_with_images.html new file mode 100644 index 0000000000..6a3c090be2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_images.html @@ -0,0 +1,10 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <img src="https://example.com/chrome/toolkit/components/extensions/test/mochitest/file_image_good.png"> + <img src="http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_image_great.png"> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_with_subframes_and_embed.html b/toolkit/components/extensions/test/mochitest/file_with_subframes_and_embed.html new file mode 100644 index 0000000000..348c51f16c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_subframes_and_embed.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<meta charset="utf-8"> + +Load a bunch of iframes with subframes. +<p> +<iframe src="file_contains_iframe.html"></iframe> +<iframe src="file_WebNavigation_page1.html"></iframe> +<iframe src="file_with_xorigin_frame.html"></iframe> + +<p> +Load an embed frame. +<p> +<embed type="text/html" src="file_sample.html"></embed> + +<p> +And an object. +<p> +<object type="text/html" data="file_contains_img.html"></embed> + +<p> +Done. diff --git a/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html b/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html new file mode 100644 index 0000000000..25c60df078 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<meta charset="utf-8"> + +<img src="file_image_great.png"/> +Load a cross-origin iframe from example.net <p> +<iframe src="https://example.net/tests/toolkit/components/extensions/test/mochitest/file_sample.html"></iframe> diff --git a/toolkit/components/extensions/test/mochitest/head.js b/toolkit/components/extensions/test/mochitest/head.js new file mode 100644 index 0000000000..ad8fb2052f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head.js @@ -0,0 +1,117 @@ +"use strict"; + +/* exported AppConstants, Assert, AppTestDelegate */ + +var { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { AppTestDelegate } = SpecialPowers.ChromeUtils.importESModule( + "resource://specialpowers/AppTestDelegate.sys.mjs" +); + +{ + let chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("chrome_cleanup_script.js") + ); + + SimpleTest.registerCleanupFunction(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + + chromeScript.sendAsyncMessage("check-cleanup"); + + let results = await chromeScript.promiseOneMessage("cleanup-results"); + chromeScript.destroy(); + + if (results.extraWindows.length || results.extraTabs.length) { + ok( + false, + `Test left extra windows or tabs: ${JSON.stringify(results)}\n` + ); + } + }); +} + +let Assert = { + // Cut-down version based on Assert.sys.mjs. Only supports regexp and objects as + // the expected variables. + rejects(promise, expected, msg) { + return promise.then( + () => { + ok(false, msg); + }, + actual => { + let matched = false; + if (Object.prototype.toString.call(expected) == "[object RegExp]") { + if (expected.test(actual)) { + matched = true; + } + } else if (actual instanceof expected) { + matched = true; + } + + if (matched) { + ok(true, msg); + } else { + ok(false, `Unexpected exception for "${msg}": ${actual}`); + } + } + ); + }, +}; + +/* exported waitForLoad */ + +function waitForLoad(win) { + return new Promise(resolve => { + win.addEventListener( + "load", + function () { + resolve(); + }, + { capture: true, once: true } + ); + }); +} + +/* exported loadChromeScript */ +function loadChromeScript(fn) { + let wrapper = ` +(${fn.toString()})();`; + + return SpecialPowers.loadChromeScript(new Function(wrapper)); +} + +/* exported consoleMonitor */ +let consoleMonitor = { + start(messages) { + this.chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("mochitest_console.js") + ); + this.chromeScript.sendAsyncMessage("consoleStart", messages); + }, + + async finished() { + let done = this.chromeScript.promiseOneMessage("consoleDone").then(done => { + this.chromeScript.destroy(); + return done; + }); + this.chromeScript.sendAsyncMessage("waitForConsole"); + let test = await done; + ok(test.ok, test.message); + }, +}; +/* exported waitForState */ + +function waitForState(sw, state) { + return new Promise(resolve => { + if (sw.state === state) { + return resolve(); + } + sw.addEventListener("statechange", function onStateChange() { + if (sw.state === state) { + sw.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); +} diff --git a/toolkit/components/extensions/test/mochitest/head_cookies.js b/toolkit/components/extensions/test/mochitest/head_cookies.js new file mode 100644 index 0000000000..610c800c94 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_cookies.js @@ -0,0 +1,287 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported testCookies */ +/* import-globals-from head.js */ + +async function testCookies(options) { + // Changing the options object is a bit of a hack, but it allows us to easily + // pass an expiration date to the background script. + options.expiry = Date.now() / 1000 + 3600; + + async function background(backgroundOptions) { + // Ask the parent scope to change some cookies we may or may not have + // permission for. + let awaitChanges = new Promise(resolve => { + browser.test.onMessage.addListener(msg => { + browser.test.assertEq("cookies-changed", msg, "browser.test.onMessage"); + resolve(); + }); + }); + + let changed = []; + browser.cookies.onChanged.addListener(event => { + changed.push(`${event.cookie.name}:${event.cause}`); + }); + browser.test.sendMessage("change-cookies"); + + // Try to access some cookies in various ways. + let { url, domain, secure } = backgroundOptions; + + let failures = 0; + let tallyFailure = error => { + failures++; + }; + + try { + await awaitChanges; + + let cookie = await browser.cookies.get({ url, name: "foo" }); + browser.test.assertEq( + backgroundOptions.shouldPass, + cookie != null, + "should pass == get cookie" + ); + + let cookies = await browser.cookies.getAll({ domain }); + if (backgroundOptions.shouldPass) { + browser.test.assertEq(2, cookies.length, "expected number of cookies"); + } else { + browser.test.assertEq(0, cookies.length, "expected number of cookies"); + } + + await Promise.all([ + browser.cookies + .set({ + url, + domain, + secure, + name: "foo", + value: "baz", + expirationDate: backgroundOptions.expiry, + }) + .catch(tallyFailure), + browser.cookies + .set({ + url, + domain, + secure, + name: "bar", + value: "quux", + expirationDate: backgroundOptions.expiry, + }) + .catch(tallyFailure), + browser.cookies.remove({ url, name: "deleted" }), + ]); + + if (backgroundOptions.shouldPass) { + // The order of eviction events isn't guaranteed, so just check that + // it's there somewhere. + let evicted = changed.indexOf("evicted:evicted"); + if (evicted < 0) { + browser.test.fail("got no eviction event"); + } else { + browser.test.succeed("got eviction event"); + changed.splice(evicted, 1); + } + + browser.test.assertEq( + "x:explicit,x:overwrite,x:explicit,x:explicit,foo:overwrite,foo:explicit,bar:explicit,deleted:explicit", + changed.join(","), + "expected changes" + ); + } else { + browser.test.assertEq("", changed.join(","), "expected no changes"); + } + + if (!(backgroundOptions.shouldPass || backgroundOptions.shouldWrite)) { + browser.test.assertEq(2, failures, "Expected failures"); + } else { + browser.test.assertEq(0, failures, "Expected no failures"); + } + + browser.test.notifyPass("cookie-permissions"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("cookie-permissions"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: options.permissions, + }, + + background: `(${background})(${JSON.stringify(options)})`, + }); + + let stepOne = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage } = this; + addMessageListener("options", options => { + let domain = options.domain.replace(/^\.?/, "."); + // This will be evicted after we add a fourth cookie. + Services.cookies.add( + domain, + "/", + "evicted", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + // This will be modified by the background script. + Services.cookies.add( + domain, + "/", + "foo", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + // This will be deleted by the background script. + Services.cookies.add( + domain, + "/", + "deleted", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + sendAsyncMessage("done"); + }); + }); + stepOne.sendAsyncMessage("options", options); + await stepOne.promiseOneMessage("done"); + stepOne.destroy(); + + await extension.startup(); + + await extension.awaitMessage("change-cookies"); + + let stepTwo = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage } = this; + addMessageListener("options", options => { + let domain = options.domain.replace(/^\.?/, "."); + + Services.cookies.add( + domain, + "/", + "x", + "y", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + Services.cookies.add( + domain, + "/", + "x", + "z", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + Services.cookies.remove(domain, "x", "/", {}); + sendAsyncMessage("done"); + }); + }); + stepTwo.sendAsyncMessage("options", options); + await stepTwo.promiseOneMessage("done"); + stepTwo.destroy(); + + extension.sendMessage("cookies-changed"); + + await extension.awaitFinish("cookie-permissions"); + await extension.unload(); + + let stepThree = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage, assert } = this; + let cookieSvc = Services.cookies; + + function getCookies(host) { + let cookies = []; + for (let cookie of cookieSvc.getCookiesFromHost(host, {})) { + cookies.push(cookie); + } + return cookies.sort((a, b) => a.name.localeCompare(b.name)); + } + + addMessageListener("options", options => { + let cookies = getCookies(options.domain); + + if (options.shouldPass) { + assert.equal(cookies.length, 2, "expected two cookies for host"); + + assert.equal(cookies[0].name, "bar", "correct cookie name"); + assert.equal(cookies[0].value, "quux", "correct cookie value"); + + assert.equal(cookies[1].name, "foo", "correct cookie name"); + assert.equal(cookies[1].value, "baz", "correct cookie value"); + } else if (options.shouldWrite) { + // Note: |shouldWrite| applies only when |shouldPass| is false. + // This is necessary because, unfortunately, websites (and therefore web + // extensions) are allowed to write some cookies which they're not allowed + // to read. + assert.equal(cookies.length, 3, "expected three cookies for host"); + + assert.equal(cookies[0].name, "bar", "correct cookie name"); + assert.equal(cookies[0].value, "quux", "correct cookie value"); + + assert.equal(cookies[1].name, "deleted", "correct cookie name"); + + assert.equal(cookies[2].name, "foo", "correct cookie name"); + assert.equal(cookies[2].value, "baz", "correct cookie value"); + } else { + assert.equal(cookies.length, 2, "expected two cookies for host"); + + assert.equal(cookies[0].name, "deleted", "correct second cookie name"); + + assert.equal(cookies[1].name, "foo", "correct cookie name"); + assert.equal(cookies[1].value, "bar", "correct cookie value"); + } + + for (let cookie of cookies) { + cookieSvc.remove(cookie.host, cookie.name, "/", {}); + } + // Make sure we don't silently poison subsequent tests if something goes wrong. + assert.equal(getCookies(options.domain).length, 0, "cookies cleared"); + sendAsyncMessage("done"); + }); + }); + stepThree.sendAsyncMessage("options", options); + await stepThree.promiseOneMessage("done"); + stepThree.destroy(); +} diff --git a/toolkit/components/extensions/test/mochitest/head_notifications.js b/toolkit/components/extensions/test/mochitest/head_notifications.js new file mode 100644 index 0000000000..bba3f59d49 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_notifications.js @@ -0,0 +1,171 @@ +"use strict"; + +/* exported MockAlertsService */ + +function mockServicesChromeScript() { + /* eslint-env mozilla/chrome-script */ + + const MOCK_ALERTS_CID = Components.ID( + "{48068bc2-40ab-4904-8afd-4cdfb3a385f3}" + ); + const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1"; + + const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" + ); + const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + + let activeNotifications = Object.create(null); + + const mockAlertsService = { + showPersistentNotification: function ( + persistentData, + alert, + alertListener + ) { + this.showAlert(alert, alertListener); + }, + + showAlert: function (alert, listener) { + activeNotifications[alert.name] = { + listener: listener, + cookie: alert.cookie, + title: alert.title, + }; + + // fake async alert show event + if (listener) { + setTimeout(function () { + listener.observe(null, "alertshow", alert.cookie); + }, 100); + } + }, + + showAlertNotification: function ( + imageUrl, + title, + text, + textClickable, + cookie, + alertListener, + name + ) { + this.showAlert( + { + name: name, + cookie: cookie, + title: title, + }, + alertListener + ); + }, + + closeAlert: function (name) { + let alertNotification = activeNotifications[name]; + if (alertNotification) { + if (alertNotification.listener) { + alertNotification.listener.observe( + null, + "alertfinished", + alertNotification.cookie + ); + } + delete activeNotifications[name]; + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIAlertsService"]), + + createInstance: function (iid) { + return this.QueryInterface(iid); + }, + }; + + registrar.registerFactory( + MOCK_ALERTS_CID, + "alerts service", + ALERTS_SERVICE_CONTRACT_ID, + mockAlertsService + ); + + function clickNotifications(doClose) { + // Until we need to close a specific notification, just click them all. + for (let [name, notification] of Object.entries(activeNotifications)) { + let { listener, cookie } = notification; + listener.observe(null, "alertclickcallback", cookie); + if (doClose) { + mockAlertsService.closeAlert(name); + } + } + } + + function closeAllNotifications() { + for (let alertName of Object.keys(activeNotifications)) { + mockAlertsService.closeAlert(alertName); + } + } + + const { addMessageListener, sendAsyncMessage } = this; + + addMessageListener("mock-alert-service:unregister", () => { + closeAllNotifications(); + activeNotifications = null; + registrar.unregisterFactory(MOCK_ALERTS_CID, mockAlertsService); + sendAsyncMessage("mock-alert-service:unregistered"); + }); + + addMessageListener( + "mock-alert-service:click-notifications", + clickNotifications + ); + + addMessageListener( + "mock-alert-service:close-notifications", + closeAllNotifications + ); + + sendAsyncMessage("mock-alert-service:registered"); +} + +const MockAlertsService = { + async register() { + if (this._chromeScript) { + throw new Error("MockAlertsService already registered"); + } + this._chromeScript = SpecialPowers.loadChromeScript( + mockServicesChromeScript + ); + await this._chromeScript.promiseOneMessage("mock-alert-service:registered"); + }, + async unregister() { + if (!this._chromeScript) { + throw new Error("MockAlertsService not registered"); + } + this._chromeScript.sendAsyncMessage("mock-alert-service:unregister"); + return this._chromeScript + .promiseOneMessage("mock-alert-service:unregistered") + .then(() => { + this._chromeScript.destroy(); + this._chromeScript = null; + }); + }, + async clickNotifications() { + // Most implementations of the nsIAlertsService automatically close upon click. + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:click-notifications", + true + ); + }, + async clickNotificationsWithoutClose() { + // The implementation on macOS does not automatically close the notification. + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:click-notifications", + false + ); + }, + async closeNotifications() { + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:close-notifications" + ); + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js b/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js new file mode 100644 index 0000000000..dfec90b3e0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js @@ -0,0 +1,45 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported checkSitePermissions */ + +const { Services } = SpecialPowers; +const { NetUtil } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/NetUtil.jsm" +); + +function checkSitePermissions(uuid, expectedPermAction, assertMessage) { + if (!uuid) { + throw new Error( + "checkSitePermissions should not be called with an undefined uuid" + ); + } + + const baseURI = NetUtil.newURI(`moz-extension://${uuid}/`); + const principal = Services.scriptSecurityManager.createContentPrincipal( + baseURI, + {} + ); + + const sitePermissions = { + webextUnlimitedStorage: Services.perms.testPermissionFromPrincipal( + principal, + "WebExtensions-unlimitedStorage" + ), + persistentStorage: Services.perms.testPermissionFromPrincipal( + principal, + "persistent-storage" + ), + }; + + for (const [sitePermissionName, actualPermAction] of Object.entries( + sitePermissions + )) { + is( + actualPermAction, + expectedPermAction, + `The extension "${sitePermissionName}" SitePermission ${assertMessage} as expected` + ); + } +} diff --git a/toolkit/components/extensions/test/mochitest/head_webrequest.js b/toolkit/components/extensions/test/mochitest/head_webrequest.js new file mode 100644 index 0000000000..9e6b5cc910 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js @@ -0,0 +1,481 @@ +"use strict"; + +let commonEvents = { + onBeforeRequest: [{ urls: ["<all_urls>"] }, ["blocking"]], + onBeforeSendHeaders: [ + { urls: ["<all_urls>"] }, + ["blocking", "requestHeaders"], + ], + onSendHeaders: [{ urls: ["<all_urls>"] }, ["requestHeaders"]], + onBeforeRedirect: [{ urls: ["<all_urls>"] }], + onHeadersReceived: [ + { urls: ["<all_urls>"] }, + ["blocking", "responseHeaders"], + ], + // Auth tests will need to set their own events object + // "onAuthRequired": [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]], + onResponseStarted: [{ urls: ["<all_urls>"] }], + onCompleted: [{ urls: ["<all_urls>"] }, ["responseHeaders"]], + onErrorOccurred: [{ urls: ["<all_urls>"] }], +}; + +function background(events) { + const IP_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + + let expect; + let ignore; + let defaultOrigin; + let watchAuth = Object.keys(events).includes("onAuthRequired"); + let expectedIp = null; + + browser.test.onMessage.addListener((msg, expected) => { + if (msg !== "set-expected") { + return; + } + expect = expected.expect; + defaultOrigin = expected.origin; + ignore = expected.ignore; + let promises = []; + // Initialize some stuff we'll need in the tests. + for (let entry of Object.values(expect)) { + // a place for the test infrastructure to store some state. + entry.test = {}; + // Each entry in expected gets a Promise that will be resolved in the + // last event for that entry. This will either be onCompleted, or the + // last entry if an events list was provided. + promises.push( + new Promise(resolve => { + entry.test.resolve = resolve; + }) + ); + // If events was left undefined, we're expecting all normal events we're + // listening for, exclude onBeforeRedirect and onErrorOccurred + if (entry.events === undefined) { + entry.events = Object.keys(events).filter( + name => name != "onErrorOccurred" && name != "onBeforeRedirect" + ); + } + if (entry.optional_events === undefined) { + entry.optional_events = []; + } + } + // When every expected entry has finished our test is done. + Promise.all(promises).then(() => { + browser.test.sendMessage("done"); + }); + browser.test.sendMessage("continue"); + }); + + // Retrieve the per-file/test expected values. + function getExpected(details) { + let url = new URL(details.url); + let filename = url.pathname.split("/").pop(); + if (ignore && ignore.includes(filename)) { + return; + } + let expected = expect[filename]; + if (!expected) { + browser.test.fail(`unexpected request ${filename}`); + return; + } + // Save filename for redirect verification. + expected.test.filename = filename; + return expected; + } + + // Process any test header modifications that can happen in request or response phases. + // If a test includes headers, it needs a complete header object, no undefined + // objects even if empty: + // request: { + // add: {"HeaderName": "value",}, + // modify: {"HeaderName": "value",}, + // remove: ["HeaderName",], + // }, + // response: { + // add: {"HeaderName": "value",}, + // modify: {"HeaderName": "value",}, + // remove: ["HeaderName",], + // }, + function processHeaders(phase, expected, details) { + // This should only happen once per phase [request|response]. + browser.test.assertFalse( + !!expected.test[phase], + `First processing of headers for ${phase}` + ); + expected.test[phase] = true; + + let headers = details[`${phase}Headers`]; + browser.test.assertTrue( + Array.isArray(headers), + `${phase}Headers array present` + ); + + let { add, modify, remove } = expected.headers[phase]; + + for (let name in add) { + browser.test.assertTrue( + !headers.find(h => h.name === name), + `header ${name} to be added not present yet in ${phase}Headers` + ); + let header = { name: name }; + if (name.endsWith("-binary")) { + header.binaryValue = Array.from(add[name], c => c.charCodeAt(0)); + } else { + header.value = add[name]; + } + headers.push(header); + } + + let modifiedAny = false; + for (let header of headers) { + if (header.name.toLowerCase() in modify) { + header.value = modify[header.name.toLowerCase()]; + modifiedAny = true; + } + } + browser.test.assertTrue( + modifiedAny, + `at least one ${phase}Headers element to modify` + ); + + let deletedAny = false; + for (let j = headers.length; j-- > 0; ) { + if (remove.includes(headers[j].name.toLowerCase())) { + headers.splice(j, 1); + deletedAny = true; + } + } + browser.test.assertTrue( + deletedAny, + `at least one ${phase}Headers element to delete` + ); + + return headers; + } + + // phase is request or response. + function checkHeaders(phase, expected, details) { + if (!/^https?:/.test(details.url)) { + return; + } + + let headers = details[`${phase}Headers`]; + browser.test.assertTrue( + Array.isArray(headers), + `valid ${phase}Headers array` + ); + + let { add, modify, remove } = expected.headers[phase]; + for (let name in add) { + let value = headers.find( + h => h.name.toLowerCase() === name.toLowerCase() + ).value; + browser.test.assertEq( + value, + add[name], + `header ${name} correctly injected in ${phase}Headers` + ); + } + + for (let name in modify) { + let value = headers.find( + h => h.name.toLowerCase() === name.toLowerCase() + ).value; + browser.test.assertEq( + value, + modify[name], + `header ${name} matches modified value` + ); + } + + for (let name of remove) { + let found = headers.find( + h => h.name.toLowerCase() === name.toLowerCase() + ); + browser.test.assertFalse( + !!found, + `deleted header ${name} still found in ${phase}Headers` + ); + } + } + + let listeners = { + onBeforeRequest(expected, details, result) { + // Save some values to test request consistency in later events. + browser.test.assertTrue( + details.tabId !== undefined, + `tabId ${details.tabId}` + ); + browser.test.assertTrue( + details.requestId !== undefined, + `requestId ${details.requestId}` + ); + // Validate requestId if it's already set, this happens with redirects. + if (expected.test.requestId !== undefined) { + browser.test.assertEq( + "string", + typeof expected.test.requestId, + `requestid ${expected.test.requestId} is string` + ); + browser.test.assertEq( + "string", + typeof details.requestId, + `requestid ${details.requestId} is string` + ); + browser.test.assertEq( + "number", + typeof parseInt(details.requestId, 10), + "parsed requestid is number" + ); + browser.test.assertEq( + expected.test.requestId, + details.requestId, + "redirects will keep the same requestId" + ); + } else { + // Save any values we want to validate in later events. + expected.test.requestId = details.requestId; + expected.test.tabId = details.tabId; + } + // Tests we don't need to do every event. + browser.test.assertTrue( + details.type.toUpperCase() in browser.webRequest.ResourceType, + `valid resource type ${details.type}` + ); + if (details.type == "main_frame") { + browser.test.assertEq( + 0, + details.frameId, + "frameId is zero when type is main_frame, see bug 1329299" + ); + } + }, + onBeforeSendHeaders(expected, details, result) { + if (expected.headers && expected.headers.request) { + result.requestHeaders = processHeaders("request", expected, details); + } + if (expected.redirect) { + browser.test.log(`${name} redirect request`); + result.redirectUrl = details.url.replace( + expected.test.filename, + expected.redirect + ); + } + }, + onBeforeRedirect() {}, + onSendHeaders(expected, details, result) { + if (expected.headers && expected.headers.request) { + checkHeaders("request", expected, details); + } + }, + onResponseStarted() {}, + onHeadersReceived(expected, details, result) { + let expectedStatus = expected.status || 200; + // If authentication is being requested we don't fail on the status code. + if (watchAuth && [401, 407].includes(details.statusCode)) { + expectedStatus = details.statusCode; + } + browser.test.assertEq( + expectedStatus, + details.statusCode, + `expected HTTP status received for ${details.url} ${details.statusLine}` + ); + if (expected.headers && expected.headers.response) { + result.responseHeaders = processHeaders("response", expected, details); + } + }, + onAuthRequired(expected, details, result) { + result.authCredentials = expected.authInfo; + }, + onCompleted(expected, details, result) { + // If we have already completed a GET request for this url, + // and it was found, we expect for the response to come fromCache. + // expected.cached may be undefined, force boolean. + if (typeof expected.cached === "boolean") { + let expectCached = + expected.cached && + details.method === "GET" && + details.statusCode != 404; + browser.test.assertEq( + expectCached, + details.fromCache, + "fromCache is correct" + ); + } + // We can only tell IPs for non-cached HTTP requests. + if (!details.fromCache && /^https?:/.test(details.url)) { + browser.test.assertTrue( + IP_PATTERN.test(details.ip), + `IP for ${details.url} looks IP-ish: ${details.ip}` + ); + + // We can't easily predict the IP ahead of time, so just make + // sure they're all consistent. + expectedIp = expectedIp || details.ip; + browser.test.assertEq( + expectedIp, + details.ip, + `correct ip for ${details.url}` + ); + } + if (expected.headers && expected.headers.response) { + checkHeaders("response", expected, details); + } + }, + onErrorOccurred(expected, details, result) { + if (expected.error) { + if (Array.isArray(expected.error)) { + browser.test.assertTrue( + expected.error.includes(details.error), + "expected error message received in onErrorOccurred" + ); + } else { + browser.test.assertEq( + expected.error, + details.error, + "expected error message received in onErrorOccurred" + ); + } + } + }, + }; + + function getListener(name) { + return details => { + let result = {}; + browser.test.log(`${name} ${details.requestId} ${details.url}`); + let expected = getExpected(details); + if (!expected) { + return result; + } + let expectedEvent = expected.events[0] == name; + if (expectedEvent) { + expected.events.shift(); + } else { + // e10s vs. non-e10s errors can end with either onCompleted or onErrorOccurred + expectedEvent = expected.optional_events.includes(name); + } + browser.test.assertTrue(expectedEvent, `received ${name}`); + browser.test.assertEq( + expected.type, + details.type, + "resource type is correct" + ); + browser.test.assertEq( + expected.origin || defaultOrigin, + details.originUrl, + "origin is correct" + ); + + if (name != "onBeforeRequest") { + // On events after onBeforeRequest, check the previous values. + browser.test.assertEq( + expected.test.requestId, + details.requestId, + "correct requestId" + ); + browser.test.assertEq( + expected.test.tabId, + details.tabId, + "correct tabId" + ); + } + try { + listeners[name](expected, details, result); + } catch (e) { + browser.test.fail(`unexpected webrequest failure ${name} ${e}`); + } + + if (expected.cancel && expected.cancel == name) { + browser.test.log(`${name} cancel request`); + browser.test.sendMessage("cancelled"); + result.cancel = true; + } + // If we've used up all the events for this test, resolve the promise. + // If something wrong happens and more events come through, there will be + // failures. + if (expected.events.length <= 0) { + expected.test.resolve(); + } + return result; + }; + } + + for (let [name, args] of Object.entries(events)) { + browser.test.log(`adding listener for ${name}`); + try { + browser.webRequest[name].addListener(getListener(name), ...args); + } catch (e) { + browser.test.assertTrue( + /\brequestBody\b/.test(e.message), + "Request body is unsupported" + ); + + // RequestBody is disabled in release builds. + if (!/\brequestBody\b/.test(e.message)) { + throw e; + } + + args.splice(args.indexOf("requestBody"), 1); + browser.webRequest[name].addListener(getListener(name), ...args); + } + } +} + +/* exported makeExtension */ + +function makeExtension(events = commonEvents) { + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + background: `(${background})(${JSON.stringify(events)})`, + }); +} + +/* exported addStylesheet */ + +function addStylesheet(file) { + let link = document.createElement("link"); + link.setAttribute("rel", "stylesheet"); + link.setAttribute("href", file); + document.body.appendChild(link); +} + +/* exported addLink */ + +function addLink(file) { + let a = document.createElement("a"); + a.setAttribute("href", file); + a.setAttribute("target", "_blank"); + a.setAttribute("rel", "opener"); + document.body.appendChild(a); + return a; +} + +/* exported addImage */ + +function addImage(file) { + let img = document.createElement("img"); + img.setAttribute("src", file); + document.body.appendChild(img); +} + +/* exported addScript */ + +function addScript(file) { + let script = document.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("src", file); + document.getElementsByTagName("head").item(0).appendChild(script); +} + +/* exported addFrame */ + +function addFrame(file) { + let frame = document.createElement("iframe"); + frame.setAttribute("width", "200"); + frame.setAttribute("height", "200"); + frame.setAttribute("src", file); + document.body.appendChild(frame); +} diff --git a/toolkit/components/extensions/test/mochitest/hsts.sjs b/toolkit/components/extensions/test/mochitest/hsts.sjs new file mode 100644 index 0000000000..52b9dd340b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/hsts.sjs @@ -0,0 +1,10 @@ +"use strict"; + +function handleRequest(request, response) { + let page = "<!DOCTYPE html><html><body><p>HSTS page</p></body></html>"; + response.setStatusLine(request.httpVersion, "200", "OK"); + response.setHeader("Strict-Transport-Security", "max-age=60"); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Length", page.length + "", false); + response.write(page); +} diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.ini b/toolkit/components/extensions/test/mochitest/mochitest-common.ini new file mode 100644 index 0000000000..83d4bdc41b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini @@ -0,0 +1,350 @@ +[DEFAULT] +tags = condprof +support-files = + chrome_cleanup_script.js + file_WebNavigation_page1.html + file_WebNavigation_page2.html + file_WebNavigation_page3.html + file_WebRequest_page3.html + file_contains_img.html + file_contains_iframe.html + file_green.html + file_green_blue.html + file_contentscript_activeTab.html + file_contentscript_activeTab2.html + file_contentscript_iframe.html + file_image_bad.png + file_image_good.png + file_image_great.png + file_image_redirect.png + file_indexedDB.html + file_mixed.html + file_remote_frame.html + file_sample.html + file_sample.txt + file_sample.txt^headers^ + file_script_bad.js + file_script_good.js + file_script_redirect.js + file_script_xhr.js + file_serviceWorker.html + file_simple_iframe_worker.html + file_simple_sandboxed_frame.html + file_simple_sandboxed_subframe.html + file_simple_xhr.html + file_simple_xhr_frame.html + file_simple_xhr_frame2.html + file_simple_sharedworker.js + file_simple_webrequest_worker.html + file_simple_worker.js + file_slowed_document.sjs + file_streamfilter.txt + file_style_bad.css + file_style_good.css + file_style_redirect.css + file_third_party.html + file_to_drawWindow.html + file_webNavigation_clientRedirect.html + file_webNavigation_clientRedirect_httpHeaders.html + file_webNavigation_clientRedirect_httpHeaders.html^headers^ + file_webNavigation_frameClientRedirect.html + file_webNavigation_frameRedirect.html + file_webNavigation_manualSubframe.html + file_webNavigation_manualSubframe_page1.html + file_webNavigation_manualSubframe_page2.html + file_with_about_blank.html + file_with_subframes_and_embed.html + file_with_xorigin_frame.html + head.js + head_cookies.js + head_notifications.js + head_unlimitedStorage.js + head_webrequest.js + hsts.sjs + mochitest_console.js + oauth.html + redirect_auto.sjs + redirection.sjs + return_headers.sjs + serviceWorker.js + slow_response.sjs + webrequest_worker.js + !/dom/tests/mochitest/geolocation/network_geolocation.sjs + !/toolkit/components/passwordmgr/test/authenticate.sjs + file_redirect_data_uri.html + file_redirect_cors_bypass.html + file_tabs_permission_page1.html + file_tabs_permission_page2.html +prefs = + security.mixed_content.upgrade_display_content=false + browser.chrome.guess_favicon=true + +[test_check_startupcache.html] +[test_ext_action.html] +[test_ext_activityLog.html] +skip-if = + os == 'android' + tsan # Times out on TSan, bug 1612707 + xorigin # Inconsistent pass/fail in opt and debug + http3 +[test_ext_async_clipboard.html] +skip-if = toolkit == 'android' || tsan # near-permafail after landing bug 1270059: Bug 1523131. tsan: bug 1612707 +[test_ext_background_canvas.html] +[test_ext_background_page.html] +skip-if = (toolkit == 'android') # android doesn't have devtools +[test_ext_background_page_dpi.html] +[test_ext_browserAction_openPopup.html] +skip-if = + http3 +[test_ext_browserAction_openPopup_incognito_window.html] +skip-if = os == "android" # cannot open private windows - bug 1372178 +[test_ext_browserAction_openPopup_windowId.html] +skip-if = os == "android" # only the current window is supported - bug 1795956 +[test_ext_browserAction_openPopup_without_pref.html] +[test_ext_browsingData_indexedDB.html] +skip-if = + http3 +[test_ext_browsingData_localStorage.html] +skip-if = + http3 +[test_ext_browsingData_pluginData.html] +[test_ext_browsingData_serviceWorkers.html] +skip-if = + condprof # "Wait for 2 service workers to be registered - timed out after 50 tries." + http3 +[test_ext_browsingData_settings.html] +[test_ext_canvas_resistFingerprinting.html] +skip-if = + http3 +[test_ext_clipboard.html] +skip-if = + os == 'android' + http3 +[test_ext_clipboard_image.html] +skip-if = headless # Bug 1405872 +[test_ext_contentscript_about_blank.html] +skip-if = + os == 'android' # bug 1369440 + condprof #: "exactly 7 more scripts ran - got 11, expected 10" + http3 +[test_ext_contentscript_activeTab.html] +skip-if = + http3 +[test_ext_contentscript_cache.html] +skip-if = (os == 'linux' && debug) || (toolkit == 'android' && debug) # bug 1348241 +fail-if = xorigin # TypeError: can't access property "staticScripts", ext is undefined - Should not throw any errors +[test_ext_contentscript_canvas.html] +skip-if = (os == 'android') || (verify && debug && (os == 'linux')) # Bug 1617062 +[test_ext_contentscript_devtools_metadata.html] +skip-if = + http3 +[test_ext_contentscript_fission_frame.html] +skip-if = + http3 +[test_ext_contentscript_getFrameId.html] +[test_ext_contentscript_incognito.html] +skip-if = + os == 'android' # Android does not support multiple windows. + http3 +[test_ext_contentscript_permission.html] +skip-if = tsan # Times out on TSan, bug 1612707 +[test_ext_cookies.html] +skip-if = + os == 'android' || tsan # Times out on TSan intermittently, bug 1615184; not supported on Android yet + condprof #: "one tabId returned for store - Expected: 1, Actual: 3" + http3 +[test_ext_cookies_containers.html] +[test_ext_cookies_expiry.html] +[test_ext_cookies_first_party.html] +[test_ext_cookies_incognito.html] +skip-if = os == 'android' # Bug 1513544 Android does not support multiple windows. +[test_ext_cookies_permissions_bad.html] +[test_ext_cookies_permissions_good.html] +[test_ext_dnr_other_extensions.html] +[test_ext_dnr_tabIds.html] +[test_ext_dnr_upgradeScheme.html] +skip-if = + http3 +[test_ext_downloads_download.html] +[test_ext_embeddedimg_iframe_frameAncestors.html] +skip-if = + http3 +[test_ext_exclude_include_globs.html] +skip-if = + http3 +[test_ext_extension_iframe_messaging.html] +skip-if = + http3 +[test_ext_external_messaging.html] +[test_ext_generate.html] +[test_ext_geolocation.html] +skip-if = os == 'android' # Android support Bug 1336194 +[test_ext_identity.html] +skip-if = + win11_2009 && !debug && socketprocess_networking # Bug 1777016 + os == 'android' || tsan # unsupported. tsan: bug 1612707 +[test_ext_idle.html] +skip-if = tsan # Times out on TSan, bug 1612707 +[test_ext_inIncognitoContext_window.html] +skip-if = os == 'android' # Android does not support multiple windows. +[test_ext_listener_proxies.html] +[test_ext_new_tab_processType.html] +skip-if = + verify && debug && (os == 'linux' || os == 'mac') + condprof #: Page URL should match - got "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_serviceWorker.html", expected "https://example.com/" + http3 +[test_ext_notifications.html] +skip-if = os == 'android' # Not supported on Android yet +[test_ext_optional_permissions.html] +[test_ext_protocolHandlers.html] +skip-if = (toolkit == 'android') # bug 1342577 +[test_ext_redirect_jar.html] +skip-if = os == 'win' && (debug || asan) # Bug 1563440 +[test_ext_request_urlClassification.html] +skip-if = + os == 'android' # Bug 1615427 + http3 +[test_ext_runtime_connect.html] +skip-if = + http3 +[test_ext_runtime_connect_iframe.html] +[test_ext_runtime_connect_twoway.html] +skip-if = + http3 +[test_ext_runtime_connect2.html] +skip-if = + http3 +[test_ext_runtime_disconnect.html] +skip-if = + http3 +[test_ext_script_filenames.html] +[test_ext_scripting_contentScripts.html] +skip-if = + http3 +[test_ext_scripting_executeScript.html] +skip-if = + http3 +[test_ext_scripting_executeScript_activeTab.html] +skip-if = + http3 +[test_ext_scripting_executeScript_injectImmediately.html] +skip-if = + http3 +[test_ext_scripting_insertCSS.html] +skip-if = + http3 +[test_ext_scripting_permissions.html] +skip-if = + http3 +[test_ext_scripting_removeCSS.html] +skip-if = + http3 +[test_ext_sendmessage_doublereply.html] +skip-if = + http3 +[test_ext_sendmessage_frameId.html] +[test_ext_sendmessage_no_receiver.html] +skip-if = + http3 +[test_ext_sendmessage_reply.html] +skip-if = + http3 +[test_ext_sendmessage_reply2.html] +skip-if = + os == 'android' + http3 +[test_ext_storage_manager_capabilities.html] +skip-if = + xorigin # JavaScript Error: "SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object" {file: "https://example.com/tests/SimpleTest/TestRunner.js" line: 157} + http3 +scheme=https +[test_ext_storage_smoke_test.html] +[test_ext_streamfilter_multiple.html] +skip-if = + !debug # Bug 1628642 + os == 'linux' # Bug 1628642 +[test_ext_streamfilter_processswitch.html] +skip-if = + http3 +[test_ext_subframes_privileges.html] +skip-if = + os == 'android' || verify # bug 1489771 + http3 +[test_ext_tabs_captureTab.html] +skip-if = + http3 +[test_ext_tabs_executeScript_good.html] +skip-if = + http3 +[test_ext_tabs_create_cookieStoreId.html] +[test_ext_tabs_query_popup.html] +[test_ext_tabs_permissions.html] +skip-if = + http3 +[test_ext_tabs_sendMessage.html] +skip-if = + http3 +[test_ext_test.html] +skip-if = + http3 +[test_ext_unlimitedStorage.html] +skip-if = os == 'android' +[test_ext_web_accessible_resources.html] +skip-if = (os == 'android' && debug) || (os == "linux" && bits == 64) # bug 1397615, bug 1618231 +[test_ext_web_accessible_incognito.html] +skip-if = (os == 'android') # bug 1397615, bug 1513544 +[test_ext_webnavigation.html] +skip-if = + (os == 'android' && debug) # bug 1397615 + http3 +[test_ext_webnavigation_filters.html] +skip-if = + (os == 'android' && debug) || (verify && (os == 'linux' || os == 'mac')) # bug 1397615 + http3 +[test_ext_webnavigation_incognito.html] +skip-if = + os == 'android' # bug 1513544 + http3 +[test_ext_webrequest_and_proxy_filter.html] +skip-if = + http3 +[test_ext_webrequest_auth.html] +skip-if = + os == 'android' + http3 +[test_ext_webrequest_background_events.html] +[test_ext_webrequest_basic.html] +skip-if = + os == 'android' && debug # bug 1397615 + tsan # bug 1612707 + xorigin # JavaScript Error: "SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object" {file: "http://mochi.false-test:8888/tests/SimpleTest/TestRunner.js" line: 157}] + os == "linux" && bits == 64 && !debug && asan # Bug 1633189 + http3 +[test_ext_webrequest_errors.html] +skip-if = + tsan + http3 +[test_ext_webrequest_filter.html] +skip-if = + os == 'android' && debug || tsan # bug 1452348. tsan: bug 1612707 + os == 'linux' && bits == 64 && !debug && xorigin # Bug 1756023 +[test_ext_webrequest_frameId.html] +[test_ext_webrequest_getSecurityInfo.html] +skip-if = + http3 +[test_ext_webrequest_hsts.html] +https_first_disabled = true +skip-if = + http3 +[test_ext_webrequest_upgrade.html] +https_first_disabled = true +[test_ext_webrequest_upload.html] +skip-if = os == 'android' # Currently fails in emulator tests +[test_ext_webrequest_redirect_bypass_cors.html] +[test_ext_webrequest_redirect_data_uri.html] +[test_ext_webrequest_worker.html] +[test_ext_window_postMessage.html] +skip-if = + http3 +# test_startup_canary.html is at the bottom to minimize the time spent waiting in the test. +[test_startup_canary.html] diff --git a/toolkit/components/extensions/test/mochitest/mochitest-remote.ini b/toolkit/components/extensions/test/mochitest/mochitest-remote.ini new file mode 100644 index 0000000000..0a66b11755 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-remote.ini @@ -0,0 +1,11 @@ +[DEFAULT] +tags = webextensions remote-webextensions +skip-if = os == 'android' # Bug 1620091: disable on android until extension process is done +prefs = + extensions.webextensions.remote=true + # We don't want to reset this at the end of the test, so that we don't have + # to spawn a new extension child process for each test unit. + dom.ipc.keepProcessesAlive.extension=1 + +[test_verify_remote_mode.html] +[include:mochitest-common.ini] diff --git a/toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini b/toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini new file mode 100644 index 0000000000..4468349d84 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini @@ -0,0 +1,28 @@ +[DEFAULT] +tags = webextensions sw-webextensions condprof +skip-if = + !e10s # Thunderbird does still run in non e10s mode (and so also with in-process-webextensions mode) + (os == 'android') # Bug 1620091: disable on android until extension process is done + http3 + +prefs = + extensions.webextensions.remote=true + # We don't want to reset this at the end of the test, so that we don't have + # to spawn a new extension child process for each test unit. + dom.ipc.keepProcessesAlive.extension=1 + extensions.backgroundServiceWorker.enabled=true + extensions.backgroundServiceWorker.forceInTestExtension=true + +dupe-manifest = true + +# `test_verify_sw_mode.html` should be the first one, even if it breaks the +# alphabetical order. +[test_verify_sw_mode.html] +[test_ext_scripting_contentScripts.html] +[test_ext_scripting_executeScript.html] +skip-if = true # Bug 1748315 - Add WebIDL bindings for `scripting.executeScript()` +[test_ext_scripting_insertCSS.html] +skip-if = true # Bug 1748318 - Add WebIDL bindings for `tabs` +[test_ext_scripting_removeCSS.html] +skip-if = true # Bug 1748318 - Add WebIDL bindings for `tabs` +[test_ext_test.html] diff --git a/toolkit/components/extensions/test/mochitest/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..f2f6117726 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest.ini @@ -0,0 +1,13 @@ +[DEFAULT] +tags = webextensions in-process-webextensions +prefs = + extensions.webextensions.remote=false + javascript.options.asyncstack_capture_debuggee_only=false +dupe-manifest = true + +[test_verify_non_remote_mode.html] +[test_ext_storage_cleanup.html] +# Bug 1426514 storage_cleanup: clearing localStorage fails with oop + +[include:mochitest-common.ini] +skip-if = os == 'win' # Windows WebExtensions always run OOP diff --git a/toolkit/components/extensions/test/mochitest/mochitest_console.js b/toolkit/components/extensions/test/mochitest/mochitest_console.js new file mode 100644 index 0000000000..582e12b48f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest_console.js @@ -0,0 +1,54 @@ +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { addMessageListener, sendAsyncMessage } = this; + +// Much of the console monitoring code is copied from TestUtils but simplified +// to our needs. +function monitorConsole(msgs) { + function msgMatches(msg, pat) { + for (let k in pat) { + if (!(k in msg)) { + return false; + } + if (pat[k] instanceof RegExp && typeof msg[k] === "string") { + if (!pat[k].test(msg[k])) { + return false; + } + } else if (msg[k] !== pat[k]) { + return false; + } + } + return true; + } + + let counter = 0; + function listener(msg) { + if (msgMatches(msg, msgs[counter])) { + counter++; + } + } + addMessageListener("waitForConsole", () => { + sendAsyncMessage("consoleDone", { + ok: counter >= msgs.length, + message: `monitorConsole | messages left expected at least ${msgs.length} got ${counter}`, + }); + Services.console.unregisterListener(listener); + }); + + Services.console.registerListener(listener); +} + +addMessageListener("consoleStart", messages => { + for (let msg of messages) { + // Message might be a RegExp object from a different compartment, but + // instanceof RegExp will fail. If we have an object, lets just make + // sure. + let message = msg.message; + if (typeof message == "object" && !(message instanceof RegExp)) { + msg.message = new RegExp(message); + } + } + monitorConsole(messages); +}); diff --git a/toolkit/components/extensions/test/mochitest/oauth.html b/toolkit/components/extensions/test/mochitest/oauth.html new file mode 100644 index 0000000000..8b9b1d65ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/oauth.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> + <script> + "use strict"; + + onload = () => { + let url = new URL(location); + if (url.searchParams.get("post")) { + let server_redirect = `${url.searchParams.get("server_uri")}?redirect_uri=${encodeURIComponent(url.searchParams.get("redirect_uri"))}`; + let form = document.forms.testform; + form.setAttribute("action", server_redirect); + form.submit(); + } else { + let end = new URL(url.searchParams.get("redirect_uri")); + end.searchParams.set("access_token", "here ya go"); + location.href = end.href; + } + }; + </script> +</head> +<body> + <form name="testform" action="" method="POST"> + </form> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/redirect_auto.sjs b/toolkit/components/extensions/test/mochitest/redirect_auto.sjs new file mode 100644 index 0000000000..bf7af2556b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/redirect_auto.sjs @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; +Cu.importGlobalProperties(["URLSearchParams", "URL"]); + +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + if (params.has("no_redirect")) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write("ok"); + } else { + if (request.method == "POST") { + response.setStatusLine(request.httpVersion, 303, "Redirected"); + } else { + response.setStatusLine(request.httpVersion, 302, "Moved Temporarily"); + } + let url = new URL( + params.get("redirect_uri") || params.get("default_redirect") + ); + url.searchParams.set("access_token", "here ya go"); + response.setHeader("Location", url.href); + } +} diff --git a/toolkit/components/extensions/test/mochitest/redirection.sjs b/toolkit/components/extensions/test/mochitest/redirection.sjs new file mode 100644 index 0000000000..873a3d41ba --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/redirection.sjs @@ -0,0 +1,6 @@ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 302); + response.setHeader("Location", "./dummy_page.html"); +} diff --git a/toolkit/components/extensions/test/mochitest/return_headers.sjs b/toolkit/components/extensions/test/mochitest/return_headers.sjs new file mode 100644 index 0000000000..46beab8185 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/return_headers.sjs @@ -0,0 +1,19 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported handleRequest */ + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + + let headers = {}; + // Why on earth is this a nsISimpleEnumerator... + let enumerator = request.headers; + while (enumerator.hasMoreElements()) { + let header = enumerator.getNext().data; + headers[header.toLowerCase()] = request.getHeader(header); + } + + response.write(JSON.stringify(headers)); +} diff --git a/toolkit/components/extensions/test/mochitest/serviceWorker.js b/toolkit/components/extensions/test/mochitest/serviceWorker.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/serviceWorker.js diff --git a/toolkit/components/extensions/test/mochitest/slow_response.sjs b/toolkit/components/extensions/test/mochitest/slow_response.sjs new file mode 100644 index 0000000000..d39c4c0bf0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/slow_response.sjs @@ -0,0 +1,60 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +/* eslint-disable no-unused-vars */ + +let { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const DELAY = AppConstants.DEBUG ? 4000 : 800; + +let nsTimer = Components.Constructor( + "@mozilla.org/timer;1", + "nsITimer", + "initWithCallback" +); + +let timer; +function delay() { + return new Promise(resolve => { + timer = nsTimer(resolve, DELAY, Ci.nsITimer.TYPE_ONE_SHOT); + }); +} + +const PARTS = [ + `<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <title></title> + </head> + <body>`, + "Lorem ipsum dolor sit amet, <br>", + "consectetur adipiscing elit, <br>", + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br>", + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <br>", + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br>", + "Excepteur sint occaecat cupidatat non proident, <br>", + "sunt in culpa qui officia deserunt mollit anim id est laborum.<br>", + ` + </body> + </html>`, +]; + +async function handleRequest(request, response) { + response.processAsync(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + + await delay(); + + for (let part of PARTS) { + response.write(`${part}\n`); + await delay(); + } + + response.finish(); +} diff --git a/toolkit/components/extensions/test/mochitest/test_check_startupcache.html b/toolkit/components/extensions/test/mochitest/test_check_startupcache.html new file mode 100644 index 0000000000..01d361ca5e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_check_startupcache.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Check StartupCache</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function check_ExtensionParent_StartupCache_is_non_empty() { + // This test aims to verify that the StartupCache of extensions is populated. + // Ideally, we would load an extension, restart the browser and confirm the + // existence of the StartupCache. That is not possible in a mochitest. + // So we will just read the contents of the StartupCache and verify that it + // populated and assume that it carries over to the next startup. + // The latter is checked in test_startup_canary.html + + const { WebExtensionPolicy } = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services); + // The Mochikit extension is part of the mochitests framework, so the fact + // that this test runs implies that the extension should have been started. + ok( + WebExtensionPolicy.getByID("mochikit@mozilla.org"), + "This test expects the Mochikit extension to be running" + ); + + let chromeScript = loadChromeScript(() => { + const { + ExtensionParent, + } = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm"); + const { StartupCache } = ExtensionParent; + this.sendAsyncMessage("StartupCache_data", StartupCache._data); + }); + + let map = await chromeScript.promiseOneMessage("StartupCache_data"); + chromeScript.destroy(); + + // "manifests" is populated by Extension's parseManifest in Extension.jsm. + const keys = ["manifests", "mochikit@mozilla.org", "2.0", "en-US"]; + for (let key of keys) { + map = map.get(key); + ok(map, `StartupCache data map contains ${key}`); + } + + // At this point `map` is expected to be the return value of + // ExtensionData's parseManifest. + + is( + map?.manifest?.applications?.gecko?.id, + "mochikit@mozilla.org", + "StartupCache.manifests contains a parsed manifest" + ); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html new file mode 100644 index 0000000000..42950c50ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test content script matching a data: URI</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script> +"use strict"; + +add_task(async function test_contentscript_data_uri() { + const target = ExtensionTestUtils.loadExtension({ + files: { + "page.html": `<!DOCTYPE html> + <meta charset="utf-8"> + <iframe id="inherited" src="data:text/html;charset=utf-8,inherited"></iframe> + `, + }, + background() { + browser.test.sendMessage("page", browser.runtime.getURL("page.html")); + }, + }); + + const scripts = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webNavigation"], + content_scripts: [{ + all_frames: true, + matches: ["<all_urls>"], + run_at: "document_start", + css: ["all_urls.css"], + js: ["all_urls.js"], + }], + }, + files: { + "all_urls.css": ` + body { background: yellow; } + `, + "all_urls.js": function() { + document.body.style.color = "red"; + browser.test.assertTrue(location.protocol !== "data:", + `Matched document not a data URI: ${location.href}`); + }, + }, + background() { + browser.webNavigation.onCompleted.addListener(({url, frameId}) => { + browser.test.log(`Document loading complete: ${url}`); + if (frameId === 0) { + browser.test.sendMessage("tab-ready", url); + } + }); + }, + }); + + await target.startup(); + await scripts.startup(); + + // Test extension page with a data: iframe. + const page = await target.awaitMessage("page"); + + // Hold on to the tab by the browser, as extension loads are COOP loads, and + // will break WindowProxy references. + let win = window.open(); + const browserFrame = win.browsingContext.embedderElement; + win.location.href = page; + + await scripts.awaitMessage("tab-ready"); + win = browserFrame.contentWindow; + is(win.location.href, page, "Extension page loaded into a tab"); + is(win.document.readyState, "complete", "Page finished loading"); + + const iframe = win.document.getElementById("inherited").contentWindow; + is(iframe.document.readyState, "complete", "iframe finished loading"); + + const style1 = iframe.getComputedStyle(iframe.document.body); + is(style1.color, "rgb(0, 0, 0)", "iframe text color is unmodified"); + is(style1.backgroundColor, "rgba(0, 0, 0, 0)", "iframe background unmodified"); + + // Test extension tab navigated to a data: URI. + const data = "data:text/html;charset=utf-8,also-inherits"; + win.location.href = data; + + await scripts.awaitMessage("tab-ready"); + win = browserFrame.contentWindow; + is(win.location.href, data, "Extension tab navigated to a data: URI"); + is(win.document.readyState, "complete", "Tab finished loading"); + + const style2 = win.getComputedStyle(win.document.body); + is(style2.color, "rgb(0, 0, 0)", "Tab text color is unmodified"); + is(style2.backgroundColor, "rgba(0, 0, 0, 0)", "Tab background unmodified"); + + win.close(); + await target.unload(); + await scripts.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html new file mode 100644 index 0000000000..224e806288 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for telemetry for content script injection</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script> +"use strict"; + +const HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS"; + +add_task(async function test_contentscript_telemetry() { + // Turn on telemetry and reset it to the previous state once the test is completed. + const telemetryCanRecordBase = SpecialPowers.Services.telemetry.canRecordBase; + SpecialPowers.Services.telemetry.canRecordBase = true; + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.Services.telemetry.canRecordBase = telemetryCanRecordBase; + }); + + function background() { + browser.test.onMessage.addListener(() => { + browser.tabs.executeScript({code: 'browser.test.sendMessage("content-script-run");'}); + }); + } + + let extensionData = { + manifest: { + permissions: ["<all_urls>"], + }, + background, + }; + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://example.com", + true + ); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let histogram = SpecialPowers.Services.telemetry.getHistogramById(HISTOGRAM); + histogram.clear(); + is(histogram.snapshot().sum, 0, + `No data recorded for histogram: ${HISTOGRAM}.`); + + await extension.startup(); + is(histogram.snapshot().sum, 0, + `No data recorded for histogram after startup: ${HISTOGRAM}.`); + + extension.sendMessage(); + await extension.awaitMessage("content-script-run"); + + let histogramSum = histogram.snapshot().sum; + ok(histogramSum > 0, + `Data recorded for first extension for histogram: ${HISTOGRAM}.`); + + await AppTestDelegate.removeTab(window, tab); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html new file mode 100644 index 0000000000..40403dea2b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script unrecognized property on manifest</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest"; + +add_task(async function test_contentscript() { + function background() { + browser.runtime.onMessage.addListener(async (msg) => { + if (msg == "loaded") { + // NOTE: we're removing the tab from here because doing a win.close() + // from the chrome test code is raising a "TypeError: can't access + // dead object" exception. + let tabs = await browser.tabs.query({active: true, currentWindow: true}); + await browser.tabs.remove(tabs[0].id); + + browser.test.notifyPass("content-script-loaded"); + } + }); + } + + function contentScript() { + chrome.runtime.sendMessage("loaded"); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + "unrecognized_property": "with-a-random-value", + }, + ], + }, + background, + + files: { + "content_script.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + SimpleTest.waitForExplicitFinish(); + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{ + message: /Reading manifest: Warning processing content_scripts.*.unrecognized_property: An unexpected property was found/, + }]); + }); + + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + + window.open(`${BASE}/file_sample.html`); + + await Promise.all([extension.awaitFinish("content-script-loaded")]); + info("test page loaded"); + + await extension.unload(); + + SimpleTest.endMonitorConsole(); + await waitForConsole; +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html new file mode 100644 index 0000000000..530937c1ac --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for permissions</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_downloads_open_permission() { + function backgroundScript() { + browser.test.assertEq(browser.downloads.open, undefined, + "`downloads.open` permission is required."); + browser.test.notifyPass("downloads tests"); + } + + let extensionData = { + background: backgroundScript, + manifest: { + permissions: ["downloads"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitFinish("downloads tests"); + await extension.unload(); +}); + +add_task(async function test_downloads_open_requires_user_interaction() { + async function backgroundScript() { + await browser.test.assertRejects( + browser.downloads.open(10), + "downloads.open may only be called from a user input handler", + "The error is informative."); + + browser.test.notifyPass("downloads tests"); + } + + let extensionData = { + background: backgroundScript, + manifest: { + permissions: ["downloads", "downloads.open"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitFinish("downloads tests"); + await extension.unload(); +}); + +add_task(async function downloads_open_invalid_id() { + async function pageScript() { + window.addEventListener("keypress", async function handler() { + try { + await browser.downloads.open(10); + browser.test.sendMessage("download-open.result", {success: true}); + } catch (e) { + browser.test.sendMessage("download-open.result", { + success: false, + error: e.message, + }); + } + window.removeEventListener("keypress", handler); + }); + + browser.test.sendMessage("page-ready"); + } + + let extensionData = { + background() { + browser.test.sendMessage("ready", browser.runtime.getURL("page.html")); + }, + files: { + "foo.txt": "It's the file called foo.txt.", + "page.html": `<html><head> + <script src="page.js"><\/script> + </head></html>`, + "page.js": pageScript, + }, + manifest: { + permissions: ["downloads", "downloads.open"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let url = await extension.awaitMessage("ready"); + let win = window.open(); + let browserFrame = win.browsingContext.embedderElement; + win.location.href = url; + await extension.awaitMessage("page-ready"); + + synthesizeKey("a", {}, browserFrame.contentWindow); + let result = await extension.awaitMessage("download-open.result"); + + is(result.success, false, "Opening download fails."); + is(result.error, "Invalid download id 10", "The error is informative."); + + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html new file mode 100644 index 0000000000..64cfcfd289 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html @@ -0,0 +1,257 @@ +<!doctype html> +<html> +<head> + <title>Test downloads.download() saveAs option</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); + +const PROMPTLESS_DOWNLOAD_PREF = "browser.download.useDownloadDir"; + +const DOWNLOAD_FILENAME = "file_download.nonext.txt"; +const DEFAULT_SUBDIR = "subdir"; + +// We need to be able to distinguish files downloaded by the file picker from +// files downloaded without it. +let pickerDir; +let pbPickerDir; // for incognito downloads +let defaultDir; + +add_task(async function setup() { + // Reset DownloadLastDir preferences in case other tests set them. + SpecialPowers.Services.obs.notifyObservers( + null, + "browser:purge-session-history" + ); + + // Set up temporary directories. + let downloadDir = FileUtils.getDir("TmpD", ["downloads"]); + pickerDir = downloadDir.clone(); + pickerDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`Using file picker download directory ${pickerDir.path}`); + pbPickerDir = downloadDir.clone(); + pbPickerDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`Using private browsing file picker download directory ${pbPickerDir.path}`); + defaultDir = downloadDir.clone(); + defaultDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`Using default download directory ${defaultDir.path}`); + let subDir = defaultDir.clone(); + subDir.append(DEFAULT_SUBDIR); + subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + isnot(pickerDir.path, defaultDir.path, + "Should be able to distinguish between files saved with or without the file picker"); + isnot(pickerDir.path, pbPickerDir.path, + "Should be able to distinguish between files saved in and out of private browsing mode"); + + await SpecialPowers.pushPrefEnv({"set": [ + ["browser.download.folderList", 2], + ["browser.download.dir", defaultDir.path], + ]}); + + SimpleTest.registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + pickerDir.remove(true); + pbPickerDir.remove(true); + defaultDir.remove(true); // This also removes DEFAULT_SUBDIR. + }); +}); + +add_task(async function test_downloads_saveAs() { + const pickerFile = pickerDir.clone(); + pickerFile.append(DOWNLOAD_FILENAME); + + const pbPickerFile = pbPickerDir.clone(); + pbPickerFile.append(DOWNLOAD_FILENAME); + + const defaultFile = defaultDir.clone(); + defaultFile.append(DOWNLOAD_FILENAME); + + const {MockFilePicker} = SpecialPowers; + MockFilePicker.init(window); + + function mockFilePickerCallback(expectedStartingDir, pickedFile) { + return fp => { + // Assert that the downloads API correctly sets the starting directory. + ok(fp.displayDirectory.equals(expectedStartingDir), "Got the expected FilePicker displayDirectory"); + + // Assert that the downloads API configures both default properties. + is(fp.defaultString, DOWNLOAD_FILENAME, "Got the expected FilePicker defaultString"); + is(fp.defaultExtension, "txt", "Got the expected FilePicker defaultExtension"); + + MockFilePicker.setFiles([pickedFile]); + }; + } + + function background() { + const url = URL.createObjectURL(new Blob(["file content"])); + browser.test.onMessage.addListener(async (filename, saveAs, isPrivate) => { + try { + let options = { + url, + filename, + incognito: isPrivate, + }; + // Only define the saveAs option if the argument was actually set + if (saveAs !== undefined) { + options.saveAs = saveAs; + } + let id = await browser.downloads.download(options); + browser.downloads.onChanged.addListener(delta => { + if (delta.id == id && delta.state.current === "complete") { + browser.test.sendMessage("done", {ok: true, id}); + } + }); + } catch ({message}) { + browser.test.sendMessage("done", {ok: false, message}); + } + }); + browser.test.sendMessage("ready"); + } + + const manifest = { + background, + incognitoOverride: "spanning", + manifest: {permissions: ["downloads"]}, + }; + const extension = ExtensionTestUtils.loadExtension(manifest); + + await extension.startup(); + await extension.awaitMessage("ready"); + + // options should have the following properties: + // saveAs (Boolean or undefined) + // isPrivate (Boolean) + // fileName (string) + // expectedStartingDir (nsIFile) + // destinationFile (nsIFile) + async function testExpectFilePicker(options) { + ok(!options.destinationFile.exists(), "the file should have been cleaned up properly previously"); + + MockFilePicker.showCallback = mockFilePickerCallback( + options.expectedStartingDir, + options.destinationFile + ); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + extension.sendMessage(options.fileName, options.saveAs, options.isPrivate); + let result = await extension.awaitMessage("done"); + ok(result.ok, `downloads.download() works with saveAs=${options.saveAs}`); + + ok(options.destinationFile.exists(), "the file exists."); + is(options.destinationFile.fileSize, 12, "downloaded file is the correct size"); + options.destinationFile.remove(false); + MockFilePicker.reset(); + + // Test the user canceling the save dialog. + MockFilePicker.returnValue = MockFilePicker.returnCancel; + + extension.sendMessage(options.fileName, options.saveAs, options.isPrivate); + result = await extension.awaitMessage("done"); + + ok(!result.ok, "download rejected if the user cancels the dialog"); + is(result.message, "Download canceled by the user", "with the correct message"); + ok(!options.destinationFile.exists(), "file was not downloaded"); + MockFilePicker.reset(); + } + + async function testNoFilePicker(saveAs) { + ok(!defaultFile.exists(), "the file should have been cleaned up properly previously"); + + extension.sendMessage(DOWNLOAD_FILENAME, saveAs, false); + let result = await extension.awaitMessage("done"); + ok(result.ok, `downloads.download() works with saveAs=${saveAs}`); + + ok(defaultFile.exists(), "the file exists."); + is(defaultFile.fileSize, 12, "downloaded file is the correct size"); + defaultFile.remove(false); + } + + info("Testing that saveAs=true uses the file picker as expected"); + let expectedStartingDir = defaultDir; + let fpOptions = { + saveAs: true, + isPrivate: false, + fileName: DOWNLOAD_FILENAME, + expectedStartingDir: expectedStartingDir, + destinationFile: pickerFile, + }; + await testExpectFilePicker(fpOptions); + + info("Testing that saveas=true reuses last file picker directory"); + fpOptions.expectedStartingDir = pickerDir; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=true in PB reuses last directory"); + let nonPBStartingDir = fpOptions.expectedStartingDir; + fpOptions.isPrivate = true; + fpOptions.destinationFile = pbPickerFile; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=true in PB uses a separate last directory"); + fpOptions.expectedStartingDir = pbPickerDir; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=true in Permanent PB mode ignores the incognito option"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.privatebrowsing.autostart", true]], + }); + fpOptions.isPrivate = false; + fpOptions.expectedStartingDir = pbPickerDir; + await testExpectFilePicker(fpOptions); + + info("Testing that saveas=true reuses the non-PB last directory after private download"); + await SpecialPowers.popPrefEnv(); + fpOptions.isPrivate = false; + fpOptions.expectedStartingDir = nonPBStartingDir; + fpOptions.destinationFile = pickerFile; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=true does not reuse last directory when filename contains a path separator"); + fpOptions.fileName = DEFAULT_SUBDIR + "/" + DOWNLOAD_FILENAME; + let destinationFile = defaultDir.clone(); + destinationFile.append(DEFAULT_SUBDIR); + fpOptions.expectedStartingDir = destinationFile.clone(); + destinationFile.append(DOWNLOAD_FILENAME); + fpOptions.destinationFile = destinationFile; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=false does not use the file picker"); + fpOptions.saveAs = false; + await testNoFilePicker(fpOptions.saveAs); + + // When saveAs is not set, the behavior should be determined by the Firefox + // pref that normally determines whether the "Save As" prompt should be + // displayed. + info(`Testing that the file picker is used when saveAs is not specified ` + + `but ${PROMPTLESS_DOWNLOAD_PREF} is disabled`); + fpOptions.saveAs = undefined; + await SpecialPowers.pushPrefEnv({"set": [ + [PROMPTLESS_DOWNLOAD_PREF, false], + ]}); + await testExpectFilePicker(fpOptions); + + info(`Testing that the file picker is NOT used when saveAs is not ` + + `specified but ${PROMPTLESS_DOWNLOAD_PREF} is enabled`); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.pushPrefEnv({"set": [ + [PROMPTLESS_DOWNLOAD_PREF, true], + ]}); + await testNoFilePicker(fpOptions.saveAs); + + await extension.unload(); + MockFilePicker.cleanup(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html new file mode 100644 index 0000000000..b5fedee7ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html @@ -0,0 +1,116 @@ +<!doctype html> +<html> +<head> + <title>Test downloads.download() uniquify option</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); + +let directory; + +add_task(async function setup() { + directory = FileUtils.getDir("TmpD", ["downloads"]); + directory.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`Using download directory ${directory.path}`); + + await SpecialPowers.pushPrefEnv({"set": [ + ["browser.download.folderList", 2], + ["browser.download.dir", directory.path], + ]}); + + SimpleTest.registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + directory.remove(true); + }); +}); + +add_task(async function test_downloads_uniquify() { + const file = directory.clone(); + file.append("file_download.txt"); + + const unique = directory.clone(); + unique.append("file_download(1).txt"); + + const {MockFilePicker} = SpecialPowers; + MockFilePicker.init(window); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + MockFilePicker.showCallback = fp => { + let file = directory.clone(); + file.append(fp.defaultString); + MockFilePicker.setFiles([file]); + }; + + function background() { + const url = URL.createObjectURL(new Blob(["file content"])); + browser.test.onMessage.addListener(async (filename, saveAs) => { + try { + let id = await browser.downloads.download({ + url, + filename, + saveAs, + conflictAction: "uniquify", + }); + browser.downloads.onChanged.addListener(delta => { + if (delta.id == id && delta.state.current === "complete") { + browser.test.sendMessage("done", {ok: true, id}); + } + }); + } catch ({message}) { + browser.test.sendMessage("done", {ok: false, message}); + } + }); + browser.test.sendMessage("ready"); + } + + const manifest = {background, manifest: {permissions: ["downloads"]}}; + const extension = ExtensionTestUtils.loadExtension(manifest); + + await extension.startup(); + await extension.awaitMessage("ready"); + + async function testUniquify(saveAs) { + info(`Testing conflictAction:"uniquify" with saveAs=${saveAs}`); + + ok(!file.exists(), "downloaded file should have been cleaned up before test ran"); + ok(!unique.exists(), "uniquified file should have been cleaned up before test ran"); + + // Test download without uniquify and create a conflicting file so we can + // test with uniquify. + extension.sendMessage("file_download.txt", saveAs); + let result = await extension.awaitMessage("done"); + ok(result.ok, "downloads.download() works with saveAs"); + + ok(file.exists(), "the file exists."); + is(file.fileSize, 12, "downloaded file is the correct size"); + + // Now that a conflicting file exists, test the uniquify behavior + extension.sendMessage("file_download.txt", saveAs); + result = await extension.awaitMessage("done"); + ok(result.ok, "downloads.download() works with saveAs and uniquify"); + + ok(unique.exists(), "the file exists."); + is(unique.fileSize, 12, "downloaded file is the correct size"); + + file.remove(false); + unique.remove(false); + } + await testUniquify(true); + await testUniquify(false); + + await extension.unload(); + MockFilePicker.cleanup(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html new file mode 100644 index 0000000000..65bf0a50d0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html @@ -0,0 +1,172 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for permissions</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function makeTest(manifestPermissions, optionalPermissions, checkFetch = true) { + return async function() { + function pageScript() { + /* global PERMISSIONS */ + browser.test.onMessage.addListener(async msg => { + if (msg == "set-cookie") { + try { + await browser.cookies.set({ + url: "http://example.com/", + name: "COOKIE", + value: "NOM NOM", + }); + browser.test.sendMessage("set-cookie.result", {success: true}); + } catch (err) { + dump(`set cookie failed with ${err.message}\n`); + browser.test.sendMessage("set-cookie.result", + {success: false, message: err.message}); + } + } else if (msg == "remove") { + browser.permissions.remove(PERMISSIONS).then(result => { + browser.test.sendMessage("remove.result", result); + }); + } else if (msg == "request") { + browser.test.withHandlingUserInput(() => { + browser.permissions.request(PERMISSIONS).then(result => { + browser.test.sendMessage("request.result", result); + }); + }); + } + }); + + browser.test.sendMessage("page-ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.sendMessage("ready", browser.runtime.getURL("page.html")); + }, + + manifest: { + permissions: manifestPermissions, + optional_permissions: [...(optionalPermissions.permissions || []), + ...(optionalPermissions.origins || [])], + + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html"], + js: ["content_script.js"], + }], + }, + + files: { + "content_script.js": async () => { + let url = new URL(window.location.pathname, "http://example.com/"); + fetch(url, {}).then(response => { + browser.test.sendMessage("fetch.result", response.ok); + }).catch(err => { + browser.test.sendMessage("fetch.result", false); + }); + }, + + "page.html": `<html><head> + <script src="page.js"><\/script> + </head></html>`, + + "page.js": `const PERMISSIONS = ${JSON.stringify(optionalPermissions)}; (${pageScript})();`, + }, + }); + + await extension.startup(); + + function call(method) { + extension.sendMessage(method); + return extension.awaitMessage(`${method}.result`); + } + + let base = window.location.href.replace(/^chrome:\/\/mochitests\/content/, + "http://mochi.test:8888"); + let file = new URL("file_sample.html", base); + + async function testContentScript() { + let win = window.open(file); + let result = await extension.awaitMessage("fetch.result"); + win.close(); + return result; + } + + let url = await extension.awaitMessage("ready"); + let win = window.open(); + win.location.href = url; + await extension.awaitMessage("page-ready"); + + // Using the cookies API from an extension page should fail + let result = await call("set-cookie"); + is(result.success, false, "setting cookie failed"); + if (manifestPermissions.includes("cookies")) { + ok(/^Permission denied/.test(result.message), + "setting cookie failed with an appropriate error due to missing host permission"); + } else { + ok(/browser\.cookies is undefined/.test(result.message), + "setting cookie failed since cookies API is not present"); + } + + // Making a cross-origin request from a content script should fail + if (checkFetch) { + result = await testContentScript(); + is(result, false, "fetch() failed from content script due to lack of host permission"); + } + + result = await call("request"); + is(result, true, "permissions.request() succeeded"); + + // Using the cookies API from an extension page should succeed + result = await call("set-cookie"); + is(result.success, true, "setting cookie succeeded"); + + // Making a cross-origin request from a content script should succeed + if (checkFetch) { + result = await testContentScript(); + is(result, true, "fetch() succeeded from content script due to lack of host permission"); + } + + // Now revoke our permissions + result = await call("remove"); + + // The cookies API should once again fail + result = await call("set-cookie"); + is(result.success, false, "setting cookie failed"); + + // As should the cross-origin request from a content script + if (checkFetch) { + result = await testContentScript(); + is(result, false, "fetch() failed from content script due to lack of host permission"); + } + + await extension.unload(); + }; +} + +add_task(function setup() { + // Don't bother with prompts in this test. + return SpecialPowers.pushPrefEnv({ + set: [["extensions.webextOptionalPermissionPrompts", false]], + }); +}); + +const ORIGIN = "*://example.com/"; +add_task(makeTest([], { + permissions: ["cookies"], + origins: [ORIGIN], +})); + +add_task(makeTest(["cookies"], {origins: [ORIGIN]})); +add_task(makeTest([ORIGIN], {permissions: ["cookies"]}, false)); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_svg_context_fill.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_svg_context_fill.html new file mode 100644 index 0000000000..3b022be5ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_svg_context_fill.html @@ -0,0 +1,204 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for permissions</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + <style> + img { + -moz-context-properties: fill; + fill: green; + } + + img, div.ref { + width: 100px; + height: 100px; + } + + div#green { + background: green; + } + + div#red { + background: red; + } + </style> + <h3>Testing on: <span id="test-params"></span></h3> + <table> + <thead> + <tr> + <th>webext image</th> + <th>allowed ref</th> + <th>disallowed ref</th> + </tr> + </thead> + <tbody> + <tr> + <td> + <img id="actual"> + </td> + <td> + <div id="green" class="ref"></div> + </td> + <td> + <div id="red" class="ref"></div> + </td> + </tr> + </tbody> + </table> + +<script type="text/javascript"> +"use strict"; + +const { TestUtils } = SpecialPowers.ChromeUtils.import( + "resource://testing-common/TestUtils.jsm" +); + +function screenshotPage(win, elementSelector) { + const el = win.document.querySelector(elementSelector); + return TestUtils.screenshotArea(el, win); +} + +async function test_moz_extension_svg_context_fill({ + addonId, + isPrivileged, + expectAllowed, +}) { + // Include current test params in the rendered html page (to be included in failure + // screenshots). + document.querySelector("#test-params").textContent = JSON.stringify({ + addonId, + isPrivileged, + expectAllowed, + }); + + let extDefinition = { + manifest: { + browser_specific_settings: { gecko: { id: addonId } }, + }, + background() { + browser.test.sendMessage("svg-url", browser.runtime.getURL("context-fill-fallback-red.svg")); + }, + files: { + "context-fill-fallback-red.svg": ` + <svg xmlns="http://www.w3.org/2000/svg" version="1.1" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <rect height="100%" width="100%" fill="context-fill red" /> + </svg> + `, + }, + } + + if (isPrivileged) { + // isPrivileged is unused when useAddonManager is set (see ExtensionTestCommon.generate), + // the internal permission being tested is only added when the extension has a startupReason + // related to new installations and upgrades/downgrades and so the `startupReason` is set here + // to be able to mock the startupReason expected when useAddonManager can't be used. + extDefinition = { + ...extDefinition, + isPrivileged, + startupReason: "ADDON_INSTALL", + }; + } else { + // useAddonManager temporary is instead used to explicitly test the other cases when the extension + // is not expected to be privileged. + extDefinition = { + ...extDefinition, + useAddonManager: "temporary", + }; + } + + const extension = ExtensionTestUtils.loadExtension(extDefinition); + + await extension.startup(); + + // Set the extension url on the img element part of the + // comparison table defined in the html part of this test file. + const svgURL = await extension.awaitMessage("svg-url"); + document.querySelector("#actual").src = svgURL; + + let screenshots; + + // Wait until the svg context fill has been applied + // (unfortunately waiting for a document reflow does + // not seem to be enough). + const expectedColor = expectAllowed ? "green" : "red"; + await TestUtils.waitForCondition( + async () => { + const result = await screenshotPage(window, "#actual"); + const reference = await screenshotPage(window, `#${expectedColor}`); + screenshots = {result, reference}; + return result == reference; + }, + `Context-fill should be ${ + expectAllowed ? "allowed" : "disallowed" + } (resulting in ${expectedColor}) on "${addonId}" extension` + ); + + // At least an assertion is required to prevent the test from + // failing. + is( + screenshots.result, + screenshots.reference, + "svg context-fill test completed, result does match reference" + ); + + await extension.unload(); +} + +// This test file verify that the non-standard svg context-fill feature is allowed +// on extensions svg files coming from Mozilla-owned extensions. +// +// NOTE: line extension permission to use context fill is tested in test_recommendations.js + +add_task(async function test_allowed_on_privileged_ext() { + await test_moz_extension_svg_context_fill({ + addonId: "privileged-addon@mochi.test", + isPrivileged: true, + expectAllowed: true, + }); +}); + +add_task(async function test_disallowed_on_non_privileged_ext() { + await test_moz_extension_svg_context_fill({ + addonId: "non-privileged-arbitrary-addon-id@mochi.test", + isPrivileged: false, + expectAllowed: false, + }); +}); + +add_task(async function test_allowed_on_privileged_ext_with_mozilla_id() { + await test_moz_extension_svg_context_fill({ + addonId: "privileged-addon@mozilla.org", + isPrivileged: true, + expectAllowed: true, + }); + + await test_moz_extension_svg_context_fill({ + addonId: "privileged-addon@mozilla.com", + isPrivileged: true, + expectAllowed: true, + }); +}); + +add_task(async function test_allowed_on_non_privileged_ext_with_mozilla_id() { + await test_moz_extension_svg_context_fill({ + addonId: "non-privileged-addon@mozilla.org", + isPrivileged: false, + expectAllowed: true, + }); + + await test_moz_extension_svg_context_fill({ + addonId: "non-privileged-addon@mozilla.com", + isPrivileged: false, + expectAllowed: true, + }); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html new file mode 100644 index 0000000000..580ea5e793 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +var {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm"); + +function tp_background(expectFail = true) { + fetch("https://tracking.example.com/example.txt").then(() => { + browser.test.assertTrue(!expectFail, "fetch received"); + browser.test.sendMessage("done"); + }, () => { + browser.test.assertTrue(expectFail, "fetch failure"); + browser.test.sendMessage("done"); + }); +} + +async function test_permission(permissions, expectFail) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + }, + background: `(${tp_background})(${expectFail})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +} + +add_task(async function setup() { + await UrlClassifierTestUtils.addTestTrackers(); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.trackingprotection.enabled", true]], + }); +}); + +// Fetch would be blocked with these tests +add_task(async function() { await test_permission([], true); }); +add_task(async function() { await test_permission(["http://*/"], true); }); +add_task(async function() { await test_permission(["http://*.example.com/"], true); }); +add_task(async function() { await test_permission(["http://localhost/*"], true); }); +// Fetch will not be blocked if the extension has host permissions. +add_task(async function() { await test_permission(["<all_urls>"], false); }); +add_task(async function() { await test_permission(["*://tracking.example.com/*"], false); }); + +add_task(async function test_contentscript() { + function contentScript() { + fetch("https://tracking.example.com/example.txt").then(() => { + browser.test.notifyPass("fetch received"); + }, () => { + browser.test.notifyFail("fetch failure"); + }); + } + + let extensionData = { + manifest: { + permissions: ["*://tracking.example.com/*"], + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }; + const url = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_sample.html"; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + let win = window.open(url); + await extension.awaitFinish(); + win.close(); + await extension.unload(); +}); + +add_task(async function teardown() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html new file mode 100644 index 0000000000..7e876694a0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function webnav_unresolved_uri_on_expected_URI_scheme() { + function background() { + let checkURLs; + + browser.webNavigation.onCompleted.addListener(async msg => { + if (checkURLs.length) { + let expectedURL = checkURLs.shift(); + browser.test.assertEq(expectedURL, msg.url, "Got the expected URL"); + await browser.tabs.remove(msg.tabId); + browser.test.sendMessage("next"); + } + }); + + browser.test.onMessage.addListener((name, urls) => { + if (name == "checkURLs") { + checkURLs = urls; + } + }); + + browser.test.sendMessage("ready", browser.runtime.getURL("/tab.html")); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background, + files: { + "tab.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + </html> + `, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let checkURLs = [ + "resource://gre/modules/XPCOMUtils.sys.mjs", + "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", + "about:mozilla", + ]; + + let tabURL = await extension.awaitMessage("ready"); + checkURLs.push(tabURL); + + extension.sendMessage("checkURLs", checkURLs); + + for (let url of checkURLs) { + window.open(url); + await extension.awaitMessage("next"); + } + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html new file mode 100644 index 0000000000..a9dfb0a902 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const {webrequest_test} = ChromeUtils.import(SimpleTest.getTestFileURL("webrequest_test.jsm")); +let {testFetch, testXHR} = webrequest_test; + +// Here we test that any requests originating from a system principal are not +// accessible through WebRequest. text_ext_webrequest_background_events tests +// non-system principal requests. + +let testExtension = { + manifest: { + permissions: [ + "webRequest", + "<all_urls>", + ], + }, + background() { + let eventNames = [ + "onBeforeRequest", + "onBeforeSendHeaders", + "onSendHeaders", + "onHeadersReceived", + "onResponseStarted", + "onCompleted", + ]; + + function listener(name, details) { + // If we get anything, we failed. Removing the system principal check + // in ext-webrequest triggers this failure. + browser.test.fail(`received ${name}`); + } + + for (let name of eventNames) { + browser.webRequest[name].addListener( + listener.bind(null, name), + {urls: ["https://example.com/*"]} + ); + } + }, +}; + +add_task(async function test_webRequest_chromeworker_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + await extension.startup(); + await new Promise(resolve => { + let worker = new ChromeWorker("webrequest_chromeworker.js"); + worker.onmessage = event => { + ok("chrome worker fetch finished"); + resolve(); + }; + worker.postMessage("go"); + }); + await extension.unload(); +}); + +add_task(async function test_webRequest_chromepage_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + await extension.startup(); + await new Promise(resolve => { + fetch("https://example.com/example.txt").then(() => { + ok("test page loaded"); + resolve(); + }); + }); + await extension.unload(); +}); + +add_task(async function test_webRequest_jsm_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + await extension.startup(); + await testFetch("https://example.com/example.txt").then(() => { + ok("fetch page loaded"); + }); + await testXHR("https://example.com/example.txt").then(() => { + ok("xhr page loaded"); + }); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html new file mode 100644 index 0000000000..19c812f59f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html @@ -0,0 +1,89 @@ +<!doctype html> +<html> +<head> + <title>Test webRequest checks host permissions</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_webRequest_host_permissions() { + function background() { + function png(details) { + browser.test.sendMessage("png", details.url); + } + browser.webRequest.onBeforeRequest.addListener(png, {urls: ["*://*/*.png"]}); + browser.test.sendMessage("ready"); + } + + const all = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "<all_urls>"]}}); + const example = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "https://example.com/"]}}); + const mochi_test = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "http://mochi.test/"]}}); + + await all.startup(); + await example.startup(); + await mochi_test.startup(); + + await all.awaitMessage("ready"); + await example.awaitMessage("ready"); + await mochi_test.awaitMessage("ready"); + + const win1 = window.open("https://example.com/chrome/toolkit/components/extensions/test/mochitest/file_with_images.html"); + let urls = [await all.awaitMessage("png"), + await all.awaitMessage("png")]; + ok(urls.some(url => url.endsWith("good.png")), "<all_urls> permission gets to see good.png"); + ok((await example.awaitMessage("png")).endsWith("good.png"), "example permission sees same-origin example.com image"); + ok(urls.some(url => url.endsWith("great.png")), "<all_urls> permission also sees great.png"); + + // Clear the in-memory image cache, it can prevent listeners from receiving events. + const imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); + imgTools.getImgCacheForDocument(win1.document).clearCache(false); + win1.close(); + + const win2 = window.open("http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_with_images.html"); + urls = [await all.awaitMessage("png"), + await all.awaitMessage("png")]; + ok(urls.some(url => url.endsWith("good.png")), "<all_urls> permission gets to see good.png"); + ok((await mochi_test.awaitMessage("png")).endsWith("great.png"), "mochi.test permission sees same-origin mochi.test image"); + ok(urls.some(url => url.endsWith("great.png")), "<all_urls> permission also sees great.png"); + win2.close(); + + await all.unload(); + await example.unload(); + await mochi_test.unload(); +}); + +add_task(async function test_webRequest_filter_permissions_warning() { + const manifest = { + permissions: ["webRequest", "http://example.com/"], + }; + + async function background() { + await browser.webRequest.onBeforeRequest.addListener(() => {}, {urls: ["http://example.org/"]}); + browser.test.notifyPass(); + } + + const extension = ExtensionTestUtils.loadExtension({manifest, background}); + + const warning = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{message: /filter doesn't overlap with host permissions/}]); + }); + + await extension.startup(); + await extension.awaitFinish(); + + SimpleTest.endMonitorConsole(); + await warning; + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html new file mode 100644 index 0000000000..6a41b9cf08 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html @@ -0,0 +1,193 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test moz-extension protocol use</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let peakAchu; +add_task(async function setup() { + peakAchu = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "<all_urls>", + ], + }, + background() { + // ID for the extension in the tests. Try to observe it to ensure we cannot. + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.notifyFail(`PeakAchu onBeforeRequest ${details.url}`); + }, {urls: ["<all_urls>", "moz-extension://*/*"]}); + + browser.test.onMessage.addListener((msg, extensionUrl) => { + browser.test.log(`spying for ${extensionUrl}`); + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.notifyFail(`PeakAchu onBeforeRequest ${details.url}`); + }, {urls: [extensionUrl]}); + }); + }, + }); + await peakAchu.startup(); +}); + +add_task(async function test_webRequest_no_mozextension_permission() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "tabs", + "moz-extension://c9e007e0-e518-ed4c-8202-83849981dd21/*", + "moz-extension://*/*", + ], + }, + background() { + browser.test.notifyPass("loaded"); + }, + }); + + let messages = [ + {message: /processing permissions\.2: Value "moz-extension:\/\/c9e007e0-e518-ed4c-8202-83849981dd21\/\*"/}, + {message: /processing permissions\.3: Value "moz-extension:\/\/\*\/\*"/}, + ]; + + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, messages); + }); + + await extension.startup(); + await extension.awaitFinish("loaded"); + await extension.unload(); + + SimpleTest.endMonitorConsole(); + await waitForConsole; +}); + +add_task(async function test_webRequest_mozextension_fetch() { + function background() { + let page = browser.runtime.getURL("fetched.html"); + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.assertEq(details.url, page, "got correct url in onBeforeRequest"); + browser.test.sendMessage("request-started"); + }, {urls: [browser.runtime.getURL("*")]}, ["blocking"]); + browser.webRequest.onCompleted.addListener(details => { + browser.test.assertEq(details.url, page, "got correct url in onCompleted"); + browser.test.sendMessage("request-complete"); + }, {urls: [browser.runtime.getURL("*")]}); + + browser.test.onMessage.addListener((msg, data) => { + fetch(page).then(() => { + browser.test.notifyPass("fetch success"); + browser.test.sendMessage("done"); + }, () => { + browser.test.fail("fetch failed"); + browser.test.sendMessage("done"); + }); + }); + browser.test.sendMessage("extensionUrl", browser.runtime.getURL("*")); + } + + // Use webrequest to monitor moz-extension:// requests + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "tabs", + "<all_urls>", + ], + }, + files: { + "fetched.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>moz-extension file</h1> + </body> + </html> + `.trim(), + }, + background, + }); + + await extension.startup(); + // send the url for this extension to the monitoring extension + peakAchu.sendMessage("extensionUrl", await extension.awaitMessage("extensionUrl")); + + extension.sendMessage("testFetch"); + await extension.awaitMessage("request-started"); + await extension.awaitMessage("request-complete"); + await extension.awaitMessage("done"); + + await extension.unload(); +}); + +add_task(async function test_webRequest_mozextension_tab_query() { + function background() { + browser.test.sendMessage("extensionUrl", browser.runtime.getURL("*")); + let page = browser.runtime.getURL("tab.html"); + + async function onUpdated(tabId, tabInfo, tab) { + if (tabInfo.status !== "complete") { + return; + } + browser.test.log(`tab created ${tabId} ${JSON.stringify(tabInfo)} ${tab.url}`); + let tabs = await browser.tabs.query({url: browser.runtime.getURL("*")}); + browser.test.assertEq(1, tabs.length, "got one tab"); + browser.test.assertEq(tabs.length && tabs[0].id, tab.id, "got the correct tab"); + browser.test.assertEq(tabs.length && tabs[0].url, page, "got correct url in tab"); + browser.tabs.remove(tabId); + browser.tabs.onUpdated.removeListener(onUpdated); + browser.test.sendMessage("tabs-done"); + } + browser.tabs.onUpdated.addListener(onUpdated); + browser.tabs.create({url: page}); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "tabs", + "<all_urls>", + ], + }, + files: { + "tab.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>moz-extension file</h1> + </body> + </html> + `.trim(), + }, + background, + }); + + await extension.startup(); + peakAchu.sendMessage("extensionUrl", await extension.awaitMessage("extensionUrl")); + await extension.awaitMessage("tabs-done"); + await extension.unload(); +}); + +add_task(async function teardown() { + await peakAchu.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html new file mode 100644 index 0000000000..c29b6286d9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// Test that the default paths searched for native host manifests +// are the ones we expect. +add_task(async function test_default_paths() { + let expectUser, expectGlobal; + switch (AppConstants.platform) { + case "macosx": { + expectUser = PathUtils.joinRelative( + Services.dirsvc.get("Home", Ci.nsIFile).path, + "Library/Application Support/Mozilla" + ); + expectGlobal = "/Library/Application Support/Mozilla"; + + break; + } + + case "linux": { + expectUser = PathUtils.join( + Services.dirsvc.get("Home", Ci.nsIFile).path, + ".mozilla" + ); + + const libdir = AppConstants.HAVE_USR_LIB64_DIR ? "lib64" : "lib"; + expectGlobal = PathUtils.join("/usr", libdir, "mozilla"); + break; + } + + default: + // Fixed filesystem paths are only defined for MacOS and Linux, + // there's nothing to test on other platforms. + ok(false, `This test does not apply on ${AppConstants.platform}`); + break; + } + + let userDir = Services.dirsvc.get("XREUserNativeManifests", Ci.nsIFile).path; + is(userDir, expectUser, "user-specific native messaging directory is correct"); + + let globalDir = Services.dirsvc.get("XRESysNativeManifests", Ci.nsIFile).path; + is(globalDir, expectGlobal, "system-wide native messaing directory is correct"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_action.html b/toolkit/components/extensions/test/mochitest/test_ext_action.html new file mode 100644 index 0000000000..1899475723 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_action.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Action with MV3</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +add_task(async function test_action_onClicked() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + action: {}, + }, + background() { + browser.action.onClicked.addListener(async () => { + browser.test.notifyPass("action-clicked"); + }); + + browser.test.sendMessage("background-ready"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + await AppTestDelegate.clickBrowserAction(window, extension); + await extension.awaitFinish("action-clicked"); + await AppTestDelegate.closeBrowserAction(window, extension); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html b/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html new file mode 100644 index 0000000000..c426913373 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html @@ -0,0 +1,390 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension activityLog test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_api() { + let URL = + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + + // Test that an unspecified extension is not logged by the watcher extension. + let unlogged = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + browser_specific_settings: { gecko: { id: "unlogged@tests.mozilla.org" } }, + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + background() { + // This privileged test extension should not affect the webRequest + // data received by non-privileged extensions (See Bug 1576272). + browser.webRequest.onBeforeRequest.addListener( + details => { + return { cancel: false }; + }, + { urls: ["http://mochi.test/*/file_sample.html"] }, + ["blocking"] + ); + }, + }); + await unlogged.startup(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "watched@tests.mozilla.org" } }, + permissions: [ + "tabs", + "tabHide", + "storage", + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + content_scripts: [ + { + matches: ["http://mochi.test/*/file_sample.html"], + js: ["content_script.js"], + run_at: "document_idle", + }, + ], + }, + files: { + "content_script.js": () => { + browser.test.sendMessage("content_script"); + }, + "registered_script.js": () => { + browser.test.sendMessage("registered_script"); + }, + }, + async background() { + let listen = () => {}; + async function runTest() { + // Test activity for a child function call. + browser.test.assertEq( + undefined, + browser.activityLog, + "activityLog requires permission" + ); + + // Test a child event manager. + browser.storage.onChanged.addListener(listen); + browser.storage.onChanged.removeListener(listen); + + // Test a parent event manager. + let webRequestListener = details => { + browser.webRequest.onBeforeRequest.removeListener(webRequestListener); + return { cancel: false }; + }; + browser.webRequest.onBeforeRequest.addListener( + webRequestListener, + { urls: ["http://mochi.test/*/file_sample.html"] }, + ["blocking"] + ); + + // A manifest based content script is already + // registered, we do a dynamic registration here. + await browser.contentScripts.register({ + js: [{ file: "registered_script.js" }], + matches: ["http://mochi.test/*/file_sample.html"], + runAt: "document_start", + }); + browser.test.sendMessage("ready"); + } + browser.test.onMessage.addListener((msg, data) => { + // Logging has started here so this listener is logged, but the + // call adding it was not. We do an additional onMessage.addListener + // call in the test function to validate child based event managers. + if (msg == "runtest") { + browser.test.assertTrue(true, msg); + runTest(); + } + if (msg == "hideTab") { + browser.tabs.hide(data); + } + }); + browser.test.sendMessage("url", browser.runtime.getURL("")); + }, + }); + + async function backgroundScript(expectedUrl, extensionUrl) { + let expecting = [ + // Test child-only api_call. + { + type: "api_call", + name: "test.assertTrue", + data: { args: [true, "runtest"] }, + }, + + // Test child-only api_call. + { + type: "api_call", + name: "test.assertEq", + data: { + args: [undefined, undefined, "activityLog requires permission"], + }, + }, + // Test child addListener calls. + { + type: "api_call", + name: "storage.onChanged.addListener", + data: { + args: [], + }, + }, + { + type: "api_call", + name: "storage.onChanged.removeListener", + data: { + args: [], + }, + }, + // Test parent addListener calls. + { + type: "api_call", + name: "webRequest.onBeforeRequest.addListener", + data: { + args: [ + { + incognito: null, + tabId: null, + types: null, + urls: ["http://mochi.test/*/file_sample.html"], + windowId: null, + }, + ["blocking"], + ], + }, + }, + // Test an api that makes use of callParentAsyncFunction. + { + type: "api_call", + name: "contentScripts.register", + data: { + args: [ + { + allFrames: null, + css: null, + excludeGlobs: null, + excludeMatches: null, + includeGlobs: null, + js: [ + { + file: `${extensionUrl}registered_script.js`, + }, + ], + matchAboutBlank: null, + matches: ["http://mochi.test/*/file_sample.html"], + runAt: "document_start", + }, + ], + }, + }, + // Test child api_event calls. + { + type: "api_event", + name: "test.onMessage", + data: { args: ["runtest"] }, + }, + { + type: "api_call", + name: "test.sendMessage", + data: { args: ["ready"] }, + }, + // Test parent api_event calls. + { + type: "api_call", + name: "webRequest.onBeforeRequest.removeListener", + data: { + args: [], + }, + }, + { + type: "api_event", + name: "webRequest.onBeforeRequest", + data: { + args: [ + { + url: expectedUrl, + method: "GET", + type: "main_frame", + frameId: 0, + parentFrameId: -1, + incognito: false, + thirdParty: false, + ip: null, + frameAncestors: [], + urlClassification: { firstParty: [], thirdParty: [] }, + requestSize: 0, + responseSize: 0, + }, + ], + result: { + cancel: false, + }, + }, + }, + // Test manifest based content script. + { + type: "content_script", + name: "content_script.js", + data: { url: expectedUrl, tabId: 1 }, + }, + // registered script test + { + type: "content_script", + name: `${extensionUrl}registered_script.js`, + data: { url: expectedUrl, tabId: 1 }, + }, + { + type: "api_call", + name: "test.sendMessage", + data: { args: ["registered_script"], tabId: 1 }, + }, + { + type: "api_call", + name: "test.sendMessage", + data: { args: ["content_script"], tabId: 1 }, + }, + // Child api call + { + type: "api_call", + name: "tabs.hide", + data: { args: ["__TAB_ID"] }, + }, + { + type: "api_event", + name: "test.onMessage", + data: { args: ["hideTab", "__TAB_ID"] }, + }, + ]; + browser.test.assertTrue(browser.activityLog, "activityLog is privileged"); + + // Slightly less than a normal deep equal, we want to know that the values + // in our expected data are the same in the actual data, but we don't care + // if actual data has additional data or if data is in the same order in objects. + // This allows us to ignore keys that may be variable, or that are set in + // the api with an undefined value. + function deepEquivalent(a, b) { + if (a === b) { + return true; + } + if ( + typeof a != "object" || + typeof b != "object" || + a === null || + b === null + ) { + return false; + } + for (let k in a) { + if (!deepEquivalent(a[k], b[k])) { + return false; + } + } + return true; + } + + let tab; + let handler = async details => { + browser.test.log(`onExtensionActivity ${JSON.stringify(details)}`); + let test = expecting.shift(); + if (!test) { + browser.test.notifyFail(`no test for ${details.name}`); + } + + // On multiple runs, tabId will be different. Set the current + // tabId where we need it. + if (test.data.tabId !== undefined) { + test.data.tabId = tab.id; + } + if (test.data.args !== undefined) { + test.data.args = test.data.args.map(value => + value === "__TAB_ID" ? tab.id : value + ); + } + + browser.test.assertEq(test.type, details.type, "type matches"); + if (test.type == "content_script") { + browser.test.assertTrue( + details.name.includes(test.name), + "content script name matches" + ); + } else { + browser.test.assertEq(test.name, details.name, "name matches"); + } + + browser.test.assertTrue( + deepEquivalent(test.data, details.data), + `expected ${JSON.stringify( + test.data + )} included in actual ${JSON.stringify(details.data)}` + ); + if (!expecting.length) { + await browser.tabs.remove(tab.id); + browser.test.notifyPass("activity"); + } + }; + browser.activityLog.onExtensionActivity.addListener( + handler, + "watched@tests.mozilla.org" + ); + + browser.test.onMessage.addListener(async msg => { + if (msg === "opentab") { + tab = await browser.tabs.create({ url: expectedUrl }); + browser.test.sendMessage("tabid", tab.id); + } + if (msg === "done") { + browser.activityLog.onExtensionActivity.removeListener( + handler, + "watched@tests.mozilla.org" + ); + } + }); + } + + await extension.startup(); + let extensionUrl = await extension.awaitMessage("url"); + + let logger = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + browser_specific_settings: { gecko: { id: "watcher@tests.mozilla.org" } }, + permissions: ["activityLog"], + }, + background: `(${backgroundScript})("${URL}", "${extensionUrl}")`, + }); + await logger.startup(); + extension.sendMessage("runtest"); + await extension.awaitMessage("ready"); + logger.sendMessage("opentab"); + let id = await logger.awaitMessage("tabid"); + + await Promise.all([ + extension.awaitMessage("content_script"), + extension.awaitMessage("registered_script"), + ]); + + extension.sendMessage("hideTab", id); + await logger.awaitFinish("activity"); + + // Stop watching because we get extra calls on extension shutdown + // such as listener removal. + logger.sendMessage("done"); + + await extension.unload(); + await unlogged.unload(); + await logger.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js new file mode 100644 index 0000000000..4c82a4575c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js @@ -0,0 +1,245 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Tests whether not too many APIs are visible by default. +// This file is used by test_ext_all_apis.html in browser/ and mobile/android/, +// which may modify the following variables to add or remove expected APIs. +/* globals expectedContentApisTargetSpecific */ +/* globals expectedBackgroundApisTargetSpecific */ + +// Generates a list of expectations. +function generateExpectations(list) { + return list + .reduce((allApis, path) => { + return allApis.concat(`browser.${path}`, `chrome.${path}`); + }, []) + .sort(); +} + +let expectedCommonApis = [ + "extension.getURL", + "extension.inIncognitoContext", + "extension.lastError", + "i18n.detectLanguage", + "i18n.getAcceptLanguages", + "i18n.getMessage", + "i18n.getUILanguage", + "runtime.OnInstalledReason", + "runtime.OnRestartRequiredReason", + "runtime.PlatformArch", + "runtime.PlatformOs", + "runtime.RequestUpdateCheckStatus", + "runtime.getManifest", + "runtime.connect", + "runtime.getFrameId", + "runtime.getURL", + "runtime.id", + "runtime.lastError", + "runtime.onConnect", + "runtime.onMessage", + "runtime.sendMessage", + // browser.test is only available in xpcshell or when + // Cu.isInAutomation is true. + "test.assertDeepEq", + "test.assertEq", + "test.assertFalse", + "test.assertRejects", + "test.assertThrows", + "test.assertTrue", + "test.fail", + "test.log", + "test.notifyFail", + "test.notifyPass", + "test.onMessage", + "test.sendMessage", + "test.succeed", + "test.withHandlingUserInput", +]; + +let expectedContentApis = [ + ...expectedCommonApis, + ...expectedContentApisTargetSpecific, +]; + +let expectedBackgroundApis = [ + ...expectedCommonApis, + ...expectedBackgroundApisTargetSpecific, + "contentScripts.register", + "experiments.APIChildScope", + "experiments.APIEvent", + "experiments.APIParentScope", + "extension.ViewType", + "extension.getBackgroundPage", + "extension.getViews", + "extension.isAllowedFileSchemeAccess", + "extension.isAllowedIncognitoAccess", + // Note: extensionTypes is not visible in Chrome. + "extensionTypes.CSSOrigin", + "extensionTypes.ImageFormat", + "extensionTypes.RunAt", + "management.ExtensionDisabledReason", + "management.ExtensionInstallType", + "management.ExtensionType", + "management.getSelf", + "management.uninstallSelf", + "permissions.getAll", + "permissions.contains", + "permissions.request", + "permissions.remove", + "permissions.onAdded", + "permissions.onRemoved", + "runtime.getBackgroundPage", + "runtime.getBrowserInfo", + "runtime.getPlatformInfo", + "runtime.onConnectExternal", + "runtime.onInstalled", + "runtime.onMessageExternal", + "runtime.onStartup", + "runtime.onSuspend", + "runtime.onSuspendCanceled", + "runtime.onUpdateAvailable", + "runtime.openOptionsPage", + "runtime.reload", + "runtime.setUninstallURL", + "theme.getCurrent", + "theme.onUpdated", + "types.LevelOfControl", + "types.SettingScope", +]; + +// APIs that are exposed to MV2 by default, but not to MV3. +const mv2onlyBackgroundApis = new Set([ + "extension.getURL", + "extension.lastError", + "contentScripts.register", + "tabs.executeScript", + "tabs.insertCSS", + "tabs.removeCSS", +]); +let expectedBackgroundApisMV3 = expectedBackgroundApis.filter( + path => !mv2onlyBackgroundApis.has(path) +); + +function sendAllApis() { + function isEvent(key, val) { + if (!/^on[A-Z]/.test(key)) { + return false; + } + let eventKeys = []; + for (let prop in val) { + eventKeys.push(prop); + } + eventKeys = eventKeys.sort().join(); + return eventKeys === "addListener,hasListener,removeListener"; + } + // Some items are removed from the namespaces in the lazy getters after the first get. This + // in one case, the events namespace, leaves a namespace that is empty. Make sure we don't + // consider those as a part of our testing. + function isEmptyObject(val) { + return val !== null && typeof val == "object" && !Object.keys(val).length; + } + function mayRecurse(key, val) { + if (Object.keys(val).filter(k => !/^[A-Z\-0-9_]+$/.test(k)).length === 0) { + // Don't recurse on constants and empty objects. + return false; + } + return !isEvent(key, val); + } + + let results = []; + function diveDeeper(path, obj) { + for (const [key, val] of Object.entries(obj)) { + if (typeof val == "object" && val !== null && mayRecurse(key, val)) { + diveDeeper(`${path}.${key}`, val); + } else if (val !== undefined && !isEmptyObject(val)) { + results.push(`${path}.${key}`); + } + } + } + diveDeeper("browser", browser); + diveDeeper("chrome", chrome); + browser.test.sendMessage("allApis", results.sort()); + browser.test.sendMessage("namespaces", browser === chrome); +} + +add_task(async function setup() { + // This test enumerates all APIs and may access a deprecated API. Just log a + // warning instead of throwing. + await ExtensionTestUtils.failOnSchemaWarnings(false); +}); + +add_task(async function test_enumerate_content_script_apis() { + let extensionData = { + manifest: { + content_scripts: [ + { + matches: ["http://mochi.test/*/file_sample.html"], + js: ["contentscript.js"], + run_at: "document_start", + }, + ], + }, + files: { + "contentscript.js": sendAllApis, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + let actualApis = await extension.awaitMessage("allApis"); + win.close(); + let expectedApis = generateExpectations(expectedContentApis); + isDeeply(actualApis, expectedApis, "content script APIs"); + + let sameness = await extension.awaitMessage("namespaces"); + ok(sameness, "namespaces are same object"); + + await extension.unload(); +}); + +add_task(async function test_enumerate_background_script_apis() { + let extensionData = { + background: sendAllApis, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let actualApis = await extension.awaitMessage("allApis"); + let expectedApis = generateExpectations(expectedBackgroundApis); + isDeeply(actualApis, expectedApis, "background script APIs"); + + let sameness = await extension.awaitMessage("namespaces"); + ok(!sameness, "namespaces are different objects"); + + await extension.unload(); +}); + +add_task(async function test_enumerate_background_script_apis_mv3() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); + let extensionData = { + background: sendAllApis, + manifest: { + manifest_version: 3, + + // Features that expose APIs in MV2, but should not do anything with MV3. + browser_action: {}, + user_scripts: {}, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let actualApis = await extension.awaitMessage("allApis"); + let expectedApis = generateExpectations(expectedBackgroundApisMV3); + isDeeply(actualApis, expectedApis, "background script APIs in MV3"); + + let sameness = await extension.awaitMessage("namespaces"); + ok(sameness, "namespaces are same object"); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html new file mode 100644 index 0000000000..458dc65d99 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html @@ -0,0 +1,401 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Async Clipboard permissions tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +// Bug 1479956 - On android-debug verify this test times out +SimpleTest.requestLongerTimeout(2); + +/* globals ClipboardItem, clipboardWriteText, clipboardWrite, clipboardReadText, clipboardRead */ +function shared() { + this.clipboardWriteText = function(txt) { + return navigator.clipboard.writeText(txt); + }; + + this.clipboardWrite = function(items) { + return navigator.clipboard.write(items); + }; + + this.clipboardReadText = function() { + return navigator.clipboard.readText(); + }; + + this.clipboardRead = function() { + return navigator.clipboard.read(); + }; +} + +/** + * Clear the clipboard. + * + * This is needed because Services.clipboard.emptyClipboard() does not clear the actual system clipboard. + */ +function clearClipboard() { + if (AppConstants.platform == "android") { + // On android, this clears the actual system clipboard + SpecialPowers.Services.clipboard.emptyClipboard(SpecialPowers.Services.clipboard.kGlobalClipboard); + return; + } + // Need to do this hack on other platforms to clear the actual system clipboard + let transf = SpecialPowers.Cc["@mozilla.org/widget/transferable;1"] + .createInstance(SpecialPowers.Ci.nsITransferable); + transf.init(null); + // Empty transferables may cause crashes, so just add an unknown type. + const TYPE = "text/x-moz-place-empty"; + transf.addDataFlavor(TYPE); + transf.setTransferData(TYPE, {}); + SpecialPowers.Services.clipboard.setData(transf, null, SpecialPowers.Services.clipboard.kGlobalClipboard); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.events.asyncClipboard.clipboardItem", true], + ]}); +}); + +// Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in background script +add_task(async function test_background_async_clipboard_no_permissions() { + function backgroundScript() { + const item = new ClipboardItem({ + "text/plain": new Blob(["HI"], {type: "text/plain"}) + }); + browser.test.assertRejects( + clipboardRead(), + (err) => err === undefined, + "Read should be denied without permission" + ); + browser.test.assertRejects( + clipboardWrite([item]), + "Clipboard write was blocked due to lack of user activation.", + "Write should be denied without permission" + ); + browser.test.assertRejects( + clipboardWriteText("blabla"), + "Clipboard write was blocked due to lack of user activation.", + "WriteText should be denied without permission" + ); + browser.test.assertRejects( + clipboardReadText(), + (err) => err === undefined, + "ReadText should be denied without permission" + ); + browser.test.sendMessage("ready"); + } + let extensionData = { + background: [shared, backgroundScript], + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitMessage("ready"); + await extension.unload(); +}); + +// Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in content script +add_task(async function test_contentscript_async_clipboard_no_permission() { + function contentScript() { + const item = new ClipboardItem({ + "text/plain": new Blob(["HI"], {type: "text/plain"}) + }); + browser.test.assertRejects( + clipboardRead(), + (err) => err === undefined, + "Read should be denied without permission" + ); + browser.test.assertRejects( + clipboardWrite([item]), + "Clipboard write was blocked due to lack of user activation.", + "Write should be denied without permission" + ); + browser.test.assertRejects( + clipboardWriteText("blabla"), + "Clipboard write was blocked due to lack of user activation.", + "WriteText should be denied without permission" + ); + browser.test.assertRejects( + clipboardReadText(), + (err) => err === undefined, + "ReadText should be denied without permission" + ); + browser.test.sendMessage("ready"); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); + +// Test that with enough permissions, we are allowed to use writeText in content script +add_task(async function test_contentscript_clipboard_permission_writetext() { + function contentScript() { + let str = "HI"; + clipboardWriteText(str).then(function() { + // nothing here + browser.test.sendMessage("ready"); + }, function(err) { + browser.test.fail("WriteText promise rejected"); + browser.test.sendMessage("ready"); + }); // clipboardWriteText + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + const actual = SpecialPowers.getClipboardData("text/plain"); + is(actual, "HI", "right string copied by write"); + win.close(); + await extension.unload(); +}); + +// Test that with enough permissions, we are allowed to use readText in content script +add_task(async function test_contentscript_clipboard_permission_readtext() { + function contentScript() { + let str = "HI"; + clipboardReadText().then(function(strData) { + if (strData == str) { + browser.test.succeed("Successfully read from clipboard"); + } else { + browser.test.fail("ReadText read the wrong thing from clipboard:" + strData); + } + browser.test.sendMessage("ready"); + }, function(err) { + browser.test.fail("ReadText promise rejected"); + browser.test.sendMessage("ready"); + }); // clipboardReadText + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + await SimpleTest.promiseClipboardChange("HI", () => { + SpecialPowers.clipboardCopyString("HI"); + }, "text/plain"); + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); + +// Test that with enough permissions, we are allowed to use write in content script +add_task(async function test_contentscript_clipboard_permission_write() { + function contentScript() { + const item = new ClipboardItem({ + "text/plain": new Blob(["HI"], {type: "text/plain"}) + }); + clipboardWrite([item]).then(function() { + // nothing here + browser.test.sendMessage("ready"); + }, function(err) { // clipboardWrite promise error function + browser.test.fail("Write promise rejected"); + browser.test.sendMessage("ready"); + }); // clipboard write + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + const actual = SpecialPowers.getClipboardData("text/plain"); + is(actual, "HI", "right string copied by write"); + win.close(); + await extension.unload(); +}); + +// Test that with enough permissions, we are allowed to use read in content script +add_task(async function test_contentscript_clipboard_permission_read() { + function contentScript() { + clipboardRead().then(async function(items) { + let blob = await items[0].getType("text/plain"); + let s = await blob.text(); + if (s == "HELLO") { + browser.test.succeed("Read promise successfully read the right thing"); + } else { + browser.test.fail("Read read the wrong string from clipboard:" + s); + } + browser.test.sendMessage("ready"); + }, function(err) { // clipboardRead promise error function + browser.test.fail("Read promise rejected"); + browser.test.sendMessage("ready"); + }); // clipboard read + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + await SimpleTest.promiseClipboardChange("HELLO", () => { + SpecialPowers.clipboardCopyString("HELLO"); + }, "text/plain"); + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); + +// Test that performing readText(...) when the clipboard is empty returns an empty string +add_task(async function test_contentscript_clipboard_nocontents_readtext() { + function contentScript() { + clipboardReadText().then(function(strData) { + if (strData == "") { + browser.test.succeed("ReadText successfully read correct thing from an empty clipboard"); + } else { + browser.test.fail("ReadText should have read an empty string, but read:" + strData); + } + browser.test.sendMessage("ready"); + }, function(err) { + browser.test.fail("ReadText promise rejected: " + err); + browser.test.sendMessage("ready"); + }); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + + await SimpleTest.promiseClipboardChange("", () => { + clearClipboard(); + }, "text/x-moz-place-empty"); + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); + +// Test that performing read(...) when the clipboard is empty returns an empty ClipboardItem +add_task(async function test_contentscript_clipboard_nocontents_read() { + function contentScript() { + clipboardRead().then(function(items) { + if (items[0].types.length) { + browser.test.fail("Read read the wrong thing from clipboard, " + + "ClipboardItem has this many entries: " + items[0].types.length); + } else { + browser.test.succeed("Read promise successfully resolved"); + } + browser.test.sendMessage("ready"); + }, function(err) { + browser.test.fail("Read promise rejected: " + err); + browser.test.sendMessage("ready"); + }); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + + await SimpleTest.promiseClipboardChange("", () => { + clearClipboard(); + }, "text/x-moz-place-empty"); + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html new file mode 100644 index 0000000000..e7745f08c5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for background page canvas rendering</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_background_canvas() { + function background() { + try { + let canvas = document.createElement("canvas"); + + let context = canvas.getContext("2d"); + + // This ensures that we have a working PresShell, and can successfully + // calculate font metrics. + context.font = "8pt fixed"; + + browser.test.notifyPass("background-canvas"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("background-canvas"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ background }); + + await extension.startup(); + await extension.awaitFinish("background-canvas"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_page.html b/toolkit/components/extensions/test/mochitest/test_ext_background_page.html new file mode 100644 index 0000000000..2f4fe3b96c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_page.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js" type="text/javascript"></script> + <link href="/tests/SimpleTest/test.css" rel="stylesheet"/> + </head> + <body> + + <script type="text/javascript"> + "use strict"; + + /* eslint-disable mozilla/balanced-listeners */ + + add_task(async function testAlertNotShownInBackgroundWindow() { + let extension = ExtensionTestUtils.loadExtension({ + background: function () { + alert("I am an alert in the background."); + + browser.test.notifyPass("alertCalled"); + } + }); + + let consoleOpened = loadChromeScript(() => { + const {sendAsyncMessage, assert} = this; + assert.ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), "Alerts should not be present at the start of the test."); + + Services.obs.addObserver(function observer() { + sendAsyncMessage("web-console-created"); + Services.obs.removeObserver(observer, "web-console-created"); + }, "web-console-created"); + }); + let opened = consoleOpened.promiseOneMessage("web-console-created"); + + consoleMonitor.start([ + { + message: /alert\(\) is not supported in background windows/ + }, { + message: /I am an alert in the background/ + } + ]); + + await extension.startup(); + await extension.awaitFinish("alertCalled"); + + let chromeScript = loadChromeScript(async () => { + const {assert} = this; + assert.ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), "Alerts should not be present after calling alert()."); + }); + chromeScript.destroy(); + + await consoleMonitor.finished(); + + await opened; + consoleOpened.destroy(); + + chromeScript = loadChromeScript(async () => { + const {sendAsyncMessage} = this; + let {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs"); + require("devtools/client/framework/devtools-browser"); + let {BrowserConsoleManager} = require("devtools/client/webconsole/browser-console-manager"); + + // And then double check that we have an actual browser console. + let haveConsole = !!BrowserConsoleManager.getBrowserConsole(); + + if (haveConsole) { + await BrowserConsoleManager.toggleBrowserConsole(); + } + sendAsyncMessage("done", haveConsole); + }); + + let consoleShown = await chromeScript.promiseOneMessage("done"); + ok(consoleShown, "console was shown"); + chromeScript.destroy(); + + await extension.unload(); + }); + </script> + + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_page_dpi.html b/toolkit/components/extensions/test/mochitest/test_ext_background_page_dpi.html new file mode 100644 index 0000000000..40772402b1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_page_dpi.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<meta charset="utf-8"> +<title>DPI of background page</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> +<script src="head.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script> +"use strict"; + +async function testDPIMatches(description) { + let extension = ExtensionTestUtils.loadExtension({ + background: function() { + browser.test.sendMessage("dpi", window.devicePixelRatio); + }, + }); + await extension.startup(); + let dpi = await extension.awaitMessage("dpi"); + await extension.unload(); + + // This assumes that the window is loaded in a device DPI. + is( + dpi, + window.devicePixelRatio, + `DPI in a background page should match DPI in primary chrome page ${description}` + ); +} + +add_task(async function test_dpi_simple() { + await testDPIMatches("by default"); +}); + +add_task(async function test_dpi_devPixelsPerPx() { + await SpecialPowers.pushPrefEnv({ + set: [["layout.css.devPixelsPerPx", 1.5]], + }); + await testDPIMatches("with devPixelsPerPx"); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_dpi_os_zoom() { + await SpecialPowers.pushPrefEnv({ set: [["ui.textScaleFactor", 200]] }); + await testDPIMatches("with OS zoom"); + await SpecialPowers.popPrefEnv(); +}); +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup.html b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup.html new file mode 100644 index 0000000000..0d038da897 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup.html @@ -0,0 +1,183 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>action.openPopup Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let extensionData = { + manifest: { + browser_specific_settings: { + gecko: { + id: "open-popup@tests.mozilla.org", + } + }, + browser_action: { + default_popup: "popup.html", + }, + permissions: ["activeTab"] + }, + + useAddonManager: "android-only", +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["extensions.openPopupWithoutUserGesture.enabled", true], + ], + }); +}); + +async function testActiveTabPermissions(withHandlingUserInput) { + const background = async function(withHandlingUserInput) { + let tabPromise; + let tabLoadedPromise = new Promise(resolve => { + // Wait for the tab to actually finish loading (bug 1589734) + browser.tabs.onUpdated.addListener(async (id, { status }) => { + if (id === (await tabPromise).id && status === "complete") { + resolve(); + } + }); + }); + tabPromise = browser.tabs.create({ url: "https://www.example.com" }); + tabLoadedPromise.then(() => { + // Once the popup opens, check if we have activeTab permission + browser.runtime.onMessage.addListener(async msg => { + if (msg === "popup-open") { + let tabs = await browser.tabs.query({}); + + browser.test.assertEq( + withHandlingUserInput ? 1 : 0, + tabs.filter((t) => typeof t.url !== "undefined").length, + "active tab permission only granted with user input" + ); + + await browser.tabs.remove((await tabPromise).id); + browser.test.sendMessage("activeTabsChecked"); + } + }); + + if (withHandlingUserInput) { + browser.test.withHandlingUserInput(() => { + browser.browserAction.openPopup(); + }); + } else { + browser.browserAction.openPopup(); + } + }) + }; + + let extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + + background: `(${background})(${withHandlingUserInput})`, + + files: { + "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`, + async "popup.js"() { + browser.runtime.sendMessage("popup-open"); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("activeTabsChecked"); + await extension.unload(); +} + +add_task(async function test_browserAction_openPopup_activeTab() { + await testActiveTabPermissions(true); +}); + +add_task(async function test_browserAction_openPopup_non_activeTab() { + await testActiveTabPermissions(false); +}); + +add_task(async function test_browserAction_openPopup_invalid_states() { + let extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + + background: async function() { + await browser.browserAction.setPopup({ popup: "" }) + await browser.test.assertRejects( + browser.browserAction.openPopup(), + "No popup URL is set", + "Should throw when no URL is set" + ); + + await browser.browserAction.disable() + await browser.test.assertRejects( + browser.browserAction.openPopup(), + "Popup is disabled", + "Should throw when disabled" + ); + + browser.test.notifyPass("invalidStates"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("invalidStates"); + await extension.unload(); +}); + +add_task(async function test_browserAction_openPopup_no_click_event() { + let extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + + background: async function() { + let clicks = 0; + + browser.browserAction.onClicked.addListener(() => { + clicks++; + }); + + // Test with popup set + await browser.browserAction.openPopup(); + browser.test.sendMessage("close-popup"); + + browser.test.onMessage.addListener(async (msg) => { + if (msg === "popup-closed") { + // Test without popup + await browser.browserAction.setPopup({ popup: "" }); + + await browser.test.assertRejects( + browser.browserAction.openPopup(), + "No popup URL is set", + "Should throw when no URL is set" + ); + + // We expect the last call to be a no-op, so there isn't really anything + // to wait on. Instead, check that no clicks are registered after waiting + // for a sufficient amount of time. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => { + browser.test.assertEq(0, clicks, "onClicked should not be called"); + browser.test.notifyPass("noClick"); + }, 1000); + } + }); + }, + }); + + extension.onMessage("close-popup", async () => { + await AppTestDelegate.closeBrowserAction(window, extension); + extension.sendMessage("popup-closed"); + }); + + await extension.startup(); + await extension.awaitFinish("noClick"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_incognito_window.html b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_incognito_window.html new file mode 100644 index 0000000000..8036d97398 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_incognito_window.html @@ -0,0 +1,151 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>action.openPopup Incognito Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let extensionData = { + manifest: { + browser_specific_settings: { + gecko: { + id: "open-popup@tests.mozilla.org", + } + }, + browser_action: { + default_popup: "popup.html", + }, + permissions: ["activeTab"] + }, + + useAddonManager: "android-only", +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["extensions.openPopupWithoutUserGesture.enabled", true], + ], + }); +}); + +async function getIncognitoWindow() { + // Since events will be limited based on incognito, we need a + // spanning extension to get the tab id so we can test access failure. + + let windowWatcher = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + background: function() { + browser.windows.create({ incognito: true }).then(({ id: windowId }) => { + browser.test.onMessage.addListener(async data => { + if (data === "close") { + await browser.windows.remove(windowId); + browser.test.sendMessage("window-closed"); + } + }); + + browser.test.sendMessage("window-id", windowId); + }); + }, + incognitoOverride: "spanning", + }); + + await windowWatcher.startup(); + let windowId = await windowWatcher.awaitMessage("window-id"); + + return { + windowId, + close: async () => { + windowWatcher.sendMessage("close"); + await windowWatcher.awaitMessage("window-closed"); + await windowWatcher.unload(); + }, + }; +} + +async function testWithIncognitoOverride(incognitoOverride) { + let extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + + incognitoOverride, + + background: async function() { + browser.test.onMessage.addListener(async ({ windowId, incognitoOverride }) => { + const openPromise = browser.browserAction.openPopup({ windowId }); + + if (incognitoOverride === "not_allowed") { + await browser.test.assertRejects( + openPromise, + /Invalid window ID/, + "Should prevent open popup call for incognito window" + ); + } else { + try { + browser.test.assertEq(await openPromise, undefined, "openPopup resolved"); + } catch (e) { + browser.test.fail(`Unexpected error: ${e}`); + } + } + + browser.test.sendMessage("incognitoWindow"); + }); + }, + + files: { + "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`, + "popup.js"() { + browser.test.sendMessage("popup"); + }, + }, + }); + + await extension.startup(); + + let incognitoWindow = await getIncognitoWindow(); + await extension.sendMessage({ windowId: incognitoWindow.windowId, incognitoOverride }); + + await extension.awaitMessage("incognitoWindow"); + + // Wait for the popup to open - bug 1800100 + if (incognitoOverride === "spanning") { + await extension.awaitMessage("popup"); + } + + await extension.unload(); + + await incognitoWindow.close(); +} + +add_task(async function test_browserAction_openPopup_incognito_window_spanning() { + if (AppConstants.platform == "android") { + // TODO bug 1372178: Cannot open private windows from an extension. + todo(false, "Cannot open private windows on Android"); + return; + } + + await testWithIncognitoOverride("spanning"); +}); + +add_task(async function test_browserAction_openPopup_incognito_window_not_allowed() { + if (AppConstants.platform == "android") { + // TODO bug 1372178: Cannot open private windows from an extension. + todo(false, "Cannot open private windows on Android"); + return; + } + + + await testWithIncognitoOverride("not_allowed"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_windowId.html b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_windowId.html new file mode 100644 index 0000000000..c528028901 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_windowId.html @@ -0,0 +1,162 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>action.openPopup Window ID Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let extensionData = { + manifest: { + browser_specific_settings: { + gecko: { + id: "open-popup@tests.mozilla.org", + } + }, + browser_action: { + default_popup: "popup.html", + default_area: "navbar", + }, + permissions: ["activeTab"] + }, + + useAddonManager: "android-only", +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["extensions.openPopupWithoutUserGesture.enabled", true], + ], + }); +}); + +async function testWithWindowState(state) { + const background = async function(state) { + const originalWindow = await browser.windows.getCurrent(); + + let newWindowPromise; + const tabLoadedPromise = new Promise(resolve => { + browser.tabs.onUpdated.addListener(async (id, { status }, tab) => { + if (tab.windowId === (await newWindowPromise).id && status === "complete") { + resolve(); + } + }); + }); + + newWindowPromise = browser.windows.create({ url: "tab.html" }); + + browser.test.onMessage.addListener(async (msg) => { + if (msg === "close-window") { + await browser.windows.remove((await newWindowPromise).id); + browser.test.sendMessage("window-closed"); + } + }); + + tabLoadedPromise.then(async () => { + const windowId = (await newWindowPromise).id; + + switch (state) { + case "inactive": + const focusChangePromise = new Promise(resolve => { + browser.windows.onFocusChanged.addListener((focusedWindowId) => { + if (focusedWindowId === originalWindow.id) { + resolve(); + } + }) + }); + await browser.windows.update(originalWindow.id, { focused: true }); + await focusChangePromise; + break; + case "minimized": + await browser.windows.update(windowId, { state: "minimized" }); + break; + default: + throw new Error(`Invalid state: ${state}`); + } + + await browser.browserAction.openPopup({ windowId }); + }); + }; + + let extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + + background: `(${background})(${JSON.stringify(state)})`, + + files: { + "tab.html": "<!DOCTYPE html>", + "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`, + "popup.js"() { + // Small timeout to ensure the popup doesn't immediately close, which can + // happen when focus moves between windows + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(async () => { + let windows = await browser.windows.getAll(); + let highestWindowIdIsFocused = Math.max(...windows.map((w) => w.id)) + === windows.find((w) => w.focused).id; + + browser.test.assertEq(true, highestWindowIdIsFocused, "new window is focused"); + + await browser.test.sendMessage("popup-open"); + + // Bug 1800100: Window leaks if not explicitly closed + window.close(); + }, 1000); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("popup-open"); + await extension.sendMessage("close-window"); + await extension.awaitMessage("window-closed"); + await extension.unload(); +} + +add_task(async function test_browserAction_openPopup_window_inactive() { + if (AppConstants.platform == "linux") { + // TODO bug 1798334: Currently unreliable on linux + todo(false, "Unreliable on linux"); + return; + } + await testWithWindowState("inactive"); +}); + +add_task(async function test_browserAction_openPopup_window_minimized() { + if (AppConstants.platform == "linux") { + // TODO bug 1798334: Currently unreliable on linux + todo(false, "Unreliable on linux"); + return; + } + await testWithWindowState("minimized"); +}); + +add_task(async function test_browserAction_openPopup_invalid_window() { + let extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + + background: async function() { + await browser.test.assertRejects( + browser.browserAction.openPopup({ windowId: Number.MAX_SAFE_INTEGER }), + /Invalid window ID/, + "Should throw for invalid window ID" + ); + browser.test.notifyPass("invalidWindow"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("invalidWindow"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_without_pref.html b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_without_pref.html new file mode 100644 index 0000000000..aa7285d5f5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browserAction_openPopup_without_pref.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>action.openPopup Preference Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let extensionData = { + manifest: { + browser_specific_settings: { + gecko: { + id: "open-popup@tests.mozilla.org", + } + }, + browser_action: { + default_popup: "popup.html", + } + }, + + useAddonManager: "android-only", +}; + +add_task(async function test_browserAction_openPopup_without_pref() { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["extensions.openPopupWithoutUserGesture.enabled", false], + ], + }); + + let extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + + background: async function() { + await browser.test.assertRejects( + browser.browserAction.openPopup(), + "openPopup requires a user gesture", + "Should throw when preference is unset" + ); + + browser.test.notifyPass("withoutPref"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("withoutPref"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html new file mode 100644 index 0000000000..f8ea41ddab --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html @@ -0,0 +1,159 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.remove indexedDB</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testIndexedDB() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); + + async function background() { + const PAGE = + "/tests/toolkit/components/extensions/test/mochitest/file_indexedDB.html"; + + let tabs = []; + + browser.test.onMessage.addListener(async msg => { + if (msg == "cleanup") { + await Promise.all(tabs.map(tabId => browser.tabs.remove(tabId))); + browser.test.sendMessage("done"); + return; + } + + await browser.browsingData.remove(msg, { indexedDB: true }); + browser.test.sendMessage("indexedDBRemoved"); + }); + + // Create two tabs. + let tab = await browser.tabs.create({ url: `https://example.org${PAGE}` }); + tabs.push(tab.id); + + tab = await browser.tabs.create({ url: `https://example.com${PAGE}` }); + tabs.push(tab.id); + + // Create tab with cookieStoreId "firefox-container-1" + tab = await browser.tabs.create({ url: `https://example.net${PAGE}`, cookieStoreId: 'firefox-container-1' }); + tabs.push(tab.id); + } + + function contentScript() { + // eslint-disable-next-line mozilla/balanced-listeners + window.addEventListener( + "message", + msg => { + browser.test.sendMessage("indexedDBCreated"); + }, + true + ); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData", "tabs", "cookies"], + content_scripts: [ + { + matches: [ + "https://example.org/*/file_indexedDB.html", + "https://example.com/*/file_indexedDB.html", + "https://example.net/*/file_indexedDB.html", + ], + js: ["script.js"], + run_at: "document_start", + }, + ], + }, + files: { + "script.js": contentScript, + }, + }); + + await extension.startup(); + + await extension.awaitMessage("indexedDBCreated"); + await extension.awaitMessage("indexedDBCreated"); + await extension.awaitMessage("indexedDBCreated"); + + function getUsage() { + return new Promise(resolve => { + let qms = SpecialPowers.Services.qms; + let cb = SpecialPowers.wrapCallback(request => resolve(request.result)); + qms.getUsage(cb); + }); + } + + async function getOrigins() { + let origins = []; + let result = await getUsage(); + for (let i = 0; i < result.length; ++i) { + if (result[i].usage === 0) { + continue; + } + if ( + result[i].origin.startsWith("https://example.org") || + result[i].origin.startsWith("https://example.com") || + result[i].origin.startsWith("https://example.net") + ) { + origins.push(result[i].origin); + } + } + return origins.sort(); + } + + let origins = await getOrigins(); + is(origins.length, 3, "IndexedDB databases have been populated."); + + // Deleting private browsing mode data is silently ignored. + extension.sendMessage({ cookieStoreId: "firefox-private" }); + await extension.awaitMessage("indexedDBRemoved"); + + origins = await getOrigins(); + is(origins.length, 3, "All indexedDB remains after clearing firefox-private"); + + // Delete by hostname + extension.sendMessage({ hostnames: ["example.com"] }); + await extension.awaitMessage("indexedDBRemoved"); + + origins = await getOrigins(); + is(origins.length, 2, "IndexedDB data only for only two domains left"); + ok(origins[0].startsWith("https://example.net"), "example.net not deleted"); + ok(origins[1].startsWith("https://example.org"), "example.org not deleted"); + + // TODO: Bug 1643740 + if (AppConstants.platform != "android") { + // Delete by cookieStoreId + extension.sendMessage({ cookieStoreId: "firefox-container-1" }); + await extension.awaitMessage("indexedDBRemoved"); + + origins = await getOrigins(); + is(origins.length, 1, "IndexedDB data only for only one domain"); + ok(origins[0].startsWith("https://example.org"), "example.org not deleted"); + } + + // Delete all + extension.sendMessage({}); + await extension.awaitMessage("indexedDBRemoved"); + + origins = await getOrigins(); + is(origins.length, 0, "All IndexedDB data has been removed."); + + await extension.sendMessage("cleanup"); + await extension.awaitMessage("done"); + + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html new file mode 100644 index 0000000000..0b61ce341f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html @@ -0,0 +1,323 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.remove indexedDB</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + // make sure userContext is enabled. + return SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); +}); + +add_task(async function testLocalStorage() { + async function background() { + function waitForTabs() { + return new Promise(resolve => { + let tabs = {}; + + let listener = async (msg, { tab }) => { + if (msg !== "content-script-ready") { + return; + } + + tabs[tab.url] = tab; + if (Object.keys(tabs).length == 3) { + browser.runtime.onMessage.removeListener(listener); + resolve(tabs); + } + }; + browser.runtime.onMessage.addListener(listener); + }); + } + + function sendMessageToTabs(tabs, message) { + return Promise.all( + Object.values(tabs).map(tab => { + return browser.tabs.sendMessage(tab.id, message); + }) + ); + } + + let tabs = await waitForTabs(); + + browser.test.assertRejects( + browser.browsingData.removeLocalStorage({ since: Date.now() }), + "Firefox does not support clearing localStorage with 'since'.", + "Expected error received when using unimplemented parameter 'since'." + ); + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await browser.browsingData.removeLocalStorage({ + hostnames: ["example.com"], + }); + await browser.tabs.sendMessage(tabs["https://example.com/"].id, "checkLocalStorageCleared"); + await browser.tabs.sendMessage(tabs["https://example.net/"].id, "checkLocalStorageSet"); + + if ( + SpecialPowers.Services.domStorageManager.nextGenLocalStorageEnabled === + false + ) { + // This assertion fails when localStorage is using the legacy + // implementation (See Bug 1595431). + browser.test.log("Skipped assertion on nextGenLocalStorageEnabled=false"); + } else { + await browser.tabs.sendMessage(tabs["https://test1.example.com/"].id, "checkLocalStorageSet"); + } + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + await browser.browsingData.removeLocalStorage({}); + await sendMessageToTabs(tabs, "checkLocalStorageCleared"); + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + await browser.browsingData.remove({}, { localStorage: true }); + await sendMessageToTabs(tabs, "checkLocalStorageCleared"); + + // Can only delete cookieStoreId with LSNG enabled. + if (SpecialPowers.Services.domStorageManager.nextGenLocalStorageEnabled) { + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + await browser.browsingData.removeLocalStorage({ + cookieStoreId: "firefox-container-1", + }); + await browser.tabs.sendMessage(tabs["https://example.com/"].id, "checkLocalStorageSet"); + await browser.tabs.sendMessage(tabs["https://example.net/"].id, "checkLocalStorageSet"); + + // TODO: containers support is lacking on GeckoView (Bug 1643740) + if (!navigator.userAgent.includes("Android")) { + await browser.tabs.sendMessage(tabs["https://test1.example.com/"].id, "checkLocalStorageCleared"); + } + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + // Hostname doesn't match, so nothing cleared. + await browser.browsingData.removeLocalStorage({ + cookieStoreId: "firefox-container-1", + hostnames: ["example.net"], + }); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + // Deleting private browsing mode data is silently ignored. + await browser.browsingData.removeLocalStorage({ + cookieStoreId: "firefox-private", + }); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + } else { + await browser.test.assertRejects( + browser.browsingData.removeLocalStorage({ + cookieStoreId: "firefox-container-1", + }), + "Firefox does not support clearing localStorage with 'cookieStoreId'.", + "removeLocalStorage with cookieStoreId requires LSNG" + ); + } + + // Cleanup (checkLocalStorageCleared creates empty LS databases). + await browser.browsingData.removeLocalStorage({}); + + browser.test.notifyPass("done"); + } + + function contentScript() { + browser.runtime.onMessage.addListener(msg => { + if (msg === "resetLocalStorage") { + localStorage.clear(); + localStorage.setItem("test", "test"); + } else if (msg === "checkLocalStorageSet") { + browser.test.assertEq( + "test", + localStorage.getItem("test"), + `checkLocalStorageSet: ${location.href}` + ); + } else if (msg === "checkLocalStorageCleared") { + browser.test.assertEq( + null, + localStorage.getItem("test"), + `checkLocalStorageCleared: ${location.href}` + ); + } + }); + browser.runtime.sendMessage("content-script-ready"); + } + + // This extension is responsible for opening tabs with a specified + // cookieStoreId, we use a separate extension to make sure that browsingData + // works without the cookies permission. + let openTabsExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + name: "Open tabs", + browser_specific_settings: { gecko: { id: "open-tabs@tests.mozilla.org" }, }, + permissions: ["cookies"], + }, + async background() { + const TABS = [ + { url: "https://example.com" }, + { url: "https://example.net" }, + { + url: "https://test1.example.com", + cookieStoreId: 'firefox-container-1', + }, + ]; + + function awaitLoad(tabId) { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) { + if (tabId == tabId_ && changed.status == "complete") { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + } + + let tabs = []; + let loaded = []; + for (let options of TABS) { + let tab = await browser.tabs.create(options); + loaded.push(awaitLoad(tab.id)); + tabs.push(tab); + } + + await Promise.all(loaded); + + browser.test.onMessage.addListener(async msg => { + if (msg === "cleanup") { + const tabIds = tabs.map(tab => tab.id); + let removedTabs = 0; + browser.tabs.onRemoved.addListener(tabId => { + browser.test.log(`Removing tab ${tabId}.`); + if (tabIds.includes(tabId)) { + removedTabs++; + if (removedTabs == tabIds.length) { + browser.test.sendMessage("done"); + } + } + }); + await browser.tabs.remove(tabIds); + } + }); + } + }); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background, + manifest: { + name: "Test Extension", + browser_specific_settings: { gecko: { id: "localStorage@tests.mozilla.org" } }, + permissions: ["browsingData", "tabs"], + content_scripts: [ + { + matches: [ + "https://example.com/", + "https://example.net/", + "https://test1.example.com/", + ], + js: ["content-script.js"], + run_at: "document_end", + }, + ], + }, + files: { + "content-script.js": contentScript, + }, + }); + + await openTabsExtension.startup(); + + await extension.startup(); + await extension.awaitFinish("done"); + await extension.unload(); + + await openTabsExtension.sendMessage("cleanup"); + await openTabsExtension.awaitMessage("done"); + await openTabsExtension.unload(); +}); + +// Verify that browsingData.removeLocalStorage doesn't break on data stored +// in about:newtab or file principals. +add_task(async function test_browserData_on_aboutnewtab_and_file_data() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + async background() { + await browser.browsingData.removeLocalStorage({}).catch(err => { + browser.test.fail(`${err} :: ${err.stack}`); + }); + browser.test.sendMessage("done"); + }, + manifest: { + browser_specific_settings: { gecko: { id: "indexed-db-file@test.mozilla.org" } }, + permissions: ["browsingData"], + }, + }); + + await new Promise(resolve => { + const chromeScript = SpecialPowers.loadChromeScript(async () => { + /* eslint-env mozilla/chrome-script */ + const { SiteDataTestUtils } = ChromeUtils.import( + "resource://testing-common/SiteDataTestUtils.jsm" + ); + await SiteDataTestUtils.addToIndexedDB("about:newtab"); + await SiteDataTestUtils.addToIndexedDB("file:///fake/file"); + sendAsyncMessage("done"); + }); + + chromeScript.addMessageListener("done", () => { + chromeScript.destroy(); + resolve(); + }); + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_browserData_should_not_remove_extension_data() { + if (SpecialPowers.getBoolPref("dom.storage.enable_unsupported_legacy_implementation")) { + // When LSNG isn't enabled, the browsingData API does still clear + // all the extensions localStorage if called without a list of specific + // origins to clear. + info("Test skipped because LSNG is currently disabled"); + return; + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + async background() { + window.localStorage.setItem("key", "value"); + await browser.browsingData.removeLocalStorage({}).catch(err => { + browser.test.fail(`${err} :: ${err.stack}`); + }); + browser.test.sendMessage("done", window.localStorage.getItem("key")); + }, + manifest: { + browser_specific_settings: { gecko: { id: "extension-data@tests.mozilla.org" } }, + permissions: ["browsingData"], + }, + }); + + await extension.startup(); + const lsValue = await extension.awaitMessage("done"); + is(lsValue, "value", "Got the expected localStorage data"); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html new file mode 100644 index 0000000000..bf4bd8fe80 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html @@ -0,0 +1,69 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.remove indexedDB</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// NB: Since plugins are disabled, there is never any data to clear. +// We are really testing that these operations are no-ops. + +add_task(async function testPluginData() { + async function background() { + const REFERENCE_DATE = Date.now(); + const TEST_CASES = [ + // Clear plugin data with no since value. + {}, + // Clear pluginData with recent since value. + { since: REFERENCE_DATE - 20000 }, + // Clear pluginData with old since value. + { since: REFERENCE_DATE - 1000000 }, + // Clear pluginData for specific hosts. + { hostnames: ["bar.com", "baz.com"] }, + // Clear pluginData for no hosts. + { hostnames: [] }, + ]; + + for (let method of ["removePluginData", "remove"]) { + for (let options of TEST_CASES) { + browser.test.log(`Testing ${method} with ${JSON.stringify(options)}`); + if (method == "removePluginData") { + await browser.browsingData.removePluginData(options); + } else { + await browser.browsingData.remove(options, { pluginData: true }); + } + } + } + + browser.test.sendMessage("done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["tabs", "browsingData"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + + // This test has no assertions because it's only meant to check that we don't + // throw when calling removePluginData and remove with pluginData: true. + ok(true, "dummy check"); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html new file mode 100644 index 0000000000..900546f32c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html @@ -0,0 +1,141 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.remove indexedDB</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const { TestUtils } = SpecialPowers.ChromeUtils.import( + "resource://testing-common/TestUtils.jsm" +); + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); +}); + +add_task(async function testServiceWorkers() { + async function background() { + const PAGE = + "/tests/toolkit/components/extensions/test/mochitest/file_serviceWorker.html"; + + browser.runtime.onMessage.addListener(msg => { + browser.test.sendMessage("serviceWorkerRegistered"); + }); + + let tabs = []; + + browser.test.onMessage.addListener(async msg => { + if (msg == "cleanup") { + await browser.tabs.remove(tabs.map(tab => tab.id)); + browser.test.sendMessage("done"); + return; + } + + await browser.browsingData.remove( + { hostnames: msg.hostnames }, + { serviceWorkers: true } + ); + browser.test.sendMessage("serviceWorkersRemoved"); + }); + + // Create two serviceWorkers. + let tab = await browser.tabs.create({ url: `http://mochi.test:8888${PAGE}` }); + tabs.push(tab); + + tab = await browser.tabs.create({ url: `https://example.com${PAGE}` }); + tabs.push(tab); + } + + function contentScript() { + // eslint-disable-next-line mozilla/balanced-listeners + window.addEventListener( + "message", + msg => { + if (msg.data == "serviceWorkerRegistered") { + browser.runtime.sendMessage("serviceWorkerRegistered"); + } + }, + true + ); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData", "tabs"], + content_scripts: [ + { + matches: [ + "http://mochi.test/*/file_serviceWorker.html", + "https://example.com/*/file_serviceWorker.html", + ], + js: ["script.js"], + run_at: "document_start", + }, + ], + }, + files: { + "script.js": contentScript, + }, + }); + + await extension.startup(); + await extension.awaitMessage("serviceWorkerRegistered"); + await extension.awaitMessage("serviceWorkerRegistered"); + + // Even though we await the registrations by waiting for the messages, + // sometimes the serviceWorkers are still not registered at this point. + async function getRegistrations(count) { + await TestUtils.waitForCondition( + async () => (await SpecialPowers.registeredServiceWorkers()).length === count, + `Wait for ${count} service workers to be registered` + ); + return SpecialPowers.registeredServiceWorkers(); + } + + let serviceWorkers = await getRegistrations(2); + is(serviceWorkers.length, 2, "ServiceWorkers have been registered."); + + extension.sendMessage({ hostnames: ["example.com"] }); + await extension.awaitMessage("serviceWorkersRemoved"); + + serviceWorkers = await getRegistrations(1); + is( + serviceWorkers.length, + 1, + "ServiceWorkers for example.com have been removed." + ); + + let { scriptSpec } = serviceWorkers[0]; + dump(`Service worker spec: ${scriptSpec}`); + ok(scriptSpec.startsWith("http://mochi.test:8888/"), + "ServiceWorkers for example.com have been removed."); + + extension.sendMessage({}); + await extension.awaitMessage("serviceWorkersRemoved"); + + serviceWorkers = await getRegistrations(0); + is(serviceWorkers.length, 0, "All ServiceWorkers have been removed."); + + extension.sendMessage("cleanup"); + await extension.awaitMessage("done"); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html new file mode 100644 index 0000000000..11c690e5bf --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html @@ -0,0 +1,65 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.settings</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const SETTINGS_LIST = [ + "cache", + "cookies", + "history", + "formData", + "downloads", +].sort(); + +add_task(async function testSettings() { + async function background() { + browser.browsingData.settings().then(settings => { + browser.test.sendMessage("settings", settings); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + await extension.startup(); + let settings = await extension.awaitMessage("settings"); + + // Verify that we get the keys back we expect. + isDeeply( + Object.entries(settings.dataToRemove) + .filter(([key, value]) => value) + .map(([key, value]) => key) + .sort(), + SETTINGS_LIST, + "dataToRemove contains expected properties." + ); + isDeeply( + Object.entries(settings.dataRemovalPermitted) + .filter(([key, value]) => value) + .map(([key, value]) => key) + .sort(), + SETTINGS_LIST, + "dataToRemove contains expected properties." + ); + is("since" in settings.options, true, "options contains |since|"); + + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html b/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html new file mode 100644 index 0000000000..7116d03235 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_cookies.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); +}); + +add_task(async function test_contentscript() { + function contentScript() { + let canvas = document.createElement("canvas"); + canvas.width = canvas.height = "100"; + + let ctx = canvas.getContext("2d"); + ctx.fillStyle = "green"; + ctx.fillRect(0, 0, 100, 100); + let data = ctx.getImageData(0, 0, 100, 100); + + browser.test.sendMessage("data-color", data.data[1]); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }; + const url = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_sample.html"; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + let win = window.open(url); + let color = await extension.awaitMessage("data-color"); + is(color, 128, "Got correct pixel data for green"); + win.close(); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html b/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html new file mode 100644 index 0000000000..77ac767391 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html @@ -0,0 +1,210 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Clipboard permissions tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +/* globals doCopy, doPaste */ +function shared() { + let field = document.createElement("textarea"); + document.body.appendChild(field); + field.contentEditable = true; + + this.doCopy = function(txt) { + field.value = txt; + field.select(); + return document.execCommand("copy"); + }; + + this.doPaste = function() { + field.select(); + return document.execCommand("paste") && field.value; + }; +} + +add_task(async function test_background_clipboard_permissions() { + function backgroundScript() { + browser.test.assertEq(false, doCopy("whatever"), + "copy should be denied without permission"); + browser.test.assertEq(false, doPaste(), + "paste should be denied without permission"); + browser.test.sendMessage("ready"); + } + let extensionData = { + background: [shared, backgroundScript], + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + await extension.awaitMessage("ready"); + + await extension.unload(); +}); + +add_task(async function test_background_clipboard_copy() { + function backgroundScript() { + browser.test.onMessage.addListener(txt => { + browser.test.assertEq(true, doCopy(txt), + "copy should be allowed with permission"); + }); + browser.test.sendMessage("ready"); + } + let extensionData = { + background: `(${shared})();(${backgroundScript})();`, + manifest: { + permissions: [ + "clipboardWrite", + ], + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitMessage("ready"); + + const DUMMY_STR = "dummy string to copy"; + await new Promise(resolve => { + SimpleTest.waitForClipboard(DUMMY_STR, () => { + extension.sendMessage(DUMMY_STR); + }, resolve, resolve); + }); + + await extension.unload(); +}); + +add_task(async function test_contentscript_clipboard_permissions() { + function contentScript() { + browser.test.assertEq(false, doCopy("whatever"), + "copy should be denied without permission"); + browser.test.assertEq(false, doPaste(), + "paste should be denied without permission"); + browser.test.sendMessage("ready"); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + + await extension.unload(); +}); + +add_task(async function test_contentscript_clipboard_copy() { + function contentScript() { + browser.test.onMessage.addListener(txt => { + browser.test.assertEq(true, doCopy(txt), + "copy should be allowed with permission"); + }); + browser.test.sendMessage("ready"); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + await extension.awaitMessage("ready"); + + const DUMMY_STR = "dummy string to copy in content script"; + await new Promise(resolve => { + SimpleTest.waitForClipboard(DUMMY_STR, () => { + extension.sendMessage(DUMMY_STR); + }, resolve, resolve); + }); + + win.close(); + + await extension.unload(); +}); + +add_task(async function test_contentscript_clipboard_paste() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "clipboardRead", + ], + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html"], + js: ["shared.js", "content_script.js"], + }], + }, + files: { + "shared.js": shared, + "content_script.js": () => { + browser.test.sendMessage("paste", doPaste()); + }, + }, + }); + + const STRANGE = "A Strange Thing"; + SpecialPowers.clipboardCopyString(STRANGE); + + await extension.startup(); + const win = window.open("file_sample.html"); + + const paste = await extension.awaitMessage("paste"); + is(paste, STRANGE, "the correct string was pasted"); + + win.close(); + await extension.unload(); +}); + +add_task(async function test_background_clipboard_paste() { + function background() { + browser.test.sendMessage("paste", doPaste()); + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["clipboardRead"], + }, + background: [shared, background], + }); + + const STRANGE = "Stranger Things"; + SpecialPowers.clipboardCopyString(STRANGE); + + await extension.startup(); + + const paste = await extension.awaitMessage("paste"); + is(paste, STRANGE, "the correct string was pasted"); + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html b/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html new file mode 100644 index 0000000000..b5d5f6764a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html @@ -0,0 +1,262 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Clipboard permissions tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; +/** + * This cannot be a xpcshell test, because: + * - On Android, copyString of nsIClipboardHelper segfaults because + * widget/android/nsClipboard.cpp calls java::Clipboard::SetText, which is + * unavailable in xpcshell. + * - On Windows, the clipboard is unavailable to xpcshell. + */ + +function resetClipboard() { + SpecialPowers.clipboardCopyString( + "This is the default value of the clipboard in the test."); +} + +async function checkClipboardHasTestImage(imageType) { + async function backgroundScript(imageType) { + async function verifyImage(img) { + // Checks whether the image is a 1x1 red image. + browser.test.assertEq(1, img.naturalWidth, "image width should match"); + browser.test.assertEq(1, img.naturalHeight, "image height should match"); + + let canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + let ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); // Draw without scaling. + let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; + let expectedColor; + if (imageType === "png") { + expectedColor = [255, 0, 0]; + } else if (imageType === "jpeg") { + expectedColor = [254, 0, 0]; + } + let {os} = await browser.runtime.getPlatformInfo(); + if (os === "mac") { + // Due to https://bugzil.la/1396587, the pasted image differs from the + // original/expected image. + // Once that bug is fixed, this whole macOS-only branch can be removed. + if (imageType === "png") { + expectedColor = [255, 38, 0]; + } else if (imageType === "jpeg") { + expectedColor = [255, 38, 0]; + } + } + browser.test.assertEq(expectedColor[0], r, "pixel should be red"); + browser.test.assertEq(expectedColor[1], g, "pixel should not contain green"); + browser.test.assertEq(expectedColor[2], b, "pixel should not contain blue"); + browser.test.assertEq(255, a, "pixel should be opaque"); + } + + let editable = document.body; + editable.contentEditable = true; + let file; + await new Promise(resolve => { + document.addEventListener("paste", function(event) { + browser.test.assertEq(1, event.clipboardData.types.length, "expected one type"); + browser.test.assertEq("Files", event.clipboardData.types[0], "expected type"); + browser.test.assertEq(1, event.clipboardData.files.length, "expected one file"); + + // After returning from the paste event, event.clipboardData is cleaned, so we + // have to store the file in a separate variable. + file = event.clipboardData.files[0]; + resolve(); + }, {once: true}); + + document.execCommand("paste"); // requires clipboardWrite permission. + }); + + // When image data is copied, its first frame is decoded and exported to the + // clipboard. The pasted result is always an unanimated PNG file, regardless + // of the input. + browser.test.assertEq("image/png", file.type, "expected file.type"); + + // event.files[0] should be an accurate representation of the input image. + { + let img = new Image(); + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = () => reject(new Error(`Failed to load image ${img.src} of size ${file.size}`)); + img.src = URL.createObjectURL(file); + }); + + await verifyImage(img); + } + + // This confirms that an image was put on the clipboard. + // In contrast, when document.execCommand('copy') + clipboardData.setData + // is used, then the 'paste' event will also have the image data (as tested + // above), but the contentEditable area will be empty. + { + let imgs = editable.querySelectorAll("img"); + browser.test.assertEq(1, imgs.length, "should have pasted one image"); + await verifyImage(imgs[0]); + } + browser.test.sendMessage("tested image on clipboard"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})("${imageType}");`, + manifest: { + permissions: ["clipboardRead"], + }, + }); + await extension.startup(); + await extension.awaitMessage("tested image on clipboard"); + await extension.unload(); +} + +add_task(async function test_without_clipboard_permission() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.assertEq(undefined, browser.clipboard, + "clipboard API requires the clipboardWrite permission."); + browser.test.notifyPass(); + }, + manifest: { + permissions: ["clipboardRead"], + }, + }); + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_copy_png() { + if (AppConstants.platform === "android") { + return; // Android does not support images on the clipboard. + } + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // A 1x1 red PNG image. + let b64data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEX/AAD///9BHTQRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg=="; + let imageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer; + await browser.clipboard.setImageData(imageData, "png"); + browser.test.sendMessage("Called setImageData with PNG"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + resetClipboard(); + + await extension.startup(); + await extension.awaitMessage("Called setImageData with PNG"); + await extension.unload(); + + await checkClipboardHasTestImage("png"); +}); + +add_task(async function test_copy_jpeg() { + if (AppConstants.platform === "android") { + return; // Android does not support images on the clipboard. + } + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // A 1x1 red JPEG image, created using: convert xc:red red.jpg. + // JPEG is lossy, and the red pixel value is actually #FE0000 instead of + // #FF0000 (also seen using: convert red.jpg text:-). + let b64data = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACP/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAVAQEBAAAAAAAAAAAAAAAAAAAHCf/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/ADoDFU3/2Q=="; + let imageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer; + await browser.clipboard.setImageData(imageData, "jpeg"); + browser.test.sendMessage("Called setImageData with JPEG"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + resetClipboard(); + + await extension.startup(); + await extension.awaitMessage("Called setImageData with JPEG"); + await extension.unload(); + + await checkClipboardHasTestImage("jpeg"); +}); + +add_task(async function test_copy_invalid_image() { + if (AppConstants.platform === "android") { + // Android does not support images on the clipboard. + return; + } + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // This is a PNG image. + let b64data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEX/AAD///9BHTQRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg=="; + let pngImageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer; + await browser.test.assertRejects( + browser.clipboard.setImageData(pngImageData, "jpeg"), + "Data is not a valid jpeg image", + "Image data that is not valid for the given type should be rejected."); + browser.test.sendMessage("finished invalid image"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("finished invalid image"); + await extension.unload(); +}); + +add_task(async function test_copy_invalid_image_type() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // setImageData expects "png" or "jpeg", but we pass "image/png" here. + browser.test.assertThrows( + () => { browser.clipboard.setImageData(new ArrayBuffer(0), "image/png"); }, + "Type error for parameter imageType (Invalid enumeration value \"image/png\") for clipboard.setImageData.", + "An invalid type for setImageData should be rejected."); + browser.test.sendMessage("finished invalid type"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("finished invalid type"); + await extension.unload(); +}); + +if (AppConstants.platform === "android") { + add_task(async function test_setImageData_unsupported_on_android() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // Android does not support images on the clipboard, + // so it should not try to decode an image but fail immediately. + await browser.test.assertRejects( + browser.clipboard.setImageData(new ArrayBuffer(0), "png"), + "Writing images to the clipboard is not supported on Android", + "Should get an error when setImageData is called on Android."); + browser.test.sendMessage("finished unsupported setImageData"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("finished unsupported setImageData"); + await extension.unload(); + }); +} + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html new file mode 100644 index 0000000000..127305715f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html @@ -0,0 +1,116 @@ +<!doctype html> +<html> +<head> + <title>Test content script match_about_blank option</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_contentscript_about_blank() { + const manifest = { + content_scripts: [ + { + match_about_blank: true, + matches: ["http://mochi.test/*/file_with_about_blank.html", "https://example.com/*"], + all_frames: true, + css: ["all.css"], + js: ["all.js"], + }, { + matches: ["http://mochi.test/*/file_with_about_blank.html"], + css: ["mochi_without.css"], + js: ["mochi_without.js"], + all_frames: true, + }, { + match_about_blank: true, + matches: ["http://mochi.test/*/file_with_about_blank.html"], + css: ["mochi_with.css"], + js: ["mochi_with.js"], + all_frames: true, + }, + ], + }; + + const files = { + "all.js": function() { + browser.runtime.sendMessage("all"); + }, + "all.css": ` + body { color: red; } + `, + "mochi_without.js": function() { + browser.runtime.sendMessage("mochi_without"); + }, + "mochi_without.css": ` + body { background: yellow; } + `, + "mochi_with.js": function() { + browser.runtime.sendMessage("mochi_with"); + }, + "mochi_with.css": ` + body { text-align: right; } + `, + }; + + function background() { + browser.runtime.onMessage.addListener((script, {url}) => { + const kind = url.startsWith("about:") ? url : "top"; + browser.test.sendMessage("script", [script, kind, url]); + browser.test.sendMessage(`${script}:${kind}`); + }); + } + + const PATH = "tests/toolkit/components/extensions/test/mochitest/file_with_about_blank.html"; + const extension = ExtensionTestUtils.loadExtension({manifest, files, background}); + await extension.startup(); + + let count = 0; + extension.onMessage("script", script => { + info(`script ran: ${script}`); + count++; + }); + + let win = window.open("https://example.com/" + PATH); + await Promise.all([ + extension.awaitMessage("all:top"), + extension.awaitMessage("all:about:blank"), + extension.awaitMessage("all:about:srcdoc"), + ]); + is(count, 3, "exactly 3 scripts ran"); + win.close(); + + win = window.open("http://mochi.test:8888/" + PATH); + await Promise.all([ + extension.awaitMessage("all:top"), + extension.awaitMessage("all:about:blank"), + extension.awaitMessage("all:about:srcdoc"), + extension.awaitMessage("mochi_without:top"), + extension.awaitMessage("mochi_with:top"), + extension.awaitMessage("mochi_with:about:blank"), + extension.awaitMessage("mochi_with:about:srcdoc"), + ]); + + let style = win.getComputedStyle(win.document.body); + is(style.color, "rgb(255, 0, 0)", "top window text color is red"); + is(style.backgroundColor, "rgb(255, 255, 0)", "top window background is yellow"); + is(style.textAlign, "right", "top window text is right-aligned"); + + let a_b = win.document.getElementById("a_b"); + style = a_b.contentWindow.getComputedStyle(a_b.contentDocument.body); + is(style.color, "rgb(255, 0, 0)", "about:blank iframe text color is red"); + is(style.backgroundColor, "rgba(0, 0, 0, 0)", "about:blank iframe background is transparent"); + is(style.textAlign, "right", "about:blank text is right-aligned"); + + is(count, 10, "exactly 7 more scripts ran"); + win.close(); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html new file mode 100644 index 0000000000..96091fd959 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html @@ -0,0 +1,711 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +// Create a test extension with the provided function as the background +// script. The background script will have a few helpful functions +// available. +/* global awaitLoad, gatherFrameSources */ +function makeExtension({ + background, + useScriptingAPI = false, + manifest_version = 2, + host_permissions, +}) { + // Wait for a webNavigation.onCompleted event where the details for the + // loaded page match the attributes of `filter`. + function awaitLoad(filter) { + return new Promise(resolve => { + const listener = details => { + if (Object.keys(filter).every(key => details[key] === filter[key])) { + browser.webNavigation.onCompleted.removeListener(listener); + resolve(); + } + }; + browser.webNavigation.onCompleted.addListener(listener); + }); + } + + // Return a string with a (sorted) list of the source of all frames + // in the given tab into which this extension can inject scripts + // (ie all frames for which it has the activeTab permission). + // Source is the hostname for frames in http sources, or the full + // location href in other documents (eg about: pages) + const gatherFrameSources = useScriptingAPI ? + async function gatherFrameSources(tabid) { + let results = await browser.scripting.executeScript({ + target: { tabId: tabid, allFrames: true }, + func: () => window.location.hostname || window.location.href, + }); + // Adjust `result` so that it looks like the one returned by + // `tabs.executeScript()`. + let result = results.map(res => res.result); + + return String(result.sort()); + } : async function gatherFrameSources(tabid) { + let result = await browser.tabs.executeScript(tabid, { + allFrames: true, + matchAboutBlank: true, + code: "window.location.hostname || window.location.href;", + }); + + return String(result.sort()); + }; + + const permissions = ["webNavigation"]; + if (useScriptingAPI) { + permissions.push("scripting"); + } + + // When host_permissions is passed, test "automatic activeTab" for ungranted + // host_permissions in mv3, else test with the normal activeTab permission. + if (!host_permissions) { + permissions.push("activeTab"); + } + + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + permissions, + host_permissions, + }, + background: [ + `const useScriptingAPI = ${useScriptingAPI};`, + `const manifest_version = ${manifest_version};`, + `${awaitLoad}`, + `${gatherFrameSources}`, + `${ExtensionTestCommon.serializeScript(background)}`, + ].join("\n") + }); +} + +// Helper function to verify that executeScript() fails without the activeTab +// permission (or any specific origin permissions). +const verifyNoActiveTab = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + await browser.test.assertRejects( + gatherFrameSources(tab.id), + /^Missing host permission/, + "executeScript should fail without activeTab permission" + ); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("no-active-tab"); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + await extension.awaitFinish("no-active-tab"); + await extension.unload(); +}; + +add_task(async function test_no_activeTab_tabs() { + await verifyNoActiveTab({ useScriptingAPI: false }); +}); + +add_task(async function test_no_activeTab_scripting() { + await verifyNoActiveTab({ useScriptingAPI: true }); +}); + +add_task(async function test_no_activeTab_scripting_mv3() { + await verifyNoActiveTab({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_no_activeTab_scripting_mv3_autoActiveTab() { + await verifyNoActiveTab({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["http://mochi.test/"], + }); +}); + +// Test helper to verify that dynamically created iframes do not get the +// activeTab permission. +const verifyDynamicFrames = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const BASE_HOST = "www.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: `https://${BASE_HOST}/`}), + awaitLoad({frameId: 0}), + ]); + + function inject() { + let nframes = 4; + function frameLoaded() { + nframes--; + if (nframes == 0) { + browser.runtime.sendMessage("frames-loaded"); + } + } + + let frame = document.createElement("iframe"); + frame.addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(frame); + + let div = document.createElement("div"); + div.innerHTML = "<iframe src='https://test1.example.com/'></iframe>"; + let framelist = div.getElementsByTagName("iframe"); + browser.test.assertEq(1, framelist.length, "Found 1 frame inside div"); + framelist[0].addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(div); + + let div2 = document.createElement("div"); + div2.innerHTML = "<iframe srcdoc=\"<iframe src='https://test2.example.com/'></iframe>\"></iframe>"; + framelist = div2.getElementsByTagName("iframe"); + browser.test.assertEq(1, framelist.length, "Found 1 frame inside div"); + framelist[0].addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(div2); + + const URL = "https://www.example.com/tests/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html"; + + let xhr = new XMLHttpRequest(); + xhr.open("GET", URL); + xhr.responseType = "document"; + xhr.overrideMimeType("text/html"); + + xhr.addEventListener("load", () => { + if (xhr.readyState != 4) { + return; + } + if (xhr.status != 200) { + browser.runtime.sendMessage("error"); + } + + let frame = xhr.response.getElementById("frame"); + browser.test.assertTrue(frame, "Found frame in response document"); + frame.addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(frame); + }, {once: true}); + xhr.addEventListener("error", () => { + browser.runtime.sendMessage("error"); + }, {once: true}); + xhr.send(); + } + + browser.test.onMessage.addListener(async msg => { + if (msg !== "go") { + browser.test.fail(`unexpected message received: ${msg}`); + return; + } + + let loadedPromise = new Promise((resolve, reject) => { + let listener = msg => { + let unlisten = () => browser.runtime.onMessage.removeListener(listener); + if (msg == "frames-loaded") { + unlisten(); + resolve(); + } else if (msg == "error") { + unlisten(); + reject(); + } + }; + browser.runtime.onMessage.addListener(listener); + }); + + if (useScriptingAPI) { + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: inject, + }); + } else { + await browser.tabs.executeScript(tab.id, { + code: `(${inject})();`, + }); + } + + await loadedPromise; + + let result = await gatherFrameSources(tab.id); + + if (manifest_version < 3) { + browser.test.assertEq( + String([BASE_HOST]), + result, + "Script is not injected into dynamically created frames" + ); + } else { + browser.test.assertEq( + String(["about:blank", "about:srcdoc", BASE_HOST]), + result, + `Script injected only into (same origin) about:blank-ish dynamically created frames` + ); + } + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("dynamic-frames"); + }); + + browser.test.sendMessage("ready", tab.id); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("dynamic-frames"); + + await extension.unload(); +}; + +add_task(async function test_dynamic_frames_tabs() { + await verifyDynamicFrames({ useScriptingAPI: false }); +}); + +add_task(async function test_dynamic_frames_scripting() { + await verifyDynamicFrames({ useScriptingAPI: true }); +}); + +add_task(async function test_dynamic_frames_scripting_mv3() { + await verifyDynamicFrames({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_dynamic_frames_scripting_mv3_autoActiveTab() { + await verifyDynamicFrames({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["https://www.example.com/"], + }); +}); + +// Test helper to verify that an iframe created from an <iframe srcdoc> gets +// the activeTab permission. +const verifySrcdoc = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html"; + const OUTER_SOURCE = "about:srcdoc"; + const PAGE_SOURCE = "mochi.test"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg !== "go") { + browser.test.fail(`unexpected message received: ${msg}`); + return; + } + + let result = await gatherFrameSources(tab.id); + + if (manifest_version < 3) { + browser.test.assertEq( + String([OUTER_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "Script is injected into frame created from <iframe srcdoc>" + ); + } else { + browser.test.assertEq( + String([OUTER_SOURCE, PAGE_SOURCE]), + result, + "Script is not injected into cross-origin frame created from <iframe srcdoc>" + ); + } + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("srcdoc"); + }); + + browser.test.sendMessage("ready", tab.id); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("srcdoc"); + + await extension.unload(); +}; + +add_task(async function test_srcdoc_tabs() { + await verifySrcdoc({ useScriptingAPI: false }); +}); + +add_task(async function test_srcdoc_scripting() { + await verifySrcdoc({ useScriptingAPI: true }); +}); + +add_task(async function test_srcdoc_scripting_mv3() { + await verifySrcdoc({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_srcdoc_scripting_mv3_autoActiveTab() { + await verifySrcdoc({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["http://mochi.test/"], + }); +}); + +// Test helper to verify that navigating frames by setting the src attribute +// from the parent page revokes the activeTab permission. +const verifyNavigateBySrc = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + const PAGE_SOURCE = "mochi.test"; + const EMPTY_SOURCE = "about:blank"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg !== "go") { + browser.test.fail(`unexpected message received: ${msg}`); + return; + } + + let result = await gatherFrameSources(tab.id); + if (manifest_version < 3) { + browser.test.assertEq( + String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "In original page, script is injected into base page and original frames" + ); + } else { + browser.test.assertEq( + String([EMPTY_SOURCE, PAGE_SOURCE]), + result, + "In original page, script is injected into same-origin frames" + ); + } + + let loadedPromise = awaitLoad({tabId: tab.id}); + + let func = () => { + document.getElementById('emptyframe').src = 'http://test2.example.com/'; + }; + + if (useScriptingAPI) { + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func, + }); + } else { + await browser.tabs.executeScript(tab.id, { code: `(${func})();` }); + } + + await loadedPromise; + + + result = await gatherFrameSources(tab.id); + if (manifest_version < 3) { + browser.test.assertEq( + String([PAGE_SOURCE, FRAME_SOURCE]), + result, + "Script is not injected into initially empty frame after navigation" + ); + } else { + browser.test.assertEq( + String([PAGE_SOURCE]), + result, + "Script is not injected into initially empty frame after navigation" + ); + } + + loadedPromise = awaitLoad({tabId: tab.id}); + + func = () => { + document.getElementById('regularframe').src = 'http://mochi.test:8888/'; + }; + + if (useScriptingAPI) { + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func, + }); + } else { + await browser.tabs.executeScript(tab.id, { code: `(${func})();` }); + } + + await loadedPromise; + + result = await gatherFrameSources(tab.id); + + if (manifest_version < 3) { + browser.test.assertEq( + String([PAGE_SOURCE]), + result, + "Script is not injected into regular frame after navigation" + ); + } else { + browser.test.assertEq( + String([PAGE_SOURCE, PAGE_SOURCE]), + result, + "Script injected into frame after navigating to same-origin" + ); + } + + await browser.tabs.remove(tab.id); + browser.test.notifyPass("test-scripts"); + }); + + browser.test.sendMessage("ready", tab.id); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("test-scripts"); + + await extension.unload(); +}; + +add_task(async function test_navigate_by_src_tabs() { + await verifyNavigateBySrc({ useScriptingAPI: false }); +}); + +add_task(async function test_navigate_by_src_scripting() { + await verifyNavigateBySrc({ useScriptingAPI: true }); +}); + +add_task(async function test_navigate_by_src_scripting_mv3() { + await verifyNavigateBySrc({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_navigate_by_src_scripting_mv3_autoActiveTab() { + await verifyNavigateBySrc({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["http://mochi.test/"], + }); +}); + +// Test helper to verify that navigating frames by setting window.location from +// inside the frame revokes the activeTab permission. +const verifyNavigateByWindowLocation = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + const PAGE_SOURCE = "mochi.test"; + const EMPTY_SOURCE = "about:blank"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg !== "go") { + browser.test.fail(`unexpected message received: ${msg}`); + return; + } + + let result = await gatherFrameSources(tab.id); + + if (manifest_version < 3) { + browser.test.assertEq( + String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "Script initially injected into all frames" + ); + } else { + browser.test.assertEq( + String([EMPTY_SOURCE, PAGE_SOURCE]), + result, + "Script initially injected into all same-origin frames" + ); + } + + let nframes = 0; + let frames = await browser.webNavigation.getAllFrames({tabId: tab.id}); + for (let frame of frames) { + if (frame.parentFrameId == -1) { + continue; + } + + if (manifest_version >= 3 && frame.url.includes(FRAME_SOURCE)) { + // In MV3, can't access cross-origin iframes from the start. + + let invalidPromise = browser.scripting.executeScript({ + target: { tabId: tab.id, frameIds: [frame.frameId] }, + func: () => window.location.hostname, + }); + await browser.test.assertRejects( + invalidPromise, + /^Missing host permission for the tab or frames/, + "executeScript should fail on cross-origin frame" + ); + + continue; + } + + let loadPromise = awaitLoad({ + tabId: tab.id, + frameId: frame.frameId, + }); + + let func = () => { + window.location.href = 'https://test2.example.com/'; + }; + + if (useScriptingAPI) { + await browser.scripting.executeScript({ + target: { tabId: tab.id, frameIds: [frame.frameId] }, + func, + }); + } else { + await browser.tabs.executeScript(tab.id, { + frameId: frame.frameId, + matchAboutBlank: true, + code: `(${func})();`, + }); + } + + await loadPromise; + + let executePromise; + func = () => window.location.hostname; + + if (useScriptingAPI) { + executePromise = browser.scripting.executeScript({ + target: { tabId: tab.id, frameIds: [frame.frameId] }, + func, + }); + } else { + executePromise = browser.tabs.executeScript(tab.id, { + frameId: frame.frameId, + matchAboutBlank: true, + code: `(${func})();`, + }); + } + + await browser.test.assertRejects( + executePromise, + /^Missing host permission for the tab or frames/, + "executeScript should have failed on navigated frame" + ); + + nframes++; + } + + if (manifest_version < 3) { + browser.test.assertEq(2, nframes, "Found 2 frames"); + } else { + browser.test.assertEq(1, nframes, "Found 1 frame"); + } + + await browser.tabs.remove(tab.id); + browser.test.notifyPass("scripted-navigation"); + }); + + browser.test.sendMessage("ready", tab.id); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("scripted-navigation"); + + await extension.unload(); +}; + +add_task(async function test_navigate_by_window_location_tabs() { + await verifyNavigateByWindowLocation({ useScriptingAPI: false }); +}); + +add_task(async function test_navigate_by_window_location_scripting() { + await verifyNavigateByWindowLocation({ useScriptingAPI: true }); +}); + +add_task(async function test_navigate_by_window_location_scripting_mv3() { + await verifyNavigateByWindowLocation({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_navigate_by_window_location_scripting_mv3_autoActiveTab() { + await verifyNavigateByWindowLocation({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["http://mochi.test/"], + }); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html new file mode 100644 index 0000000000..6e2420e1c5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script caching</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// This file defines content scripts. + +const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + +add_task(async function test_contentscript_cache() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + + permissions: ["<all_urls>", "tabs"], + }, + + async background() { + // Force our extension instance to be initialized for the current content process. + await browser.tabs.insertCSS({code: ""}); + + browser.test.sendMessage("origin", location.origin); + }, + + files: { + "content_script.js": function() { + browser.test.sendMessage("content-script-loaded"); + }, + }, + }); + + await extension.startup(); + + let origin = await extension.awaitMessage("origin"); + let scriptUrl = `${origin}/content_script.js`; + + const { ExtensionProcessScript } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/ExtensionProcessScript.jsm" + ); + let ext = ExtensionProcessScript.getExtensionChild(extension.id); + + ext.staticScripts.expiryTimeout = 3000; + is(ext.staticScripts.size, 0, "Should have no cached scripts"); + + let win = window.open(`${BASE}/file_sample.html`); + await extension.awaitMessage("content-script-loaded"); + + if (AppConstants.platform !== "android") { + is(ext.staticScripts.size, 1, "Should have one cached script"); + ok(ext.staticScripts.has(scriptUrl), "Script cache should contain script URL"); + } + + let chromeScript, chromeScriptDone; + let { appinfo } = SpecialPowers.Services; + if (appinfo.processType === appinfo.PROCESS_TYPE_CONTENT) { + /* globals addMessageListener, assert */ + chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("check-script-cache", extensionId => { + const { ExtensionProcessScript } = ChromeUtils.import( + "resource://gre/modules/ExtensionProcessScript.jsm" + ); + let ext = ExtensionProcessScript.getExtensionChild(extensionId); + + if (ext && ext.staticScripts) { + assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process"); + } + + sendAsyncMessage("done"); + }); + }); + chromeScript.sendAsyncMessage("check-script-cache", extension.id); + chromeScriptDone = chromeScript.promiseOneMessage("done"); + } + + SimpleTest.requestFlakyTimeout("Required to test expiry timeout"); + await new Promise(resolve => setTimeout(resolve, 3000)); + is(ext.staticScripts.size, 0, "Should have no cached scripts"); + + if (chromeScript) { + await chromeScriptDone; + chromeScript.destroy(); + } + + win.close(); + + win = window.open(`${BASE}/file_sample.html`); + await extension.awaitMessage("content-script-loaded"); + + is(ext.staticScripts.size, 1, "Should have one cached script"); + ok(ext.staticScripts.has(scriptUrl)); + + SpecialPowers.Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize"); + + is(ext.staticScripts.size, 0, "Should have no cached scripts after heap-minimize"); + + win.close(); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html new file mode 100644 index 0000000000..8659d8c409 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html @@ -0,0 +1,134 @@ +<!doctype html> +<html> +<head> + <title>Test content script access to canvas drawWindow()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +"use strict"; + +add_task(async function test_drawWindow() { + const permissions = [ + "<all_urls>", + ]; + + const content_scripts = [{ + matches: ["https://example.org/*"], + js: ["content_script.js"], + }]; + + const files = { + "content_script.js": () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + try { + ctx.drawWindow(window, 0, 0, 10, 10, "red"); + const {data} = ctx.getImageData(0, 0, 10, 10); + browser.test.sendMessage("success", data.slice(0, 3).join()); + } catch (e) { + browser.test.sendMessage("error", e.message); + } + }, + }; + + const first = ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + content_scripts + }, + files + }); + const second = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts + }, + files + }); + + consoleMonitor.start([{ message: /Use of drawWindow [\w\s]+ is deprecated. Use tabs.captureTab/ }]); + + await first.startup(); + await second.startup(); + + const win = window.open("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html"); + + const colour = await first.awaitMessage("success"); + is(colour, "255,255,153", "drawWindow() call was successful: #ff9 == rgb(255,255,153)"); + + const error = await second.awaitMessage("error"); + is(error, "ctx.drawWindow is not a function", "drawWindow() method not awailable without permission"); + + win.close(); + await first.unload(); + await second.unload(); + await consoleMonitor.finished(); +}); + +add_task(async function test_tainted_canvas() { + const permissions = [ + "<all_urls>", + ]; + + const content_scripts = [{ + matches: ["https://example.org/*"], + js: ["content_script.js"], + }]; + + const files = { + "content_script.js": () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = new Image(); + + img.onload = function() { + ctx.drawImage(img, 0, 0); + try { + const png = canvas.toDataURL(); + const {data} = ctx.getImageData(0, 0, 10, 10); + browser.test.sendMessage("success", {png, colour: data.slice(0, 4).join()}); + } catch (e) { + browser.test.log(`Exception: ${e.message}`); + browser.test.sendMessage("error", e.message); + } + }; + + // Cross-origin image from example.com. + img.src = "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_good.png"; + }, + }; + + const first = ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + content_scripts + }, + files + }); + const second = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts + }, + files + }); + + await first.startup(); + await second.startup(); + + const win = window.open("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html"); + + const {png, colour} = await first.awaitMessage("success"); + ok(png.startsWith("data:image/png;base64,"), "toDataURL() call was successful."); + is(colour, "0,0,0,0", "getImageData() returned the correct colour (transparent)."); + + const error = await second.awaitMessage("error"); + is(error, "The operation is insecure.", "toDataURL() throws without permission."); + + win.close(); + await first.unload(); + await second.unload(); +}); + +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html new file mode 100644 index 0000000000..d7030258b3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Sandbox metadata on WebExtensions ContentScripts</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_contentscript_devtools_sandbox_metadata() { + function contentScript() { + browser.runtime.sendMessage("contentScript.executed"); + } + + function background() { + browser.runtime.onMessage.addListener((msg) => { + if (msg == "contentScript.executed") { + browser.test.notifyPass("contentScript.executed"); + } + }); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }, + ], + }, + + background, + files: { + "content_script.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let win = window.open("file_sample.html"); + + let innerWindowID = SpecialPowers.wrap(win).windowGlobalChild.innerWindowId; + + await extension.awaitFinish("contentScript.executed"); + + const { ExtensionContent } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/ExtensionContent.jsm" + ); + + let res = ExtensionContent.getContentScriptGlobals(win); + is(res.length, 1, "Got the expected array of globals"); + let metadata = SpecialPowers.Cu.getSandboxMetadata(res[0]) || {}; + + is(metadata.addonId, extension.id, "Got the expected addonId"); + is(metadata["inner-window-id"], innerWindowID, "Got the expected inner-window-id"); + + await extension.unload(); + info("extension unloaded"); + + res = ExtensionContent.getContentScriptGlobals(win); + is(res.length, 0, "No content scripts globals found once the extension is unloaded"); + + win.close(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html new file mode 100644 index 0000000000..6e03c3e9cd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html @@ -0,0 +1,109 @@ +<!doctype html> +<head> + <title>Test content script in cross-origin frame</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<script> +"use strict"; + +add_task(async function test_content_script_cross_origin_frame() { + + const extensionData = { + manifest: { + content_scripts: [{ + matches: ["https://example.net/*/file_sample.html"], + all_frames: true, + js: ["cs.js"], + }], + permissions: ["https://example.net/"], + }, + + background() { + browser.runtime.onConnect.addListener(port => { + port.onMessage.addListener(async num => { + let { tab, url, frameId } = port.sender; + + browser.test.assertTrue(frameId > 0, "sender frameId is ok"); + browser.test.assertTrue(url.endsWith("file_sample.html"), "url is ok"); + + let shared = await browser.tabs.executeScript(tab.id, { + allFrames: true, + code: `window.sharedVal`, + }); + browser.test.assertEq(shared[0], 357, "CS runs in a shared Sandbox"); + + let code = "does.not.exist"; + await browser.test.assertRejects( + browser.tabs.executeScript(tab.id, { allFrames: true, code }), + /does is not defined/, + "Got the expected rejection from tabs.executeScript" + ); + + code = "() => {}"; + await browser.test.assertRejects( + browser.tabs.executeScript(tab.id, { allFrames: true, code }), + /Script .* result is non-structured-clonable data/, + "Got the expected rejection from tabs.executeScript" + ); + + let result = await browser.tabs.sendMessage(tab.id, num); + port.postMessage(result); + port.disconnect(); + }); + }); + }, + + files: { + "cs.js"() { + let text = document.getElementById("test").textContent; + browser.test.assertEq(text, "Sample text", "CS can access page DOM"); + + let manifest = browser.runtime.getManifest(); + browser.test.assertEq(manifest.version, "1.0"); + browser.test.assertEq(manifest.name, "Generated extension"); + + browser.runtime.onMessage.addListener(async num => { + browser.test.log("content script received tabs.sendMessage"); + return num * 3; + }) + + let response; + window.sharedVal = 357; + + let port = browser.runtime.connect(); + port.onMessage.addListener(num => { + response = num; + }); + port.onDisconnect.addListener(() => { + browser.test.assertEq(response, 21, "Got correct response"); + browser.test.notifyPass(); + }); + port.postMessage(7); + }, + }, + }; + + info("Load first extension"); + let ext1 = ExtensionTestUtils.loadExtension(extensionData); + await ext1.startup(); + + info("Load a page, test content scripts in new frame with extension loaded"); + let base = "https://example.org/tests/toolkit/components/extensions/test"; + let win = window.open(`${base}/mochitest/file_with_xorigin_frame.html`); + + await ext1.awaitFinish(); + await ext1.unload(); + + info("Load second extension, test content scripts in existing frame"); + let ext2 = ExtensionTestUtils.loadExtension(extensionData); + await ext2.startup(); + await ext2.awaitFinish(); + + win.close(); + await ext2.unload(); +}); + +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_getFrameId.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_getFrameId.html new file mode 100644 index 0000000000..d679634030 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_getFrameId.html @@ -0,0 +1,189 @@ +<!doctype html> +<head> + <title>Test content script runtime.getFrameId</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<script> +"use strict"; + +add_task(async function test_runtime_getFrameId_invalid() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + let proxy = new Proxy(window, {}); + let proto = Object.create(window); + + class FakeFrame extends HTMLIFrameElement { + constructor() { + super(); + console.log("FakeFrame ctor"); // eslint-disable-line + } + } + customElements.define('fake-frame', FakeFrame, { extends: 'iframe' }); + let custom = document.createElement("fake-frame"); + + let invalid = [null, 13, "blah", document.body, proxy, proto, custom]; + + for (let value of invalid) { + browser.test.assertThrows( + () => browser.runtime.getFrameId(value), + /Invalid argument/, + "Correct exception thrown." + ); + } + + let detached = document.createElement("iframe"); + let id = browser.runtime.getFrameId(detached); + browser.test.assertEq(id, -1, "Detached iframe has no frameId."); + + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_contentscript_runtime_getFrameId() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webNavigation", "tabs"], + host_permissions: ["https://example.org/"], + }, + + files: { + "cs.js"() { + browser.test.log(`Content script loaded on: ${location.href}`); + let parents = {}; + + // Recursivelly walk descendant frames and get parent frameIds. + function visit(win) { + let frameId = browser.runtime.getFrameId(win); + let parentId = browser.runtime.getFrameId(win.parent); + parents[frameId] = (win.parent != win) ? parentId : -1; + + try { + let frameEl = browser.runtime.getFrameId(win.frameElement); + browser.test.assertEq(frameId, frameEl, "frameElement id correct"); + } catch (e) { + // Can't access a cross-origin .frameElement. + } + + for (let i = 0; i < win.frames.length; i++) { + visit(win.frames[i]); + } + } + visit(window); + + // Add the <embed> frame if it exists. + let embed = document.querySelector("embed"); + if (embed) { + let id = browser.runtime.getFrameId(embed); + parents[id] = 0; + } + + // Add the <object> frame if it exists. + let object = document.querySelector("object"); + if (object) { + let id = browser.runtime.getFrameId(object); + parents[id] = 0; + } + + browser.test.log(`Parents tree: ${JSON.stringify(parents)}`); + return parents; + }, + + async "closedPopup.js"() { + let popup = window.open("https://example.org/?popup"); + popup.close(); + for (let i = 0; i < 100; i++) { + await new Promise(r => setTimeout(r, 50)); + try { + popup.blur(); + } catch(e) { + if (e.message === "can't access dead object") { + browser.test.assertThrows( + () => browser.runtime.getFrameId(popup), + /An exception was thrown/, + "Passing a dead object throws." + ); + browser.test.sendMessage("done"); + return; + } + } + } + browser.test.fail("Timed out while waiting for popup to close."); + }, + "closedPopup.html": ` + <!doctype html> + <meta charset="utf-8"> + <script src="closedPopup.js"><\/script> + `, + }, + + async background() { + let base = "https://example.org/tests/toolkit/components/extensions/test/mochitest"; + let files = { + "file_contains_iframe.html": 2, + "file_WebNavigation_page1.html": 2, + "file_with_xorigin_frame.html": 2, + // Contains all of the above. + "file_with_subframes_and_embed.html": 9, + }; + + for (let [file, count] of Object.entries(files)) { + let tab; + let completed = new Promise(resolve => { + browser.webNavigation.onCompleted.addListener(function cb(details) { + browser.test.log(`onCompleted: ${JSON.stringify(details)}`); + + if (details.tabId === tab?.id && details.frameId === 0) { + browser.webNavigation.onCompleted.removeListener(cb); + resolve(); + } + }); + }); + + browser.test.log(`Load a test page: ${file}`); + tab = await browser.tabs.create({ url: `${base}/${file}` }); + await completed; + + let [parents] = await browser.tabs.executeScript(tab.id, { + file: "cs.js" + }); + + let all = await browser.webNavigation.getAllFrames({ tabId: tab.id }); + browser.test.log(`getAllFrames: ${JSON.stringify(all)}`); + + browser.test.assertEq(all.length, count, "All frames accounted for."); + + browser.test.assertEq( + Object.keys(parents).length, + count, + "All frames accounted for from content script." + ); + + for (let frame of all) { + browser.test.assertEq( + frame.parentFrameId, + parents[frame.frameId], + "Correct frame ancestor info." + ); + } + + await browser.tabs.remove(tab.id); + } + + browser.tabs.create({ url: browser.runtime.getURL("closedPopup.html" )}); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html new file mode 100644 index 0000000000..de2a2571a9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script private browsing ID</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ChromeTask.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +async function test_contentscript_incognito() { + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + }, + ], + }, + + background() { + let windowId; + + browser.test.onMessage.addListener(([msg, url]) => { + if (msg === "open-window") { + browser.windows.create({url, incognito: true}).then(window => { + windowId = window.id; + }); + } else if (msg === "close-window") { + browser.windows.remove(windowId).then(() => { + browser.test.sendMessage("done"); + }); + } + }); + }, + + files: { + "content_script.js": async () => { + const COOKIE = "foo=florgheralzps"; + document.cookie = COOKIE; + + let url = new URL("return_headers.sjs", location.href); + + let responses = [ + new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.onload = () => resolve(JSON.parse(xhr.responseText)); + xhr.send(); + }), + + fetch(url, {credentials: "include"}).then(body => body.json()), + ]; + + try { + for (let response of await Promise.all(responses)) { + browser.test.assertEq(COOKIE, response.cookie, "Got expected cookie header"); + } + browser.test.notifyPass("cookies"); + } catch (e) { + browser.test.fail(`Error: ${e}`); + browser.test.notifyFail("cookies"); + } + }, + }, + }); + + await extension.startup(); + + extension.sendMessage(["open-window", SimpleTest.getTestFileURL("file_sample.html")]); + + await extension.awaitFinish("cookies"); + + extension.sendMessage(["close-window"]); + await extension.awaitMessage("done"); + + await extension.unload(); +} + +add_task(async function() { + await test_contentscript_incognito(); +}); + +add_task(async function() { + await SpecialPowers.pushPrefEnv({set: [ + ["network.cookie.cookieBehavior", 3], + ]}); + await test_contentscript_incognito(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html new file mode 100644 index 0000000000..8ab4b1fb28 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_contentscript() { + function background() { + browser.test.onMessage.addListener(async url => { + let tab = await browser.tabs.create({url}); + + let executed = true; + try { + await browser.tabs.executeScript(tab.id, {code: "true;"}); + } catch (e) { + executed = false; + } + + await browser.tabs.remove([tab.id]); + browser.test.sendMessage("executed", executed); + }); + } + + let extensionData = { + manifest: { + permissions: ["<all_urls>"], + }, + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + extension.sendMessage("https://example.com"); + let result = await extension.awaitMessage("executed"); + is(result, true, "Content script can be run in a page without mozAddonManager"); + + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); + + extension.sendMessage("https://example.com"); + result = await extension.awaitMessage("executed"); + is(result, false, "Content script cannot be run in a page with mozAddonManager"); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html new file mode 100644 index 0000000000..cdec628975 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html @@ -0,0 +1,367 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_cookies() { + await SpecialPowers.pushPrefEnv({set: [ + ["dom.security.https_first_pbm", false], + ["dom.security.https_first", false], + ]}); + + async function background() { + function assertExpected(expected, cookie) { + for (let key of Object.keys(cookie)) { + browser.test.assertTrue(key in expected, `found property ${key}`); + browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`); + } + browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found"); + } + + async function getDocumentCookie(tabId) { + let results = await browser.tabs.executeScript(tabId, { + code: "document.cookie", + }); + browser.test.assertEq(1, results.length, "executeScript returns one result"); + return results[0]; + } + + async function testIpCookie(ipAddress, setHostOnly) { + const IP_TEST_HOST = ipAddress; + const IP_TEST_URL = `http://${IP_TEST_HOST}/`; + const IP_THE_FUTURE = Date.now() + 5 * 60; + const IP_STORE_ID = "firefox-default"; + + let expectedCookie = { + name: "name1", + value: "value1", + domain: IP_TEST_HOST, + hostOnly: true, + path: "/", + secure: false, + httpOnly: false, + sameSite: "no_restriction", + session: false, + expirationDate: IP_THE_FUTURE, + storeId: IP_STORE_ID, + firstPartyDomain: "", + partitionKey: null, + }; + + await browser.browsingData.removeCookies({}); + let ip_cookie = await browser.cookies.set({ + url: IP_TEST_URL, + domain: setHostOnly ? ipAddress : undefined, + name: "name1", + value: "value1", + expirationDate: IP_THE_FUTURE, + }); + assertExpected(expectedCookie, ip_cookie); + + let ip_cookies = await browser.cookies.getAll({name: "name1"}); + browser.test.assertEq(1, ip_cookies.length, "ip cookie can be added"); + assertExpected(expectedCookie, ip_cookies[0]); + + ip_cookies = await browser.cookies.getAll({domain: IP_TEST_HOST, name: "name1"}); + browser.test.assertEq(1, ip_cookies.length, "can get ip cookie by host"); + assertExpected(expectedCookie, ip_cookies[0]); + + let ip_details = await browser.cookies.remove({url: IP_TEST_URL, name: "name1"}); + assertExpected({url: IP_TEST_URL, name: "name1", storeId: IP_STORE_ID, firstPartyDomain: "", partitionKey: null}, ip_details); + + ip_cookies = await browser.cookies.getAll({name: "name1"}); + browser.test.assertEq(0, ip_cookies.length, "ip cookie can be removed"); + } + + async function openPrivateWindowAndTab(TEST_URL) { + // Add some random suffix to make sure that we select the right tab. + const PRIVATE_TEST_URL = TEST_URL + "?random" + Math.random(); + + let tabReadyPromise = new Promise((resolve) => { + browser.webNavigation.onDOMContentLoaded.addListener(function listener({tabId}) { + browser.webNavigation.onDOMContentLoaded.removeListener(listener); + resolve(tabId); + }, { + url: [{ + urlPrefix: PRIVATE_TEST_URL, + }], + }); + }); + // This tab is opened for two purposes: + // 1. To allow tests to run content scripts in the context of a tab, + // for fetching the value of document.cookie. + // 2. TODO Bug 1309637 To work around cookies in incognito windows, + // based on the analysis in comment 8. + let {id: windowId} = await browser.windows.create({ + incognito: true, + url: PRIVATE_TEST_URL, + }); + let tabId = await tabReadyPromise; + return {windowId, tabId}; + } + + function changePort(href, port) { + let url = new URL(href); + url.port = port; + return url.href; + } + + await testIpCookie("[2a03:4000:6:310e:216:3eff:fe53:99b]", false); + await testIpCookie("[2a03:4000:6:310e:216:3eff:fe53:99b]", true); + await testIpCookie("192.168.1.1", false); + await testIpCookie("192.168.1.1", true); + + const TEST_URL = "http://example.org/"; + const TEST_SECURE_URL = "https://example.org/"; + const THE_FUTURE = Date.now() + 5 * 60; + const TEST_PATH = "set_path"; + const TEST_URL_WITH_PATH = TEST_URL + TEST_PATH; + const TEST_COOKIE_PATH = `/${TEST_PATH}`; + const STORE_ID = "firefox-default"; + const PRIVATE_STORE_ID = "firefox-private"; + + let expected = { + name: "name1", + value: "value1", + domain: "example.org", + hostOnly: true, + path: "/", + secure: false, + httpOnly: false, + sameSite: "no_restriction", + session: false, + expirationDate: THE_FUTURE, + storeId: STORE_ID, + firstPartyDomain: "", + partitionKey: null, + }; + + // Remove all cookies before starting the test. + await browser.browsingData.removeCookies({}); + + let cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", expirationDate: THE_FUTURE}); + assertExpected(expected, cookie); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + assertExpected(expected, cookie); + + let cookies = await browser.cookies.getAll({name: "name1"}); + browser.test.assertEq(1, cookies.length, "one cookie found for matching name"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({domain: "example.org"}); + browser.test.assertEq(1, cookies.length, "one cookie found for matching domain"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({domain: "example.net"}); + browser.test.assertEq(0, cookies.length, "no cookies found for non-matching domain"); + + cookies = await browser.cookies.getAll({secure: false}); + browser.test.assertEq(1, cookies.length, "one non-secure cookie found"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({secure: true}); + browser.test.assertEq(0, cookies.length, "no secure cookies found"); + + cookies = await browser.cookies.getAll({storeId: STORE_ID}); + browser.test.assertEq(1, cookies.length, "one cookie found for valid storeId"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({storeId: "invalid_id"}); + browser.test.assertEq(0, cookies.length, "no cookies found for invalid storeId"); + + let details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + // Ports in cookie URLs should be ignored. Every API call uses a different port number for better coverage. + cookie = await browser.cookies.set({url: changePort(TEST_URL, 1234), name: "name1", value: "value1", expirationDate: THE_FUTURE}); + assertExpected(expected, cookie); + + cookie = await browser.cookies.get({url: changePort(TEST_URL, 65535), name: "name1"}); + assertExpected(expected, cookie); + + cookies = await browser.cookies.getAll({url: TEST_URL}); + browser.test.assertEq(cookies.length, 1, "Found cookie using getAll without port"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({url: changePort(TEST_URL, 1)}); + browser.test.assertEq(cookies.length, 1, "Found cookie using getAll with port"); + assertExpected(expected, cookies[0]); + + // .remove should return the URL of the API call, so the port is included in the return value. + const TEST_URL_TO_REMOVE = changePort(TEST_URL, 1023); + details = await browser.cookies.remove({url: TEST_URL_TO_REMOVE, name: "name1"}); + assertExpected({url: TEST_URL_TO_REMOVE, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + let stores = await browser.cookies.getAllCookieStores(); + browser.test.assertEq(1, stores.length, "expected number of stores returned"); + browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned"); + browser.test.assertEq(1, stores[0].tabIds.length, "one tabId returned for store"); + browser.test.assertEq("number", typeof stores[0].tabIds[0], "tabId is a number"); + + // TODO bug 1372178: Opening private windows/tabs is not supported on Android + if (browser.windows) { + let {windowId} = await openPrivateWindowAndTab(TEST_URL); + let stores = await browser.cookies.getAllCookieStores(); + + browser.test.assertEq(2, stores.length, "expected number of stores returned"); + browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned"); + browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store"); + browser.test.assertEq(PRIVATE_STORE_ID, stores[1].id, "expected private store id returned"); + browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for private store"); + + await browser.windows.remove(windowId); + } + + cookie = await browser.cookies.set({url: TEST_URL, name: "name2", domain: ".example.org", expirationDate: THE_FUTURE}); + browser.test.assertEq(false, cookie.hostOnly, "cookie is not a hostOnly cookie"); + + details = await browser.cookies.remove({url: TEST_URL, name: "name2"}); + assertExpected({url: TEST_URL, name: "name2", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + // Create a session cookie. + cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1"}); + browser.test.assertEq(true, cookie.session, "session cookie set"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(true, cookie.session, "got session cookie"); + + cookies = await browser.cookies.getAll({session: true}); + browser.test.assertEq(1, cookies.length, "one session cookie found"); + browser.test.assertEq(true, cookies[0].session, "found session cookie"); + + cookies = await browser.cookies.getAll({session: false}); + browser.test.assertEq(0, cookies.length, "no non-session cookies found"); + + details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + cookie = await browser.cookies.set({url: TEST_SECURE_URL, name: "name1", value: "value1", secure: true}); + browser.test.assertEq(true, cookie.secure, "secure cookie set"); + + cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"}); + browser.test.assertEq(true, cookie.session, "got secure cookie"); + + cookies = await browser.cookies.getAll({secure: true}); + browser.test.assertEq(1, cookies.length, "one secure cookie found"); + browser.test.assertEq(true, cookies[0].secure, "found secure cookie"); + + cookies = await browser.cookies.getAll({secure: false}); + browser.test.assertEq(0, cookies.length, "no non-secure cookies found"); + + details = await browser.cookies.remove({url: TEST_SECURE_URL, name: "name1"}); + assertExpected({url: TEST_SECURE_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + cookie = await browser.cookies.set({url: TEST_URL_WITH_PATH, path: TEST_COOKIE_PATH, name: "name1", value: "value1", expirationDate: THE_FUTURE}); + browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "created cookie with path"); + + cookie = await browser.cookies.get({url: TEST_URL_WITH_PATH, name: "name1"}); + browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "got cookie with path"); + + cookies = await browser.cookies.getAll({path: TEST_COOKIE_PATH}); + browser.test.assertEq(1, cookies.length, "one cookie with path found"); + browser.test.assertEq(TEST_COOKIE_PATH, cookies[0].path, "found cookie with path"); + + cookie = await browser.cookies.get({url: TEST_URL + "invalid_path", name: "name1"}); + browser.test.assertEq(null, cookie, "get with invalid path returns null"); + + cookies = await browser.cookies.getAll({path: "/invalid_path"}); + browser.test.assertEq(0, cookies.length, "getAll with invalid path returns 0 cookies"); + + details = await browser.cookies.remove({url: TEST_URL_WITH_PATH, name: "name1"}); + assertExpected({url: TEST_URL_WITH_PATH, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: true}); + browser.test.assertEq(true, cookie.httpOnly, "httpOnly cookie set"); + + cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: false}); + browser.test.assertEq(false, cookie.httpOnly, "non-httpOnly cookie set"); + + details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.set({url: TEST_URL}); + browser.test.assertEq("", cookie.name, "default name set"); + browser.test.assertEq("", cookie.value, "default value set"); + browser.test.assertEq(true, cookie.session, "no expiry date created session cookie"); + + // TODO bug 1372178: Opening private windows/tabs is not supported on Android + if (browser.windows) { + let {tabId, windowId} = await openPrivateWindowAndTab(TEST_URL); + + browser.test.assertEq("", await getDocumentCookie(tabId), "initially no cookie"); + + let cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "private", expirationDate: THE_FUTURE, storeId: PRIVATE_STORE_ID}); + browser.test.assertEq("private", cookie.value, "set the private cookie"); + + cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "default", expirationDate: THE_FUTURE, storeId: STORE_ID}); + browser.test.assertEq("default", cookie.value, "set the default cookie"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); + browser.test.assertEq("private", cookie.value, "get the private cookie"); + browser.test.assertEq(PRIVATE_STORE_ID, cookie.storeId, "get the private cookie storeId"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID}); + browser.test.assertEq("default", cookie.value, "get the default cookie"); + browser.test.assertEq(STORE_ID, cookie.storeId, "get the default cookie storeId"); + + browser.test.assertEq("store=private", await getDocumentCookie(tabId), "private document.cookie should be set"); + + let details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: STORE_ID}); + assertExpected({url: TEST_URL, name: "store", storeId: STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID}); + browser.test.assertEq(null, cookie, "deleted the default cookie"); + + details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); + assertExpected({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID, firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); + browser.test.assertEq(null, cookie, "deleted the private cookie"); + + browser.test.assertEq("", await getDocumentCookie(tabId), "private document.cookie should be removed"); + + await browser.windows.remove(windowId); + } + + browser.test.notifyPass("cookies"); + } + + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + background, + manifest: { + permissions: ["cookies", "*://example.org/", "*://[2a03:4000:6:310e:216:3eff:fe53:99b]/", "*://192.168.1.1/", "webNavigation", "browsingData"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("cookies"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html new file mode 100644 index 0000000000..d4bbd61177 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + // make sure userContext is enabled. + await SpecialPowers.pushPrefEnv({"set": [ + ["privacy.userContext.enabled", true], + ]}); +}); + +add_task(async function test_cookie_containers() { + async function background() { + // Sometimes there is a cookie without name/value when running tests. + let cookiesAtStart = await browser.cookies.getAll({storeId: "firefox-default"}); + + function assertExpected(expected, cookie) { + for (let key of Object.keys(cookie)) { + browser.test.assertTrue(key in expected, `found property ${key}`); + browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`); + } + browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found"); + } + + const TEST_URL = "http://example.org/"; + const THE_FUTURE = Date.now() + 5 * 60; + + let expected = { + name: "name1", + value: "value1", + domain: "example.org", + hostOnly: true, + path: "/", + secure: false, + httpOnly: false, + sameSite: "no_restriction", + session: false, + expirationDate: THE_FUTURE, + storeId: "firefox-container-1", + firstPartyDomain: "", + partitionKey: null, + }; + + let cookie = await browser.cookies.set({ + url: TEST_URL, name: "name1", value: "value1", + expirationDate: THE_FUTURE, storeId: "firefox-container-1", + }); + browser.test.assertEq("firefox-container-1", cookie.storeId, "the cookie has the correct storeId"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "get() without storeId returns null"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); + assertExpected(expected, cookie); + + let cookies = await browser.cookies.getAll({storeId: "firefox-default"}); + browser.test.assertEq(0, cookiesAtStart.length - cookies.length, "getAll() with default storeId hasn't added cookies"); + + cookies = await browser.cookies.getAll({storeId: "firefox-container-1"}); + browser.test.assertEq(1, cookies.length, "one cookie found for matching domain"); + assertExpected(expected, cookies[0]); + + let details = await browser.cookies.remove({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: "firefox-container-1", firstPartyDomain: "", partitionKey: null}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + browser.test.notifyPass("cookies"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["cookies", "*://example.org/"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("cookies"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html new file mode 100644 index 0000000000..fa118f5271 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension cookies test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_cookies_expiry() { + function background() { + let expectedEvents = []; + + browser.cookies.onChanged.addListener(event => { + expectedEvents.push(`${event.removed}:${event.cause}`); + if (expectedEvents.length === 1) { + browser.test.assertEq("true:expired", expectedEvents[0], "expired cookie removed"); + browser.test.assertEq("first", event.cookie.name, "expired cookie has the expected name"); + browser.test.assertEq("one", event.cookie.value, "expired cookie has the expected value"); + } else { + browser.test.assertEq("false:explicit", expectedEvents[1], "new cookie added"); + browser.test.assertEq("first", event.cookie.name, "new cookie has the expected name"); + browser.test.assertEq("one-again", event.cookie.value, "new cookie has the expected value"); + browser.test.notifyPass("cookie-expiry"); + } + }); + + setTimeout(() => { + browser.test.sendMessage("change-cookies"); + }, 1000); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://example.com/", "cookies"], + }, + background, + }); + + let chromeScript = loadChromeScript(() => { + const {sendAsyncMessage} = this; + Services.cookies.add(".example.com", "/", "first", "one", false, false, false, Date.now() / 1000 + 1, {}, Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_HTTP); + sendAsyncMessage("done"); + }); + await chromeScript.promiseOneMessage("done"); + chromeScript.destroy(); + + await extension.startup(); + await extension.awaitMessage("change-cookies"); + + chromeScript = loadChromeScript(() => { + const {sendAsyncMessage} = this; + Services.cookies.add(".example.com", "/", "first", "one-again", false, false, false, Date.now() / 1000 + 10, {}, Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_HTTP); + sendAsyncMessage("done"); + }); + await chromeScript.promiseOneMessage("done"); + chromeScript.destroy(); + + await extension.awaitFinish("cookie-expiry"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html new file mode 100644 index 0000000000..7e33f4731d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html @@ -0,0 +1,316 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> +<script src="head.js"></script> +<script> +"use strict"; + +async function background() { + const url = "http://ext-cookie-first-party.mochi.test/"; + const firstPartyDomain = "ext-cookie-first-party.mochi.test"; + // A first party domain with invalid characters for the file system, which just happens to be a IPv6 address. + const firstPartyDomainInvalidChars = "[2606:4700:4700::1111]"; + const expectedError = "First-Party Isolation is enabled, but the required 'firstPartyDomain' attribute was not set."; + + const assertExpectedCookies = (expected, cookies, message) => { + let matches = (cookie, expected) => { + if (!cookie || !expected) { + return cookie === expected; // true if both are null. + } + for (let key of Object.keys(expected)) { + if (cookie[key] !== expected[key]) { + return false; + } + } + return true; + }; + browser.test.assertEq(expected.length, cookies.length, `Got expected number of cookies - ${message}`); + if (cookies.length !== expected.length) { + return; + } + for (let expect of expected) { + let foundCookies = cookies.filter(cookie => matches(cookie, expect)); + browser.test.assertEq(1, foundCookies.length, + `Expected cookie ${JSON.stringify(expect)} found - ${message}`); + } + }; + + // Test when FPI is disabled. + const test_fpi_disabled = async () => { + let cookie, cookies; + + // set + cookie = await browser.cookies.set({url, name: "foo1", value: "bar1"}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "set: FPI off, w/ empty firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.set({url, name: "foo2", value: "bar2", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + ], [cookie], "set: FPI off, w/ firstPartyDomain, FP cookie"); + + // get + // When FPI is disabled, missing key/null/undefined is equivalent to "". + cookie = await browser.cookies.get({url, name: "foo1"}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI off, w/o firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: ""}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI off, w/ empty firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: null}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI off, w/ null firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: undefined}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI off, w/ undefined firstPartyDomain, non-FP cookie"); + + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + ], [cookie], "get: FPI off, w/ firstPartyDomain, FP cookie"); + // There is no match for non-FP cookies with name "foo2". + cookie = await browser.cookies.get({url, name: "foo2"}); + assertExpectedCookies([null], [cookie], "get: FPI off, w/o firstPartyDomain, no cookie"); + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: ""}); + assertExpectedCookies([null], [cookie], "get: FPI off, w/ empty firstPartyDomain, no cookie"); + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: null}); + assertExpectedCookies([null], [cookie], "get: FPI off, w/ null firstPartyDomain, no cookie"); + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: undefined}); + assertExpectedCookies([null], [cookie], "get: FPI off, w/ undefined firstPartyDomain, no cookie"); + + // getAll + for (let extra of [{}, {url}, {domain: firstPartyDomain}]) { + const prefix = `getAll(${JSON.stringify(extra)})`; + cookies = await browser.cookies.getAll({...extra}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], cookies, `${prefix}: FPI off, w/o firstPartyDomain, non-FP cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: ""}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], cookies, `${prefix}: FPI off, w/ empty firstPartyDomain, non-FP cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: null}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + {name: "foo2", value: "bar2", firstPartyDomain}, + ], cookies, `${prefix}: FPI off, w/ null firstPartyDomain, all cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: undefined}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + {name: "foo2", value: "bar2", firstPartyDomain}, + ], cookies, `${prefix}: FPI off, w/ undefined firstPartyDomain, all cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + ], cookies, `${prefix}: FPI off, w/ firstPartyDomain, FP cookies`); + } + + // remove + cookie = await browser.cookies.remove({url, name: "foo1"}); + assertExpectedCookies([ + {url, name: "foo1", firstPartyDomain: ""}, + ], [cookie], "remove: FPI off, w/ empty firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.remove({url, name: "foo2", firstPartyDomain}); + assertExpectedCookies([ + {url, name: "foo2", firstPartyDomain}, + ], [cookie], "remove: FPI off, w/ firstPartyDomain, FP cookie"); + + // Test if FP cookies set when FPI off can be accessed when FPI on. + await browser.cookies.set({url, name: "foo1", value: "bar1"}); + await browser.cookies.set({url, name: "foo2", value: "bar2", firstPartyDomain}); + + browser.test.sendMessage("test_fpi_disabled"); + }; + + // Test when FPI is enabled. + const test_fpi_enabled = async () => { + let cookie, cookies; + + // set + await browser.test.assertRejects( + browser.cookies.set({url, name: "foo3", value: "bar3"}), + expectedError, + "set: FPI on, w/o firstPartyDomain, rejection"); + cookie = await browser.cookies.set({url, name: "foo4", value: "bar4", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo4", value: "bar4", firstPartyDomain}, + ], [cookie], "set: FPI on, w/ firstPartyDomain, FP cookie"); + + // get + await browser.test.assertRejects( + browser.cookies.get({url, name: "foo3"}), + expectedError, + "get: FPI on, w/o firstPartyDomain, rejection"); + await browser.test.assertRejects( + browser.cookies.get({url, name: "foo3", firstPartyDomain: null}), + expectedError, + "get: FPI on, w/ null firstPartyDomain, rejection"); + await browser.test.assertRejects( + browser.cookies.get({url, name: "foo3", firstPartyDomain: undefined}), + expectedError, + "get: FPI on, w/ undefined firstPartyDomain, rejection"); + cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: ""}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI on, w/ empty firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.get({url, name: "foo4", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo4", value: "bar4", firstPartyDomain}, + ], [cookie], "get: FPI on, w/ firstPartyDomain, FP cookie"); + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + ], [cookie], "get: FPI on, w/ firstPartyDomain, FP cookie (set when FPI off)"); + + // getAll + for (let extra of [{}, {url}, {domain: firstPartyDomain}]) { + const prefix = `getAll(${JSON.stringify(extra)})`; + await browser.test.assertRejects( + browser.cookies.getAll({...extra}), + expectedError, + `${prefix}: FPI on, w/o firstPartyDomain, rejection`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: ""}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], cookies, `${prefix}: FPI on, w/ empty firstPartyDomain, non-FP cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: null}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + {name: "foo2", value: "bar2", firstPartyDomain}, + {name: "foo4", value: "bar4", firstPartyDomain}, + ], cookies, `${prefix}: FPI on, w/ null firstPartyDomain, all cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: undefined}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + {name: "foo2", value: "bar2", firstPartyDomain}, + {name: "foo4", value: "bar4", firstPartyDomain}, + ], cookies, `${prefix}: FPI on, w/ undefined firstPartyDomain, all cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + {name: "foo4", value: "bar4", firstPartyDomain}, + ], cookies, `${prefix}: FPI on, w/ firstPartyDomain, FP cookies`); + } + + // remove + await browser.test.assertRejects( + browser.cookies.remove({url, name: "foo3"}), + expectedError, + "remove: FPI on, w/o firstPartyDomain, rejection"); + cookie = await browser.cookies.remove({url, name: "foo4", firstPartyDomain}); + assertExpectedCookies([ + {url, name: "foo4", firstPartyDomain}, + ], [cookie], "remove: FPI on, w/ firstPartyDomain, FP cookie"); + cookie = await browser.cookies.remove({url, name: "foo2", firstPartyDomain}); + assertExpectedCookies([ + {url, name: "foo2", firstPartyDomain}, + ], [cookie], "remove: FPI on, w/ firstPartyDomain, FP cookie (set when FPI off)"); + + // Test if FP cookies set when FPI on can be accessed when FPI off. + await browser.cookies.set({url, name: "foo4", value: "bar4", firstPartyDomain}); + + browser.test.sendMessage("test_fpi_enabled"); + }; + + // Test FPI with a first party domain with invalid characters for + // the file system. + const test_fpi_with_invalid_characters = async () => { + let cookie; + + // Test setting a cookie with a first party domain with invalid characters + // for the file system. + cookie = await browser.cookies.set({url, name: "foo5", value: "bar5", + firstPartyDomain: firstPartyDomainInvalidChars}); + assertExpectedCookies([ + {name: "foo5", value: "bar5", firstPartyDomain: firstPartyDomainInvalidChars}, + ], [cookie], "set: FPI on, w/ firstPartyDomain with invalid characters, FP cookie"); + + // Test getting a cookie with a first party domain with invalid characters + // for the file system. + cookie = await browser.cookies.get({url, name: "foo5", + firstPartyDomain: firstPartyDomainInvalidChars}); + assertExpectedCookies([ + {name: "foo5", value: "bar5", firstPartyDomain: firstPartyDomainInvalidChars}, + ], [cookie], "get: FPI on, w/ firstPartyDomain with invalid characters, FP cookie"); + + // Test removing a cookie with a first party domain with invalid characters + // for the file system. + cookie = await browser.cookies.remove({url, name: "foo5", + firstPartyDomain: firstPartyDomainInvalidChars}); + assertExpectedCookies([ + {url, name: "foo5", firstPartyDomain: firstPartyDomainInvalidChars}, + ], [cookie], "remove: FPI on, w/ firstPartyDomain with invalid characters, FP cookie"); + + browser.test.sendMessage("test_fpi_with_invalid_characters"); + }; + + // Test when FPI is disabled again, accessing FP cookies set when FPI is enabled. + const test_fpd_cookies_on_fpi_disabled = async () => { + let cookie, cookies; + cookie = await browser.cookies.get({url, name: "foo4", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo4", value: "bar4", firstPartyDomain}, + ], [cookie], "get: FPI off, w/ firstPartyDomain, FP cookie (set when FPI on)"); + cookie = await browser.cookies.remove({url, name: "foo4", firstPartyDomain}); + assertExpectedCookies([ + {url, name: "foo4", firstPartyDomain}, + ], [cookie], "remove: FPI off, w/ firstPartyDomain, FP cookie (set when FPI on)"); + + // Clean up. + await browser.cookies.remove({url, name: "foo1"}); + + cookies = await browser.cookies.getAll({firstPartyDomain: null}); + assertExpectedCookies([], cookies, "Test is finishing, all cookies removed"); + + browser.test.sendMessage("test_fpd_cookies_on_fpi_disabled"); + }; + + browser.test.onMessage.addListener((message) => { + switch (message) { + case "test_fpi_disabled": return test_fpi_disabled(); + case "test_fpi_enabled": return test_fpi_enabled(); + case "test_fpi_with_invalid_characters": return test_fpi_with_invalid_characters(); + case "test_fpd_cookies_on_fpi_disabled": return test_fpd_cookies_on_fpi_disabled(); + default: return browser.test.notifyFail("unknown-message"); + } + }); +} + +function enableFirstPartyIsolation() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.firstparty.isolate", true], + ], + }); +} + +function disableFirstPartyIsolation() { + return SpecialPowers.popPrefEnv(); +} + +add_task(async () => { + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["cookies", "*://ext-cookie-first-party.mochi.test/"], + }, + }); + await extension.startup(); + extension.sendMessage("test_fpi_disabled"); + await extension.awaitMessage("test_fpi_disabled"); + await enableFirstPartyIsolation(); + extension.sendMessage("test_fpi_enabled"); + await extension.awaitMessage("test_fpi_enabled"); + extension.sendMessage("test_fpi_with_invalid_characters"); + await extension.awaitMessage("test_fpi_with_invalid_characters"); + await disableFirstPartyIsolation(); + extension.sendMessage("test_fpd_cookies_on_fpi_disabled"); + await extension.awaitMessage("test_fpd_cookies_on_fpi_disabled"); + await extension.unload(); +}); +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html new file mode 100644 index 0000000000..b33ceecf06 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_cookies_incognito_not_allowed() { + let privateExtension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + async background() { + let window = await browser.windows.create({incognito: true}); + browser.test.onMessage.addListener(async () => { + await browser.windows.remove(window.id); + browser.test.sendMessage("done"); + }); + browser.test.sendMessage("ready"); + }, + manifest: { + permissions: ["cookies", "*://example.org/"], + }, + }); + await privateExtension.startup(); + await privateExtension.awaitMessage("ready"); + + async function background() { + const storeId = "firefox-private"; + const url = "http://example.org/"; + + // Getting the wrong storeId will fail, otherwise we should finish the test fine. + browser.cookies.onChanged.addListener(changeInfo => { + let {cookie} = changeInfo; + browser.test.assertTrue(cookie.storeId != storeId, "cookie store is correct"); + }); + + browser.test.onMessage.addListener(async () => { + let stores = await browser.cookies.getAllCookieStores(); + let store = stores.find(s => s.incognito); + browser.test.assertTrue(!store, "incognito cookie store should not be available"); + browser.test.notifyPass("cookies"); + }); + + await browser.test.assertRejects( + browser.cookies.set({url, name: "test", storeId}), + /Extension disallowed access/, + "API should reject setting cookie"); + await browser.test.assertRejects( + browser.cookies.get({url, name: "test", storeId}), + /Extension disallowed access/, + "API should reject getting cookie"); + await browser.test.assertRejects( + browser.cookies.getAll({url, storeId}), + /Extension disallowed access/, + "API should reject getting cookie"); + await browser.test.assertRejects( + browser.cookies.remove({url, name: "test", storeId}), + /Extension disallowed access/, + "API should reject getting cookie"); + await browser.test.assertRejects( + browser.cookies.getAll({url, storeId}), + /Extension disallowed access/, + "API should reject getting cookie"); + + browser.test.sendMessage("set-cookies"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["cookies", "*://example.org/"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("set-cookies"); + + let chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + Services.cookies.add("example.org", "/", "public", `foo${Math.random()}`, + false, false, false, Number.MAX_SAFE_INTEGER, {}, + Ci.nsICookie.SAMESITE_NONE); + Services.cookies.add("example.org", "/", "private", `foo${Math.random()}`, + false, false, false, Number.MAX_SAFE_INTEGER, {privateBrowsingId: 1}, + Ci.nsICookie.SAMESITE_NONE); + }); + extension.sendMessage("test-cookie-store"); + await extension.awaitFinish("cookies"); + + await extension.unload(); + privateExtension.sendMessage("close"); + await privateExtension.awaitMessage("done"); + await privateExtension.unload(); + chromeScript.destroy(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html new file mode 100644 index 0000000000..0bd2852075 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_cookies.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function init() { + // We need to trigger a cookie eviction in order to test our batch delete + // observer. + + // Set quotaPerHost to maxPerHost - 1, so there is only one cookie + // will be evicted everytime. + SpecialPowers.setIntPref("network.cookie.quotaPerHost", 2); + SpecialPowers.setIntPref("network.cookie.maxPerHost", 3); + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.quotaPerHost"); + SpecialPowers.clearUserPref("network.cookie.maxPerHost"); + }); +}); + +add_task(async function test_bad_cookie_permissions() { + info("Test non-matching, non-secure domain with non-secure cookie"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.net/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test non-matching, secure domain with non-secure cookie"); + await testCookies({ + permissions: ["https://example.com/", "cookies"], + url: "https://example.net/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test non-matching, secure domain with secure cookie"); + await testCookies({ + permissions: ["https://example.com/", "cookies"], + url: "https://example.net/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test matching subdomain with superdomain privileges, secure cookie (http)"); + await testCookies({ + permissions: ["http://foo.bar.example.com/", "cookies"], + url: "http://foo.bar.example.com/", + domain: ".example.com", + secure: true, + shouldPass: false, + shouldWrite: true, + }); + + info("Test matching, non-secure domain with secure cookie"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.com/", + domain: "example.com", + secure: true, + shouldPass: false, + shouldWrite: true, + }); + + info("Test matching, non-secure host, secure URL"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "https://example.com/", + domain: "example.com", + secure: true, + shouldPass: false, + shouldWrite: false, + }); + + info("Test non-matching domain"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.com/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test invalid scheme"); + await testCookies({ + permissions: ["ftp://example.com/", "cookies"], + url: "ftp://example.com/", + domain: "example.com", + secure: false, + shouldPass: false, + shouldWrite: false, + }); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html new file mode 100644 index 0000000000..bd76f2b9c0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_cookies.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function init() { + // We need to trigger a cookie eviction in order to test our batch delete + // observer. + + // Set quotaPerHost to maxPerHost - 1, so there is only one cookie + // will be evicted everytime. + SpecialPowers.setIntPref("network.cookie.quotaPerHost", 2); + SpecialPowers.setIntPref("network.cookie.maxPerHost", 3); + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.quotaPerHost"); + SpecialPowers.clearUserPref("network.cookie.maxPerHost"); + }); +}); + +add_task(async function test_good_cookie_permissions() { + info("Test matching, non-secure domain with non-secure cookie"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.com/", + domain: "example.com", + secure: false, + shouldPass: true, + }); + + info("Test matching, secure domain with non-secure cookie"); + await testCookies({ + permissions: ["https://example.com/", "cookies"], + url: "https://example.com/", + domain: "example.com", + secure: false, + shouldPass: true, + }); + + info("Test matching, secure domain with secure cookie"); + await testCookies({ + permissions: ["https://example.com/", "cookies"], + url: "https://example.com/", + domain: "example.com", + secure: true, + shouldPass: true, + }); + + info("Test matching subdomain with superdomain privileges, secure cookie (https)"); + await testCookies({ + permissions: ["https://foo.bar.example.com/", "cookies"], + url: "https://foo.bar.example.com/", + domain: ".example.com", + secure: true, + shouldPass: true, + }); + + info("Test matching subdomain with superdomain privileges, non-secure cookie (https)"); + await testCookies({ + permissions: ["https://foo.bar.example.com/", "cookies"], + url: "https://foo.bar.example.com/", + domain: ".example.com", + secure: false, + shouldPass: true, + }); + + info("Test matching subdomain with superdomain privileges, non-secure cookie (http)"); + await testCookies({ + permissions: ["http://foo.bar.example.com/", "cookies"], + url: "http://foo.bar.example.com/", + domain: ".example.com", + secure: false, + shouldPass: true, + }); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_dnr_other_extensions.html b/toolkit/components/extensions/test/mochitest/test_ext_dnr_other_extensions.html new file mode 100644 index 0000000000..d3074b3dec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_dnr_other_extensions.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>DNR and tabs.create from other extension</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +"use strict"; + + +// While most DNR tests are xpcshell tests, this one is a mochitest because the +// tabs.create API does not work in a xpcshell test. + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.manifestV3.enabled", true], + ["extensions.dnr.enabled", true], + ], + }); +}); + + +add_task(async function tabs_create_can_be_redirected_by_other_dnr_extension() { + let dnrExtension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["declarativeNetRequestWithHostAccess"], + // redirect action requires host permissions: + host_permissions: ["*://example.com/*"], + }, + async background() { + await browser.declarativeNetRequest.updateSessionRules({ + addRules: [ + { + id: 1, + condition: { + resourceTypes: ["main_frame"], + urlFilter: "?dnr_redir_me_pls", + }, + action: { + type: "redirect", + redirect: { + transform: { + query: "?dnr_redir_target" + }, + }, + }, + }, + ], + }); + browser.test.sendMessage("dnr_registered"); + }, + }); + await dnrExtension.startup(); + await dnrExtension.awaitMessage("dnr_registered"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webNavigation"], + }, + async background() { + async function createTabAndGetFinalUrl(url) { + let navigationDonePromise = new Promise(resolve => { + browser.webNavigation.onDOMContentLoaded.addListener( + function listener(details) { + browser.webNavigation.onDOMContentLoaded.removeListener(listener); + resolve(details); + }, + // All input URLs and redirection targets match this URL filter: + { url: [{ queryPrefix: "dnr_redir_" }] } + ); + }); + const tab = await browser.tabs.create({ url }); + browser.test.log(`Waiting for navigation done, starting from ${url}`); + const result = await navigationDonePromise; + browser.test.assertEq( + tab.id, + result.tabId, + `Observed load completion for navigation tab with initial URL ${url}` + ); + await browser.tabs.remove(tab.id); + return result.url; + } + + browser.test.assertEq( + "https://example.com/?dnr_redir_target", + await createTabAndGetFinalUrl("https://example.com/?dnr_redir_me_pls"), + "DNR rule from other extension should have redirected the navigation" + ); + + browser.test.assertEq( + "https://example.org/?dnr_redir_me_pls", + await createTabAndGetFinalUrl("https://example.org/?dnr_redir_me_pls"), + "DNR redirect ignored for URLs without host permission" + ); + browser.test.sendMessage("done"); + } + }); + await extension.startup(); + await extension.awaitMessage("done"); + + await dnrExtension.unload(); + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_dnr_tabIds.html b/toolkit/components/extensions/test/mochitest/test_ext_dnr_tabIds.html new file mode 100644 index 0000000000..0278a8ccc8 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_dnr_tabIds.html @@ -0,0 +1,137 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>DNR with tabIds condition</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +"use strict"; + +// While most DNR tests are xpcshell tests, this one is a mochitest because it +// is not possible to create a tab and get a tabId in a xpcshell test. + +// toolkit/components/extensions/test/xpcshell/test_ext_dnr_tabIds.js does +// exist, as an isolated xpcshell is needed to verify that the internals are +// working as expected. A mochitest is not a good fit for that because it has +// built-in add-ons that may affect the observed behavior. + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.manifestV3.enabled", true], + ["extensions.dnr.enabled", true], + ], + }); +}); + +add_task(async function match_by_tabIds() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + async function createTabAndPort() { + let portPromise = new Promise(resolve => { + browser.runtime.onConnect.addListener(function listener(port) { + browser.runtime.onConnect.removeListener(listener); + browser.test.assertEq("port_from_tab", port.name, "Got port"); + resolve(port); + }); + }); + const tab = await browser.tabs.create({ url: "tab.html" }); + const port = await portPromise; + browser.test.assertEq(tab.id, port.sender.tab.id, "Got port from tab"); + browser.test.assertTrue(tab.id > 0, `tabId must be valid: ${tab.id}`); + tab.port = port; + return tab; + } + async function getFinalUrlForFetchInTab(tabWithPort, url) { + const port = tabWithPort.port; // from createTabAndPort. + return new Promise(resolve => { + port.onMessage.addListener(function listener(responseUrl) { + port.onMessage.removeListener(listener); + resolve(responseUrl); + }); + port.postMessage(url); + }); + } + let tab1 = await createTabAndPort(); + let tab2 = await createTabAndPort(); + + const URL_PREFIX = "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.txt"; + + function makeRedirect(id, condition, url) { + return { + id, + // The test sends a request to example.net and expects a redirect to + // URL_PREFIX (example.com). + condition: { requestDomains: ["example.net"], ...condition }, + action: { type: "redirect", redirect: { url }}, + }; + } + await browser.declarativeNetRequest.updateSessionRules({ + addRules: [ + makeRedirect(1, { tabIds: [-1] }, `${URL_PREFIX}?tabId/-1`), + makeRedirect(2, { tabIds: [tab1.id] }, `${URL_PREFIX}?tabId/tab1`), + makeRedirect( + 3, + { excludedTabIds: [-1, tab1.id] }, + `${URL_PREFIX}?tabId/not-1,not-tab1` + ), + ], + }); + + browser.test.assertEq( + `${URL_PREFIX}?tabId/-1`, + (await fetch("https://example.net/?pre-redirect-bg")).url, + "Request from background should match tabIds: [-1]" + ); + browser.test.assertEq( + `${URL_PREFIX}?tabId/tab1`, + await getFinalUrlForFetchInTab(tab1, "https://example.net/?pre-tab1"), + "Request from tab1 should match tabIds: [tab1]" + ); + browser.test.assertEq( + `${URL_PREFIX}?tabId/not-1,not-tab1`, + await getFinalUrlForFetchInTab(tab2, "https://example.net/?pre-tab2"), + "Request from tab2 should match excludedTabIds: [-1, tab1]" + ); + + await browser.tabs.remove(tab1.id); + await browser.tabs.remove(tab2.id); + + browser.test.sendMessage("done"); + }, + temporarilyInstalled: true, // Needed for granted_host_permissions + manifest: { + manifest_version: 3, + host_permissions: ["*://example.com/*", "*://example.net/*"], + permissions: ["declarativeNetRequest"], + granted_host_permissions: true, + }, + files: { + "tab.html": `<!DOCTYPE html><script src="tab.js"><\/script>`, + "tab.js": () => { + let port = browser.runtime.connect({ name: "port_from_tab" }); + port.onMessage.addListener(async url => { + try { + let res = await fetch(url); + port.postMessage(res.url); + } catch (e) { + port.postMessage(e.message); + } + }); + }, + }, + }); + await extension.startup(); + await extension.awaitMessage("done"); + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html b/toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html new file mode 100644 index 0000000000..43bc8a5a00 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html @@ -0,0 +1,137 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>DNR with upgradeScheme action</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +"use strict"; + +// This test is not a xpcshell test, because we want to test upgrades to https, +// and HttpServer helper does not support https (bug 1742061). + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.manifestV3.enabled", true], + ["extensions.dnr.enabled", true], + ["extensions.dnr.match_requests_from_other_extensions", true], + ], + }); +}); + +// Tests that the upgradeScheme action works as expected: +// - http should be upgraded to https +// - after the https upgrade the request should happen instead of being stuck +// in a upgrade redirect loop. +add_task(async function upgradeScheme_with_dnr() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + await browser.declarativeNetRequest.updateSessionRules({ + addRules: [{ id: 1, condition: { requestDomains: ["example.com"] }, action: { type: "upgradeScheme" } }], + }); + + let sanityCheckResponse = await fetch( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_sample.txt" + ); + browser.test.assertEq( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_sample.txt", + sanityCheckResponse.url, + "non-matching request should not be upgraded" + ); + + let res = await fetch( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.txt" + ); + browser.test.assertEq( + "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.txt", + res.url, + "upgradeScheme should have upgraded to https" + ); + // Server adds "Access-Control-Allow-Origin: *" to file_sample.txt, so + // we should be able to read the response despite no host_permissions. + browser.test.assertEq("Sample", await res.text(), "read body with CORS"); + + browser.test.sendMessage("dnr_registered"); + }, + manifest: { + manifest_version: 3, + // Note: host_permissions missing. upgradeScheme should not need it. + permissions: ["declarativeNetRequest"], + }, + allowInsecureRequests: true, + }); + await extension.startup(); + await extension.awaitMessage("dnr_registered"); + + // The request made by otherExtension is affected by the DNR rule from the + // extension because extensions.dnr.match_requests_from_other_extensions was + // set to true. A realistic alternative would have been to trigger the fetch + // requests from a content page instead of the extension. + let otherExtension = ExtensionTestUtils.loadExtension({ + async background() { + let firstRequestPromise = new Promise(resolve => { + let count = 0; + browser.webRequest.onBeforeRequest.addListener( + ({ url }) => { + ++count; + browser.test.assertTrue( + count <= 2, + `Expected at most two requests; got ${count} to ${url}` + ); + resolve(url); + }, + { urls: ["*://example.com/?test_dnr_upgradeScheme"] } + ); + }); + // Round-trip through ext-webRequest.js implementation to ensure that the + // listener has been registered (workaround for bug 1300234). + await browser.webRequest.handlerBehaviorChanged(); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const insecureInitialUrl = "http://example.com/?test_dnr_upgradeScheme"; + browser.test.log(`Requesting insecure URL: ${insecureInitialUrl}`); + + let req = await fetch(insecureInitialUrl); + browser.test.assertEq( + "https://example.com/?test_dnr_upgradeScheme", + req.url, + "upgradeScheme action upgraded http to https" + ); + browser.test.assertEq(200, req.status, "Correct HTTP status"); + + await req.text(); // Verify that the body can be read, just in case. + + // Sanity check that the test did not pass trivially due to an automatic + // https upgrade of the extension / test environment. + browser.test.assertEq( + insecureInitialUrl, + await firstRequestPromise, + "Initial URL should be http" + ); + + browser.test.sendMessage("tested_dnr_upgradeScheme"); + }, + manifest: { + host_permissions: ["*://example.com/*"], + permissions: ["webRequest"], + }, + }); + await otherExtension.startup(); + await otherExtension.awaitMessage("tested_dnr_upgradeScheme"); + await otherExtension.unload(); + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html b/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html new file mode 100644 index 0000000000..23058c35ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Downloads Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +async function background() { + const url = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html"; + + browser.test.assertThrows( + () => browser.downloads.download(), + /Incorrect argument types for downloads.download/, + "Should fail without options" + ); + + browser.test.assertThrows( + () => browser.downloads.download({url: "invalid url"}), + /invalid url is not a valid URL/, + "Should fail on invalid URL" + ); + + browser.test.assertThrows( + () => browser.downloads.download({}), + /Property "url" is required/, + "Should fail with no URL" + ); + + browser.test.assertThrows( + () => browser.downloads.download({url, method: "DELETE"}), + /Invalid enumeration value "DELETE"/, + "Should fail with invalid method" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, headers: [{name: "Host", value: "Banana"}]}), + /Forbidden request header name/, + "Should fail with a forbidden header" + ); + + const absoluteFilename = SpecialPowers.Services.appinfo.OS === "WINNT" + ? "C:\\tmp\\file.gif" + : "/tmp/file.gif"; + + await browser.test.assertRejects( + browser.downloads.download({url, filename: absoluteFilename}), + /filename must not be an absolute path/, + "Should fail with an absolute file path" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, filename: ""}), + /filename must not be empty/, + "Should fail with an empty file path" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, filename: "file."}), + /filename must not contain illegal characters/, + "Should fail with a dot in the filename" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, filename: "../file.gif"}), + /filename must not contain back-references/, + "Should fail with a file path that contains back-references" + ); + + browser.test.notifyPass("download.done"); +} + +add_task(async function test_invalid_download_parameters() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: {permissions: ["downloads"]}, + background, + }); + await extension.startup(); + + await extension.awaitFinish("download.done"); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html b/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html new file mode 100644 index 0000000000..d6702da4d3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test checking webRequest.onBeforeRequest details object</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let expected = { + "file_contains_iframe.html": { + type: "main_frame", + frameAncestor_length: 0, + }, + "file_contains_img.html": { + type: "sub_frame", + frameAncestor_length: 1, + }, + "file_image_good.png": { + type: "image", + frameAncestor_length: 1, + } +}; + +function checkDetails(details) { + let url = new URL(details.url); + let filename = url.pathname.split("/").pop(); + ok(expected.hasOwnProperty(filename), `Should be expecting a request for ${filename}`); + let expect = expected[filename]; + is(expect.type, details.type, `${details.type} type matches`); + is(expect.frameAncestor_length, details.frameAncestors.length, "incorrect frameAncestors length"); + if (filename == "file_contains_img.html") { + is(details.frameAncestors[0].frameId, details.parentFrameId, + "frameAncestors[0] should match parentFrameId"); + expected["file_image_good.png"].frameId = details.frameId; + } else if (filename == "file_image_good.png") { + is(details.frameAncestors[0].frameId, details.parentFrameId, + "frameAncestors[0] should match parentFrameId"); + is(details.frameId, expect.frameId, + "frameId for image and iframe should match"); + } +} + +add_task(async () => { + // Clear the image cache, since it gets in the way otherwise. + let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); + let cache = imgTools.getImgCacheForDocument(document); + cache.clearCache(false); + await SpecialPowers.spawnChrome([], async () => { + Services.cache2.clear(); + }); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "<all_urls>"], + }, + background() { + browser.webRequest.onBeforeRequest.addListener( + details => { + browser.test.sendMessage("onBeforeRequest", details); + }, + { + urls: [ + "http://example.org/*/file_contains_img.html", + "http://mochi.test/*/file_contains_iframe.html", + "*://*/*.png", + ], + } + ); + }, + }); + + await extension.startup(); + const FILE_URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html"; + let win = window.open(FILE_URL); + await new Promise(resolve => win.addEventListener("load", () => resolve(), {once: true})); + + for (let i = 0; i < Object.keys(expected).length; i++) { + checkDetails(await extension.awaitMessage("onBeforeRequest")); + } + + win.close(); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html new file mode 100644 index 0000000000..f87b5620d6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_contentscript() { + function background() { + browser.runtime.onMessage.addListener(([script], sender) => { + browser.test.sendMessage("run", {script}); + browser.test.sendMessage("run-" + script); + }); + browser.test.sendMessage("running"); + } + + function contentScriptAll() { + browser.runtime.sendMessage(["all"]); + } + function contentScriptIncludesTest1() { + browser.runtime.sendMessage(["includes-test1"]); + } + function contentScriptExcludesTest1() { + browser.runtime.sendMessage(["excludes-test1"]); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["https://example.org/", "https://*.example.org/"], + "exclude_globs": [], + "include_globs": ["*"], + "js": ["content_script_all.js"], + }, + { + "matches": ["https://example.org/", "https://*.example.org/"], + "include_globs": ["*test1*"], + "js": ["content_script_includes_test1.js"], + }, + { + "matches": ["https://example.org/", "https://*.example.org/"], + "exclude_globs": ["*test1*"], + "js": ["content_script_excludes_test1.js"], + }, + ], + }, + background, + + files: { + "content_script_all.js": contentScriptAll, + "content_script_includes_test1.js": contentScriptIncludesTest1, + "content_script_excludes_test1.js": contentScriptExcludesTest1, + }, + + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let ran = 0; + extension.onMessage("run", ({script}) => { + ran++; + }); + + await Promise.all([extension.startup(), extension.awaitMessage("running")]); + info("extension loaded"); + + let win = window.open("https://example.org/"); + await Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-excludes-test1")]); + win.close(); + is(ran, 2); + + win = window.open("https://test1.example.org/"); + await Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-includes-test1")]); + win.close(); + is(ran, 4); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_extension_iframe_messaging.html b/toolkit/components/extensions/test/mochitest/test_ext_extension_iframe_messaging.html new file mode 100644 index 0000000000..403782ab7d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_extension_iframe_messaging.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test moz-extension iframe messaging</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +"use strict"; + +add_task(async function test_moz_extension_iframe_messaging() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.content_web_accessible.enabled", true], + ], + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + js: ["cs.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + web_accessible_resources: ["iframe.html"], + permissions: ["tabs"], + }, + files: { + "cs.js"() { + let iframe = document.createElement("iframe"); + iframe.src = browser.runtime.getURL("iframe.html"); + document.body.append(iframe); + }, + + "iframe.html": `<!doctype html><script src=iframe.js><\/script>`, + async "iframe.js"() { + browser.runtime.onMessage.addListener(async msg => { + browser.test.assertEq(msg, "from-background", "Correct message."); + return "iframe-response"; + }); + + browser.runtime.onConnect.addListener(async port => { + port.postMessage("port-message"); + }); + + await browser.test.assertRejects( + browser.runtime.sendMessage("from-iframe"), + "Could not establish connection. Receiving end does not exist.", + "No onMessage listener in the background." + ); + + await new Promise(resolve => { + let port = browser.runtime.connect(); + port.onDisconnect.addListener(() => { + browser.test.assertEq( + port.error.message, + "Could not establish connection. Receiving end does not exist.", + "No onConnect listener in the background." + ); + resolve(); + }) + }); + + // TODO: If/when the tabs API is available from extension iframes, test + // that it won't send a message to itself via browser.tabs.sendMessage() + browser.test.assertEq(browser.tabs, undefined, "No tabs API"); + + browser.test.sendMessage("iframe-done"); + }, + }, + background() { + browser.test.onMessage.addListener(async msg => { + + await browser.test.assertRejects( + browser.runtime.sendMessage("from-background"), + "Could not establish connection. Receiving end does not exist.", + "No onMessage listener in another extension page." + ); + + await new Promise(resolve => { + let port = browser.runtime.connect(); + port.onDisconnect.addListener(() => { + browser.test.assertEq( + port.error.message, + "Could not establish connection. Receiving end does not exist.", + "No onConnect listener in another extension page." + ); + resolve(); + }) + }); + + let [tab] = await browser.tabs.query({ + url: "http://mochi.test/*/file_sample.html", + }); + let res = await browser.tabs.sendMessage(tab.id, "from-background"); + browser.test.assertEq(res, "iframe-response", "Correct response."); + + let port = browser.tabs.connect(tab.id); + port.onMessage.addListener(msg => { + browser.test.assertEq(msg, "port-message", "Correct port message."); + browser.test.notifyPass("done"); + }); + }) + } + }); + + await extension.startup(); + + let win = window.open("file_sample.html"); + await extension.awaitMessage("iframe-done"); + + extension.sendMessage("run-background"); + await extension.awaitFinish("done"); + win.close(); + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html b/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html new file mode 100644 index 0000000000..639cacef28 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension external messaging</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function backgroundScript(id, otherId) { + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.fail(`Got unexpected message: ${uneval(msg)} ${uneval(sender)}`); + }); + + browser.runtime.onConnect.addListener(port => { + browser.test.fail(`Got unexpected connection: ${uneval(port.sender)}`); + }); + + browser.runtime.onMessageExternal.addListener((msg, sender) => { + browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`); + browser.test.assertEq(`helo-${id}`, msg, "Got expected message"); + + browser.test.sendMessage("onMessage-done"); + + return Promise.resolve(`ehlo-${otherId}`); + }); + + browser.runtime.onConnectExternal.addListener(port => { + browser.test.assertEq(otherId, port.sender.id, `${id}: Got expected external connecter ID`); + + port.onMessage.addListener(msg => { + browser.test.assertEq(`helo-${id}`, msg, "Got expected port message"); + + port.postMessage(`ehlo-${otherId}`); + + browser.test.sendMessage("onConnect-done"); + }); + }); + + browser.test.onMessage.addListener(msg => { + if (msg === "go") { + browser.runtime.sendMessage(otherId, `helo-${otherId}`).then(result => { + browser.test.assertEq(`ehlo-${id}`, result, "Got expected reply"); + browser.test.sendMessage("sendMessage-done"); + }); + + let port = browser.runtime.connect(otherId); + port.postMessage(`helo-${otherId}`); + + port.onMessage.addListener(msg => { + port.disconnect(); + + browser.test.assertEq(msg, `ehlo-${id}`, "Got expected port reply"); + browser.test.sendMessage("connect-done"); + }); + } + }); +} + +function makeExtension(id, otherId) { + let args = `${JSON.stringify(id)}, ${JSON.stringify(otherId)}`; + + let extensionData = { + background: `(${backgroundScript})(${args})`, + manifest: { + browser_specific_settings: {gecko: {id}}, + }, + }; + + return ExtensionTestUtils.loadExtension(extensionData); +} + +add_task(async function test_contentscript() { + const ID1 = "foo-message@mochitest.mozilla.org"; + const ID2 = "bar-message@mochitest.mozilla.org"; + + let extension1 = makeExtension(ID1, ID2); + let extension2 = makeExtension(ID2, ID1); + + await Promise.all([extension1.startup(), extension2.startup()]); + + extension1.sendMessage("go"); + extension2.sendMessage("go"); + + await Promise.all([ + extension1.awaitMessage("sendMessage-done"), + extension2.awaitMessage("sendMessage-done"), + + extension1.awaitMessage("onMessage-done"), + extension2.awaitMessage("onMessage-done"), + + extension1.awaitMessage("connect-done"), + extension2.awaitMessage("connect-done"), + + extension1.awaitMessage("onConnect-done"), + extension2.awaitMessage("onConnect-done"), + ]); + + await extension1.unload(); + await extension2.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_generate.html b/toolkit/components/extensions/test/mochitest/test_ext_generate.html new file mode 100644 index 0000000000..ba88d16ca3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_generate.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for generating WebExtensions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + browser.test.log("running background script"); + + browser.test.onMessage.addListener((x, y) => { + browser.test.assertEq(x, 10, "x is 10"); + browser.test.assertEq(y, 20, "y is 20"); + + browser.test.notifyPass("background test passed"); + }); + + browser.test.sendMessage("running", 1); +} + +let extensionData = { + background, +}; + +add_task(async function test_background() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + info("load complete"); + let [, x] = await Promise.all([extension.startup(), extension.awaitMessage("running")]); + is(x, 1, "got correct value from extension"); + info("startup complete"); + extension.sendMessage(10, 20); + await extension.awaitFinish(); + info("test complete"); + await extension.unload(); + info("extension unloaded successfully"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html b/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html new file mode 100644 index 0000000000..9f326372bb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +add_task(async function test_geolocation_nopermission() { + let GEO_URL = "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs"; + await SpecialPowers.pushPrefEnv({"set": [["geo.provider.network.url", GEO_URL]]}); +}); + +add_task(async function test_geolocation() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "geolocation", + ], + }, + background() { + navigator.geolocation.getCurrentPosition(() => { + browser.test.notifyPass("success geolocation call"); + }, (error) => { + browser.test.notifyFail(`geolocation call ${error}`); + }); + }, + }); + + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_geolocation_nopermission() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + navigator.geolocation.getCurrentPosition(() => { + browser.test.notifyFail("success geolocation call"); + }, (error) => { + browser.test.notifyPass(`geolocation call ${error}`); + }); + }, + }); + + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_geolocation_prompt() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.tabs.create({url: "tab.html"}); + }, + files: { + "tab.html": `<html><head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head></html>`, + "tab.js": () => { + navigator.geolocation.getCurrentPosition(() => { + browser.test.notifyPass("success geolocation call"); + }, (error) => { + browser.test.notifyFail(`geolocation call ${error}`); + }); + }, + }, + }); + + // Bypass the actual prompt, but the prompt result is to allow access. + await SpecialPowers.pushPrefEnv({"set": [["geo.prompt.testing", true], ["geo.prompt.testing.allow", true]]}); + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); +</script> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_identity.html b/toolkit/components/extensions/test/mochitest/test_ext_identity.html new file mode 100644 index 0000000000..7aa590ec22 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_identity.html @@ -0,0 +1,390 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebExtension Identity</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webextensions.identity.redirectDomain", "example.com"], + // Disable the network cache first-party partition during this + // test (TODO: look more closely to how that is affecting the intermittency + // of this test on MacOS, see Bug 1626482). + ["privacy.partition.network_state", false], + ], + }); +}); + +add_task(async function test_noPermission() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.assertEq( + undefined, + browser.identity, + "No identity api without permission" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_getRedirectURL() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["identity", "https://example.com/"], + }, + async background() { + let redirect_base = + "https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/"; + await browser.test.assertEq( + redirect_base, + browser.identity.getRedirectURL(), + "redirect url ok" + ); + await browser.test.assertEq( + redirect_base, + browser.identity.getRedirectURL(""), + "redirect url ok" + ); + await browser.test.assertEq( + redirect_base + "foobar", + browser.identity.getRedirectURL("foobar"), + "redirect url ok" + ); + await browser.test.assertEq( + redirect_base + "callback", + browser.identity.getRedirectURL("/callback"), + "redirect url ok" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_badAuthURI() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["identity", "https://example.com/"], + }, + async background() { + for (let url of [ + "foobar", + "about:addons", + "about:blank", + "ftp://example.com/test", + ]) { + await browser.test.assertThrows( + () => { + browser.identity.launchWebAuthFlow({ interactive: true, url }); + }, + /Type error for parameter details/, + "details.url is invalid" + ); + } + + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_badRequestURI() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["identity", "https://example.com/"], + }, + async background() { + let base_uri = + "https://example.com/tests/toolkit/components/extensions/test/mochitest/"; + let url = `${base_uri}?redirect_uri=badrobot}`; + await browser.test.assertRejects( + browser.identity.launchWebAuthFlow({ interactive: true, url }), + "redirect_uri is invalid", + "invalid redirect url" + ); + url = `${base_uri}?redirect_uri=https://somesite.com`; + await browser.test.assertRejects( + browser.identity.launchWebAuthFlow({ interactive: true, url }), + "redirect_uri not allowed", + "invalid redirect url" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function background_launchWebAuthFlow_requires_interaction() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["identity", "https://example.com/"], + }, + async background() { + let base_uri = + "https://example.com/tests/toolkit/components/extensions/test/mochitest/"; + let url = `${base_uri}?redirect_uri=${browser.identity.getRedirectURL( + "redirect" + )}`; + await browser.test.assertRejects( + browser.identity.launchWebAuthFlow({ interactive: false, url }), + "Requires user interaction", + "Rejects on required user interaction" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +function background_launchWebAuthFlow({ + interactive = false, + path = "redirect_auto.sjs", + params = {}, + redirect = true, + useRedirectUri = true, +} = {}) { + let uri_path = useRedirectUri ? "identity_cb" : ""; + let expected_redirect = `https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/${uri_path}`; + let base_uri = + "https://example.com/tests/toolkit/components/extensions/test/mochitest/"; + let redirect_uri = browser.identity.getRedirectURL( + useRedirectUri ? uri_path : undefined + ); + browser.test.assertEq( + expected_redirect, + redirect_uri, + "expected redirect uri matches hash" + ); + let url = `${base_uri}${path}`; + if (useRedirectUri) { + params.redirect_uri = redirect_uri; + } else { + // We kind of fake it with the redirect url that would normally be configured + // in the oauth service. This does still test that the identity service falls back + // to the extensions redirect url. + params.default_redirect = expected_redirect; + } + if (!redirect) { + params.no_redirect = 1; + } + let query = []; + for (let [param, value] of Object.entries(params)) { + query.push(`${param}=${encodeURIComponent(value)}`); + } + url = `${url}?${query.join("&")}`; + + // Ensure we do not start the actual request for the redirect url. In the case + // of a 303 POST redirect we are getting a request started. + let watchRedirectRequest = () => {}; + if (params.post !== 303) { + watchRedirectRequest = details => { + if (details.url.startsWith(expected_redirect)) { + browser.test.fail(`onBeforeRequest called for redirect url: ${JSON.stringify(details)}`); + } + }; + + browser.webRequest.onBeforeRequest.addListener( + watchRedirectRequest, + { + urls: [ + "https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/*", + ], + } + ); + } + + browser.identity + .launchWebAuthFlow({ interactive, url }) + .then(redirectURL => { + browser.test.assertTrue( + redirectURL.startsWith(redirect_uri), + `correct redirect url ${redirectURL}` + ); + if (redirect) { + let url = new URL(redirectURL); + browser.test.assertEq( + "here ya go", + url.searchParams.get("access_token"), + "Handled auto redirection" + ); + } + }) + .catch(error => { + if (redirect) { + browser.test.fail(error.message); + } else { + browser.test.assertEq( + "Requires user interaction", + error.message, + "Auth page loaded, interaction required." + ); + } + }).then(() => { + browser.webRequest.onBeforeRequest.removeListener(watchRedirectRequest); + browser.test.sendMessage("done"); + }); +} + +// Tests the situation where the oauth provider has already granted access and +// simply redirects the oauth client to provide the access key or code. +add_task(async function test_autoRedirect() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})()`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_autoRedirect_noRedirectURI() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})({useRedirectUri: false})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +// Tests the situation where the oauth provider has not granted access and interactive=false +add_task(async function test_noRedirect() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})({redirect: false})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +// Tests the situation where the oauth provider must show a window where +// presumably the user interacts, then the redirect occurs and access key or +// code is provided. We bypass any real interaction, but want the window to +// open and result in a redirect. +add_task(async function test_interaction() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})({interactive: true, path: "oauth.html"})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +// Tests the situation where the oauth provider redirects with a 303. +add_task(async function test_auto303Redirect() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})({interactive: true, path: "oauth.html", params: {post: 303, server_uri: "redirect_auto.sjs"}})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_loopbackRedirectURI() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["identity"], + }, + async background() { + let redirectURL = "http://127.0.0.1/mozoauth2/35b64b676900f491c00e7f618d43f7040e88422e"; + let actualRedirect = await browser.identity.launchWebAuthFlow({ + interactive: true, + url: `https://example.com/tests/toolkit/components/extensions/test/mochitest/oauth.html?redirect_uri=${encodeURIComponent(redirectURL)}` + }).catch(error => { + browser.test.fail(error.message) + }); + browser.test.assertTrue( + actualRedirect.startsWith(redirectURL), + "Expected redirect url to be loopback address" + ) + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_idle.html b/toolkit/components/extensions/test/mochitest/test_ext_idle.html new file mode 100644 index 0000000000..381687ee38 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_idle.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testWithRealIdleService() { + function background() { + browser.test.onMessage.addListener(async (msg, ...args) => { + let detectionInterval = args[0]; + if (msg == "addListener") { + let status = await browser.idle.queryState(detectionInterval); + browser.test.assertEq("active", status, "Idle status is active"); + browser.idle.setDetectionInterval(detectionInterval); + browser.idle.onStateChanged.addListener(newState => { + browser.test.assertEq("idle", newState, "listener fired with the expected state"); + browser.test.sendMessage("listenerFired"); + }); + browser.test.sendMessage("listenerAdded"); + } else if (msg == "checkState") { + let status = await browser.idle.queryState(detectionInterval); + browser.test.assertEq("idle", status, "Idle status is idle"); + browser.test.notifyPass("idle"); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + await extension.startup(); + + let chromeScript = loadChromeScript(() => { + const {sendAsyncMessage} = this; + const idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService(Ci.nsIUserIdleService); + let idleTime = idleService.idleTime; + sendAsyncMessage("detectionInterval", Math.max(Math.ceil(idleTime / 1000) + 10, 15)); + }); + let detectionInterval = await chromeScript.promiseOneMessage("detectionInterval"); + chromeScript.destroy(); + + info(`Setting interval to ${detectionInterval}`); + extension.sendMessage("addListener", detectionInterval); + await extension.awaitMessage("listenerAdded"); + info("Listener added"); + await extension.awaitMessage("listenerFired"); + info("Listener fired"); + extension.sendMessage("checkState", detectionInterval); + await extension.awaitFinish("idle"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html new file mode 100644 index 0000000000..5b36902581 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_in_incognito_context_true() { + function background() { + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq(true, msg, "inIncognitoContext is true"); + browser.test.notifyPass("inIncognitoContext"); + }); + + browser.windows.create({url: browser.runtime.getURL("/tab.html"), incognito: true}); + } + + function tabScript() { + browser.runtime.sendMessage(browser.extension.inIncognitoContext); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + files: { + "tab.js": tabScript, + "tab.html": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head></html>`, + }, + incognitoOverride: "spanning", + }); + + await extension.startup(); + await extension.awaitFinish("inIncognitoContext"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html new file mode 100644 index 0000000000..cc161f735f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_listener_proxies() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + + manifest: { + "permissions": ["storage"], + }, + + async background() { + // Test that adding multiple listeners for the same event works as + // expected. + + let awaitChanged = () => new Promise(resolve => { + browser.storage.onChanged.addListener(function listener() { + browser.storage.onChanged.removeListener(listener); + resolve(); + }); + }); + + let promises = [ + awaitChanged(), + awaitChanged(), + ]; + + function removedListener() {} + browser.storage.onChanged.addListener(removedListener); + browser.storage.onChanged.removeListener(removedListener); + + promises.push(awaitChanged(), awaitChanged()); + + browser.storage.local.set({foo: "bar"}); + + await Promise.all(promises); + + browser.test.notifyPass("onchanged-listeners"); + }, + }); + + await extension.startup(); + + await extension.awaitFinish("onchanged-listeners"); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html b/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html new file mode 100644 index 0000000000..2c5ae1c0ba --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html @@ -0,0 +1,168 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for opening links in new tabs from extension frames</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function promiseObserved(topic, check) { + return new Promise(resolve => { + let obs = SpecialPowers.Services.obs; + + function observer(subject, topic, data) { + subject = SpecialPowers.wrap(subject); + if (check(subject, data)) { + obs.removeObserver(observer, topic); + resolve({subject, data}); + } + } + obs.addObserver(observer, topic); + }); +} + +add_task(async function test_target_blank_link_no_opener_from_privileged() { + const linkURL = "https://example.com/"; + + function extension_tab() { + document.getElementById("link").click(); + } + + function content_script() { + browser.runtime.sendMessage("content_page_loaded"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + js: ["content_script.js"], + matches: ["https://example.com/*"], + run_at: "document_idle", + }], + permissions: ["tabs"], + }, + files: { + "page.html": `<!DOCTYPE html> + <html> + <head><meta charset="utf-8"></html> + <body> + <a href="${linkURL}" target="_blank" id="link">link</a> + <script src="extension_tab.js"><\/script> + </body> + </html>`, + "extension_tab.js": extension_tab, + "content_script.js": content_script, + }, + async background() { + let pageTab; + browser.test.onMessage.addListener(async (msg) => { + if (msg !== "close_tab") { + browser.test.fail("Unexpected test message: " + msg); + return; + } + if (!pageTab) { + browser.test.fail("Unexpected close-tab test message received when there is no pageTab"); + return; + } + await browser.tabs.remove(pageTab.id); + browser.test.sendMessage("close_tab_done"); + }); + browser.runtime.onMessage.addListener(async (msg, sender) => { + if (sender.tab) { + await browser.tabs.remove(sender.tab.id); + browser.test.sendMessage(msg, sender.tab.url); + } + }); + pageTab = await browser.tabs.create({ url: browser.runtime.getURL("page.html") }); + browser.test.sendMessage("tab_created"); + }, + }); + + await extension.startup(); + + await extension.awaitMessage("tab_created"); + + // Make sure page is loaded correctly + const url = await extension.awaitMessage("content_page_loaded"); + is(url, linkURL, "Page URL should match"); + + // Clean up opened tab. + extension.sendMessage("close_tab"); + await extension.awaitMessage("close_tab_done"); + + await extension.unload(); +}); + +add_task(async function test_target_blank_link() { + const linkURL = "http://mochi.test:8888/tests/toolkit/"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_security_policy: "script-src 'self' 'unsafe-eval'; object-src 'self';", + + web_accessible_resources: ["iframe.html"], + }, + files: { + "iframe.html": `<!DOCTYPE html> + <html> + <head><meta charset="utf-8"></html> + <body> + <a href="${linkURL}" target="_blank" id="link" rel="opener">link</a> + </body> + </html>`, + }, + background() { + browser.test.sendMessage("frame_url", browser.runtime.getURL("iframe.html")); + }, + }); + + await extension.startup(); + + let url = await extension.awaitMessage("frame_url"); + + let iframe = document.createElement("iframe"); + iframe.src = url; + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", () => setTimeout(resolve, 0), {once: true})); + + let win = SpecialPowers.wrap(iframe).contentWindow; + + { + // Flush layout so that synthesizeMouseAtCenter on a cross-origin iframe + // works as expected. + document.body.getBoundingClientRect(); + + let promise = promiseObserved("document-element-inserted", doc => doc.documentURI === linkURL); + + await SpecialPowers.spawn(iframe, [], async () => { + this.content.document.getElementById("link").click(); + }); + + let {subject: doc} = await promise; + info("Link opened"); + doc.defaultView.close(); + info("Window closed"); + } + + { + let promise = promiseObserved("document-element-inserted", doc => doc.documentURI === linkURL); + + let res = win.eval(`window.open("${linkURL}")`); + let {subject: doc} = await promise; + is(SpecialPowers.unwrap(res), SpecialPowers.unwrap(doc.defaultView), "window.open worked as expected"); + + doc.defaultView.close(); + } + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_notifications.html b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html new file mode 100644 index 0000000000..7a91320373 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html @@ -0,0 +1,340 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for notifications</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_notifications.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); +const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer; + +add_task(async function setup_mock_alert_service() { + await MockAlertsService.register(); +}); + +add_task(async function test_notification() { + async function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + let id = await browser.notifications.create(opts); + + browser.test.sendMessage("running", id); + browser.test.notifyPass("background test passed"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + await extension.startup(); + let x = await extension.awaitMessage("running"); + is(x, "0", "got correct id from notifications.create"); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_notification_events() { + async function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + let createdId = "98"; + + // Test an ignored listener. + browser.notifications.onButtonClicked.addListener(function() {}); + + // We cannot test onClicked listener without a mock + // but we can attempt to add a listener. + browser.notifications.onClicked.addListener(async function(id) { + browser.test.assertEq(createdId, id, "onClicked has the expected ID"); + browser.test.sendMessage("notification-event", "clicked"); + }); + + browser.notifications.onShown.addListener(async function listener(id) { + browser.test.assertEq(createdId, id, "onShown has the expected ID"); + browser.test.sendMessage("notification-event", "shown"); + }); + + browser.test.onMessage.addListener(async function(msg, expectedCount) { + if (msg === "create-again") { + let newId = await browser.notifications.create(createdId, opts); + browser.test.assertEq(createdId, newId, "create returned the expected id."); + browser.test.sendMessage("notification-created-twice"); + } else if (msg === "check-count") { + let notifications = await browser.notifications.getAll(); + let ids = Object.keys(notifications); + browser.test.assertEq(expectedCount, ids.length, `getAll() = ${ids}`); + browser.test.sendMessage("check-count-result"); + } + }); + + // Test onClosed listener. + browser.notifications.onClosed.addListener(function listener(id) { + browser.test.assertEq(createdId, id, "onClosed received the expected id."); + browser.test.sendMessage("notification-event", "closed"); + }); + + await browser.notifications.create(createdId, opts); + + browser.test.sendMessage("notification-created-once"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + + await extension.startup(); + + async function waitForNotificationEvent(name) { + info(`Waiting for notification event: ${name}`); + is(name, await extension.awaitMessage("notification-event"), + "Expected notification event"); + } + async function checkNotificationCount(expectedCount) { + extension.sendMessage("check-count", expectedCount); + await extension.awaitMessage("check-count-result"); + } + + await extension.awaitMessage("notification-created-once"); + await waitForNotificationEvent("shown"); + await checkNotificationCount(1); + + // On most platforms, clicking the notification closes it. + // But on macOS, the notification can repeatedly be clicked without closing. + await MockAlertsService.clickNotificationsWithoutClose(); + await waitForNotificationEvent("clicked"); + await checkNotificationCount(1); + await MockAlertsService.clickNotificationsWithoutClose(); + await waitForNotificationEvent("clicked"); + await checkNotificationCount(1); + await MockAlertsService.clickNotifications(); + await waitForNotificationEvent("clicked"); + await waitForNotificationEvent("closed"); + await checkNotificationCount(0); + + extension.sendMessage("create-again"); + await extension.awaitMessage("notification-created-twice"); + await waitForNotificationEvent("shown"); + await checkNotificationCount(1); + + await MockAlertsService.closeNotifications(); + await waitForNotificationEvent("closed"); + await checkNotificationCount(0); + + await extension.unload(); +}); + +add_task(async function test_notification_clear() { + function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + let createdId = "99"; + + browser.notifications.onShown.addListener(async id => { + browser.test.assertEq(createdId, id, "onShown received the expected id."); + let wasCleared = await browser.notifications.clear(id); + browser.test.assertTrue(wasCleared, "notifications.clear returned true."); + }); + + browser.notifications.onClosed.addListener(id => { + browser.test.assertEq(createdId, id, "onClosed received the expected id."); + browser.test.notifyPass("background test passed"); + }); + + browser.notifications.create(createdId, opts); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_notifications_empty_getAll() { + async function background() { + let notifications = await browser.notifications.getAll(); + + browser.test.assertEq("object", typeof notifications, "getAll() returned an object"); + browser.test.assertEq(0, Object.keys(notifications).length, "the object has no properties"); + browser.test.notifyPass("getAll empty"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + await extension.startup(); + await extension.awaitFinish("getAll empty"); + await extension.unload(); +}); + +add_task(async function test_notifications_populated_getAll() { + async function background() { + let opts = { + type: "basic", + iconUrl: "a.png", + title: "Testing Notification", + message: "Carry on", + }; + + await browser.notifications.create("p1", opts); + await browser.notifications.create("p2", opts); + let notifications = await browser.notifications.getAll(); + + browser.test.assertEq("object", typeof notifications, "getAll() returned an object"); + browser.test.assertEq(2, Object.keys(notifications).length, "the object has 2 properties"); + + for (let notificationId of ["p1", "p2"]) { + for (let key of Object.keys(opts)) { + browser.test.assertEq( + opts[key], + notifications[notificationId][key], + `the notification has the expected value for option: ${key}` + ); + } + } + + browser.test.notifyPass("getAll populated"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + files: { + "a.png": IMAGE_ARRAYBUFFER, + }, + }); + await extension.startup(); + await extension.awaitFinish("getAll populated"); + await extension.unload(); +}); + +add_task(async function test_buttons_unsupported() { + function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + buttons: [{title: "Button title"}], + }; + + let exception = {}; + try { + browser.notifications.create(opts); + } catch (e) { + exception = e; + } + + browser.test.assertTrue( + String(exception).includes('Property "buttons" is unsupported by Firefox'), + "notifications.create with buttons option threw an expected exception" + ); + browser.test.notifyPass("buttons-unsupported"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + await extension.startup(); + await extension.awaitFinish("buttons-unsupported"); + await extension.unload(); +}); + +add_task(async function test_notifications_different_contexts() { + async function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + let id = await browser.notifications.create(opts); + + browser.runtime.onMessage.addListener(async (message, sender) => { + await browser.tabs.remove(sender.tab.id); + + // We should be able to clear the notification after creating and + // destroying the tab.html page. + let wasCleared = await browser.notifications.clear(id); + browser.test.assertTrue(wasCleared, "The notification was cleared."); + browser.test.notifyPass("notifications"); + }); + + browser.tabs.create({url: browser.runtime.getURL("/tab.html")}); + } + + async function tabScript() { + // We should be able to see the notification created in the background page + // in this page. + let notifications = await browser.notifications.getAll(); + browser.test.assertEq(1, Object.keys(notifications).length, + "One notification found."); + browser.runtime.sendMessage("continue-test"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + files: { + "tab.js": tabScript, + "tab.html": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head></html>`, + }, + }); + + await extension.startup(); + await extension.awaitFinish("notifications"); + await extension.unload(); +}); + +add_task(async function teardown_mock_alert_service() { + await MockAlertsService.unregister(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_optional_permissions.html b/toolkit/components/extensions/test/mochitest/test_ext_optional_permissions.html new file mode 100644 index 0000000000..659a55f5c9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_optional_permissions.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>optional permissions and preloaded processes</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webextOptionalPermissionPrompts", false]], + }); +}); + +// This test case verifies that newly granted optional permissions are +// propagated to all processes, especially preloaded processes. +add_task(async function test_optional_permissions_should_be_propagated() { + let anOptionalPermission = "*://example.org/*"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "scripting", + "*://example.com/*", + ], + optional_permissions: [anOptionalPermission], + }, + async background() { + browser.test.onMessage.addListener(async (msg, value) => { + browser.test.assertEq("grant-permission", msg, "expected message"); + + let granted = await new Promise(resolve => { + browser.test.withHandlingUserInput(() => { + resolve(browser.permissions.request(value)); + }); + }); + browser.test.assertTrue(granted, "permission request succeeded"); + browser.test.sendMessage("permission-granted"); + }); + + await browser.scripting.registerContentScripts([ + { + id: "script", + js: ["script.js"], + matches: ["*://example.com/*", "*://example.org/*"], + persistAcrossSessions: false, + }, + ]); + + browser.test.sendMessage("background-ready"); + }, + files: { + "script.js": () => { + browser.test.sendMessage("script-ran", window.location.host); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "http://example.com/", + true + ); + let host = await extension.awaitMessage("script-ran"); + is(host, "example.com", "expected host: example.com"); + await AppTestDelegate.removeTab(window, tab); + + extension.sendMessage("grant-permission", { + origins: ["*://example.org/*"], + }); + await extension.awaitMessage("permission-granted"); + + tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://example.org/", + true + ); + host = await extension.awaitMessage("script-ran"); + is(host, "example.org", "expected host: example.org"); + await AppTestDelegate.removeTab(window, tab); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html new file mode 100644 index 0000000000..41db82eff7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html @@ -0,0 +1,580 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for protocol handlers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* eslint-disable mozilla/balanced-listeners */ + +function protocolChromeScript() { + const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler"; + const PERMISSION_KEY_DELIMITER = "^"; + + /* eslint-env mozilla/chrome-script */ + addMessageListener("setup", ({ protocol, principalOrigins }) => { + let data = {}; + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo(protocol); + data.preferredAction = protoInfo.preferredAction == protoInfo.useHelperApp; + + let handlers = protoInfo.possibleApplicationHandlers; + data.handlers = handlers.length; + + let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp); + data.isWebHandler = handler instanceof Ci.nsIWebHandlerApp; + data.uriTemplate = handler.uriTemplate; + + // ext+ protocols should be set as default when there is only one + data.preferredApplicationHandler = + protoInfo.preferredApplicationHandler == handler; + data.alwaysAskBeforeHandling = protoInfo.alwaysAskBeforeHandling; + const handlerSvc = Cc[ + "@mozilla.org/uriloader/handler-service;1" + ].getService(Ci.nsIHandlerService); + handlerSvc.store(protoInfo); + + for (let origin of principalOrigins) { + let principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(origin), + {} + ); + let pbPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(origin), + { + privateBrowsingId: 1, + } + ); + let permKey = + PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER + protocol; + Services.perms.addFromPrincipal( + principal, + permKey, + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_NEVER + ); + Services.perms.addFromPrincipal( + pbPrincipal, + permKey, + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_NEVER + ); + } + + sendAsyncMessage("handlerData", data); + }); + addMessageListener("setPreferredAction", data => { + let { protocol, template } = data; + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo(protocol); + + for (let handler of protoInfo.possibleApplicationHandlers.enumerate()) { + if (handler.uriTemplate.startsWith(template)) { + protoInfo.preferredApplicationHandler = handler; + protoInfo.preferredAction = protoInfo.useHelperApp; + protoInfo.alwaysAskBeforeHandling = false; + } + } + const handlerSvc = Cc[ + "@mozilla.org/uriloader/handler-service;1" + ].getService(Ci.nsIHandlerService); + handlerSvc.store(protoInfo); + sendAsyncMessage("set"); + }); +} + +add_task(async function test_protocolHandler() { + let extensionData = { + manifest: { + protocol_handlers: [ + { + protocol: "ext+foo", + name: "a foo protocol handler", + uriTemplate: "foo.html?val=%s", + }, + ], + }, + + background() { + browser.test.onMessage.addListener(async (msg, arg) => { + if (msg == "open") { + let tab = await browser.tabs.create({ url: arg }); + browser.test.sendMessage("opened", tab.id); + } else if (msg == "close") { + await browser.tabs.remove(arg); + browser.test.sendMessage("closed"); + } + }); + browser.test.sendMessage("test-url", browser.runtime.getURL("foo.html")); + }, + + files: { + "foo.js": function() { + browser.test.sendMessage("test-query", location.search); + browser.tabs.getCurrent().then(tab => browser.test.sendMessage("test-tab", tab.id)); + }, + "foo.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="foo.js"><\/script> + </head> + </html>`, + }, + }; + + let pb_extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.onMessage.addListener(async (msg, arg) => { + if (msg == "open") { + let win = await browser.windows.create({ url: arg, incognito: true }); + browser.test.sendMessage("opened", { + windowId: win.id, + tabId: win.tabs[0].id, + }); + } else if (msg == "nav") { + await browser.tabs.update(arg.tabId, { url: arg.url }); + browser.test.sendMessage("navigated"); + } else if (msg == "close") { + await browser.windows.remove(arg); + browser.test.sendMessage("closed"); + } + }); + }, + incognitoOverride: "spanning", + }); + await pb_extension.startup(); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let handlerUrl = await extension.awaitMessage("test-url"); + + // Ensure that the protocol handler is configured, and set it as default to + // bypass the dialog. + let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript); + + let msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup", { + protocol: "ext+foo", + principalOrigins: [ + `moz-extension://${extension.uuid}/`, + `moz-extension://${pb_extension.uuid}/`, + ], + }); + let data = await msg; + ok( + data.preferredAction, + "using a helper application is the preferred action" + ); + ok(data.preferredApplicationHandler, "handler was set as default handler"); + is(data.handlers, 1, "one handler is set"); + ok(!data.alwaysAskBeforeHandling, "will not show dialog"); + ok(data.isWebHandler, "the handler is a web handler"); + is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template"); + chromeScript.destroy(); + + extension.sendMessage("open", "ext+foo:test"); + let id = await extension.awaitMessage("opened"); + + let query = await extension.awaitMessage("test-query"); + is(query, "?val=ext%2Bfoo%3Atest", "test query ok"); + is(id, await extension.awaitMessage("test-tab"), "id should match opened tab"); + + extension.sendMessage("close", id); + await extension.awaitMessage("closed"); + + // Test that handling a URL from the commandline works. + chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService( + Ci.nsICommandLineHandler + ); + let fakeCmdLine = Cu.createCommandLine( + ["-url", "ext+foo:cmdline"], + null, + Ci.nsICommandLine.STATE_REMOTE_EXPLICIT + ); + cmdLineHandler.handle(fakeCmdLine); + }); + query = await extension.awaitMessage("test-query"); + is(query, "?val=ext%2Bfoo%3Acmdline", "cmdline query ok"); + id = await extension.awaitMessage("test-tab"); + extension.sendMessage("close", id); + await extension.awaitMessage("closed"); + chromeScript.destroy(); + + // Test the protocol in a private window, watch for the + // console error. + consoleMonitor.start([{ message: /NS_ERROR_FILE_NOT_FOUND/ }]); + + // Expect the chooser window to be open, close it. + chromeScript = SpecialPowers.loadChromeScript(async () => { + /* eslint-env mozilla/chrome-script */ + const CONTENT_HANDLING_URL = + "chrome://mozapps/content/handling/appChooser.xhtml"; + const { BrowserTestUtils } = ChromeUtils.import( + "resource://testing-common/BrowserTestUtils.jsm" + ); + + let windowOpen = BrowserTestUtils.domWindowOpenedAndLoaded(); + + sendAsyncMessage("listenWindow"); + + let window = await windowOpen; + let gBrowser = window.gBrowser; + let tabDialogBox = gBrowser.getTabDialogBox(gBrowser.selectedBrowser); + let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack; + + let checkFn = dialogEvent => + dialogEvent.detail.dialog?._openedURL == CONTENT_HANDLING_URL; + + let eventPromise = BrowserTestUtils.waitForEvent( + dialogStack, + "dialogopen", + true, + checkFn + ); + + sendAsyncMessage("listenDialog"); + + let event = await eventPromise; + + let { dialog } = event.detail; + + let entry = dialog._frame.contentDocument.getElementById("items") + .firstChild; + sendAsyncMessage("handling", { + name: entry.getAttribute("name"), + disabled: entry.disabled, + }); + + dialog.close(); + }); + + // Wait for the chrome script to attach window listener + await chromeScript.promiseOneMessage("listenWindow"); + + let listenDialog = chromeScript.promiseOneMessage("listenDialog"); + let windowOpen = pb_extension.awaitMessage("opened"); + + pb_extension.sendMessage("open", "ext+foo:test"); + + // Wait for chrome script to attach dialog listener + await listenDialog; + let { tabId, windowId } = await windowOpen; + + let testData = chromeScript.promiseOneMessage("handling"); + let navPromise = pb_extension.awaitMessage("navigated"); + pb_extension.sendMessage("nav", { url: "ext+foo:test", tabId }); + await navPromise; + await consoleMonitor.finished(); + let entry = await testData; + + is(entry.name, "a foo protocol handler", "entry is correct"); + ok(entry.disabled, "handler is disabled"); + + let promiseClosed = pb_extension.awaitMessage("closed"); + pb_extension.sendMessage("close", windowId); + await promiseClosed; + await pb_extension.unload(); + + // Shutdown the addon, then ensure the protocol was removed. + await extension.unload(); + chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("setup", () => { + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo"); + sendAsyncMessage( + "preferredApplicationHandler", + !protoInfo.preferredApplicationHandler + ); + let handlers = protoInfo.possibleApplicationHandlers; + + sendAsyncMessage("handlerData", { + preferredApplicationHandler: !protoInfo.preferredApplicationHandler, + handlers: handlers.length, + }); + }); + }); + + msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup"); + data = await msg; + ok(data.preferredApplicationHandler, "no preferred handler is set"); + is(data.handlers, 0, "no handler is set"); + chromeScript.destroy(); +}); + +add_task(async function test_protocolHandler_two() { + let extensionData = { + manifest: { + protocol_handlers: [ + { + protocol: "ext+foo", + name: "a foo protocol handler", + uriTemplate: "foo.html?val=%s", + }, + { + protocol: "ext+foo", + name: "another foo protocol handler", + uriTemplate: "foo2.html?val=%s", + }, + ], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + // Ensure that the protocol handler is configured, and set it as default, + // but because there are two handlers, the dialog is not bypassed. We + // don't test the actual dialog ui, it's been here forever and works based + // on the alwaysAskBeforeHandling value. + let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript); + + let msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup", { + protocol: "ext+foo", + principalOrigins: [], + }); + let data = await msg; + ok( + data.preferredAction, + "using a helper application is the preferred action" + ); + ok(data.preferredApplicationHandler, "preferred handler is set"); + is(data.handlers, 2, "two handlers are set"); + ok(data.alwaysAskBeforeHandling, "will show dialog"); + ok(data.isWebHandler, "the handler is a web handler"); + chromeScript.destroy(); + await extension.unload(); +}); + +add_task(async function test_protocolHandler_https_target() { + let extensionData = { + manifest: { + protocol_handlers: [ + { + protocol: "ext+foo", + name: "http target", + uriTemplate: "https://example.com/foo.html?val=%s", + }, + ], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + ok(true, "https uriTemplate target works"); + await extension.unload(); +}); + +add_task(async function test_protocolHandler_http_target() { + let extensionData = { + manifest: { + protocol_handlers: [ + { + protocol: "ext+foo", + name: "http target", + uriTemplate: "http://example.com/foo.html?val=%s", + }, + ], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + ok(true, "http uriTemplate target works"); + await extension.unload(); +}); + +add_task(async function test_protocolHandler_restricted_protocol() { + let extensionData = { + manifest: { + protocol_handlers: [ + { + protocol: "http", + name: "take over the http protocol", + uriTemplate: "http.html?val=%s", + }, + ], + }, + }; + + consoleMonitor.start([ + { message: /processing protocol_handlers\.0\.protocol/ }, + ]); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await Assert.rejects( + extension.startup(), + /startup failed/, + "unable to register restricted handler protocol" + ); + + await consoleMonitor.finished(); +}); + +add_task(async function test_protocolHandler_restricted_uriTemplate() { + let extensionData = { + manifest: { + protocol_handlers: [ + { + protocol: "ext+foo", + name: "take over the http protocol", + uriTemplate: "ftp://example.com/file.txt", + }, + ], + }, + }; + + consoleMonitor.start([ + { message: /processing protocol_handlers\.0\.uriTemplate/ }, + ]); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await Assert.rejects( + extension.startup(), + /startup failed/, + "unable to register restricted handler uriTemplate" + ); + + await consoleMonitor.finished(); +}); + +add_task(async function test_protocolHandler_duplicate() { + let extensionData = { + manifest: { + protocol_handlers: [ + { + protocol: "ext+foo", + name: "foo protocol", + uriTemplate: "foo.html?val=%s", + }, + { + protocol: "ext+foo", + name: "foo protocol", + uriTemplate: "foo.html?val=%s", + }, + ], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + // Get the count of handlers installed. + let chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("setup", () => { + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo"); + let handlers = protoInfo.possibleApplicationHandlers; + sendAsyncMessage("handlerData", handlers.length); + }); + }); + + let msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup"); + let data = await msg; + is(data, 1, "cannot re-register the same handler config"); + chromeScript.destroy(); + await extension.unload(); +}); + +// Test that a protocol handler will work if ftp is enabled +add_task(async function test_ftp_protocolHandler() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disabling the external protocol permission prompt. We don't need it + // for this test. + ["security.external_protocol_requires_permission", false], + ], + }); + let extensionData = { + manifest: { + protocol_handlers: [ + { + protocol: "ftp", + name: "an ftp protocol handler", + uriTemplate: "ftp.html?val=%s", + }, + ], + }, + + async background() { + let url = "ftp://example.com/file.txt"; + browser.test.onMessage.addListener(async () => { + await browser.tabs.create({ url }); + }); + }, + + files: { + "ftp.js": function() { + browser.test.sendMessage("test-query", location.search); + }, + "ftp.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="ftp.js"><\/script> + </head> + </html>`, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + const handlerUrl = `moz-extension://${extension.uuid}/ftp.html`; + + let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript); + + // Set the preferredAction to this extension as ftp will default to system. If + // we didn't bypass the dialog for this test, the user would get asked in this case. + let msg = chromeScript.promiseOneMessage("set"); + chromeScript.sendAsyncMessage("setPreferredAction", { + protocol: "ftp", + template: handlerUrl, + }); + await msg; + + msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup", { protocol: "ftp", principalOrigins: [] }); + let data = await msg; + ok( + data.preferredAction, + "using a helper application is the preferred action" + ); + ok(data.preferredApplicationHandler, "handler was set as default handler"); + is(data.handlers, 1, "one handler is set"); + ok(!data.alwaysAskBeforeHandling, "will not show dialog"); + ok(data.isWebHandler, "the handler is a web handler"); + is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template"); + + chromeScript.destroy(); + + extension.sendMessage("run"); + let query = await extension.awaitMessage("test-query"); + is(query, "?val=ftp%3A%2F%2Fexample.com%2Ffile.txt", "test query ok"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html b/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html new file mode 100644 index 0000000000..18ff14a6de --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script> +"use strict"; + +function getExtension() { + return ExtensionTestUtils.loadExtension({ + manifest: { + "browser_specific_settings": { + "gecko": { + "id": "redirect-to-jar@mochi.test", + }, + }, + "permissions": [ + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + "web_accessible_resources": [ + "finished.html", + ], + }, + useAddonManager: "temporary", + files: { + "finished.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>redirected!</h1> + </body> + </html> + `, + }, + background: async () => { + let redirectUrl = browser.runtime.getURL("finished.html"); + browser.webRequest.onBeforeRequest.addListener(details => { + return {redirectUrl}; + }, {urls: ["*://*/intercept*"]}, ["blocking"]); + + let code = `new Promise(resolve => { + var s = document.createElement('iframe'); + s.src = "/intercept?r=" + Math.random(); + s.onload = async () => { + let url = await window.wrappedJSObject.SpecialPowers.spawn(s, [], () => content.location.href ); + resolve(['loaded', url]); + } + s.onerror = () => resolve(['error']); + document.documentElement.appendChild(s); + });`; + + async function testSubFrameResource(tabId, code) { + let [result] = await browser.tabs.executeScript(tabId, { code }); + return result; + } + + let tab = await browser.tabs.create({url: "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_sample.html"}); + let result = await testSubFrameResource(tab.id, code); + browser.test.assertEq("loaded", result[0], "frame 1 loaded"); + browser.test.assertEq(redirectUrl, result[1], "frame 1 redirected"); + // If jar caching breaks redirects, this next test will fail (See Bug 1390346). + result = await testSubFrameResource(tab.id, code); + browser.test.assertEq("loaded", result[0], "frame 2 loaded"); + browser.test.assertEq(redirectUrl, result[1], "frame 2 redirected"); + await browser.tabs.remove(tab.id); + browser.test.sendMessage("requestsCompleted"); + }, + }); +} + +add_task(async function test_redirect_to_jar() { + let extension = getExtension(); + await extension.startup(); + await extension.awaitMessage("requestsCompleted"); + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html b/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html new file mode 100644 index 0000000000..a5c7024a83 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebRequest urlClassification</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.trackingprotection.enabled", true]], + }); + + let chromeScript = SpecialPowers.loadChromeScript(async _ => { + /* eslint-env mozilla/chrome-script */ + const {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm"); + await UrlClassifierTestUtils.addTestTrackers(); + sendAsyncMessage("trackersLoaded"); + }); + await chromeScript.promiseOneMessage("trackersLoaded"); + chromeScript.destroy(); +}); + +add_task(async function test_urlClassification() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "proxy", "<all_urls>"], + }, + background() { + let expected = { + "http://tracking.example.org/": {first: "tracking", thirdParty: false, }, + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=tracking.example.org": { thirdParty: false, }, + "http://tracking.example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png": {third: "tracking", thirdParty: true, }, + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=example.net": { thirdParty: false, }, + "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png": { thirdParty: true, }, + }; + function testRequest(details) { + let expect = expected[details.url]; + if (expect) { + if (expect.first) { + browser.test.assertTrue(details.urlClassification.firstParty.includes("tracking"), "tracking firstParty"); + } else { + browser.test.assertEq(details.urlClassification.firstParty.length, 0, "not tracking firstParty"); + } + if (expect.third) { + browser.test.assertTrue(details.urlClassification.thirdParty.includes("tracking"), "tracking thirdParty"); + } else { + browser.test.assertEq(details.urlClassification.thirdParty.length, 0, "not tracking thirdParty"); + } + + browser.test.assertEq(details.thirdParty, expect.thirdParty, "3rd party flag matches"); + return true; + } + return false; + } + + browser.proxy.onRequest.addListener(details => { + browser.test.log(`proxy.onRequest ${JSON.stringify(details)}`); + testRequest(details); + }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]}); + browser.webRequest.onBeforeRequest.addListener(async (details) => { + browser.test.log(`webRequest.onBeforeRequest ${JSON.stringify(details)}`); + testRequest(details); + }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]}, ["blocking"]); + browser.webRequest.onCompleted.addListener(async (details) => { + browser.test.log(`webRequest.onCompleted ${JSON.stringify(details)}`); + if (testRequest(details)) { + browser.test.sendMessage("classification", details.url); + } + }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]}); + }, + }); + await extension.startup(); + + // Test first party tracking classification. + let url = "http://tracking.example.org/"; + let win = window.open(url); + is(await extension.awaitMessage("classification"), url, "request completed"); + win.close(); + + // Test third party tracking classification, expecting two results. + url = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=tracking.example.org"; + win = window.open(url); + is(await extension.awaitMessage("classification"), url); + is(await extension.awaitMessage("classification"), + "http://tracking.example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", + "request completed"); + win.close(); + + // Test third party tracking classification, expecting two results. + url = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=example.net"; + win = window.open(url); + is(await extension.awaitMessage("classification"), url); + is(await extension.awaitMessage("classification"), + "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", + "request completed"); + win.close(); + + await extension.unload(); +}); + +add_task(async function teardown() { + let chromeScript = SpecialPowers.loadChromeScript(async _ => { + /* eslint-env mozilla/chrome-script */ + // Cleanup cache + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + + const {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm"); + await UrlClassifierTestUtils.cleanupTestTrackers(); + sendAsyncMessage("trackersUnloaded"); + }); + await chromeScript.promiseOneMessage("trackersUnloaded"); + chromeScript.destroy(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html new file mode 100644 index 0000000000..85f98d5034 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + browser.runtime.onConnect.addListener(port => { + browser.test.assertEq(port.name, "ernie", "port name correct"); + browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "URL correct"); + browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "tab URL correct"); + browser.test.assertEq(port.sender.frameId, 0, "frameId of top frame"); + + let expected = "message 1"; + port.onMessage.addListener(msg => { + browser.test.assertEq(msg, expected, "message is expected"); + if (expected == "message 1") { + port.postMessage("message 2"); + expected = "message 3"; + } else if (expected == "message 3") { + expected = "disconnect"; + browser.test.notifyPass("runtime.connect"); + } + }); + port.onDisconnect.addListener(() => { + browser.test.assertEq(null, port.error, "No error because port is closed by disconnect() at other end"); + browser.test.assertEq(expected, "disconnect", "got disconnection at right time"); + }); + }); +} + +function contentScript() { + let port = browser.runtime.connect({name: "ernie"}); + port.postMessage("message 1"); + port.onMessage.addListener(msg => { + if (msg == "message 2") { + port.postMessage("message 3"); + port.disconnect(); + } + }); +} + +let extensionData = { + background, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(async function test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + + await Promise.all([waitForLoad(win), extension.awaitFinish("runtime.connect")]); + + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html new file mode 100644 index 0000000000..13b9029c48 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function backgroundScript(token) { + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq(msg, "done"); + browser.test.notifyPass("sendmessage_reply"); + }); + + browser.runtime.onConnect.addListener(port => { + browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "sender url correct"); + browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + let tabId = port.sender.tab.id; + browser.tabs.connect(tabId, {name: token}); + + browser.test.assertEq(port.name, token, "token matches"); + port.postMessage(token + "-done"); + }); + + browser.test.sendMessage("background-ready"); +} + +function contentScript(token) { + let gotTabMessage = false; + let badTabMessage = false; + browser.runtime.onConnect.addListener(port => { + if (port.name == token) { + gotTabMessage = true; + } else { + badTabMessage = true; + } + port.disconnect(); + }); + + let port = browser.runtime.connect(null, {name: token}); + port.onMessage.addListener(function(msg) { + if (msg != token + "-done" || !gotTabMessage || badTabMessage) { + return; // test failed + } + + // FIXME: Removing this line causes the test to fail: + // resource://gre/modules/ExtensionUtils.jsm, line 651: NS_ERROR_NOT_INITIALIZED + port.disconnect(); + browser.runtime.sendMessage("done"); + }); +} + +function makeExtension() { + let token = Math.random(); + let extensionData = { + background: `(${backgroundScript})("${token}")`, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + }, + + files: { + "content_script.js": `(${contentScript})("${token}")`, + }, + }; + return extensionData; +} + +add_task(async function test_contentscript() { + let extension1 = ExtensionTestUtils.loadExtension(makeExtension()); + let extension2 = ExtensionTestUtils.loadExtension(makeExtension()); + await Promise.all([extension1.startup(), extension2.startup()]); + + await extension1.awaitMessage("background-ready"); + await extension2.awaitMessage("background-ready"); + + let win = window.open("file_sample.html"); + + await Promise.all([waitForLoad(win), + extension1.awaitFinish("sendmessage_reply"), + extension2.awaitFinish("sendmessage_reply")]); + + win.close(); + + await extension1.unload(); + await extension2.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html new file mode 100644 index 0000000000..9c64635063 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html @@ -0,0 +1,136 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// The purpose of this test is to verify that the port.sender properties are +// not set for messages from iframes in background scripts. This is the toolkit +// version of the browser_ext_contentscript_nontab_connect.js test, and exists +// to provide test coverage for non-toolkit builds (e.g. Android). +// +// This used to be a xpcshell test (from bug 1488105), but became a mochitest +// because port.sender.tab and port.sender.frameId do not represent the real +// values in xpcshell tests. +// Specifically, ProxyMessenger.prototype.getSender uses the tabTracker, which +// expects real tabs instead of browsers from the ContentPage API in xpcshell +// tests. +add_task(async function connect_from_background_frame() { + if (!SpecialPowers.getBoolPref("extensions.webextensions.remote", true)) { + info("Cannot load remote content in parent process; skipping test task"); + return; + } + async function background() { + const FRAME_URL = "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + browser.runtime.onConnect.addListener(port => { + // The next two assertions are the reason for this being a mochitest + // instead of a xpcshell test. + browser.test.assertEq(port.sender.tab, undefined, "Sender is not a tab"); + browser.test.assertEq(port.sender.frameId, undefined, "frameId unset"); + browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL"); + port.onMessage.addListener(msg => { + browser.test.assertEq("pong", msg, "Reply from content script"); + port.disconnect(); + }); + port.postMessage("ping"); + }); + + await browser.contentScripts.register({ + matches: [FRAME_URL], + js: [{ file: "contentscript.js" }], + allFrames: true, + }); + + let f = document.createElement("iframe"); + f.src = FRAME_URL; + document.body.appendChild(f); + } + + function contentScript() { + browser.test.log(`Running content script at ${document.URL}`); + + let port = browser.runtime.connect(); + port.onMessage.addListener(msg => { + browser.test.assertEq("ping", msg, "Expected message to content script"); + port.postMessage("pong"); + }); + port.onDisconnect.addListener(() => { + browser.test.sendMessage("disconnected_in_content_script"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["https://example.com/*"], + }, + files: { + "contentscript.js": contentScript, + }, + background, + }); + await extension.startup(); + await extension.awaitMessage("disconnected_in_content_script"); + await extension.unload(); +}); + +// The test_ext_contentscript_fission_frame.html test already checks the +// behavior of onConnect in cross-origin frames, so here we just limit the test +// to checking that the port.sender properties are sensible. +add_task(async function connect_from_content_script_in_frame() { + async function background() { + const TAB_URL = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html"; + const FRAME_URL = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_img.html"; + let createdTab; + browser.runtime.onConnect.addListener(port => { + // The next two assertions are the reason for this being a mochitest + // instead of a xpcshell test. + browser.test.assertEq(port.sender.tab.url, TAB_URL, "Sender is the tab"); + browser.test.assertTrue(port.sender.frameId > 0, "frameId is set"); + browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL"); + + browser.test.assertEq(createdTab.id, port.sender.tab.id, "Tab to close"); + browser.tabs.remove(port.sender.tab.id).then(() => { + browser.test.sendMessage("tab_port_checked_and_tab_closed"); + }); + }); + + await browser.contentScripts.register({ + matches: [FRAME_URL], + js: [{ file: "contentscript.js" }], + allFrames: true, + }); + + createdTab = await browser.tabs.create({ url: TAB_URL }); + } + + function contentScript() { + browser.test.log(`Running content script at ${document.URL}`); + + browser.runtime.connect(); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["https://example.org/*"], + }, + files: { + "contentscript.js": contentScript, + }, + background, + }); + await extension.startup(); + await extension.awaitMessage("tab_port_checked_and_tab_closed"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html new file mode 100644 index 0000000000..b671cba23d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +add_task(async function test_connect_bidirectionally_and_postMessage() { + function background() { + let onConnectCount = 0; + browser.runtime.onConnect.addListener(port => { + // 3. onConnect by connect() from CS. + browser.test.assertEq("from-cs", port.name); + browser.test.assertEq(1, ++onConnectCount, + "BG onConnect should be called once"); + + let tabId = port.sender.tab.id; + browser.test.assertTrue(tabId, "content script must have a tab ID"); + + let port2; + let postMessageCount1 = 0; + port.onMessage.addListener(msg => { + // 11. port.onMessage by port.postMessage in CS. + browser.test.assertEq("from CS to port", msg); + browser.test.assertEq(1, ++postMessageCount1, + "BG port.onMessage should be called once"); + + // 12. should trigger port2.onMessage in CS. + port2.postMessage("from BG to port2"); + }); + + // 4. Should trigger onConnect in CS. + port2 = browser.tabs.connect(tabId, {name: "from-bg"}); + let postMessageCount2 = 0; + port2.onMessage.addListener(msg => { + // 7. onMessage by port2.postMessage in CS. + browser.test.assertEq("from CS to port2", msg); + browser.test.assertEq(1, ++postMessageCount2, + "BG port2.onMessage should be called once"); + + // 8. Should trigger port.onMessage in CS. + port.postMessage("from BG to port"); + }); + }); + + // 1. Notify test runner to create a new tab. + browser.test.sendMessage("ready"); + } + + function contentScript() { + let onConnectCount = 0; + let port; + browser.runtime.onConnect.addListener(port2 => { + // 5. onConnect by connect() from BG. + browser.test.assertEq("from-bg", port2.name); + browser.test.assertEq(1, ++onConnectCount, + "CS onConnect should be called once"); + + let postMessageCount2 = 0; + port2.onMessage.addListener(msg => { + // 12. port2.onMessage by port2.postMessage in BG. + browser.test.assertEq("from BG to port2", msg); + browser.test.assertEq(1, ++postMessageCount2, + "CS port2.onMessage should be called once"); + + // TODO(robwu): Do not explicitly disconnect, it should not be a problem + // if we keep the ports open. However, not closing the ports causes the + // test to fail with NS_ERROR_NOT_INITIALIZED in ExtensionUtils.jsm, in + // Port.prototype.disconnect (nsIMessageSender.sendAsyncMessage). + port.disconnect(); + port2.disconnect(); + browser.test.notifyPass("ping pong done"); + }); + // 6. should trigger port2.onMessage in BG. + port2.postMessage("from CS to port2"); + }); + + // 2. should trigger onConnect in BG. + port = browser.runtime.connect({name: "from-cs"}); + let postMessageCount1 = 0; + port.onMessage.addListener(msg => { + // 9. onMessage by port.postMessage in BG. + browser.test.assertEq("from BG to port", msg); + browser.test.assertEq(1, ++postMessageCount1, + "CS port.onMessage should be called once"); + + // 10. should trigger port.onMessage in BG. + port.postMessage("from CS to port"); + }); + } + + let extensionData = { + background, + manifest: { + content_scripts: [{ + js: ["contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + }, + files: { + "contentscript.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + info("extension loaded"); + + await extension.awaitMessage("ready"); + + let win = window.open("file_sample.html"); + await extension.awaitFinish("ping pong done"); + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> +</body> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html new file mode 100644 index 0000000000..f18190bf8b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + browser.runtime.onConnect.addListener(port => { + browser.test.assertEq(port.name, "ernie", "port name correct"); + port.onDisconnect.addListener(() => { + browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads"); + // Closing an already-disconnected port is a no-op. + port.disconnect(); + port.disconnect(); + browser.test.sendMessage("disconnected"); + }); + browser.test.sendMessage("connected"); + }); +} + +function contentScript() { + browser.runtime.connect({name: "ernie"}); +} + +let extensionData = { + background, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(async function test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + await Promise.all([waitForLoad(win), extension.awaitMessage("connected")]); + win.close(); + await extension.awaitMessage("disconnected"); + + info("win.close() succeeded"); + + win = window.open("file_sample.html"); + await Promise.all([waitForLoad(win), extension.awaitMessage("connected")]); + + // Add an "unload" listener so that we don't put the window in the + // bfcache. This way it gets destroyed immediately upon navigation. + win.addEventListener("unload", function() {}); // eslint-disable-line mozilla/balanced-listeners + + win.location = "http://example.com"; + await extension.awaitMessage("disconnected"); + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_script_filenames.html b/toolkit/components/extensions/test/mochitest/test_ext_script_filenames.html new file mode 100644 index 0000000000..de0993c33d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_script_filenames.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Script Filenames Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_tabs_executeScript() { + let validFileName = "script.js"; + let invalidFileName = "script.xyz"; + + async function background() { + await browser.tabs.executeScript({ file: "script.js" }); + + await browser.test.assertRejects( + browser.tabs.executeScript({ file: "script.xyz" }), + Error, + "invalid filename does not execute" + ); + browser.test.notifyPass("execute-script"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["<all_urls>"], + }, + + background, + + files: { + [validFileName]: function contentScript1() { + browser.test.sendMessage("content-script-loaded"); + }, + [invalidFileName]: function contentScript2() { + browser.test.fail("this script should not be loaded"); + }, + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "http://mochi.test:8888/", + true + ); + await extension.startup(); + + await extension.awaitMessage("content-script-loaded"); + await extension.awaitFinish("execute-script"); + + await extension.unload(); + await AppTestDelegate.removeTab(window, tab); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html new file mode 100644 index 0000000000..c8457b6d36 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html @@ -0,0 +1,1532 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests scripting.*ContentScripts()</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + +"use strict"; + +const MOCHITEST_HOST_PERMISSIONS = [ + "*://mochi.test/", + "*://mochi.xorigin-test/", + "*://test1.example.com/", +]; + +const makeExtension = ({ manifest: manifestProps, ...otherProps }) => { + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + permissions: ["scripting"], + host_permissions: [ + ...MOCHITEST_HOST_PERMISSIONS, + // Used in `file_contains_iframe.html` + "*://example.org/", + ], + granted_host_permissions: true, + ...manifestProps, + }, + useAddonManager: "temporary", + ...otherProps, + }); +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +add_task(async function test_validate_registerContentScripts_params() { + let extension = makeExtension({ + async background() { + const TEST_CASES = [ + { + title: "no js and no css", + params: [ + { + id: "script", + matches: ["*://mochi.test/*"], + persistAcrossSessions: false, + }, + ], + expectedError: "At least one js or css must be specified.", + }, + { + title: "empty js", + params: [ + { + id: "script", + js: [], + matches: ["*://mochi.test/*"], + persistAcrossSessions: false, + }, + ], + expectedError: "At least one js or css must be specified.", + }, + { + title: "empty css", + params: [ + { + id: "script", + css: [], + matches: ["*://mochi.test/*"], + persistAcrossSessions: false, + }, + ], + expectedError: "At least one js or css must be specified.", + }, + { + title: "no matches", + params: [ + { + id: "script", + js: ["script.js"], + persistAcrossSessions: false, + }, + ], + expectedError: "matches must be specified.", + }, + { + title: "empty matches", + params: [ + { + id: "script", + js: ["script.js"], + matches: [], + persistAcrossSessions: false, + }, + ], + expectedError: "matches must be specified.", + }, + { + title: "one empty match", + params: [ + { + id: "script", + js: ["script.js"], + matches: [""], + persistAcrossSessions: false, + }, + ], + expectedError: "Invalid url pattern: ", + }, + { + title: "invalid match", + params: [ + { + id: "script", + js: ["script.js"], + matches: ["not-a-pattern"], + persistAcrossSessions: false, + }, + ], + expectedError: "Invalid url pattern: not-a-pattern", + }, + { + title: "invalid match and valid match", + params: [ + { + id: "script", + js: ["script.js"], + matches: ["*://mochi.test/*", "not-a-pattern"], + persistAcrossSessions: false, + }, + ], + expectedError: "Invalid url pattern: not-a-pattern", + }, + { + title: "one empty value in excludeMatches", + params: [ + { + id: "script", + js: ["script.js"], + matches: ["*://mochi.test/*"], + excludeMatches: [""], + persistAcrossSessions: false, + }, + ], + expectedError: "Invalid url pattern: ", + }, + { + title: "invalid value in excludeMatches", + params: [ + { + id: "script", + js: ["script.js"], + matches: ["*://mochi.test/*"], + excludeMatches: ["not-a-pattern"], + persistAcrossSessions: false, + }, + ], + expectedError: "Invalid url pattern: not-a-pattern", + }, + { + title: "duplicate IDs", + params: [ + { + id: "script-1", + js: ["script.js"], + matches: ["*://mochi.test/*"], + persistAcrossSessions: false, + }, + { + id: "script-1", + js: ["script.js"], + matches: ["*://mochi.test/*"], + persistAcrossSessions: false, + }, + ], + expectedError: `Script ID "script-1" found more than once in 'scripts' array.`, + }, + { + title: "empty id", + params: [ + { + id: "", + js: ["script.js"], + matches: ["*://mochi.test/*"], + persistAcrossSessions: false, + }, + ], + expectedError: "Invalid content script id.", + }, + { + title: "id starting with _", + params: [ + { + id: "_foo", + js: ["script.js"], + matches: ["*://mochi.test/*"], + persistAcrossSessions: false, + }, + ], + expectedError: "Invalid content script id.", + }, + ]; + + for (const { title, params, expectedError } of TEST_CASES) { + await browser.test.assertRejects( + browser.scripting.registerContentScripts(params), + expectedError, + `${title} - got expected error` + ); + } + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no registered script"); + + browser.test.notifyPass("test-finished"); + }, + files: { + "script.js": "", + }, + }); + + await extension.startup(); + await extension.awaitFinish("test-finished"); + await extension.unload(); +}); + +add_task(async function test_registerContentScripts_with_already_registered_id() { + let extension = makeExtension({ + async background() { + const script = { + id: "script-1", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }; + + await browser.scripting.registerContentScripts([script]); + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(1, scripts.length, "expected 1 registered script"); + + await browser.test.assertRejects( + browser.scripting.registerContentScripts([script]), + `Content script with id "${script.id}" is already registered.`, + "got expected error" + ); + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(1, scripts.length, "expected 1 registered script"); + + browser.test.notifyPass("test-finished"); + }, + files: { + "script.js": "", + }, + }); + + await extension.startup(); + await extension.awaitFinish("test-finished"); + await extension.unload(); +}); + +add_task(async function test_validate_getRegisteredContentScripts_params() { + let extension = makeExtension({ + async background() { + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no registered scripts"); + + scripts = await browser.scripting.getRegisteredContentScripts({ + ids: ["non-existent-id"] + }); + browser.test.assertEq(0, scripts.length, "expected no registered scripts"); + + browser.test.log("test call with undefined filter and a chrome-compatible callback"); + scripts = await new Promise(resolve => { + browser.scripting.getRegisteredContentScripts(undefined, resolve); + }); + browser.test.assertEq(0, scripts.length, "expected no registered scripts"); + + browser.test.log("test call with only the chrome-compatible callback"); + scripts = await new Promise(resolve => { + browser.scripting.getRegisteredContentScripts(resolve); + }); + browser.test.assertEq(0, scripts.length, "expected no registered scripts"); + + browser.test.notifyPass("test-finished"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("test-finished"); + await extension.unload(); +}); + +add_task(async function test_getRegisteredContentScripts() { + let extension = makeExtension({ + async background() { + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no registered scripts"); + + const aScript = { + id: "a-script", + js: ["script.js"], + matches: ["<all_urls>"], + persistAcrossSessions: false, + }; + + await browser.scripting.registerContentScripts([aScript]); + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(1, scripts.length, "expected 1 registered script"); + browser.test.assertEq(aScript.id, scripts[0].id, "expected correct id"); + + // This should return no registered scripts. + scripts = await browser.scripting.getRegisteredContentScripts({ ids: [] }); + browser.test.assertEq(0, scripts.length, "expected 0 registered script"); + + // Verify that invalid IDs are omitted but valid IDs are used to return + // registered scripts. + scripts = await browser.scripting.getRegisteredContentScripts({ + ids: ["non-existent-id", aScript.id] + }); + browser.test.assertEq(1, scripts.length, "expected 1 registered script"); + browser.test.assertEq(aScript.id, scripts[0].id, "expected correct id"); + + browser.test.notifyPass("test-finished"); + }, + files: { + "script.js": "", + }, + }); + + await extension.startup(); + await extension.awaitFinish("test-finished"); + await extension.unload(); +}); + +add_task(async function test_registerContentScripts_js() { + let extension = makeExtension({ + async background() { + const TEST_CASES = [ + // This should have no effect but it should not throw. + { + title: "no script", + params: [], + }, + { + title: "one script", + params: [ + { + id: "script-1", + js: ["script-1.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + } + ], + }, + { + title: "one script in all frames", + params: [ + { + id: "script-2", + js: ["script-2.js"], + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + allFrames: true, + persistAcrossSessions: false, + } + ], + }, + { + title: "one script in all frames with excludeMatches set", + params: [ + { + id: "script-3", + js: ["script-3.js"], + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + allFrames: true, + excludeMatches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + } + ], + }, + { + title: "one script, two js paths", + params: [ + { + id: "script-4", + js: ["script-4-1.js", "script-4-2.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + } + ], + }, + { + title: "empty excludeMatches", + params: [ + { + id: "script-5", + // This path should be normalized. + js: ["/script-5.js"], + matches: ["*://test1.example.com/*"], + excludeMatches: [], + persistAcrossSessions: false, + } + ], + }, + ]; + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no registered script"); + + for (const { title, params } of TEST_CASES) { + const res = await browser.scripting.registerContentScripts(params); + browser.test.assertEq( + undefined, + res, + `${title} - expected no result` + ); + + const script = await browser.scripting.getRegisteredContentScripts({ + ids: params.map(param => param.id) + }); + browser.test.assertEq( + params.length, + script.length, + `${title} - got the expected number of registered scripts` + ); + } + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq( + // A test case declared above does not contain any script to register. + TEST_CASES.length - 1, + scripts.length, + "got the expected number of registered scripts" + ); + browser.test.assertEq( + JSON.stringify([ + { + id: "script-1", + allFrames: false, + matches: ["*://test1.example.com/*"], + runAt: "document_idle", + persistAcrossSessions: false, + js: ["script-1.js"], + }, + { + id: "script-2", + allFrames: true, + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + runAt: "document_idle", + persistAcrossSessions: false, + js: ["script-2.js"], + }, + { + id: "script-3", + allFrames: true, + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + runAt: "document_idle", + persistAcrossSessions: false, + excludeMatches: ["*://test1.example.com/*"], + js: ["script-3.js"], + }, + { + id: "script-4", + allFrames: false, + matches: ["*://test1.example.com/*"], + runAt: "document_idle", + persistAcrossSessions: false, + js: ["script-4-1.js", "script-4-2.js"], + }, + { + id: "script-5", + allFrames: false, + matches: ["*://test1.example.com/*"], + runAt: "document_idle", + persistAcrossSessions: false, + js: ["script-5.js"], + }, + ]), + JSON.stringify(scripts), + "got expected scripts" + ); + + browser.test.sendMessage("background-ready"); + }, + files: { + "script-1.js": () => { + browser.test.sendMessage( + "script-ran", + { file: "script-1.js", value: document.title } + ); + }, + "script-2.js": () => { + browser.test.sendMessage( + "script-ran", + { file: "script-2.js", value: document.title } + ); + }, + "script-3.js": () => { + browser.test.sendMessage( + "script-ran", + { file: "script-3.js", value: document.title } + ); + }, + "script-4-1.js": () => { + // We inject this script (first) as well as the one defined right + // after. The order should be respected, which is why we define a + // property here and check it in the second script. + window.SCRIPT_4_INJECTED = "SCRIPT_4_INJECTED"; + }, + "script-4-2.js": () => { + browser.test.sendMessage( + "script-ran", + { file: "script-4-2.js", value: window.SCRIPT_4_INJECTED } + ); + delete window.SCRIPT_4_INJECTED; + }, + "script-5.js": () => { + browser.test.sendMessage( + "script-ran", + { file: "script-5.js", value: document.title } + ); + }, + }, + }); + + let scriptsRan = 0; + let results = []; + let completePromise = new Promise(resolve => { + extension.onMessage("script-ran", result => { + results.push(result); + scriptsRan++; + + // The value below should be updated when TEST_CASES above is changed. + if (scriptsRan === 6) { + resolve(); + } + }); + }); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + // Load a page that will trigger the content scripts previously registered. + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + // Wait for all content scripts to be executed. + await completePromise; + + // Verify that the scripts have been executed correctly. We sort the results + // to compare them against expected values. + results.sort((a, b) => { + return a.file.localeCompare(b.file) || a.value.localeCompare(b.value); + }); + ok( + JSON.stringify([ + { file: "script-1.js", value: "file contains iframe" }, + // script-2.js should be injected in two frames + { file: "script-2.js", value: "file contains iframe" }, + { file: "script-2.js", value: "file contains img" }, + { file: "script-3.js", value: "file contains img" }, + // script-4-1.js will add a prop to the `window` object, which should be + // read by `script-4-2.js`. + { file: "script-4-2.js", value: "SCRIPT_4_INJECTED" }, + { file: "script-5.js", value: "file contains iframe" }, + ]) === JSON.stringify(results), + "got expected script results" + JSON.stringify(results) + ); + + await AppTestDelegate.removeTab(window, tab); + await extension.unload(); +}); + +add_task(async function test_registerContentScripts_are_not_unregistered() { + let extension = makeExtension({ + files: { + "background.html": `<!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body> + <script src="background.js"><\/script> + </body> + </html> + `, + "background.js": async () => { + await browser.scripting.registerContentScripts([ + { + id: "a-script", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }, + ]); + + browser.test.sendMessage("background-executed"); + }, + "script.js": () => { + browser.test.sendMessage("script-executed"); + }, + }, + }); + + await extension.startup(); + + // Load the background page that registers a content script. + let tab = await AppTestDelegate.openNewForegroundTab( + window, + `moz-extension://${extension.uuid}/background.html`, + true + ); + await extension.awaitMessage("background-executed"); + await AppTestDelegate.removeTab(window, tab); + + // Load a page that will trigger the content scripts previously registered. + tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + await extension.awaitMessage("script-executed"); + + await AppTestDelegate.removeTab(window, tab); + await extension.unload(); +}); + +add_task(async function test_scripts_dont_run_after_shutdown() { + let extension = makeExtension({ + async background() { + await browser.scripting.registerContentScripts([ + { + id: "script-that-should-not-run", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }, + ]); + + browser.test.sendMessage("background-ready"); + }, + files: { + "script.js": () => { + browser.test.fail("this script should not be executed."); + }, + }, + }); + // We use a second extension to wait enough time to confirm that the script + // registered in the previous extension has not been executed at all, in case + // the tab closes before the scheduled content script has had a chance to + // run. + let anotherExtension = makeExtension({ + async background() { + await browser.scripting.registerContentScripts([ + { + id: "this-script-should-run", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }, + ]); + + browser.test.sendMessage("background-ready"); + }, + files: { + "script.js": () => { + browser.test.sendMessage("script-ran"); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + await anotherExtension.startup(); + await anotherExtension.awaitMessage("background-ready"); + + await extension.unload(); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + await anotherExtension.awaitMessage("script-ran"); + await AppTestDelegate.removeTab(window, tab); + + await anotherExtension.unload(); +}); + +add_task(async function test_registerContentScripts_with_wrong_matches() { + let extension = makeExtension({ + async background() { + // Register a content script that should not be injected in this test + // case because the `matches` values don't match the host permissions. + await browser.scripting.registerContentScripts([ + { + id: "script-that-should-not-run", + js: ["script.js"], + matches: ["*://mozilla.org/*"], + persistAcrossSessions: false, + }, + ]); + + browser.test.sendMessage("background-ready"); + }, + files: { + "script.js": () => { + browser.test.fail("this script should not be executed."); + }, + }, + }); + // We use a second extension to wait enough time to confirm that the script + // registered in the previous extension has not been executed at all, in case + // the tab closes before the scheduled content script has had a chance to + // run. + let anotherExtension = makeExtension({ + async background() { + await browser.scripting.registerContentScripts([ + { + id: "this-script-should-run", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }, + ]); + + browser.test.sendMessage("background-ready"); + }, + files: { + "script.js": () => { + browser.test.sendMessage("script-ran"); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + await anotherExtension.startup(); + await anotherExtension.awaitMessage("background-ready"); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + await anotherExtension.awaitMessage("script-ran"); + + await extension.unload(); + await anotherExtension.unload(); + + // We remove the tab after having unloaded the extensions to avoid failures + // on Windows, see: Bug 1761550. + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_registerContentScripts_twice_with_same_id() { + let extension = makeExtension({ + async background() { + const script = { + id: "script-that-should-not-run", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }; + + const results = await Promise.allSettled([ + browser.scripting.registerContentScripts([script]), + browser.scripting.registerContentScripts([script]), + ]); + + browser.test.assertEq(2, results.length, "got expected length"); + browser.test.assertEq( + "fulfilled", + results[0].status, + "expected fulfilled promise" + ); + browser.test.assertEq( + "rejected", + results[1].status, + "expected rejected promise" + ); + browser.test.assertEq( + `Content script with id "script-that-should-not-run" is already registered.`, + results[1].reason.message, + "expected reason" + ); + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(1, scripts.length, "expected 1 registered script"); + + browser.test.sendMessage("background-done"); + }, + files: { + "script.js": "", + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-done"); + await extension.unload(); +}); + +add_task(async function test_getRegisteredContentScripts_during_a_registration() { + let extension = makeExtension({ + async background() { + browser.scripting.registerContentScripts([ + { + id: "a-script", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }, + ]); + + const scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq( + JSON.stringify([ + { + id: "a-script", + allFrames: false, + matches: ["*://test1.example.com/*"], + runAt: "document_idle", + persistAcrossSessions: false, + js: ["script.js"], + }, + ]), + JSON.stringify(scripts), + "expected 1 registered script" + ); + + browser.test.sendMessage("background-done"); + }, + files: { + "script.js": "", + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-done"); + await extension.unload(); +}); + +add_task(async function test_validate_unregisterContentScripts_params() { + let extension = makeExtension({ + async background() { + const TEST_CASES = [ + { + title: "unknown id", + params: { + ids: ["non-existent-id"], + }, + expectedError: `Content script with id "non-existent-id" does not exist.` + }, + { + title: "invalid id", + params: { + ids: ["_invalid-id"], + }, + expectedError: "Invalid content script id.", + }, + ]; + + for (const { title, params, expectedError } of TEST_CASES) { + await browser.test.assertRejects( + browser.scripting.unregisterContentScripts(params), + expectedError, + `${title} - got expected error` + ); + } + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no script"); + + browser.test.sendMessage("background-done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-done"); + await extension.unload(); +}); + +add_task(async function test_unregisterContentScripts_with_chrome_compatible_callback() { + let extension = makeExtension({ + async background() { + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no script"); + + // Register a script that we can unregister after. + await browser.scripting.registerContentScripts([ + { + id: "script-1", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }, + ]); + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(1, scripts.length, "expected 1 registered script"); + + browser.test.log("test call with undefined filter and a chrome-compatible callback"); + await new Promise(resolve => { + browser.scripting.unregisterContentScripts(undefined, resolve); + }); + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no registered scripts"); + + // Re-register a script that we can unregister after. + await browser.scripting.registerContentScripts([ + { + id: "script-1", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }, + ]); + + browser.test.log("test call with only the chrome-compatible callback"); + await new Promise(resolve => { + browser.scripting.unregisterContentScripts(resolve); + }); + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no registered scripts"); + + browser.test.sendMessage("background-done"); + }, + files: { + "script.js": "", + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-done"); + await extension.unload(); +}); + +add_task(async function test_unregisterContentScripts() { + let extension = makeExtension({ + async background() { + const script1 = { + id: "script-1", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }; + const script2 = { + id: "script-2", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + } + const script3 = { + id: "script-3", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + } + + let res = await browser.scripting.registerContentScripts([ + script1, + script2, + script3, + ]); + browser.test.assertEq(undefined, res, "expected no result"); + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(3, scripts.length, "expected 3 scripts"); + browser.test.assertEq(script1.id, scripts[0].id, "expected correct id"); + browser.test.assertEq(script2.id, scripts[1].id, "expected correct id"); + browser.test.assertEq(script3.id, scripts[2].id, "expected correct id"); + + // No unregistration when unknown IDs are passed along with valid IDs. + await browser.test.assertRejects( + browser.scripting.unregisterContentScripts({ + ids: [script2.id, "non-existent-id"], + }), + `Content script with id "non-existent-id" does not exist.` + ); + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(3, scripts.length, "expected 3 scripts"); + + // Unregister 1 script. + res = await browser.scripting.unregisterContentScripts({ + ids: [script2.id] + }); + browser.test.assertEq(undefined, res, "expected no result"); + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(2, scripts.length, "expected 2 scripts"); + browser.test.assertEq(script1.id, scripts[0].id, "expected correct id"); + browser.test.assertEq(script3.id, scripts[1].id, "expected correct id"); + + // This should unregister all the remaining registered scripts. + res = await browser.scripting.unregisterContentScripts(); + browser.test.assertEq(undefined, res, "expected no result"); + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected no script"); + + browser.test.sendMessage("background-done"); + }, + files: { + "script.js": "", + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-done"); + await extension.unload(); +}); + +add_task(async function test_unregisterContentScripts_twice_with_same_id() { + let extension = makeExtension({ + async background() { + const script = { + id: "script-to-unregister", + js: ["script.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }; + + await browser.scripting.registerContentScripts([script]); + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(1, scripts.length, "expected 1 registered script"); + + const results = await Promise.allSettled([ + browser.scripting.unregisterContentScripts({ ids: [script.id] }), + browser.scripting.unregisterContentScripts({ ids: [script.id] }), + ]); + + browser.test.assertEq(2, results.length, "got expected length"); + browser.test.assertEq( + "fulfilled", + results[0].status, + "expected fulfilled promise" + ); + browser.test.assertEq( + "rejected", + results[1].status, + "expected rejected promise" + ); + browser.test.assertEq( + `Content script with id "script-to-unregister" does not exist.`, + results[1].reason.message, + "expected reason" + ); + + scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(0, scripts.length, "expected 0 registered script"); + + browser.test.sendMessage("background-done"); + }, + files: { + "script.js": "", + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-done"); + await extension.unload(); +}); + +add_task(async function test_validate_updateContentScripts_params() { + let extension = makeExtension({ + async background() { + const script = { + id: "registered-script", + js: ["script-1.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }; + + const TEST_CASES = [ + { + title: "invalid script ID", + params: [ + { + id: "_invalid-id", + }, + ], + expectedError: 'Invalid content script id.', + }, + { + title: "empty script ID", + params: [ + { + id: "", + }, + ], + expectedError: 'Invalid content script id.', + }, + { + title: "unknown script ID", + params: [ + { + id: "unknown-id", + }, + ], + expectedError: 'Content script with id "unknown-id" does not exist.', + }, + { + title: "duplicate valid script IDs", + params: [ + { + id: script.id, + }, + { + id: script.id, + }, + ], + expectedError: `Script ID "${script.id}" found more than once in 'scripts' array.`, + }, + { + title: "empty matches", + params: [ + { + id: script.id, + matches: [], + }, + ], + expectedError: "matches must be specified.", + }, + { + title: "one empty match", + params: [ + { + id: script.id, + matches: [""], + }, + ], + expectedError: "Invalid url pattern: ", + }, + { + title: "invalid match", + params: [ + { + id: script.id, + matches: ["not-a-pattern"], + }, + ], + expectedError: "Invalid url pattern: not-a-pattern", + }, + { + title: "invalid match and valid match", + params: [ + { + id: script.id, + matches: ["*://mochi.test/*", "not-a-pattern"], + }, + ], + expectedError: "Invalid url pattern: not-a-pattern", + }, + { + title: "one empty value in excludeMatches", + params: [ + { + id: script.id, + excludeMatches: [""], + }, + ], + expectedError: "Invalid url pattern: ", + }, + { + title: "invalid value in excludeMatches", + params: [ + { + id: script.id, + excludeMatches: ["not-a-pattern"], + }, + ], + expectedError: "Invalid url pattern: not-a-pattern", + }, + { + title: "empty js", + params: [ + { + id: script.id, + js: [], + }, + ], + expectedError: "At least one js or css must be specified.", + }, + { + title: "empty js and css", + params: [ + { + id: script.id, + js: [], + css: [], + }, + ], + expectedError: "At least one js or css must be specified.", + }, + ]; + + // Register a valid script so that we can verify update params beyond + // script IDs. + await browser.scripting.registerContentScripts([script]); + + for (const { title, params, expectedError } of TEST_CASES) { + await browser.test.assertRejects( + browser.scripting.updateContentScripts(params), + expectedError, + `${title} - got expected error` + ); + } + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq(1, scripts.length, "expected 1 registered script"); + browser.test.assertEq( + JSON.stringify([ + { + id: script.id, + allFrames: false, + matches: script.matches, + runAt: "document_idle", + persistAcrossSessions: false, + js: script.js, + }, + ]), + JSON.stringify(scripts), + "expected script to not have been modified" + ); + + browser.test.sendMessage("background-done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-done"); + await extension.unload(); +}); + +add_task(async function test_updateContentScripts() { + let extension = makeExtension({ + async background() { + const SCRIPT_ID = "script-to-update"; + + await browser.scripting.registerContentScripts([ + { + id: SCRIPT_ID, + js: ["script-1.js"], + matches: ["*://test1.example.com/*"], + persistAcrossSessions: false, + }, + ]); + + browser.test.onMessage.addListener(async (msg, params) => { + switch (msg) { + case "updateContentScripts": { + const { + title, + updateContentScriptsParams, + expectedRegisteredContentScript + } = params; + + let result = await browser.scripting.updateContentScripts([ + updateContentScriptsParams, + ]); + browser.test.assertEq( + undefined, + result, + `${title} - expected no return value` + ); + + let scripts = await browser.scripting.getRegisteredContentScripts(); + browser.test.assertEq( + JSON.stringify([expectedRegisteredContentScript]), + JSON.stringify(scripts), + `${title} - expected registered script` + ); + + browser.test.sendMessage(`${msg}-done`); + break; + } + + default: + browser.test.fail(`invalid message received: ${msg}`); + } + }); + + browser.test.sendMessage("background-ready"); + }, + files: { + "script-1.js": () => { + browser.test.sendMessage( + `script-1 executed in ${location.pathname.split("/").pop()}` + ); + }, + "script-2.js": () => { + browser.test.sendMessage( + `script-2 executed in ${location.pathname.split("/").pop()}` + ); + }, + "script-3.js": () => { + browser.test.sendMessage( + `script-3 executed in ${location.pathname.split("/").pop()}` + ); + }, + "script-4.js": () => { + browser.test.sendMessage( + `script-4 executed in ${location.pathname.split("/").pop()}` + ); + }, + "style.css": "body { background-color: rgb(0, 255, 0); }", + "script-check-style.js": () => { + browser.test.assertEq( + "rgb(0, 255, 0)", + getComputedStyle(document.querySelector('body')).backgroundColor, + "expected background color" + ); + browser.test.sendMessage( + `script-check-style executed in ${location.pathname.split("/").pop()}` + ); + }, + }, + }); + + const SCRIPT_ID = "script-to-update"; + const TEST_PAGE = "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html"; + + const runTestCase = async ({ + title, + updateContentScriptsParams, + expectedRegisteredContentScript, + expectedMessages + }) => { + // Register content script and verify results. + extension.sendMessage("updateContentScripts", { + title, + updateContentScriptsParams, + expectedRegisteredContentScript, + }); + await extension.awaitMessage("updateContentScripts-done"); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + TEST_PAGE, + true + ); + + await Promise.all(expectedMessages.map(msg => extension.awaitMessage(msg))); + + await AppTestDelegate.removeTab(window, tab); + }; + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + // Load a page that will trigger the content script initially registered. + let tab = await AppTestDelegate.openNewForegroundTab(window, TEST_PAGE, true); + await extension.awaitMessage("script-1 executed in file_contains_iframe.html"); + await AppTestDelegate.removeTab(window, tab); + + // Now, let's update this content script a few times. + await runTestCase({ + title: "update ID only", + updateContentScriptsParams: { + id: SCRIPT_ID, + }, + expectedRegisteredContentScript: { + id: SCRIPT_ID, + allFrames: false, + matches: ["*://test1.example.com/*"], + runAt: "document_idle", + persistAcrossSessions: false, + js: ["script-1.js"], + }, + expectedMessages: ["script-1 executed in file_contains_iframe.html"], + }); + + await runTestCase({ + title: "update js", + updateContentScriptsParams: { + id: SCRIPT_ID, + js: ["script-2.js"], + }, + expectedRegisteredContentScript: { + id: SCRIPT_ID, + allFrames: false, + matches: ["*://test1.example.com/*"], + runAt: "document_idle", + persistAcrossSessions: false, + js: ["script-2.js"], + }, + expectedMessages: ["script-2 executed in file_contains_iframe.html"], + }); + + await runTestCase({ + title: "update allFrames and matches", + updateContentScriptsParams: { + id: SCRIPT_ID, + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + allFrames: true, + }, + expectedRegisteredContentScript: { + id: SCRIPT_ID, + allFrames: true, + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + runAt: "document_idle", + persistAcrossSessions: false, + js: ["script-2.js"], + }, + expectedMessages: [ + "script-2 executed in file_contains_iframe.html", + "script-2 executed in file_contains_img.html", + ], + }); + + await runTestCase({ + title: "update excludeMatches and js", + updateContentScriptsParams: { + id: SCRIPT_ID, + js: ["script-3.js"], + excludeMatches: ["*://test1.example.com/*"], + allFrames: true, + }, + expectedRegisteredContentScript: { + id: SCRIPT_ID, + allFrames: true, + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + runAt: "document_idle", + persistAcrossSessions: false, + excludeMatches: ["*://test1.example.com/*"], + js: ["script-3.js"], + }, + expectedMessages: [ + "script-3 executed in file_contains_img.html", + ], + }); + + await runTestCase({ + title: "update allFrames, excludeMatches, js and runAt", + updateContentScriptsParams: { + id: SCRIPT_ID, + allFrames: false, + excludeMatches: [], + js: ["script-4.js"], + runAt: "document_start", + }, + expectedRegisteredContentScript: { + id: SCRIPT_ID, + allFrames: false, + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + runAt: "document_start", + persistAcrossSessions: false, + js: ["script-4.js"], + }, + expectedMessages: [ + "script-4 executed in file_contains_iframe.html", + ], + }); + + await runTestCase({ + title: "update allFrames, css, js and runAt", + updateContentScriptsParams: { + id: SCRIPT_ID, + allFrames: true, + css: ["style.css"], + js: ["script-check-style.js"], + runAt: "document_idle", + }, + expectedRegisteredContentScript: { + id: SCRIPT_ID, + allFrames: true, + matches: [ + "*://test1.example.com/*", + "*://example.org/*", + ], + runAt: "document_idle", + persistAcrossSessions: false, + css: ["style.css"], + js: ["script-check-style.js"], + }, + expectedMessages: [ + "script-check-style executed in file_contains_iframe.html", + "script-check-style executed in file_contains_img.html", + ], + }); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript.html new file mode 100644 index 0000000000..a2d741606f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript.html @@ -0,0 +1,1479 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests scripting.executeScript()</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<iframe src="https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"></iframe> + +<script type="text/javascript"> + +"use strict"; + +const MOCHITEST_HOST_PERMISSIONS = [ + "*://mochi.test/", + "*://mochi.xorigin-test/", + "*://test1.example.com/", +]; + +const makeExtension = ({ manifest: manifestProps, ...otherProps }) => { + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + permissions: ["scripting"], + host_permissions: [ + ...MOCHITEST_HOST_PERMISSIONS, + "https://example.com/", + // Used in `file_contains_iframe.html` + "https://example.org/", + ], + granted_host_permissions: true, + ...manifestProps, + }, + useAddonManager: "temporary", + ...otherProps, + }); +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +add_task(async function test_executeScript_params_validation() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + const tabId = tabs[0].id; + + const TEST_CASES = [ + { + title: "no files and no func", + executeScriptParams: {}, + expectedError: /Exactly one of files and func must be specified/, + }, + { + title: "both files and func are passed", + executeScriptParams: { files: ["script.js"], func() {} }, + expectedError: /Exactly one of files and func must be specified/, + }, + { + title: "non-empty args is passed with files", + executeScriptParams: { files: ["script.js"], args: [123] }, + expectedError: /'args' may not be used with file injections/, + }, + { + title: "empty args is passed with files", + executeScriptParams: { files: ["script.js"], args: [] }, + expectedError: /'args' may not be used with file injections/, + }, + { + title: "unserializable argument", + executeScriptParams: { func() {}, args: [window] }, + expectedError: /Unserializable arguments/, + }, + { + title: "both allFrames and frameIds are passed", + executeScriptParams: { + target: { + tabId, + allFrames: true, + frameIds: [1, 2, 3], + }, + files: ["script.js"], + }, + expectedError: /Cannot specify both 'allFrames' and 'frameIds'/, + }, + { + title: "invalid IDs in frameIds", + executeScriptParams: { + target: { tabId, frameIds: [0, 1, 2] }, + func: () => {}, + }, + expectedError: "Invalid frame IDs: [1, 2].", + }, + { + title: "throw non-structurally cloneable data in all frames", + executeScriptParams: { + target: { + tabId, + allFrames: true, + }, + func: () => { + throw window; + }, + }, + expectedError: /Script '<anonymous code>' result is non-structured-clonable data/, + }, + ]; + + for (const { title, executeScriptParams, expectedError } of TEST_CASES) { + await browser.test.assertRejects( + browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + ...executeScriptParams, + }), + expectedError, + `expected error when: ${title}` + ); + } + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_main_world() { + let extension = makeExtension({ + async background() { + browser.test.assertThrows( + () => { + browser.scripting.executeScript({ + target: { tabId: 123 }, + func: () => {}, + world: "MAIN", + }); + }, + /world: Invalid enumeration value "MAIN"/, + "expected 'MAIN' world to not be supported yet" + ); + + browser.test.notifyPass("background-done"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("background-done"); + await extension.unload(); +}); + +add_task(async function test_executeScript_isolated_world() { + let extension = makeExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "@isolated-addon-id" }, + }, + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + let results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + globalThis.defaultWorldVar = browser.runtime.id; + return "default world"; + }, + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "default world", + results[0].result, + "got expected return value" + ); + + results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + return `isolated: ${browser.runtime.id}; existing default var: ${typeof defaultWorldVar}`; + }, + world: "ISOLATED", + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "isolated: @isolated-addon-id; existing default var: string", + results[0].result, + "got expected return value" + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_execution_world_constants() { + let extension = makeExtension({ + async background() { + browser.test.assertTrue( + !!browser.scripting.ExecutionWorld, + "expected scripting.ExecutionWorld to be defined" + ); + browser.test.assertEq( + 1, + Object.keys(browser.scripting.ExecutionWorld).length, + "expected 1 ExecutionWorld constant" + ); + browser.test.assertEq( + "ISOLATED", + browser.scripting.ExecutionWorld.ISOLATED, + "expected ISOLATED constant to be defined" + ); + // TODO: Bug 1736575 - Add support for other execution worlds like MAIN. + browser.test.assertEq( + undefined, + browser.scripting.ExecutionWorld.MAIN, + "expected MAIN constant to be undefined" + ); + + browser.test.notifyPass("background-done"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("background-done"); + await extension.unload(); +}); + +add_task(async function test_executeScript_with_wrong_host_permissions() { + let extension = makeExtension({ + manifest: { + host_permissions: [], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + await browser.test.assertRejects( + browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + browser.test.fail("Unexpected execution"); + }, + }), + "Missing host permission for the tab", + "expected host permission error" + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_with_invalid_tabId() { + let extension = makeExtension({ + async background() { + // This tab ID should not exist. + const tabId = 123456789; + + await browser.test.assertRejects( + browser.scripting.executeScript({ + target: { tabId }, + func: () => { + browser.test.fail("Unexpected execution"); + }, + }), + `Invalid tab ID: ${tabId}` + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_with_func() { + let extension = makeExtension({ + async background() { + const getTitle = () => { + return document.title; + }; + + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: getTitle, + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "file sample", + results[0].result, + "got the expected title" + ); + browser.test.assertEq(0, results[0].frameId, "got the expected frameId"); + + browser.test.notifyPass("execute-script"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_executeScript_with_func_and_args() { + let extension = makeExtension({ + async background() { + const formatArgs = (a, b, c) => { + return `received ${a}, ${b} and ${c}`; + }; + + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: formatArgs, + args: [true, undefined, "str"], + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + // undefined is converted to null when json-stringified in an array. + "received true, null and str", + results[0].result, + "got the expected return value" + ); + browser.test.assertEq(0, results[0].frameId, "got the expected frameId"); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_returns_nothing() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => {}, + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + undefined, + results[0].result, + "got expected undefined result" + ); + browser.test.assertEq(0, results[0].frameId, "got the expected frameId"); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_returns_null() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + return null; + }, + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + null, + results[0].result, + "got expected null result" + ); + browser.test.assertEq(0, results[0].frameId, "got the expected frameId"); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_with_error_in_func() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + throw new Error(`Thrown at ${location.pathname.split("/").pop()}`); + }, + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq(0, results[0].frameId, "got the expected frameId"); + browser.test.assertEq( + "Thrown at file_sample.html", + results[0].error.message, + "got the expected error message" + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_executeScript_with_a_file() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + files: ["script.js"], + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "value from script.js", + results[0].result, + "got the expected result" + ); + browser.test.assertEq(0, results[0].frameId, "got the expected frameId"); + + browser.test.notifyPass("execute-script"); + }, + files: { + "script.js": function() { + return "value from script.js"; + }, + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_in_one_frame() { + let extension = makeExtension({ + manifest: { + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame with the MochiTest runner + // 2. Frame for this file + // 3. Frame that loads `file_sample.html` at the top of this file + browser.test.assertEq(3, frames.length, "expected 3 frames"); + + const fileSampleFrameId = frames[2].frameId; + browser.test.assertTrue( + frames[2].url.includes("file_sample.html"), + "expected frame URL" + ); + + const TEST_CASES = [ + { + title: "with a file and a frame ID", + params: { + target: { tabId, frameIds: [fileSampleFrameId] }, + files: ["script.js"], + }, + expectedResults: [ + { + frameId: fileSampleFrameId, + result: "Sample text", + }, + ], + }, + { + title: "with no frame ID", + params: { + target: { tabId }, + func: () => { + return 123; + }, + }, + expectedResults: [{ frameId: 0, result: 123 }], + }, + ]; + + for (const { title, params, expectedResults } of TEST_CASES) { + const results = await browser.scripting.executeScript(params); + + browser.test.assertEq( + expectedResults.length, + results.length, + `${title} - got expected number of results` + ); + expectedResults.forEach(({ frameId, result }, index) => { + browser.test.assertEq( + result, + results[index].result, + `${title} - got the expected results[${index}].result` + ); + browser.test.assertEq( + frameId, + results[index].frameId, + `${title} - got the expected results[${index}].frameId` + ); + }); + } + + browser.test.notifyPass("execute-script"); + }, + files: { + "script.js": function() { + return document.getElementById("test").textContent; + }, + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_in_multiple_frameIds() { + let extension = makeExtension({ + manifest: { + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame that loads `file_contains_iframe.html` + // 2. Frame that loads `file_contains_img.html` + browser.test.assertEq(2, frames.length, "expected 2 frames"); + + const frameIds = frames.map(frame => frame.frameId); + + const getTitle = () => { + return document.title; + }; + + const TEST_CASES = [ + { + title: "multiple frame IDs", + params: { + target: { tabId, frameIds }, + func: getTitle, + }, + expectedResults: [ + { + frameId: frameIds[0], + result: "file contains iframe", + }, + { + frameId: frameIds[1], + result: "file contains img", + }, + ], + }, + { + title: "empty list of frame IDs", + params: { + target: { tabId, frameIds: [] }, + func: getTitle, + }, + expectedResults: [], + }, + ]; + + for (const { title, params, expectedResults } of TEST_CASES) { + const results = await browser.scripting.executeScript(params); + + browser.test.assertEq( + expectedResults.length, + results.length, + `${title} - got expected number of results` + ); + // Sort injection results by frameId to always assert the results in + // the same order. + results.sort((a, b) => a.frameId - b.frameId); + + browser.test.assertEq( + JSON.stringify(expectedResults), + JSON.stringify(results), + `${title} - got expected results` + ); + } + + browser.test.notifyPass("execute-script"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_executeScript_with_errors_in_multiple_frameIds() { + let extension = makeExtension({ + manifest: { + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame that loads `file_contains_iframe.html` + // 2. Frame that loads `file_contains_img.html` + browser.test.assertEq(2, frames.length, "expected 2 frames"); + + const frameIds = frames.map(frame => frame.frameId); + + const results = await browser.scripting.executeScript({ + target: { tabId, frameIds }, + func: () => { + throw new Error(`Thrown at ${location.pathname.split("/").pop()}`); + }, + }); + + browser.test.assertEq( + 2, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "Thrown at file_contains_iframe.html", + results[0].error.message, + "got expected error message in results[0]" + ); + browser.test.assertEq( + "Thrown at file_contains_img.html", + results[1].error.message, + "got expected error message in results[1]" + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_executeScript_with_frameId_and_wrong_host_permission() { + let extension = makeExtension({ + manifest: { + host_permissions: MOCHITEST_HOST_PERMISSIONS, + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame with the MochiTest runner + // 2. Frame for this file + // 3. Frame that loads `file_sample.html` at the top of this file + browser.test.assertEq(3, frames.length, "expected 3 frames"); + + const frameIds = frames.map(frame => frame.frameId); + + await browser.test.assertRejects( + browser.scripting.executeScript({ + target: { tabId, frameIds: [frameIds[2]] }, + func: () => { + browser.test.fail("Unexpected execution"); + }, + }), + "Missing host permission for the tab or frames", + "got the expected error message" + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_with_multiple_frameIds_and_wrong_host_permissions() { + let extension = makeExtension({ + manifest: { + host_permissions: MOCHITEST_HOST_PERMISSIONS, + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame with the MochiTest runner + // 2. Frame for this file + // 3. Frame that loads `file_sample.html` at the top of this file + browser.test.assertEq(3, frames.length, "expected 3 frames"); + + const frameIds = frames.map(frame => frame.frameId); + + const results = await browser.scripting.executeScript({ + target: { tabId, frameIds }, + func: () => {}, + }); + + // We get 2 results because we cannot inject into the 3rd frame. + browser.test.assertEq( + 2, + results.length, + "got expected number of results" + ); + browser.test.assertTrue( + typeof results[0].error === "undefined", + "expected no error in results[0]" + ); + browser.test.assertTrue( + typeof results[1].error === "undefined", + "expected no error in results[1]" + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_with_iframe_srcdoc_and_aboutblank() { + let iframe = document.createElement("iframe"); + iframe.srcdoc = `<!DOCTYPE html> + <html> + <head><title>iframe with srcdoc</title></head> + </html>`; + await new Promise(resolve => { + iframe.onload = resolve; + document.body.appendChild(iframe); + }); + + let iframeAboutBlank = document.createElement("iframe"); + iframeAboutBlank.src = "about:blank"; + await new Promise(resolve => { + iframeAboutBlank.onload = resolve; + document.body.appendChild(iframeAboutBlank); + }); + + let extension = makeExtension({ + manifest: { + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame with the MochiTest runner + // 2. Frame for this file + // 3. Frame that loads `file_sample.html` at the top of this file + // 4. Frame that loads the `srcdoc` + // 5. Frame for `about:blank` + browser.test.assertEq(5, frames.length, "expected 5 frames"); + + const frameIds = frames.map(frame => frame.frameId); + + const TEST_CASES = [ + { + title: "with frameIds for all frames", + params: { + target: { tabId, frameIds }, + }, + expectedResults: { + count: 5, + entriesAtIndex: { + 3: { + frameId: frameIds[3], + result: "iframe with srcdoc", + }, + 4: { + frameId: frameIds[4], + result: "about:blank", + }, + }, + }, + }, + { + title: "with allFrames: true", + params: { + target: { tabId, allFrames: true }, + }, + expectedResults: { + count: 5, + entriesAtIndex: { + 3: { + frameId: frameIds[3], + result: "iframe with srcdoc", + }, + 4: { + frameId: frameIds[4], + result: "about:blank", + }, + }, + }, + }, + { + title: "with a single frame specified", + params: { + target: { tabId, frameIds: [frameIds[3]] }, + }, + expectedResults: { + count: 1, + entriesAtIndex: { + 0: { + frameId: frameIds[3], + result: "iframe with srcdoc", + }, + }, + }, + }, + ]; + + for (const { title, params, expectedResults } of TEST_CASES) { + const results = await browser.scripting.executeScript({ + ...params, + func: () => { + return document.title || document.URL; + }, + }); + // Sort injection results by frameId to always assert the results in + // the same order. + results.sort((a, b) => a.frameId - b.frameId); + + browser.test.assertEq( + expectedResults.count, + results.length, + `${title} - got the expected number of results` + ); + Object.keys(expectedResults.entriesAtIndex).forEach(index => { + browser.test.assertEq( + JSON.stringify(expectedResults.entriesAtIndex[index]), + JSON.stringify(results[index]), + `${title} - got expected results[${index}]` + ); + }); + } + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + iframe.remove(); + iframeAboutBlank.remove(); +}); + +add_task(async function test_executeScript_with_multiple_files() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + files: ["1.js", "2.js"], + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "value from 2.js", + results[0].result, + "got the expected result" + ); + browser.test.assertEq(0, results[0].frameId, "got the expected frameId"); + + browser.test.notifyPass("execute-script"); + }, + files: { + "1.js": function() { + return "value from 1.js"; + }, + "2.js": function() { + return "value from 2.js"; + }, + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); +}); + +add_task(async function test_executeScript_with_multiple_files_and_an_error() { + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + files: ["1.js", "2.js"], + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq(0, results[0].frameId, "got the expected frameId"); + browser.test.assertEq( + "Thrown at file_contains_iframe.html", + results[0].error.message, + "got the expected error message" + ); + + browser.test.notifyPass("execute-script"); + }, + files: { + "1.js": function() { + throw new Error(`Thrown at ${location.pathname.split("/").pop()}`); + }, + "2.js": function() { + return "value from 2.js"; + }, + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_executeScript_with_file_not_in_extension() { + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + await browser.test.assertRejects( + browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + files: ["https://example.com/script.js"], + }), + /Files to be injected must be within the extension/, + "got the expected error message" + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_executeScript_allFrames() { + let extension = makeExtension({ + manifest: { + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame that loads `file_contains_iframe.html` + // 2. Frame that loads `file_contains_img.html` + browser.test.assertEq(2, frames.length, "expected 2 frames"); + const frameIds = frames.map(frame => frame.frameId); + + const getTitle = () => { + return document.title; + }; + + const TEST_CASES = [ + { + title: "allFrames set to true", + scriptingParams: { + target: { tabId, allFrames: true }, + func: getTitle, + }, + expectedResults: [ + { + frameId: frameIds[0], + result: "file contains iframe", + }, + { + frameId: frameIds[1], + result: "file contains img", + }, + ], + }, + { + title: "allFrames set to false", + scriptingParams: { + target: { tabId, allFrames: false }, + func: getTitle, + }, + expectedResults: [ + { + frameId: frameIds[0], + result: "file contains iframe", + }, + ], + }, + ]; + + for (const { title, scriptingParams, expectedResults } of TEST_CASES) { + const results = await browser.scripting.executeScript(scriptingParams); + // Sort injection results by frameId to always assert the results in + // the same order. + results.sort((a, b) => a.frameId - b.frameId); + + browser.test.assertDeepEq( + expectedResults, + results, + `${title} - got expected results` + ); + + // Make sure the `error` prop is never set. + for (const result of results) { + browser.test.assertFalse( + "error" in result, + `${title} - expected error property to be unset` + ); + } + } + + browser.test.notifyPass("execute-script"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_executeScript_runtime_errors() { + let extension = makeExtension({ + manifest: { + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame that loads `file_contains_iframe.html` + // 2. Frame that loads `file_contains_img.html` + browser.test.assertEq(2, frames.length, "expected 2 frames"); + + const TEST_CASES = [ + { + title: "reference error", + scriptingParams: { + target: { tabId }, + func: () => { + // We do not define `e` on purpose. + // eslint-disable-next-line no-undef + return String(e); + }, + }, + expectedErrors: [ + { type: "Error", stringRepr: "ReferenceError: e is not defined" }, + ], + }, + { + title: "eval error", + scriptingParams: { + target: { tabId }, + func: () => { + // We use `eval()` on purpose. + // eslint-disable-next-line no-eval + eval(""); + }, + }, + expectedErrors: [ + { type: "Error", stringRepr: "EvalError: call to eval() blocked by CSP" }, + ], + }, + { + title: "errors thrown in allFrames", + scriptingParams: { + target: { tabId, allFrames: true }, + func: () => { + throw new Error(`Thrown at ${location.pathname.split("/").pop()}`); + }, + }, + expectedErrors: [ + { type: "Error", stringRepr: "Error: Thrown at file_contains_iframe.html" }, + { type: "Error", stringRepr: "Error: Thrown at file_contains_img.html" }, + ], + }, + { + title: "custom error", + scriptingParams: { + target: { tabId }, + func: () => { + class CustomError extends Error { + constructor(message) { + super(message); + + this.name = 'CustomError'; + } + } + + throw new CustomError("a custom error message"); + }, + }, + // See Bug 1556604 for why a custom (derived) error looks like a + // normal error object after cloning. + expectedErrors: [ + { type: "Error", stringRepr: "Error: a custom error message" }, + ], + }, + { + title: "promise rejection with a string value", + scriptingParams: { + target: { tabId }, + func: () => { + // eslint-disable-next-line no-throw-literal + throw 'an error message'; + }, + }, + expectedErrors: [ + { type: "String", stringRepr: "an error message" }, + ], + }, + { + title: "promise rejection with an error", + scriptingParams: { + target: { tabId }, + func: () => { + throw new Error('ooops'); + }, + }, + expectedErrors: [ + { type: "Error", stringRepr: "Error: ooops" }, + ], + }, + { + title: "promise rejection with null", + scriptingParams: { + target: { tabId }, + func: () => { + throw null; // eslint-disable-line no-throw-literal + }, + }, + expectedErrors: [ + // This means we would receive `error: null`. + { type: "Null", stringRepr: "null" }, + ], + }, + { + title: "promise rejection with undefined", + scriptingParams: { + target: { tabId }, + func: () => { + return new Promise((resolve, reject) => { + reject(undefined); + }); + }, + }, + expectedErrors: [ + // This means we would receive `error: undefined`. + { type: "Undefined", stringRepr: "undefined" }, + ], + }, + { + title: "promise rejection with empty string", + scriptingParams: { + target: { tabId }, + func: () => { + throw ""; // eslint-disable-line no-throw-literal + }, + }, + expectedErrors: [ + { type: "String", stringRepr: "" }, + ], + }, + { + title: "promise rejection with zero", + scriptingParams: { + target: { tabId }, + func: () => { + throw 0; // eslint-disable-line no-throw-literal + }, + }, + expectedErrors: [ + { type: "Number", stringRepr: "0" }, + ], + }, + { + title: "promise rejection with false", + scriptingParams: { + target: { tabId }, + func: () => { + throw false; // eslint-disable-line no-throw-literal + }, + }, + expectedErrors: [ + { type: "Boolean", stringRepr: "false" }, + ], + }, + ]; + + for (const { title, scriptingParams, expectedErrors } of TEST_CASES) { + const results = await browser.scripting.executeScript(scriptingParams); + // Sort injection results by frameId to always assert the results in + // the same order. + results.sort((a, b) => a.frameId - b.frameId); + + browser.test.assertEq( + expectedErrors.length, + results.length, + `expected ${expectedErrors.length} results` + ); + + for (const [i, { type, stringRepr }] of expectedErrors.entries()) { + browser.test.assertTrue( + "error" in results[i], + `${title} - expected error property to be set` + ); + browser.test.assertFalse( + "result" in results[i], + `${title} - expected result property to be unset` + ); + + const { frameId, error } = results[i]; + + browser.test.assertEq( + `[object ${type}]`, + Object.prototype.toString.call(error), + `${title} - expected instance of ${type} - ${frameId}` + ); + browser.test.assertEq( + stringRepr, + String(error), + `${title} - got expected errors - ${frameId}` + ); + } + } + + browser.test.notifyPass("execute-script"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task( + async function test_executeScript_with_allFrames_and_wrong_host_permissions() { + let extension = makeExtension({ + manifest: { + host_permissions: MOCHITEST_HOST_PERMISSIONS, + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame with the MochiTest runner + // 2. Frame for this file + // 3. Frame that loads `file_sample.html` at the top of this file + browser.test.assertEq(3, frames.length, "expected 3 frames"); + + const results = await browser.scripting.executeScript({ + target: { tabId, allFrames: true }, + func: () => {}, + }); + + browser.test.assertEq( + 2, + results.length, + "got expected number of results" + ); + browser.test.assertTrue( + typeof results[0].error === "undefined", + "expected no error in results[0]" + ); + browser.test.assertTrue( + typeof results[1].error === "undefined", + "expected no error in results[1]" + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + } +); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html new file mode 100644 index 0000000000..5eb2193409 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests scripting.executeScript() and activeTab</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + +"use strict"; + +const makeExtension = ({ manifest: manifestProps, ...otherProps }) => { + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + action: {}, + ...manifestProps, + }, + ...otherProps, + }); +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +async function verifyExecuteScriptActiveTab(permissions, host_permissions) { + let extension = makeExtension({ + manifest: { + permissions: ["scripting", ...permissions], + host_permissions, + }, + background() { + browser.action.onClicked.addListener(async tab => { + const results = await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: () => document.title, + }); + + browser.test.assertEq( + 1, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "file sample", + results[0].result, + "got the expected title" + ); + browser.test.assertEq( + 0, + results[0].frameId, + "got the expected frameId" + ); + + browser.test.sendMessage("execute-script"); + }); + + browser.test.onMessage.addListener(async msg => { + switch (msg) { + case "reload-and-execute": + const tabs = await browser.tabs.query({ active: true }); + const tabId = tabs[0].id; + + let promiseTabLoad = new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(updatedTabId, changeInfo) { + browser.test.assertEq(tabId, updatedTabId, "got expected tabId"); + + if (tabId === updatedTabId && changeInfo.status === "complete") { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + + await browser.tabs.reload(); + await promiseTabLoad; + + await browser.test.assertRejects( + browser.scripting.executeScript({ + target: { tabId }, + func: () => { + browser.test.fail("Unexpected execution"); + }, + }), + "Missing host permission for the tab", + "expected host permission error" + ); + + browser.test.sendMessage("execute-script-after-reload"); + + break; + default: + browser.test.fail(`invalid message received: ${msg}`); + } + }); + + browser.test.sendMessage("background-ready"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html", + true + ); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + await AppTestDelegate.clickBrowserAction(window, extension); + await extension.awaitMessage("execute-script"); + await AppTestDelegate.closeBrowserAction(window, extension); + + extension.sendMessage("reload-and-execute"); + await extension.awaitMessage("execute-script-after-reload"); + + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +} + +// Test executeScript works with the standard activeTab permission. +add_task(async function test_executeScript_activeTab_permission() { + await verifyExecuteScriptActiveTab(["activeTab"], []); +}); + +// Test executeScript works with automatic activeTab granted from optional +// host permissions. +add_task(async function test_executeScript_activeTab_automatic_originControls() { + await verifyExecuteScriptActiveTab([], ["*://test1.example.com/*"]); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_injectImmediately.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_injectImmediately.html new file mode 100644 index 0000000000..9d05925adc --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_injectImmediately.html @@ -0,0 +1,215 @@ +<!DOCTYPE HTML> + <html> +<head> + <meta charset="utf-8"> + <title>Tests scripting.executeScript() and injectImmediately</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + +"use strict"; + +const MOCHITEST_HOST_PERMISSIONS = [ + "*://mochi.test/", + "*://mochi.xorigin-test/", + "*://test1.example.com/", +]; + +const makeExtension = ({ manifest: manifestProps, ...otherProps }) => { + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + permissions: ["scripting"], + host_permissions: [ + ...MOCHITEST_HOST_PERMISSIONS, + // Used in `file_contains_iframe.html` + "https://example.org/", + ], + granted_host_permissions: true, + ...manifestProps, + }, + useAddonManager: "temporary", + ...otherProps, + }); +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +add_task(async function test_executeScript_injectImmediately() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + const tabId = tabs[0].id; + + let onUpdatedPromise = (tabId, url, status) => { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(_, changed, tab) { + if (tabId == tab.id && changed.status == status && tab.url == url) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + }; + + const url = [ + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/", + `file_slowed_document.sjs?with-iframe&r=${Math.random()}`, + ].join(""); + const loadingPromise = onUpdatedPromise(tabId, url, "loading"); + const completePromise = onUpdatedPromise(tabId, url, "complete"); + + await browser.tabs.update(tabId, { url }); + await loadingPromise; + + const func = () => { + window.counter = (window.counter || 0) + 1; + + return window.counter; + }; + + let results = await Promise.all([ + // counter = 1 + browser.scripting.executeScript({ + target: { tabId }, + func, + injectImmediately: true, + }), + // counter = 3 + browser.scripting.executeScript({ + target: { tabId }, + func, + injectImmediately: false, + }), + // counter = 4 + browser.scripting.executeScript({ + target: { tabId }, + func, + // `injectImmediately` is `false` by default + }), + // counter = 2 + browser.scripting.executeScript({ + target: { tabId }, + func, + injectImmediately: true, + }), + // counter = 5 + browser.scripting.executeScript({ + target: { tabId }, + func, + injectImmediately: false, + }), + ]); + browser.test.assertEq( + 5, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "1 3 4 2 5", + results.map(res => res[0].result).join(" "), + `got expected results: ${JSON.stringify(results)}` + ); + + await completePromise; + + browser.test.notifyPass("execute-script"); + }, + }); + + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +add_task(async function test_executeScript_injectImmediately_after_document_idle() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + + const func = () => { + window.counter = (window.counter || 0) + 1; + + return window.counter; + }; + + let results = await Promise.all([ + browser.scripting.executeScript({ + target: { tabId }, + func, + injectImmediately: true, + }), + browser.scripting.executeScript({ + target: { tabId }, + func, + injectImmediately: false, + }), + browser.scripting.executeScript({ + target: { tabId }, + func, + // `injectImmediately` is `false` by default + }), + browser.scripting.executeScript({ + target: { tabId }, + func, + injectImmediately: true, + }), + browser.scripting.executeScript({ + target: { tabId }, + func, + injectImmediately: false, + }), + ]); + browser.test.assertEq( + 5, + results.length, + "got expected number of results" + ); + browser.test.assertEq( + "1 2 3 4 5", + results.map(res => res[0].result).join(" "), + `got expected results: ${JSON.stringify(results)}` + ); + + browser.test.notifyPass("execute-script"); + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("execute-script"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_insertCSS.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_insertCSS.html new file mode 100644 index 0000000000..3e2cef8721 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_insertCSS.html @@ -0,0 +1,395 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests scripting.insertCSS()</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const MOCHITEST_HOST_PERMISSIONS = [ + "*://mochi.test/", + "*://mochi.xorigin-test/", + "*://test1.example.com/", +]; + +const makeExtension = ({ manifest: manifestProps, ...otherProps }) => { + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + permissions: ["scripting"], + host_permissions: [ + ...MOCHITEST_HOST_PERMISSIONS, + // Used in `file_contains_iframe.html` + "https://example.org/", + ], + granted_host_permissions: true, + ...manifestProps, + }, + useAddonManager: "temporary", + ...otherProps, + }); +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +add_task(async function test_insertCSS_and_removeCSS_params_validation() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + + const TEST_CASES = [ + { + title: "no files and no css", + cssParams: {}, + expectedError: "Exactly one of files and css must be specified.", + }, + { + title: "both files and css are passed", + cssParams: { + files: ["styles.css"], + css: "* { background: rgb(1, 1, 1) }", + }, + expectedError: "Exactly one of files and css must be specified.", + }, + { + title: "both allFrames and frameIds are passed", + cssParams: { + target: { + tabId: tabs[0].id, + allFrames: true, + frameIds: [1, 2, 3], + }, + files: ["styles.css"], + }, + expectedError: "Cannot specify both 'allFrames' and 'frameIds'.", + }, + { + title: "empty css string with a file", + cssParams: { + css: "", + files: ["styles.css"], + }, + expectedError: "Exactly one of files and css must be specified.", + }, + ]; + + for (const { title, cssParams, expectedError } of TEST_CASES) { + await browser.test.assertRejects( + browser.scripting.insertCSS({ + target: { tabId: tabs[0].id }, + ...cssParams, + }), + expectedError, + `${title} - expected error for insertCSS()` + ); + + await browser.test.assertRejects( + browser.scripting.removeCSS({ + target: { tabId: tabs[0].id }, + ...cssParams, + }), + expectedError, + `${title} - expected error for removeCSS()` + ); + } + + browser.test.notifyPass("checks-done"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("checks-done"); + await extension.unload(); +}); + +add_task(async function test_insertCSS_with_invalid_tabId() { + let extension = makeExtension({ + async background() { + // This tab ID should not exist. + const tabId = 123456789; + + await browser.test.assertRejects( + browser.scripting.insertCSS({ + target: { tabId }, + css: "* { background: rgb(1, 1, 1) }", + }), + `Invalid tab ID: ${tabId}` + ); + + browser.test.notifyPass("insert-css"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("insert-css"); + await extension.unload(); +}); + +add_task(async function test_insertCSS_with_wrong_host_permissions() { + let extension = makeExtension({ + manifest: { + host_permissions: [], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + browser.test.assertRejects( + browser.scripting.insertCSS({ + target: { tabId: tabs[0].id }, + css: "* { background: rgb(1, 1, 1) }", + }), + /Missing host permission for the tab/, + "expected host permission error" + ); + + browser.test.notifyPass("insert-css"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("insert-css"); + await extension.unload(); +}); + +add_task(async function test_insertCSS_and_removeCSS() { + let extension = makeExtension({ + manifest: { + permissions: ["scripting", "webNavigation"], + }, + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const tabId = tabs[0].id; + + const frames = await browser.webNavigation.getAllFrames({ tabId }); + // 1. Top-level frame that loads `file_contains_iframe.html` + // 2. Frame that loads `file_contains_img.html` + browser.test.assertEq(2, frames.length, "expected 2 frames"); + const frameIds = frames.map(frame => frame.frameId); + + const cssColor1 = "rgb(1, 1, 1)"; + const cssColor2 = "rgb(2, 2, 2)"; + const cssColorInFile1 = "rgb(3, 3, 3)"; + const defaultColor = "rgba(0, 0, 0, 0)"; + + const TEST_CASES = [ + { + title: "with css prop", + elementId: "div-1", + cssParams: [ + { + target: { tabId }, + css: `#div-1 { background: ${cssColor1} }`, + }, + ], + expectedResults: [cssColor1, defaultColor], + }, + { + title: "with a file", + elementId: "div-2", + cssParams: [ + { + target: { tabId }, + files: ["file1.css"], + }, + ], + expectedResults: [cssColorInFile1, defaultColor], + }, + { + title: "css prop in a single frame", + elementId: "div-3", + cssParams: [ + { + target: { tabId, frameIds: [frameIds[0]] }, + css: `#div-3 { background: ${cssColor2} }`, + }, + ], + expectedResults: [cssColor2, defaultColor], + }, + { + title: "css prop in multiple frames", + elementId: "div-4", + cssParams: [ + { + target: { tabId, frameIds }, + css: `#div-4 { background: ${cssColor1} }`, + }, + ], + expectedResults: [cssColor1, cssColor1], + }, + { + title: "allFrames is true", + elementId: "div-5", + cssParams: [ + { + target: { tabId, allFrames: true }, + css: `#div-5 { background: ${defaultColor} }`, + }, + ], + expectedResults: [defaultColor, defaultColor], + }, + { + title: "origin: 'AUTHOR'", + elementId: "div-6", + cssParams: [ + { + target: { tabId }, + css: `#div-6 { background: ${cssColor1} }`, + origin: "AUTHOR", + }, + { + target: { tabId }, + css: `#div-6 { background: ${cssColor2} }`, + origin: "AUTHOR", + }, + ], + expectedResults: [cssColor2, defaultColor], + }, + { + title: "origin: 'USER'", + elementId: "div-7", + cssParams: [ + { + target: { tabId }, + css: `#div-7 { background: ${cssColor1} !important }`, + origin: "USER", + }, + { + target: { tabId }, + css: `#div-7 { background: ${cssColor2} !important }`, + origin: "AUTHOR", + }, + ], + // User has higher importance. + expectedResults: [cssColor1, defaultColor], + }, + { + title: "empty css string", + elementId: "div-8", + cssParams: [ + { + target: { tabId }, + css: "", + }, + ], + expectedResults: [defaultColor, defaultColor], + }, + { + title: "allFrames is false", + elementId: "div-9", + cssParams: [ + { + target: { tabId, allFrames: false }, + css: `#div-9 { background: ${cssColor1} }`, + }, + ], + expectedResults: [cssColor1, defaultColor], + }, + ]; + + const getBackgroundColor = elementId => { + return window.getComputedStyle(document.getElementById(elementId)) + .backgroundColor; + }; + + for (const { + title, + elementId, + cssParams, + expectedResults, + } of TEST_CASES) { + // Create a unique element for the current test case. + await browser.scripting.executeScript({ + target: { tabId, allFrames: true }, + func: elementId => { + const element = document.createElement("div"); + element.setAttribute("id", elementId); + document.body.appendChild(element); + }, + args: [elementId], + }); + + for (const params of cssParams) { + const result = await browser.scripting.insertCSS(params); + // `insertCSS()` should not resolve to a value. + browser.test.assertEq(undefined, result, "got expected empty result"); + } + + let results = await browser.scripting.executeScript({ + target: { tabId, allFrames: true }, + func: getBackgroundColor, + args: [elementId], + }); + results.sort((a, b) => a.frameId - b.frameId); + + browser.test.assertEq( + expectedResults.length, + results.length, + `${title} - got the expected number of results` + ); + results.forEach((result, index) => { + browser.test.assertEq( + expectedResults[index], + result.result, + `${title} - got expected result (index=${index}): ${title}` + ); + }); + + results = await Promise.all( + cssParams.map(params => browser.scripting.removeCSS(params)) + ); + // `removeCSS()` should not resolve to a value. + results.forEach(result => { + browser.test.assertEq(undefined, result, "got expected empty result"); + }); + + results = await browser.scripting.executeScript({ + target: { tabId, allFrames: true }, + func: getBackgroundColor, + args: [elementId], + }); + + browser.test.assertTrue( + results.every(({ result }) => result === defaultColor), + "got expected default color in all frames" + ); + } + + browser.test.notifyPass("insert-and-remove-css"); + }, + files: { + "file1.css": "#div-2 { background: rgb(3, 3, 3) }", + }, + }); + + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html", + true + ); + + await extension.startup(); + await extension.awaitFinish("insert-and-remove-css"); + await extension.unload(); + + await AppTestDelegate.removeTab(window, tab); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_permissions.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_permissions.html new file mode 100644 index 0000000000..e3e6552290 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_permissions.html @@ -0,0 +1,149 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests scripting APIs and permissions</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + +"use strict"; + +const verifyRegisterContentScripts = async ({ manifest_version }) => { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + permissions: ["scripting"], + host_permissions: ["*://example.com/*"], + optional_permissions: ["*://example.org/*"], + + }, + async background() { + browser.test.onMessage.addListener(async (msg, value) => { + switch (msg) { + case "grant-permission": + let granted = await new Promise(resolve => { + browser.test.withHandlingUserInput(() => { + resolve(browser.permissions.request(value)); + }); + }); + browser.test.assertTrue(granted, "permission request succeeded"); + browser.test.sendMessage("permission-granted"); + break; + + default: + browser.test.fail(`invalid message received: ${msg}`); + } + }); + + await browser.scripting.registerContentScripts([ + { + id: "script", + js: ["script.js"], + matches: [ + "*://example.com/*", + "*://example.net/*", + "*://example.org/*", + ], + persistAcrossSessions: false, + }, + ]); + + browser.test.sendMessage("background-ready"); + }, + files: { + "script.js": () => { + browser.test.sendMessage( + "script-ran", + window.location.host + window.location.search + ); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + if (manifest_version > 2) { + extension.sendMessage("grant-permission", { + origins: ["*://example.com/*"], + }); + await extension.awaitMessage("permission-granted"); + } + + // `example.net` is not declared in the list of `permissions`. + let tabExampleNet = await AppTestDelegate.openNewForegroundTab( + window, + "https://example.net/", + true + ); + // `example.org` is listed in `optional_permissions`. + let tabExampleOrg = await AppTestDelegate.openNewForegroundTab( + window, + "https://example.org/", + true + ); + // `example.com` is listed in `permissions`. + let tabExampleCom = await AppTestDelegate.openNewForegroundTab( + window, + "https://example.com/", + true + ); + + let value = await extension.awaitMessage("script-ran"); + ok( + value === "example.com", + `expected: example.com, received: ${value}` + ); + + extension.sendMessage("grant-permission", { + origins: ["*://example.org/*"], + }); + await extension.awaitMessage("permission-granted"); + + let tabExampleOrg2 = await AppTestDelegate.openNewForegroundTab( + window, + "https://example.org/?2", + true + ); + + value = await extension.awaitMessage("script-ran"); + ok( + value === "example.org?2", + `expected: example.org?2, received: ${value}` + ); + + await AppTestDelegate.removeTab(window, tabExampleNet); + await AppTestDelegate.removeTab(window, tabExampleOrg); + await AppTestDelegate.removeTab(window, tabExampleCom); + await AppTestDelegate.removeTab(window, tabExampleOrg2); + + await extension.unload(); +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.manifestV3.enabled", true], + ["extensions.webextOptionalPermissionPrompts", false], + ], + }); +}); + +add_task(async function test_scripting_registerContentScripts_mv2() { + await verifyRegisterContentScripts({ manifest_version: 2 }); +}); + +add_task(async function test_scripting_registerContentScripts_mv3() { + await verifyRegisterContentScripts({ manifest_version: 3 }); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_removeCSS.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_removeCSS.html new file mode 100644 index 0000000000..3036e49761 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_removeCSS.html @@ -0,0 +1,135 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests scripting.removeCSS()</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const MOCHITEST_HOST_PERMISSIONS = [ + "*://mochi.test/", + "*://mochi.xorigin-test/", + "*://test1.example.com/", +]; + +const makeExtension = ({ manifest: manifestProps, ...otherProps }) => { + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + permissions: ["scripting"], + host_permissions: [...MOCHITEST_HOST_PERMISSIONS], + granted_host_permissions: true, + ...manifestProps, + }, + useAddonManager: "temporary", + ...otherProps, + }); +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +add_task(async function test_removeCSS_with_invalid_tabId() { + let extension = makeExtension({ + async background() { + // This tab ID should not exist. + const tabId = 123456789; + + await browser.test.assertRejects( + browser.scripting.removeCSS({ + target: { tabId }, + css: "* { background: rgb(42, 42, 42) }", + }), + `Invalid tab ID: ${tabId}` + ); + + browser.test.notifyPass("remove-css"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("remove-css"); + await extension.unload(); +}); + +add_task(async function test_removeCSS_without_insertCSS_called_before() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + browser.scripting + .removeCSS({ + target: { tabId: tabs[0].id }, + css: "* { background: rgb(42, 42, 42) }", + }) + .then(() => { + browser.test.notifyPass("remove-css"); + }) + .catch(() => { + browser.test.notifyFail("remove-css"); + }); + }, + }); + + await extension.startup(); + await extension.awaitFinish("remove-css"); + await extension.unload(); +}); + +add_task(async function test_removeCSS_with_origin_mismatch() { + let extension = makeExtension({ + async background() { + const tabs = await browser.tabs.query({ active: true }); + browser.test.assertEq(1, tabs.length, "expected 1 tab"); + + const cssColor = "rgb(42, 42, 42)"; + const cssParams = { + target: { tabId: tabs[0].id }, + css: `* { background: ${cssColor} !important }`, + }; + + await browser.scripting.insertCSS({ ...cssParams, origin: "AUTHOR" }); + + let results = await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + return window.getComputedStyle(document.body).backgroundColor; + }, + }); + + browser.test.assertEq(cssColor, results[0].result, "got expected color"); + + // Here, we pass a different origin, which should result in no CSS + // removal. + await browser.scripting.removeCSS({ ...cssParams, origin: "USER" }); + + browser.test.assertEq( + cssColor, + results[0].result, + "got expected color after removeCSS" + ); + + browser.test.notifyPass("remove-css"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("remove-css"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html new file mode 100644 index 0000000000..ffdbc90efb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + // Add two listeners that both send replies. We're supposed to ignore all but one + // of them. Which one is chosen is non-deterministic. + + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == "getreply") { + sendReply("reply1"); + } + }); + + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == "getreply") { + sendReply("reply2"); + } + }); + + function sleep(callback, n = 10) { + if (n == 0) { + callback(); + } else { + setTimeout(function() { sleep(callback, n - 1); }, 0); + } + } + + let done_count = 0; + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == "done") { + done_count++; + browser.test.assertEq(done_count, 1, "got exactly one reply"); + + // Go through the event loop a few times to make sure we don't get multiple replies. + sleep(function() { + browser.test.notifyPass("sendmessage_doublereply"); + }); + } + }); +} + +function contentScript() { + browser.runtime.sendMessage("getreply", function(resp) { + if (resp != "reply1" && resp != "reply2") { + return; // test failed + } + browser.runtime.sendMessage("done"); + }); +} + +let extensionData = { + background, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(async function test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + + await Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_doublereply")]); + + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html new file mode 100644 index 0000000000..6b42073031 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html @@ -0,0 +1,45 @@ +<!doctype html> +<head> + <title>Test sendMessage frameId</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<script> +"use strict"; + +add_task(async function test_sendMessage_frameId() { + const html = `<!doctype html><meta charset="utf-8"><script src="script.js"><\/script>`; + + const extension = ExtensionTestUtils.loadExtension({ + background() { + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.sendMessage(msg, sender); + }); + browser.tabs.create({url: "tab.html"}); + }, + files: { + "iframe.html": html, + "tab.html": `${html}<iframe src="iframe.html"></iframe>`, + "script.js": () => { + browser.runtime.sendMessage(window.top === window ? "tab" : "iframe"); + }, + }, + }); + + await extension.startup(); + + const tab = await extension.awaitMessage("tab"); + ok(tab.url.endsWith("tab.html"), "Got the message from the tab"); + is(tab.frameId, 0, "And sender.frameId is zero"); + + const iframe = await extension.awaitMessage("iframe"); + ok(iframe.url.endsWith("iframe.html"), "Got the message from the iframe"); + is(typeof iframe.frameId, "number", "With sender.frameId of type number"); + ok(iframe.frameId > 0, "And sender.frameId greater than zero"); + + await extension.unload(); +}); + +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html new file mode 100644 index 0000000000..a18b003e48 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +"use strict"; + +async function testFn(expectPromise) { + browser.test.assertEq(null, chrome.runtime.lastError, "no lastError before call"); + let retval = chrome.runtime.sendMessage("msg"); + browser.test.assertEq(null, chrome.runtime.lastError, "no lastError after call"); + if (expectPromise) { + browser.test.assertTrue(retval instanceof Promise, "chrome.runtime.sendMessage should return a promise"); + } else { + browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback"); + } + + let isAsyncCall = false; + retval = chrome.runtime.sendMessage("msg", reply => { + browser.test.assertEq(undefined, reply, "no reply"); + browser.test.assertTrue(isAsyncCall, "chrome.runtime.sendMessage's callback must be called asynchronously"); + browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback"); + browser.test.assertEq("Could not establish connection. Receiving end does not exist.", chrome.runtime.lastError.message); + browser.test.sendMessage("finished", retval); + }); + isAsyncCall = true; +} + +add_task(async function test_content_script_sendMessage_without_listener() { + async function contentScript() { + await browser.test.assertRejects( + browser.runtime.sendMessage("msg"), + "Could not establish connection. Receiving end does not exist."); + + browser.test.notifyPass("sendMessage callback was invoked"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + js: ["contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + }, + files: { + "contentscript.js": contentScript, + }, + }); + + await extension.startup(); + + let win = window.open("file_sample.html"); + await extension.awaitFinish("sendMessage callback was invoked"); + win.close(); + + await extension.unload(); +}); + +add_task(async function test_content_script_chrome_sendMessage_without_listener() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 2, + content_scripts: [{ + js: ["contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + }, + // In MV2, chrome namespace in content scripts do get promises, however in background pages they do not. + background: `(${testFn})(false)`, + files: { + "contentscript.js": `(${testFn})(true)`, + }, + }); + + await extension.startup(); + await extension.awaitMessage("finished"); + + let win = window.open("file_sample.html"); + await extension.awaitMessage("finished"); + win.close(); + + await extension.unload(); +}); + +add_task(async function test_chrome_sendMessage_without_listener_v3() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); + + // We only test the background here because content script behavior + // is independant of the manifest version. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + }, + background: `(${testFn})(true)`, + }); + + await extension.startup(); + + await extension.awaitMessage("finished"); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html new file mode 100644 index 0000000000..a7f6314efd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == 0) { + sendReply("reply1"); + } else if (msg == 1) { + window.setTimeout(function() { + sendReply("reply2"); + }, 0); + return true; + } else if (msg == 2) { + browser.test.notifyPass("sendmessage_reply"); + } + }); +} + +function contentScript() { + browser.runtime.sendMessage(0, function(resp1) { + if (resp1 != "reply1") { + return; // test failed + } + browser.runtime.sendMessage(1, function(resp2) { + if (resp2 != "reply2") { + return; // test failed + } + browser.runtime.sendMessage(2); + }); + }); +} + +let extensionData = { + background, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(async function test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + + await Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_reply")]); + + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html new file mode 100644 index 0000000000..8cce833b49 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html @@ -0,0 +1,202 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function backgroundScript(token, id, otherId) { + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); + + if (msg === `content-${token}`) { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), + `${id}: sender url correct`); + + let tabId = sender.tab.id; + browser.tabs.sendMessage(tabId, `${token}-contentMessage`); + + sendReply(`${token}-done`); + } else if (msg === `tab-${token}`) { + browser.runtime.sendMessage(otherId, `${otherId}-tabMessage`); + browser.runtime.sendMessage(`${token}-tabMessage`); + + sendReply(`${token}-done`); + } else { + browser.test.fail(`${id}: Unexpected runtime message received: ${msg} ${uneval(sender)}`); + } + }); + + browser.runtime.onMessageExternal.addListener((msg, sender, sendReply) => { + browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`); + + if (msg === `content-${id}`) { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), + `${id}: external sender url correct`); + + sendReply(`${otherId}-done`); + } else if (msg === `tab-${id}`) { + sendReply(`${otherId}-done`); + } else if (msg !== `${id}-tabMessage`) { + browser.test.fail(`${id}: Unexpected runtime external message received: ${msg} ${uneval(sender)}`); + } + }); + + browser.tabs.create({url: "tab.html"}); +} + +function contentScript(token, id, otherId) { + let gotContentMessage = false; + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); + + browser.test.assertEq(`${token}-contentMessage`, msg, + `${id}: Correct content script message`); + if (msg === `${token}-contentMessage`) { + gotContentMessage = true; + } + }); + + Promise.all([ + browser.runtime.sendMessage(otherId, `content-${otherId}`).then(resp => { + browser.test.assertEq(`${id}-done`, resp, `${id}: Correct content script external response token`); + }), + + browser.runtime.sendMessage(`content-${token}`).then(resp => { + browser.test.assertEq(`${token}-done`, resp, `${id}: Correct content script response token`); + }).catch(e => { + browser.test.fail(`content-${token} rejected with ${e.message}`); + }), + ]).then(() => { + browser.test.assertTrue(gotContentMessage, `${id}: Got content script message`); + + browser.test.sendMessage("content-script-done"); + }); +} + +async function tabScript(token, id, otherId) { + let gotTabMessage = false; + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); + + if (String(msg).startsWith("content-")) { + return; + } + + browser.test.assertEq(`${token}-tabMessage`, msg, + `${id}: Correct tab script message`); + if (msg === `${token}-tabMessage`) { + gotTabMessage = true; + } + }); + + browser.test.sendMessage("tab-script-loaded"); + + await new Promise(resolve => { + const listener = (msg) => { + if (msg !== "run-tab-script") { + return; + } + browser.test.onMessage.removeListener(listener); + resolve(); + }; + browser.test.onMessage.addListener(listener); + }); + + Promise.all([ + browser.runtime.sendMessage(otherId, `tab-${otherId}`).then(resp => { + browser.test.assertEq(`${id}-done`, resp, `${id}: Correct tab script external response token`); + }), + + browser.runtime.sendMessage(`tab-${token}`).then(resp => { + browser.test.assertEq(`${token}-done`, resp, `${id}: Correct tab script response token`); + }), + ]).then(() => { + browser.test.assertTrue(gotTabMessage, `${id}: Got tab script message`); + + window.close(); + + browser.test.sendMessage("tab-script-done"); + }); +} + +function makeExtension(id, otherId) { + let token = Math.random(); + + let args = `${token}, ${JSON.stringify(id)}, ${JSON.stringify(otherId)}`; + + let extensionData = { + background: `(${backgroundScript})(${args})`, + manifest: { + "browser_specific_settings": {"gecko": {id}}, + + "permissions": ["tabs"], + + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + }, + + files: { + "tab.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head> + </html>`, + + "tab.js": `(${tabScript})(${args})`, + + "content_script.js": `(${contentScript})(${args})`, + }, + }; + return extensionData; +} + +add_task(async function test_contentscript() { + const ID1 = "sendmessage1@mochitest.mozilla.org"; + const ID2 = "sendmessage2@mochitest.mozilla.org"; + + let extension1 = ExtensionTestUtils.loadExtension(makeExtension(ID1, ID2)); + let extension2 = ExtensionTestUtils.loadExtension(makeExtension(ID2, ID1)); + + await Promise.all([ + extension1.startup(), + extension2.startup(), + extension1.awaitMessage("tab-script-loaded"), + extension2.awaitMessage("tab-script-loaded"), + ]); + + extension1.sendMessage("run-tab-script"); + extension2.sendMessage("run-tab-script"); + + let win = window.open("file_sample.html"); + + await waitForLoad(win); + + await Promise.all([ + extension1.awaitMessage("content-script-done"), + extension2.awaitMessage("content-script-done"), + extension1.awaitMessage("tab-script-done"), + extension2.awaitMessage("tab-script-done"), + ]); + + win.close(); + + await extension1.unload(); + await extension2.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html new file mode 100644 index 0000000000..71ef3a2214 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html @@ -0,0 +1,277 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const { ExtensionStorageIDB } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/ExtensionStorageIDB.jsm" +); + +const storageTestHelpers = { + storageLocal: { + async writeData() { + await browser.storage.local.set({hello: "world"}); + browser.test.sendMessage("finished"); + }, + + async readData() { + const matchBrowserStorage = await browser.storage.local.get("hello").then(result => { + return (Object.keys(result).length == 1 && result.hello == "world"); + }); + + browser.test.sendMessage("results", {matchBrowserStorage}); + }, + + assertResults({results, keepOnUninstall}) { + if (keepOnUninstall) { + is(results.matchBrowserStorage, true, "browser.storage.local data is still present"); + } else { + is(results.matchBrowserStorage, false, "browser.storage.local data was cleared"); + } + }, + }, + storageSync: { + async writeData() { + await browser.storage.sync.set({hello: "world"}); + browser.test.sendMessage("finished"); + }, + + async readData() { + const matchBrowserStorage = await browser.storage.sync.get("hello").then(result => { + return (Object.keys(result).length == 1 && result.hello == "world"); + }); + + browser.test.sendMessage("results", {matchBrowserStorage}); + }, + + assertResults({results, keepOnUninstall}) { + if (keepOnUninstall) { + is(results.matchBrowserStorage, true, "browser.storage.sync data is still present"); + } else { + is(results.matchBrowserStorage, false, "browser.storage.sync data was cleared"); + } + }, + }, + webAPIs: { + async readData() { + let matchLocalStorage = (localStorage.getItem("hello") == "world"); + + let idbPromise = new Promise((resolve, reject) => { + let req = indexedDB.open("test"); + req.onerror = e => { + reject(new Error(`indexedDB open failed with ${e.errorCode}`)); + }; + + req.onupgradeneeded = e => { + // no database, data is not present + resolve(false); + }; + + req.onsuccess = e => { + let db = e.target.result; + let transaction = db.transaction("store", "readwrite"); + let addreq = transaction.objectStore("store").get("hello"); + addreq.onerror = addreqError => { + reject(new Error(`read from indexedDB failed with ${addreqError.errorCode}`)); + }; + addreq.onsuccess = () => { + let match = (addreq.result.value == "world"); + resolve(match); + }; + }; + }); + + await idbPromise.then(matchIDB => { + let result = {matchLocalStorage, matchIDB}; + browser.test.sendMessage("results", result); + }); + }, + + async writeData() { + localStorage.setItem("hello", "world"); + + let idbPromise = new Promise((resolve, reject) => { + let req = indexedDB.open("test"); + req.onerror = e => { + reject(new Error(`indexedDB open failed with ${e.errorCode}`)); + }; + + req.onupgradeneeded = e => { + let db = e.target.result; + db.createObjectStore("store", {keyPath: "name"}); + }; + + req.onsuccess = e => { + let db = e.target.result; + let transaction = db.transaction("store", "readwrite"); + let addreq = transaction.objectStore("store") + .add({name: "hello", value: "world"}); + addreq.onerror = addreqError => { + reject(new Error(`add to indexedDB failed with ${addreqError.errorCode}`)); + }; + addreq.onsuccess = () => { + resolve(); + }; + }; + }); + + await idbPromise.then(() => { + browser.test.sendMessage("finished"); + }); + }, + + assertResults({results, keepOnUninstall}) { + if (keepOnUninstall) { + is(results.matchLocalStorage, true, "localStorage data is still present"); + is(results.matchIDB, true, "indexedDB data is still present"); + } else { + is(results.matchLocalStorage, false, "localStorage data was cleared"); + is(results.matchIDB, false, "indexedDB data was cleared"); + } + }, + }, +}; + +async function test_uninstall({extensionId, writeData, readData, assertResults}) { + // Set the pref to prevent cleaning up storage on uninstall in a separate prefEnv + // so we can pop it below, leaving flags set in the previous prefEnvs unmodified. + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webextensions.keepStorageOnUninstall", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + background: writeData, + manifest: { + browser_specific_settings: {gecko: {id: extensionId}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + await extension.awaitMessage("finished"); + await extension.unload(); + + // Check that we can still see data we wrote to storage but clear the + // "leave storage" flag so our storaged gets cleared on the next uninstall. + // This effectively tests the keepUuidOnUninstall logic, which ensures + // that when we read storage again and check that it is cleared, that + // it is actually a meaningful test! + await SpecialPowers.popPrefEnv(); + + extension = ExtensionTestUtils.loadExtension({ + background: readData, + manifest: { + browser_specific_settings: {gecko: {id: extensionId}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + let results = await extension.awaitMessage("results"); + + assertResults({results, keepOnUninstall: true}); + + await extension.unload(); + + // Read again. This time, our data should be gone. + extension = ExtensionTestUtils.loadExtension({ + background: readData, + manifest: { + browser_specific_settings: {gecko: {id: extensionId}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + results = await extension.awaitMessage("results"); + + assertResults({results, keepOnUninstall: false}); + + await extension.unload(); +} + + +add_task(async function test_setup_keep_uuid_on_uninstall() { + // Use a test-only pref to leave the addonid->uuid mapping around after + // uninstall so that we can re-attach to the same storage (this prefEnv + // is kept for this entire file and cleared automatically once all the + // tests in this file have been executed). + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webextensions.keepUuidOnUninstall", true]], + }); +}); + +// Test extension indexedDB and localStorage storages get cleaned up when the +// extension is uninstalled. +add_task(async function test_uninstall_with_webapi_storages() { + await test_uninstall({ + extensionId: "storage.cleanup-WebAPIStorages@tests.mozilla.org", + ...(storageTestHelpers.webAPIs), + }); +}); + +// Test browser.storage.local with JSONFile backend gets cleaned up when the +// extension is uninstalled. +add_task(async function test_uninistall_with_storage_local_file_backend() { + await SpecialPowers.pushPrefEnv({ + set: [[ExtensionStorageIDB.BACKEND_ENABLED_PREF, false]], + }); + + await test_uninstall({ + extensionId: "storage.cleanup-JSONFileBackend@tests.mozilla.org", + ...(storageTestHelpers.storageLocal), + }); + + await SpecialPowers.popPrefEnv(); +}); + +// Repeat the cleanup test when the storage.local IndexedDB backend is enabled. +add_task(async function test_uninistall_with_storage_local_idb_backend() { + await SpecialPowers.pushPrefEnv({ + set: [[ExtensionStorageIDB.BACKEND_ENABLED_PREF, true]], + }); + + await test_uninstall({ + extensionId: "storage.cleanup-IDBBackend@tests.mozilla.org", + ...(storageTestHelpers.storageLocal), + }); + + await SpecialPowers.popPrefEnv(); +}); + +// Legacy storage.sync backend is still being used on GeckoView builds. +const storageSyncOldKintoBackend = SpecialPowers.Services.prefs.getBoolPref( + "webextensions.storage.sync.kinto", + false +); + +// Verify browser.storage.sync rust backend is also cleared on uninstall. +async function test_uninistall_with_storage_sync() { + await test_uninstall({ + extensionId: "storage.cleanup-sync@tests.mozilla.org", + ...(storageTestHelpers.storageSync), + }); +} + +// NOTE: ideally we would be using a skip_if option on the add_task call, +// but we don't support that in the add_task defined in mochitest-plain. +if (!storageSyncOldKintoBackend) { + add_task(test_uninistall_with_storage_sync); +} + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html new file mode 100644 index 0000000000..135d2b9589 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test Storage API </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.storageManager.enabled", true], + ["dom.storageManager.prompt.testing", true], + ["dom.storageManager.prompt.testing.allow", true], + ], + }); +}); + +add_task(async function test_backgroundScript() { + function background() { + browser.test.assertTrue(navigator.storage !== undefined, "Has storage api interface"); + + // Test estimate. + browser.test.assertTrue("estimate" in navigator.storage, "Has estimate function"); + browser.test.assertEq("function", typeof navigator.storage.estimate, "estimate is function"); + browser.test.assertTrue(navigator.storage.estimate() instanceof Promise, "estimate returns a promise"); + + return browser.test.notifyPass("navigation_storage_api.done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + }); + + await extension.startup(); + await extension.awaitFinish("navigation_storage_api.done"); + await extension.unload(); +}); + +add_task(async function test_contentScript() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + + function contentScript() { + // Should not access storage api in non-secure context. + browser.test.assertEq(undefined, navigator.storage, + "A page from the unsecure http protocol " + + "doesn't have access to the navigator.storage API"); + + return browser.test.notifyPass("navigation_storage_api.done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + "matches": ["http://example.com/*/file_sample.html"], + "js": ["content_script.js"], + }], + }, + + files: { + "content_script.js": `(${contentScript})()`, + }, + }); + + await extension.startup(); + + // Open an explicit URL for testing Storage API in an insecure context. + let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + + await extension.awaitFinish("navigation_storage_api.done"); + + await extension.unload(); + win.close(); +}); + +add_task(async function test_contentScriptSecure() { + function contentScript() { + browser.test.assertTrue(navigator.storage !== undefined, "Has storage api interface"); + + // Test estimate. + browser.test.assertTrue("estimate" in navigator.storage, "Has estimate function"); + browser.test.assertEq("function", typeof navigator.storage.estimate, "estimate is function"); + + // The promise that estimate function returns belongs to the content page, + // but the Promise constructor belongs to the content script sandbox. + // Check window.Promise here. + browser.test.assertTrue(navigator.storage.estimate() instanceof window.Promise, "estimate returns a promise"); + + return browser.test.notifyPass("navigation_storage_api.done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + "matches": ["https://example.com/*/file_sample.html"], + "js": ["content_script.js"], + }], + }, + + files: { + "content_script.js": `(${contentScript})()`, + }, + }); + + await extension.startup(); + + // Open an explicit URL for testing Storage API in a secure context. + let win = window.open("file_sample.html"); + + await extension.awaitFinish("navigation_storage_api.done"); + + await extension.unload(); + win.close(); +}); + +add_task(async function cleanup() { + await SpecialPowers.popPrefEnv(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html new file mode 100644 index 0000000000..e68caa7e55 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// The purpose of this test is making sure that the implementation enabled by +// default for the storage.local and storage.sync APIs does work across all +// platforms/builds/apps +add_task(async function test_storage_smoke_test() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + for (let storageArea of ["sync", "local"]) { + let storage = browser.storage[storageArea]; + + browser.test.assertTrue(!!storage, `StorageArea ${storageArea} is present.`) + + let data = await storage.get(); + browser.test.assertEq(0, Object.keys(data).length, + `Storage starts out empty for ${storageArea}`); + + data = await storage.get("test"); + browser.test.assertEq(0, Object.keys(data).length, + `Can read non-existent keys for ${storageArea}`); + + await storage.set({ + "test1": "test-value1", + "test2": "test-value2", + "test3": "test-value3" + }); + + browser.test.assertEq( + "test-value1", + (await storage.get("test1")).test1, + `Can set and read back single values for ${storageArea}`); + + browser.test.assertEq( + "test-value2", + (await storage.get("test2")).test2, + `Can set and read back single values for ${storageArea}`); + + data = await storage.get(); + browser.test.assertEq(3, Object.keys(data).length, + `Can set and read back all values for ${storageArea}`); + browser.test.assertEq("test-value1", data.test1, + `Can set and read back all values for ${storageArea}`); + browser.test.assertEq("test-value2", data.test2, + `Can set and read back all values for ${storageArea}`); + browser.test.assertEq("test-value3", data.test3, + `Can set and read back all values for ${storageArea}`); + + data = await storage.get(["test1", "test2"]); + browser.test.assertEq(2, Object.keys(data).length, + `Can set and read back array of values for ${storageArea}`); + browser.test.assertEq("test-value1", data.test1, + `Can set and read back array of values for ${storageArea}`); + browser.test.assertEq("test-value2", data.test2, + `Can set and read back array of values for ${storageArea}`); + + await storage.remove("test1"); + data = await storage.get(["test1", "test2"]); + browser.test.assertEq(1, Object.keys(data).length, + `Data can be removed for ${storageArea}`); + browser.test.assertEq("test-value2", data.test2, + `Data can be removed for ${storageArea}`); + + data = await storage.get({ + test1: 1, + test2: 2, + }); + browser.test.assertEq(2, Object.keys(data).length, + `Expected a key-value pair for every property for ${storageArea}`); + browser.test.assertEq(1, data.test1, + `Use default value if key was deleted for ${storageArea}`); + browser.test.assertEq("test-value2", data.test2, + `Use stored value if found for ${storageArea}`); + + await storage.clear(); + data = await storage.get(); + browser.test.assertEq(0, Object.keys(data).length, + `Data is empty after clear for ${storageArea}`); + } + + browser.test.sendMessage("done"); + }, + manifest: { + permissions: ["storage"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html new file mode 100644 index 0000000000..d1bfbd824b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for multiple extensions trying to filterResponseData on the same request</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const TEST_URL = + "http://example.org/tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt"; + +add_task(async () => { + const firstExtension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + + background() { + browser.webRequest.onBeforeRequest.addListener( + ({ requestId }) => { + const filter = browser.webRequest.filterResponseData(requestId); + filter.ondata = event => { + filter.write(new TextEncoder().encode("Start ")); + filter.write(event.data); + filter.disconnect(); + }; + }, + { + urls: [ + "http://example.org/*/file_streamfilter.txt", + ], + }, + ["blocking"] + ); + }, + }); + + const secondExtension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + + background() { + browser.webRequest.onBeforeRequest.addListener( + ({ requestId }) => { + const filter = browser.webRequest.filterResponseData(requestId); + filter.ondata = event => { + filter.write(event.data); + }; + filter.onstop = event => { + filter.write(new TextEncoder().encode(" End")); + filter.close(); + }; + }, + { + urls: [ + "http://example.org/tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt", + ], + }, + ["blocking"] + ); + }, + }); + + await firstExtension.startup(); + await secondExtension.startup(); + + let iframe = document.createElement("iframe"); + iframe.src = TEST_URL; + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true})); + + let content = await SpecialPowers.spawn(iframe, [], async () => { + return this.content.document.body.textContent; + }); + SimpleTest.is(content, "Start Middle\n End", "Correctly intercepted page content"); + + await firstExtension.unload(); + await secondExtension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html new file mode 100644 index 0000000000..049178cad0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for using filterResponseData to intercept a cross-origin navigation that will involve a process switch with fission</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const TEST_HOST = "http://example.com/"; +const CROSS_ORIGIN_HOST = "http://example.org/"; +const TEST_PATH = + "tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt"; + +const TEST_URL = TEST_HOST + TEST_PATH; +const CROSS_ORIGIN_URL = CROSS_ORIGIN_HOST + TEST_PATH; + +add_task(async () => { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + + background() { + browser.webRequest.onBeforeRequest.addListener( + ({ requestId }) => { + const filter = browser.webRequest.filterResponseData(requestId); + filter.onerror = () => browser.test.fail( + `Unexpected filterResponseData error: ${filter.error}` + ); + filter.ondata = event => { + filter.write(event.data); + }; + filter.onstop = event => { + filter.write(new TextEncoder().encode(" End")); + filter.close(); + }; + }, + { + urls: [ + "http://example.org/*/file_streamfilter.txt", + ], + }, + ["blocking"] + ); + }, + }); + + await extension.startup(); + + let iframe = document.createElement("iframe"); + iframe.src = TEST_URL; + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true})); + + + iframe.src = CROSS_ORIGIN_URL; + await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true})); + + let content = await SpecialPowers.spawn(iframe, [], async () => { + return this.content.document.body.textContent; + }); + SimpleTest.is(content, "Middle\n End", "Correctly intercepted page content"); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html new file mode 100644 index 0000000000..fd034f0b65 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html @@ -0,0 +1,340 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; +/* eslint-disable mozilla/balanced-listeners */ + +add_task(async function test_webext_tab_subframe_privileges() { + function background() { + browser.runtime.onMessage.addListener(async ({msg, success, tabId, error}) => { + if (msg == "webext-tab-subframe-privileges") { + if (success) { + await browser.tabs.remove(tabId); + + browser.test.notifyPass(msg); + } else { + browser.test.log(`Got an unexpected error: ${error}`); + + let tabs = await browser.tabs.query({active: true}); + await browser.tabs.remove(tabs[0].id); + + browser.test.notifyFail(msg); + } + } + }); + browser.tabs.create({url: browser.runtime.getURL("/tab.html")}); + } + + async function tabSubframeScript() { + browser.test.assertTrue(browser.tabs != undefined, + "Subframe of a privileged page has access to privileged APIs"); + if (browser.tabs) { + try { + let tab = await browser.tabs.getCurrent(); + browser.runtime.sendMessage({ + msg: "webext-tab-subframe-privileges", + success: true, + tabId: tab.id, + }); + } catch (e) { + browser.runtime.sendMessage({msg: "webext-tab-subframe-privileges", success: false, error: `${e}`}); + } + } else { + browser.runtime.sendMessage({ + msg: "webext-tab-subframe-privileges", + success: false, + error: `Privileged APIs missing in WebExtension tab sub-frame`, + }); + } + } + + let extensionData = { + background, + files: { + "tab.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + </head> + <body> + <iframe src="tab-subframe.html"></iframe> + </body> + </html>`, + "tab-subframe.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + <script src="tab-subframe.js"><\/script> + </head> + </html>`, + "tab-subframe.js": tabSubframeScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + await extension.awaitFinish("webext-tab-subframe-privileges"); + await extension.unload(); +}); + +add_task(async function test_webext_background_subframe_privileges() { + function backgroundSubframeScript() { + browser.test.assertTrue(browser.tabs != undefined, + "Subframe of a background page has access to privileged APIs"); + browser.test.notifyPass("webext-background-subframe-privileges"); + } + + let extensionData = { + manifest: { + background: { + page: "background.html", + }, + }, + files: { + "background.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + </head> + <body> + <iframe src="background-subframe.html"></iframe> + </body> + </html>`, + "background-subframe.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + <script src="background-subframe.js"><\/script> + </head> + </html>`, + "background-subframe.js": backgroundSubframeScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + await extension.awaitFinish("webext-background-subframe-privileges"); + await extension.unload(); +}); + +add_task(async function test_webext_contentscript_iframe_subframe_privileges() { + function background() { + browser.runtime.onMessage.addListener(({name, hasTabsAPI, hasStorageAPI}) => { + if (name == "contentscript-iframe-loaded") { + browser.test.assertFalse(hasTabsAPI, + "Subframe of a content script privileged iframes has no access to privileged APIs"); + browser.test.assertTrue(hasStorageAPI, + "Subframe of a content script privileged iframes has access to content script APIs"); + + browser.test.notifyPass("webext-contentscript-subframe-privileges"); + } + }); + } + + function subframeScript() { + browser.runtime.sendMessage({ + name: "contentscript-iframe-loaded", + hasTabsAPI: browser.tabs != undefined, + hasStorageAPI: browser.storage != undefined, + }); + } + + function contentScript() { + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", browser.runtime.getURL("/contentscript-iframe.html")); + document.body.appendChild(iframe); + } + + let extensionData = { + background, + manifest: { + "permissions": ["storage"], + "content_scripts": [{ + "matches": ["https://example.com/*"], + "js": ["contentscript.js"], + }], + web_accessible_resources: [ + "contentscript-iframe.html", + ], + }, + files: { + "contentscript.js": contentScript, + "contentscript-iframe.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + </head> + <body> + <iframe src="contentscript-iframe-subframe.html"></iframe> + </body> + </html>`, + "contentscript-iframe-subframe.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + <script src="contentscript-iframe-subframe.js"><\/script> + </head> + </html>`, + "contentscript-iframe-subframe.js": subframeScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let win = window.open("https://example.com"); + + await extension.awaitFinish("webext-contentscript-subframe-privileges"); + + win.close(); + + await extension.unload(); +}); + +add_task(async function test_webext_background_remote_subframe_privileges() { + function backgroundSubframeScript() { + window.addEventListener("message", evt => { + browser.test.assertEq("http://mochi.test:8888", evt.origin, "postmessage origin ok"); + browser.test.assertFalse(evt.data.tabs, "remote frame cannot access webextension APIs"); + browser.test.assertEq("cookie=monster", evt.data.cookie, "Expected cookie value"); + browser.test.notifyPass("webext-background-subframe-privileges"); + }, {once: true}); + browser.cookies.set({url: "http://mochi.test:8888", name: "cookie", "value": "monster"}); + } + + let extensionData = { + manifest: { + permissions: ["cookies", "*://mochi.test/*", "tabs"], + background: { + page: "background.html", + }, + }, + files: { + "background.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + <script src="background-subframe.js"><\/script> + </head> + <body> + <iframe src='${SimpleTest.getTestFileURL("file_remote_frame.html")}'></iframe> + </body> + </html>`, + "background-subframe.js": backgroundSubframeScript, + }, + }; + // Need remote webextensions to be able to load remote content from a background page. + if (!SpecialPowers.getBoolPref("extensions.webextensions.remote", true)) { + return; + } + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + await extension.awaitFinish("webext-background-subframe-privileges"); + await extension.unload(); +}); + +// Test a moz-extension:// iframe inside a content iframe in an extension page. +add_task(async function test_sub_subframe_conduit_verified_env() { + let manifest = { + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html"], + all_frames: true, + js: ["cs.js"], + }], + background: { + page: "background.html", + }, + web_accessible_resources: ["iframe.html"], + }; + + let files = { + "iframe.html": `<!DOCTYPE html><meta charset=utf-8> iframe`, + "cs.js"() { + // A compromised content sandbox shouldn't be able to trick the parent + // process into giving it extension privileges by sending false metadata. + async function faker(extensionId, envType) { + try { + let id = envType + "-xyz1234"; + let wgc = this.content.windowGlobalChild; + + let conduit = wgc.getActor("Conduits").openConduit({}, { + id, + envType, + extensionId, + query: ["CreateProxyContext"], + }); + + return await conduit.queryCreateProxyContext({ + childId: id, + extensionId, + envType: "addon_parent", + url: this.content.location.href, + viewType: "tab", + }); + } catch (e) { + return e.message; + } + } + + let iframe = document.createElement("iframe"); + iframe.src = browser.runtime.getURL("iframe.html"); + + iframe.onload = async () => { + for (let envType of ["content_child", "addon_child"]) { + let msg = await this.wrappedJSObject.SpecialPowers.spawn( + iframe, [browser.runtime.id, envType], faker); + browser.test.sendMessage(envType, msg); + } + }; + document.body.appendChild(iframe); + }, + "background.html": `<!DOCTYPE html> + <meta charset=utf-8> + <iframe src="${SimpleTest.getTestFileURL("file_sample.html")}"> + </iframe> + page + `, + }; + + async function expectErrors(ext, log) { + let err = await ext.awaitMessage("content_child"); + is(err, "Bad sender context envType: content_child"); + + err = await ext.awaitMessage("addon_child"); + is(err, "Unknown sender or wrong actor for recvCreateProxyContext"); + } + + let remote = SpecialPowers.getBoolPref("extensions.webextensions.remote"); + + let badProcess = { message: /Bad {[\w-]+} process: web/ }; + let badPrincipal = { message: /Bad {[\w-]+} principal: http/ }; + consoleMonitor.start(remote ? [badPrincipal, badProcess] : [badProcess]); + + let extension = ExtensionTestUtils.loadExtension({ manifest, files }); + await extension.startup(); + + if (remote) { + info("Need OOP to spoof from a web iframe inside background page."); + await expectErrors(extension); + } + + info("Try spoofing from the web process."); + let win = window.open("./file_sample.html"); + await expectErrors(extension); + win.close(); + + await extension.unload(); + await consoleMonitor.finished(); + info("Conduit creation logged correct exception(s)."); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html new file mode 100644 index 0000000000..ab06a965ed --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html @@ -0,0 +1,324 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests tabs.captureTab and tabs.captureVisibleTab</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="text/javascript"> +"use strict"; + +async function runTest({ html, fullZoom, coords, rect, scale }) { + let url = `data:text/html,${encodeURIComponent(html)}#scroll`; + + async function background({ coords, rect, scale, method, fullZoom }) { + try { + // Wait for the page to load + await new Promise(resolve => { + browser.webNavigation.onCompleted.addListener( + () => resolve(), + {url: [{schemes: ["data"]}]}); + }); + + let [tab] = await browser.tabs.query({ + currentWindow: true, + active: true, + }); + + // TODO: Bug 1665429 - on mobile we ignore zoom for now + if (browser.tabs.setZoom) { + await browser.tabs.setZoom(tab.id, fullZoom ?? 1); + } + + let id = method === "captureVisibleTab" ? tab.windowId : tab.id; + + let [jpeg, png, ...pngs] = await Promise.all([ + browser.tabs[method](id, { format: "jpeg", quality: 95, rect, scale }), + browser.tabs[method](id, { format: "png", quality: 95, rect, scale }), + browser.tabs[method](id, { quality: 95, rect, scale }), + browser.tabs[method](id, { rect, scale }), + ]); + + browser.test.assertTrue( + pngs.every(url => url == png), + "All PNGs are identical" + ); + + browser.test.assertTrue( + jpeg.startsWith("data:image/jpeg;base64,"), + "jpeg is JPEG" + ); + browser.test.assertTrue( + png.startsWith("data:image/png;base64,"), + "png is PNG" + ); + + let promises = [jpeg, png].map( + url => + new Promise(resolve => { + let img = new Image(); + img.src = url; + img.onload = () => resolve(img); + }) + ); + + let width = (rect?.width ?? tab.width) * (scale ?? devicePixelRatio); + let height = (rect?.height ?? tab.height) * (scale ?? devicePixelRatio); + + [jpeg, png] = await Promise.all(promises); + let images = { jpeg, png }; + for (let format of Object.keys(images)) { + let img = images[format]; + + // WGP.drawSnapshot() deals in int coordinates, and rounds down. + browser.test.assertTrue( + Math.abs(width - img.width) <= 1, + `${format} ok image width: ${img.width}, expected: ${width}` + ); + browser.test.assertTrue( + Math.abs(height - img.height) <= 1, + `${format} ok image height ${img.height}, expected: ${height}` + ); + + let canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + canvas.mozOpaque = true; + + let ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + + for (let { x, y, color } of coords) { + x = (x + img.width) % img.width; + y = (y + img.height) % img.height; + let imageData = ctx.getImageData(x, y, 1, 1).data; + + if (format == "png") { + browser.test.assertEq( + `rgba(${color},255)`, + `rgba(${[...imageData]})`, + `${format} image color is correct at (${x}, ${y})` + ); + } else { + // Allow for some deviation in JPEG version due to lossy compression. + const SLOP = 3; + + browser.test.log( + `Testing ${format} image color at (${x}, ${y}), have rgba(${[ + ...imageData, + ]}), expecting approx. rgba(${color},255)` + ); + + browser.test.assertTrue( + Math.abs(color[0] - imageData[0]) <= SLOP, + `${format} image color.red is correct at (${x}, ${y})` + ); + browser.test.assertTrue( + Math.abs(color[1] - imageData[1]) <= SLOP, + `${format} image color.green is correct at (${x}, ${y})` + ); + browser.test.assertTrue( + Math.abs(color[2] - imageData[2]) <= SLOP, + `${format} image color.blue is correct at (${x}, ${y})` + ); + browser.test.assertEq( + 255, + imageData[3], + `${format} image color.alpha is correct at (${x}, ${y})` + ); + } + } + } + + browser.test.notifyPass("captureTab"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("captureTab"); + } + } + + for (let method of ["captureTab", "captureVisibleTab"]) { + let options = { coords, rect, scale, method, fullZoom }; + info(`Testing configuration: ${JSON.stringify(options)}`); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["<all_urls>", "webNavigation"], + }, + + background: `(${background})(${JSON.stringify(options)})`, + }); + + await extension.startup(); + + let testWindow = window.open(url); + await extension.awaitFinish("captureTab"); + + testWindow.close(); + await extension.unload(); + } +} + +async function testEdgeToEdge({ color, fullZoom }) { + let neutral = [0xaa, 0xaa, 0xaa]; + + let html = ` + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + </head> + <body style="background-color: rgb(${color})"> + <!-- Fill most of the image with a neutral color to test edge-to-edge scaling. --> + <div style="position: absolute; + left: 2px; + right: 2px; + top: 2px; + bottom: 2px; + background: rgb(${neutral});"></div> + </body> + </html> + `; + + // Check the colors of the first and last pixels of the image, to make + // sure we capture the entire frame, and scale it correctly. + let coords = [ + { x: 0, y: 0, color }, + { x: -1, y: -1, color }, + { x: 300, y: 200, color: neutral }, + ]; + + info(`Test edge to edge color ${color} at fullZoom=${fullZoom}`); + await runTest({ html, fullZoom, coords }); +} + +add_task(async function testCaptureEdgeToEdge() { + await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 1 }); + await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 2 }); + await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 0.5 }); + await testEdgeToEdge({ color: [255, 255, 255], fullZoom: 1 }); +}); + +const tallDoc = `<!DOCTYPE html> + <meta charset=utf-8> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <div style="background: yellow; width: 50%; height: 500px;"></div> + <div id=scroll style="background: red; width: 25%; height: 5000px;"></div> + Opened with the #scroll fragment, scrolls the div ^ into view. +`; + +// Test currently visible viewport is captured if scrolling is involved. +add_task(async function testScrolledViewport() { + await runTest({ + html: tallDoc, + coords: [ + { x: 50, y: 50, color: [255, 0, 0] }, + { x: 50, y: -50, color: [255, 0, 0] }, + { x: -50, y: -50, color: [255, 255, 255] }, + ], + }); +}); + +// Test rect and scale options. +add_task(async function testRectAndScale() { + await runTest({ + html: tallDoc, + rect: { x: 50, y: 50, width: 10, height: 1000 }, + scale: 4, + coords: [ + { x: 0, y: 0, color: [255, 255, 0] }, + { x: -1, y: 0, color: [255, 255, 0] }, + { x: 0, y: -1, color: [255, 0, 0] }, + { x: -1, y: -1, color: [255, 0, 0] }, + ], + }); +}); + +// Test OOP iframes are captured, for Fission compatibility. +add_task(async function testOOPiframe() { + await runTest({ + html: `<!DOCTYPE html> + <meta charset=utf-8> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <iframe src="http://example.net/tests/toolkit/components/extensions/test/mochitest/file_green.html"></iframe> + `, + coords: [ + { x: 50, y: 50, color: [0, 255, 0] }, + { x: 50, y: -50, color: [255, 255, 255] }, + { x: -50, y: 50, color: [255, 255, 255] }, + ], + }); +}); + +add_task(async function testOOPiframeScale() { + let scale = 2; + await runTest({ + html: `<!DOCTYPE html> + <meta charset=utf-8> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <style> + body { + background: yellow; + margin: 0; + } + </style> + <iframe frameborder="0" style="width: 300px; height: 300px" src="http://example.net/tests/toolkit/components/extensions/test/mochitest/file_green_blue.html"></iframe> + `, + coords: [ + { x: 20 * scale, y: 20 * scale, color: [0, 255, 0] }, + { x: 200 * scale, y: 20 * scale, color: [0, 0, 255] }, + { x: 20 * scale, y: 200 * scale, color: [0, 0, 255] }, + ], + scale, + }); +}); + +add_task(async function testCaptureTabPermissions() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + + background() { + browser.test.assertEq( + undefined, + browser.tabs.captureTab, + 'Extension without "<all_urls>" permission should not have access to captureTab' + ); + browser.test.notifyPass("captureTabPermissions"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("captureTabPermissions"); + await extension.unload(); +}); + +add_task(async function testCaptureVisibleTabPermissions() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + + background() { + browser.test.assertEq( + undefined, + browser.tabs.captureVisibleTab, + 'Extension without "<all_urls>" permission should not have access to captureVisibleTab' + ); + browser.test.notifyPass("captureVisibleTabPermissions"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("captureVisibleTabPermissions"); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_create_cookieStoreId.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_create_cookieStoreId.html new file mode 100644 index 0000000000..331faca016 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_create_cookieStoreId.html @@ -0,0 +1,210 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Test tabs.create(cookieStoreId)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="text/javascript"> +"use strict"; + +add_task(async function no_cookies_permission() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + async background() { + await browser.test.assertRejects( + browser.tabs.create({ cookieStoreId: "firefox-container-1" }), + /No permission for cookieStoreId/, + "cookieStoreId requires cookies permission" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function invalid_cookieStoreId() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs", "cookies"], + }, + async background() { + await browser.test.assertRejects( + browser.tabs.create({ cookieStoreId: "not-firefox-container-1" }), + /Illegal cookieStoreId/, + "cookieStoreId must be valid" + ); + + await browser.test.assertRejects( + browser.tabs.create({ cookieStoreId: "firefox-private" }), + /Illegal to set private cookieStoreId in a non-private window/, + "cookieStoreId cannot be private in a non-private window" + ); + + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function perma_private_browsing_mode() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.privatebrowsing.autostart", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + manifest: { + permissions: ["tabs", "cookies"], + }, + async background() { + await browser.test.assertRejects( + browser.tabs.create({ cookieStoreId: "firefox-container-1" }), + /Contextual identities are unavailable in permanent private browsing mode/, + "cookieStoreId cannot be a container tab ID in perma-private browsing mode" + ); + + browser.test.sendMessage("done"); + }, + }); + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function userContext_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", false]], + }); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs", "cookies"], + }, + async background() { + await browser.test.assertRejects( + browser.tabs.create({ cookieStoreId: "firefox-container-1" }), + /Contextual identities are currently disabled/, + "cookieStoreId cannot be a container tab ID when contextual identities are disabled" + ); + browser.test.sendMessage("done"); + }, + }); + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function valid_cookieStoreId() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); + + const testCases = [ + { + description: "no explicit URL", + createProperties: { + cookieStoreId: "firefox-container-1", + }, + expectedCookieStoreId: "firefox-container-1", + }, + { + description: "pass explicit url", + createProperties: { + url: "about:blank", + cookieStoreId: "firefox-container-1", + }, + expectedCookieStoreId: "firefox-container-1", + },{ + description: "pass explicit not-blank url", + createProperties: { + url: "https://example.com/", + cookieStoreId: "firefox-container-1", + }, + expectedCookieStoreId: "firefox-container-1", + },{ + description: "pass extension page url", + createProperties: { + url: "blank.html", + cookieStoreId: "firefox-container-1", + }, + expectedCookieStoreId: "firefox-container-1", + } + ]; + + async function background(testCases) { + for (let { createProperties, expectedCookieStoreId } of testCases) { + const { url } = createProperties; + const updatedPromise = new Promise(resolve => { + const onUpdated = (changedTabId, changed) => { + // Loading an extension page causes two `about:blank` messages + // because of the process switch + if (changed.url && (url == "about:blank" || changed.url != "about:blank")) { + browser.tabs.onUpdated.removeListener(onUpdated); + resolve({tabId: changedTabId, url: changed.url}); + } + }; + browser.tabs.onUpdated.addListener(onUpdated); + }); + + const tab = await browser.tabs.create(createProperties); + browser.test.assertEq( + expectedCookieStoreId, + tab.cookieStoreId, + "Expected cookieStoreId for container tab" + ); + + if (url && url !== "about:blank") { + // Make sure tab can load successfully + const updated = await updatedPromise; + browser.test.assertEq(tab.id, updated.tabId, `Expected value for tab.id`); + if (updated.url.startsWith("moz-extension")) { + browser.test.assertEq(browser.runtime.getURL(url), updated.url, + `Expected value for extension page url`); + } else { + browser.test.assertEq(url, updated.url, `Expected value for tab.url`); + } + } + + await browser.tabs.remove(tab.id); + } + browser.test.sendMessage("done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs", "cookies"], + }, + files: { + "blank.html": `<html><head><meta charset="utf-8"></head></html>`, + }, + background: `(${background})(${JSON.stringify(testCases)})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_executeScript_good.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_executeScript_good.html new file mode 100644 index 0000000000..ab3b9de5a3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_executeScript_good.html @@ -0,0 +1,162 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tabs executeScript Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +async function testHasPermission(params) { + let contentSetup = params.contentSetup || (() => Promise.resolve()); + + async function background(contentSetup) { + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.assertEq(msg, "script ran", "script ran"); + browser.test.notifyPass("executeScript"); + }); + + browser.test.onMessage.addListener(msg => { + browser.test.assertEq(msg, "execute-script"); + + browser.tabs.executeScript({ + file: "script.js", + }); + }); + + await contentSetup(); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: params.manifest, + + background: `(${background})(${contentSetup})`, + + files: { + "panel.html": `<!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body> + </body> + </html>`, + "script.js": function() { + browser.runtime.sendMessage("script ran"); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + + if (params.setup) { + await params.setup(extension); + } + + extension.sendMessage("execute-script"); + + await extension.awaitFinish("executeScript"); + + if (params.tearDown) { + await params.tearDown(extension); + } + + await extension.unload(); +} + +add_task(async function testGoodPermissions() { + let tab = await AppTestDelegate.openNewForegroundTab( + window, + "http://mochi.test:8888/", + true + ); + + info("Test explicit host permission"); + await testHasPermission({ + manifest: { permissions: ["http://mochi.test/"] }, + }); + + info("Test explicit host subdomain permission"); + await testHasPermission({ + manifest: { permissions: ["http://*.mochi.test/"] }, + }); + + info("Test explicit <all_urls> permission"); + await testHasPermission({ + manifest: { permissions: ["<all_urls>"] }, + }); + + info("Test activeTab permission with a browser action click"); + await testHasPermission({ + manifest: { + permissions: ["activeTab"], + browser_action: {}, + }, + contentSetup: function() { + browser.browserAction.onClicked.addListener(() => { + browser.test.log("Clicked."); + }); + return Promise.resolve(); + }, + setup: extension => AppTestDelegate.clickBrowserAction(window, extension), + tearDown: extension => AppTestDelegate.closeBrowserAction(window, extension), + }); + + info("Test activeTab permission with a page action click"); + await testHasPermission({ + manifest: { + permissions: ["activeTab"], + page_action: {}, + }, + contentSetup: async () => { + let [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + await browser.pageAction.show(tab.id); + }, + setup: extension => AppTestDelegate.clickPageAction(window, extension), + tearDown: extension => AppTestDelegate.closePageAction(window, extension), + }); + + info("Test activeTab permission with a browser action w/popup click"); + await testHasPermission({ + manifest: { + permissions: ["activeTab"], + browser_action: { default_popup: "panel.html" }, + }, + setup: async extension => { + await AppTestDelegate.clickBrowserAction(window, extension); + return AppTestDelegate.awaitExtensionPanel(window, extension); + }, + tearDown: extension => AppTestDelegate.closeBrowserAction(window, extension), + }); + + info("Test activeTab permission with a page action w/popup click"); + await testHasPermission({ + manifest: { + permissions: ["activeTab"], + page_action: { default_popup: "panel.html" }, + }, + contentSetup: async () => { + let [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + await browser.pageAction.show(tab.id); + }, + setup: extension => AppTestDelegate.clickPageAction(window, extension), + tearDown: extension => AppTestDelegate.closePageAction(window, extension), + }); + + await AppTestDelegate.removeTab(window, tab); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html new file mode 100644 index 0000000000..217139f12b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html @@ -0,0 +1,752 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tabs permissions test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const URL1 = + "https://www.example.com/tests/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html"; +const URL2 = + "https://example.net/tests/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html"; + +const helperExtensionDef = { + manifest: { + permissions: ["webNavigation", "<all_urls>"], + }, + + async background() { + browser.test.onMessage.addListener(async message => { + switch (message.subject) { + case "createTab": { + const tabLoaded = new Promise(resolve => { + browser.webNavigation.onCompleted.addListener(function listener( + details + ) { + if (details.url === message.data.url) { + browser.webNavigation.onCompleted.removeListener(listener); + resolve(); + } + }); + }); + + const tab = await browser.tabs.create({ url: message.data.url }); + await tabLoaded; + browser.test.sendMessage("tabCreated", tab.id); + break; + } + + case "changeTabURL": { + const tabLoaded = new Promise(resolve => { + browser.webNavigation.onCompleted.addListener(function listener( + details + ) { + if (details.url === message.data.url) { + browser.webNavigation.onCompleted.removeListener(listener); + resolve(); + } + }); + }); + + await browser.tabs.update(message.data.tabId, { + url: message.data.url, + }); + await tabLoaded; + browser.test.sendMessage("tabURLChanged", message.data.tabId); + break; + } + + case "changeTabHashAndTitle": { + const tabChanged = new Promise(resolve => { + let hasURLChangeInfo = false, + hasTitleChangeInfo = false; + browser.tabs.onUpdated.addListener(function listener( + tabId, + changeInfo, + tab + ) { + if (changeInfo.url?.endsWith(message.data.urlHash)) { + hasURLChangeInfo = true; + } + if (changeInfo.title === message.data.title) { + hasTitleChangeInfo = true; + } + if (hasURLChangeInfo && hasTitleChangeInfo) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + + await browser.tabs.executeScript(message.data.tabId, { + code: ` + document.location.hash = ${JSON.stringify(message.data.urlHash)}; + document.title = ${JSON.stringify(message.data.title)}; + `, + }); + await tabChanged; + browser.test.sendMessage("tabHashAndTitleChanged"); + break; + } + + case "removeTab": { + await browser.tabs.remove(message.data.tabId); + browser.test.sendMessage("tabRemoved"); + break; + } + + default: + browser.test.fail(`Received unexpected message: ${message}`); + } + }); + }, +}; + +/* + * Test tabs.query function + * Check if the correct tabs are queried by url or title based on the granted permissions + */ +async function test_query(testCases, permissions) { + const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + }, + + async background() { + // wait for start message + const [testCases, tabIdFromURL1, tabIdFromURL2] = await new Promise( + resolve => { + browser.test.onMessage.addListener(message => resolve(message)); + } + ); + + for (const testCase of testCases) { + const query = testCase.query; + const matchingTabs = testCase.matchingTabs; + + let tabQuery = await browser.tabs.query(query); + // ignore other tabs in the window + tabQuery = tabQuery.filter(tab => { + return tab.id === tabIdFromURL1 || tab.id === tabIdFromURL2; + }); + + browser.test.assertEq(matchingTabs, tabQuery.length, `Tabs queried`); + } + // send end message + browser.test.notifyPass("tabs.query"); + }, + }); + + await helperExtension.startup(); + await extension.startup(); + + helperExtension.sendMessage({ + subject: "createTab", + data: { url: URL1 }, + }); + const tabIdFromURL1 = await helperExtension.awaitMessage("tabCreated"); + + helperExtension.sendMessage({ + subject: "createTab", + data: { url: URL2 }, + }); + const tabIdFromURL2 = await helperExtension.awaitMessage("tabCreated"); + + if (permissions.includes("activeTab")) { + extension.grantActiveTab(tabIdFromURL2); + } + + extension.sendMessage([testCases, tabIdFromURL1, tabIdFromURL2]); + await extension.awaitFinish("tabs.query"); + + helperExtension.sendMessage({ + subject: "removeTab", + data: { tabId: tabIdFromURL1 }, + }); + await helperExtension.awaitMessage("tabRemoved"); + + helperExtension.sendMessage({ + subject: "removeTab", + data: { tabId: tabIdFromURL2 }, + }); + await helperExtension.awaitMessage("tabRemoved"); + + await extension.unload(); + await helperExtension.unload(); +} + +// https://www.example.com host permission +add_task(function query_with_host_permission_url1() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 1, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 1, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 1, + }, + { + query: { title: "The Title" }, + matchingTabs: 1, + }, + { + query: { title: "Another Title" }, + matchingTabs: 0, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["*://www.example.com/*"] + ); +}); + +// https://example.net host permission +add_task(function query_with_host_permission_url2() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 0, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 1, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 1, + }, + { + query: { title: "The Title" }, + matchingTabs: 0, + }, + { + query: { title: "Another Title" }, + matchingTabs: 1, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["*://example.net/*"] + ); +}); + +// <all_urls> permission +add_task(function query_with_host_permission_all_urls() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 1, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 2, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 2, + }, + { + query: { title: "The Title" }, + matchingTabs: 1, + }, + { + query: { title: "Another Title" }, + matchingTabs: 1, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["<all_urls>"] + ); +}); + +// tabs permission +add_task(function query_with_tabs_permission() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 1, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 2, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 2, + }, + { + query: { title: "The Title" }, + matchingTabs: 1, + }, + { + query: { title: "Another Title" }, + matchingTabs: 1, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["tabs"] + ); +}); + +// activeTab permission +add_task(function query_with_activeTab_permission() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 0, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 1, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 1, + }, + { + query: { title: "The Title" }, + matchingTabs: 0, + }, + { + query: { title: "Another Title" }, + matchingTabs: 1, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["activeTab"] + ); +}); +// no permission +add_task(function query_without_permission() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 0, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 0, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 0, + }, + { + query: { title: "The Title" }, + matchingTabs: 0, + }, + { + query: { title: "Another Title" }, + matchingTabs: 0, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + [] + ); +}); + +/* + * Test tabs.onUpdate and tabs.get function + * Check if the changeInfo or tab object contains the restricted properties + * url and title only when the right permissions are granted + * The tab is updated without causing navigation in order to also test activeTab permission + */ +async function test_restricted_properties( + permissions, + hasRestrictedProperties +) { + const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + }, + + async background() { + // wait for test start signal and data + const [ + hasRestrictedProperties, + tabId, + urlHash, + title, + ] = await new Promise(resolve => { + browser.test.onMessage.addListener(message => { + resolve(message); + }); + }); + + let hasURLChangeInfo = false, + hasTitleChangeInfo = false; + function onUpdateListener(tabId, changeInfo, tab) { + if (changeInfo.url?.endsWith(urlHash)) { + hasURLChangeInfo = true; + } + if (changeInfo.title === title) { + hasTitleChangeInfo = true; + } + } + browser.tabs.onUpdated.addListener(onUpdateListener); + + // wait for test evaluation signal and data + await new Promise(resolve => { + browser.test.onMessage.addListener(message => { + if (message === "collectTestResults") { + resolve(message); + } + }); + browser.test.sendMessage("waitingForTabPropertyChanges"); + }); + + // check onUpdate changeInfo + browser.test.assertEq( + hasRestrictedProperties, + hasURLChangeInfo, + `Has changeInfo property "url"` + ); + browser.test.assertEq( + hasRestrictedProperties, + hasTitleChangeInfo, + `Has changeInfo property "title"` + ); + // check tab properties + const tabGet = await browser.tabs.get(tabId); + browser.test.assertEq( + hasRestrictedProperties, + !!tabGet.url?.endsWith(urlHash), + `Has tab property "url"` + ); + browser.test.assertEq( + hasRestrictedProperties, + tabGet.title === title, + `Has tab property "title"` + ); + // send end message + browser.test.notifyPass("tabs.restricted_properties"); + }, + }); + + const urlHash = "#ChangedURL"; + const title = "Changed Title"; + + await helperExtension.startup(); + await extension.startup(); + + helperExtension.sendMessage({ + subject: "createTab", + data: { url: URL1 }, + }); + const tabId = await helperExtension.awaitMessage("tabCreated"); + + if (permissions.includes("activeTab")) { + extension.grantActiveTab(tabId); + } + // send test start signal and data + extension.sendMessage([hasRestrictedProperties, tabId, urlHash, title]); + await extension.awaitMessage("waitingForTabPropertyChanges"); + + helperExtension.sendMessage({ + subject: "changeTabHashAndTitle", + data: { + tabId, + urlHash, + title, + }, + }); + await helperExtension.awaitMessage("tabHashAndTitleChanged"); + + // send end signal and evaluate results + extension.sendMessage("collectTestResults"); + await extension.awaitFinish("tabs.restricted_properties"); + + helperExtension.sendMessage({ + subject: "removeTab", + data: { tabId }, + }); + await helperExtension.awaitMessage("tabRemoved"); + + await extension.unload(); + await helperExtension.unload(); +} + +// https://www.example.com host permission +add_task(function has_restricted_properties_with_host_permission_url1() { + return test_restricted_properties(["*://www.example.com/*"], true); +}); +// https://example.net host permission +add_task(function has_restricted_properties_with_host_permission_url2() { + return test_restricted_properties(["*://example.net/*"], false); +}); +// <all_urls> permission +add_task(function has_restricted_properties_with_host_permission_all_urls() { + return test_restricted_properties(["<all_urls>"], true); +}); +// tabs permission +add_task(function has_restricted_properties_with_tabs_permission() { + return test_restricted_properties(["tabs"], true); +}); +// activeTab permission +add_task(function has_restricted_properties_with_activeTab_permission() { + return test_restricted_properties(["activeTab"], true); +}).skip(); // TODO bug 1686080: support changeInfo.url with activeTab +// no permission +add_task(function has_restricted_properties_without_permission() { + return test_restricted_properties([], false); +}); + + +/* + * Test tabs.onUpdate filter functionality + * Check if the restricted filter properties only work if the + * right permissions are granted + */ +async function test_onUpdateFilter(testCases, permissions) { + // Filters for onUpdated are not supported on Android. + if (AppConstants.platform === "android") { + return; + } + + const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + }, + + async background() { + let listenerGotCalled = false; + function onUpdateListener(tabId, changeInfo, tab) { + listenerGotCalled = true; + } + + browser.test.onMessage.addListener(async message => { + switch (message.subject) { + case "setup": { + browser.tabs.onUpdated.addListener( + onUpdateListener, + message.data.filter + ); + browser.test.sendMessage("done"); + break; + } + + case "collectTestResults": { + browser.test.assertEq( + message.data.expectEvent, + listenerGotCalled, + `Update listener called` + ); + browser.tabs.onUpdated.removeListener(onUpdateListener); + listenerGotCalled = false; + browser.test.sendMessage("done"); + break; + } + + default: + browser.test.fail(`Received unexpected message: ${message}`); + } + }); + }, + }); + + await helperExtension.startup(); + await extension.startup(); + + for (const testCase of testCases) { + helperExtension.sendMessage({ + subject: "createTab", + data: { url: URL1 }, + }); + const tabId = await helperExtension.awaitMessage("tabCreated"); + + extension.sendMessage({ + subject: "setup", + data: { + filter: testCase.filter, + }, + }); + await extension.awaitMessage("done"); + + helperExtension.sendMessage({ + subject: "changeTabURL", + data: { + tabId, + url: URL2, + }, + }); + await helperExtension.awaitMessage("tabURLChanged"); + + extension.sendMessage({ + subject: "collectTestResults", + data: { + expectEvent: testCase.expectEvent, + }, + }); + await extension.awaitMessage("done"); + + helperExtension.sendMessage({ + subject: "removeTab", + data: { tabId }, + }); + await helperExtension.awaitMessage("tabRemoved"); + } + + await extension.unload(); + await helperExtension.unload(); +} + +// https://mozilla.org host permission +add_task(function onUpdateFilter_with_host_permission_url3() { + return test_onUpdateFilter( + [ + { + filter: { urls: ["*://mozilla.org/*"] }, + expectEvent: false, + }, + { + filter: { urls: ["<all_urls>"] }, + expectEvent: false, + }, + { + filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] }, + expectEvent: false, + }, + { + filter: { properties: ["title"] }, + expectEvent: false, + }, + { + filter: {}, + expectEvent: true, + }, + ], + ["*://mozilla.org/*"] + ); +}); + +// https://example.net host permission +add_task(function onUpdateFilter_with_host_permission_url2() { + return test_onUpdateFilter( + [ + { + filter: { urls: ["*://mozilla.org/*"] }, + expectEvent: false, + }, + { + filter: { urls: ["<all_urls>"] }, + expectEvent: true, + }, + { + filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] }, + expectEvent: true, + }, + { + filter: { properties: ["title"] }, + expectEvent: true, + }, + { + filter: {}, + expectEvent: true, + }, + ], + ["*://example.net/*"] + ); +}); + +// <all_urls> permission +add_task(function onUpdateFilter_with_host_permission_all_urls() { + return test_onUpdateFilter( + [ + { + filter: { urls: ["*://mozilla.org/*"] }, + expectEvent: false, + }, + { + filter: { urls: ["<all_urls>"] }, + expectEvent: true, + }, + { + filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] }, + expectEvent: true, + }, + { + filter: { properties: ["title"] }, + expectEvent: true, + }, + { + filter: {}, + expectEvent: true, + }, + ], + ["<all_urls>"] + ); +}); + +// tabs permission +add_task(function onUpdateFilter_with_tabs_permission() { + return test_onUpdateFilter( + [ + { + filter: { urls: ["*://mozilla.org/*"] }, + expectEvent: false, + }, + { + filter: { urls: ["<all_urls>"] }, + expectEvent: true, + }, + { + filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] }, + expectEvent: true, + }, + { + filter: { properties: ["title"] }, + expectEvent: true, + }, + { + filter: {}, + expectEvent: true, + }, + ], + ["tabs"] + ); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html new file mode 100644 index 0000000000..80b6def0ab --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tabs create Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_setup(async () => { + // TODO bug 1799344: remove this when the pref is true by default. + await SpecialPowers.pushPrefEnv({ + "set": [ + ["extensions.openPopupWithoutUserGesture.enabled", true], + ], + }); +}); + +async function test_query(query) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "current-window@tests.mozilla.org", + } + }, + permissions: ["tabs"], + browser_action: { + default_popup: "popup.html", + }, + }, + + useAddonManager: "permanent", + + background: async function() { + let query = await new Promise(resolve => { + browser.test.onMessage.addListener(message => { + resolve(message); + }); + }); + let tab = await browser.tabs.create({ url: "http://www.example.com", active: true }); + browser.runtime.onMessage.addListener(message => { + if (message === "popup-loaded") { + browser.runtime.sendMessage({ tab, query }); + } + }); + browser.browserAction.openPopup(); + }, + + files: { + "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`, + "popup.js"() { + browser.runtime.onMessage.addListener(async function({ tab, query }) { + let tabs = await browser.tabs.query(query); + browser.test.assertEq(tabs.length, 1, `Got one tab`); + browser.test.assertEq(tabs[0].id, tab.id, "The tab is the right one"); + + // Create a new tab and verify that we still see the right result + let newTab = await browser.tabs.create({ url: "http://www.example.com", active: true }); + tabs = await browser.tabs.query(query); + browser.test.assertEq(tabs.length, 1, `Got one tab`); + browser.test.assertEq(tabs[0].id, newTab.id, "Got the newly-created tab"); + + await browser.tabs.remove(newTab.id); + + // Remove the tab and verify that we see the old tab + tabs = await browser.tabs.query(query); + browser.test.assertEq(tabs.length, 1, `Got one tab`); + browser.test.assertEq(tabs[0].id, tab.id, "Got the tab that was active before"); + + // Cleanup + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("tabs.query"); + }); + browser.runtime.sendMessage("popup-loaded"); + }, + }, + }); + + await extension.startup(); + extension.sendMessage(query); + await extension.awaitFinish("tabs.query"); + await extension.unload(); +} + +add_task(function test_query_currentWindow_from_popup() { + return test_query({ currentWindow: true, active: true }); +}); + +add_task(function test_query_lastActiveWindow_from_popup() { + return test_query({ lastFocusedWindow: true, active: true }); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html new file mode 100644 index 0000000000..4b230c258c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html @@ -0,0 +1,152 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test tabs.sendMessage</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> +"use strict"; + +add_task(async function test_tabs_sendMessage_to_extension_page_frame() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html?tabs.sendMessage"], + js: ["cs.js"], + }], + web_accessible_resources: ["page.html", "page.js"], + }, + + async background() { + let tab; + + browser.runtime.onMessage.addListener(async (msg, sender) => { + browser.test.assertEq(msg, "page-script-ready"); + browser.test.assertEq(sender.url, browser.runtime.getURL("page.html")); + + let tabId = sender.tab.id; + let response = await browser.tabs.sendMessage(tabId, "tab-sendMessage"); + + switch (response) { + case "extension-tab": + browser.test.assertEq(tab.id, tabId, "Extension tab responded"); + browser.test.assertEq(sender.frameId, 0, "Response from top level"); + await browser.tabs.remove(tab.id); + browser.test.sendMessage("extension-tab-responded"); + break; + + case "extension-frame": + browser.test.assertTrue(sender.frameId > 0, "Response from iframe"); + browser.test.sendMessage("extension-frame-responded"); + break; + + default: + browser.test.fail("Unexpected response: " + response); + } + }); + + tab = await browser.tabs.create({ url: "page.html" }); + }, + + files: { + "cs.js"() { + let iframe = document.createElement("iframe"); + iframe.src = browser.runtime.getURL("page.html"); + document.body.append(iframe); + browser.test.sendMessage("content-script-done"); + }, + + "page.html": `<!DOCTYPE html> + <meta charset=utf-8> + <script src=page.js><\/script> + Extension page`, + + "page.js"() { + browser.runtime.onMessage.addListener(async msg => { + browser.test.assertEq(msg, "tab-sendMessage"); + return window.parent === window ? "extension-tab" : "extension-frame"; + }); + browser.runtime.sendMessage("page-script-ready"); + }, + } + }); + + await extension.startup(); + await extension.awaitMessage("extension-tab-responded"); + + let win = window.open("file_sample.html?tabs.sendMessage"); + await extension.awaitMessage("content-script-done"); + await extension.awaitMessage("extension-frame-responded"); + win.close(); + + await extension.unload(); +}); + +add_task(async function test_tabs_sendMessage_using_frameId() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["http://mochi.test/*/file_contains_iframe.html"], + run_at: "document_start", + js: ["cs_top.js"], + }, + { + matches: ["http://example.org/*/file_contains_img.html"], + js: ["cs_iframe.js"], + all_frames: true, + }, + ], + }, + + background() { + browser.runtime.onMessage.addListener(async (msg, sender) => { + let { tab, frameId } = sender; + browser.test.assertEq(msg, "cs_iframe_ready", "Iframe cs ready."); + browser.test.assertTrue(frameId > 0, "Not from the top frame."); + + let response = await browser.tabs.sendMessage(tab.id, "msg"); + browser.test.assertEq(response, "cs_top", "Top cs responded first."); + + response = await browser.tabs.sendMessage(tab.id, "msg", { frameId }); + browser.test.assertEq(response, "cs_iframe", "Iframe cs reponded."); + + browser.test.sendMessage("done"); + }); + browser.test.sendMessage("ready"); + }, + + files: { + "cs_top.js"() { + browser.test.log("Top content script loaded.") + browser.runtime.onMessage.addListener(async () => "cs_top"); + }, + "cs_iframe.js"() { + browser.test.log("Iframe content script loaded.") + browser.runtime.onMessage.addListener((msg, sender, sendResponse) => { + browser.test.log("Iframe content script received message.") + setTimeout(() => sendResponse("cs_iframe"), 100); + return true; + }); + browser.runtime.sendMessage("cs_iframe_ready"); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + + let win = window.open("file_contains_iframe.html"); + await extension.awaitMessage("done"); + win.close(); + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_test.html b/toolkit/components/extensions/test/mochitest/test_ext_test.html new file mode 100644 index 0000000000..bf68786465 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html @@ -0,0 +1,341 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Testing test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +function loadExtensionAndInterceptTest(extensionData) { + let results = []; + let testResolve; + let testDone = new Promise(resolve => { testResolve = resolve; }); + let handler = { + testResult(...result) { + result.pop(); + results.push(result); + SimpleTest.info(`Received test result: ${JSON.stringify(result)}`); + }, + + testMessage(msg, ...args) { + results.push(["test-message", msg, ...args]); + SimpleTest.info(`Received message: ${msg} ${JSON.stringify(args)}`); + if (msg === "This is the last browser.test call") { + testResolve(); + } + }, + }; + let extension = SpecialPowers.loadExtension(extensionData, handler); + SimpleTest.registerCleanupFunction(() => { + if (extension.state == "pending" || extension.state == "running") { + SimpleTest.ok(false, "Extension left running at test shutdown"); + return extension.unload(); + } else if (extension.state == "unloading") { + SimpleTest.ok(false, "Extension not fully unloaded at test shutdown"); + } + }); + extension.awaitResults = () => testDone.then(() => results); + return extension; +} + +// NOTE: This test does not verify the behavior expected by calling the browser.test API methods. +// +// On the contrary it tests what messages ext-test.js sends to the parent process as a result of +// processing different kind of parameters (e.g. how a dom element or a JS object with a custom +// toString method are being serialized into strings). +// +// All browser.test calls results are intercepted by the test itself, see verifyTestResults for +// the expectations of each browser.test call. +function testScript() { + browser.test.notifyPass("dot notifyPass"); + browser.test.notifyFail("dot notifyFail"); + browser.test.log("dot log"); + browser.test.fail("dot fail"); + browser.test.succeed("dot succeed"); + browser.test.assertTrue(true); + browser.test.assertFalse(false); + browser.test.assertEq("", ""); + + let obj = {}; + let arr = []; + browser.test.assertTrue(obj, "Object truthy"); + browser.test.assertTrue(arr, "Array truthy"); + browser.test.assertTrue(true, "True truthy"); + browser.test.assertTrue(false, "False truthy"); + browser.test.assertTrue(null, "Null truthy"); + browser.test.assertTrue(undefined, "Void truthy"); + + browser.test.assertFalse(obj, "Object falsey"); + browser.test.assertFalse(arr, "Array falsey"); + browser.test.assertFalse(true, "True falsey"); + browser.test.assertFalse(false, "False falsey"); + browser.test.assertFalse(null, "Null falsey"); + browser.test.assertFalse(undefined, "Void falsey"); + + browser.test.assertEq(obj, obj, "Object equality"); + browser.test.assertEq(arr, arr, "Array equality"); + browser.test.assertEq(null, null, "Null equality"); + browser.test.assertEq(undefined, undefined, "Void equality"); + + browser.test.assertEq({}, {}, "Object reference inequality"); + browser.test.assertEq([], [], "Array reference inequality"); + browser.test.assertEq(true, 1, "strict: true and 1 inequality"); + browser.test.assertEq("1", 1, "strict: '1' and 1 inequality"); + browser.test.assertEq(null, undefined, "Null and void inequality"); + + browser.test.assertDeepEq({a: 1, b: 1}, {b: 1, a: 1}, "Object deep eq"); + browser.test.assertDeepEq([[2], [1]], [[2], [1]], "Array deep eq"); + browser.test.assertDeepEq(true, 1, "strict: true and 1 deep ineq"); + browser.test.assertDeepEq("1", 1, "strict: '1' and 1 deep ineq"); + // Key with undefined value should be different from object without key: + browser.test.assertDeepEq(null, undefined, "Null and void deep ineq"); + browser.test.assertDeepEq({c: undefined}, {c: null}, "void+null deep ineq"); + browser.test.assertDeepEq({a: undefined, b: 1}, {b: 1}, "void/- deep ineq"); + + browser.test.assertDeepEq(NaN, NaN, "NaN deep eq"); + browser.test.assertDeepEq(NaN, null, "NaN+null deep ineq"); + browser.test.assertDeepEq(Infinity, Infinity, "Infinity deep eq"); + browser.test.assertDeepEq(Infinity, null, "Infinity+null deep ineq"); + + obj = { + toString() { + return "Dynamic toString"; + }, + }; + browser.test.assertEq(obj, obj, "obj with dynamic toString()"); + + browser.test.assertThrows( + () => { throw new Error("dummy"); }, + /dummy2/, + "intentional failure" + ); + browser.test.assertThrows( + () => { throw new Error("dummy2"); }, + /dummy3/ + ); + browser.test.assertThrows( + () => {}, + /dummy/ + ); + + // The WebIDL version of assertDeepEq structurally clones before sending the + // params to the main thread. This check verifies that the behavior is + // consistent between the WebIDL and Schemas.jsm-generated API bindings. + browser.test.assertThrows( + () => browser.test.assertDeepEq(obj, obj, "obj with func"), + /An unexpected error occurred/, + "assertDeepEq obj with function throws" + ); + browser.test.assertThrows( + () => browser.test.assertDeepEq(() => {}, () => {}, "func to assertDeepEq"), + /An unexpected error occurred/, + "assertDeepEq with function throws" + ); + browser.test.assertThrows( + () => browser.test.assertDeepEq(/./, /./, "regexp"), + /Unsupported obj type: RegExp/, + "assertDeepEq with RegExp throws" + ); + + // Set of additional tests to only run on background page and content script + // (but skip on background service worker). + if (self === self.window) { + let dom = document.createElement("body"); + browser.test.assertTrue(dom, "Element truthy"); + browser.test.assertTrue(false, document.createElement("html")); + browser.test.assertFalse(dom, "Element falsey"); + browser.test.assertFalse(true, document.createElement("head")); + browser.test.assertEq(dom, dom, "Element equality"); + browser.test.assertEq(dom, document.createElement("body"), "Element inequality"); + browser.test.assertEq(true, false, document.createElement("div")); + } + + browser.test.sendMessage("Ran test at", location.protocol); + browser.test.sendMessage("This is the last browser.test call"); +} + +function verifyTestResults(results, shortName, expectedProtocol, useServiceWorker) { + let expectations = [ + ["test-done", true, "dot notifyPass"], + ["test-done", false, "dot notifyFail"], + ["test-log", true, "dot log"], + ["test-result", false, "dot fail"], + ["test-result", true, "dot succeed"], + ["test-result", true, "undefined"], + ["test-result", true, "undefined"], + ["test-eq", true, "undefined", "", ""], + + ["test-result", true, "Object truthy"], + ["test-result", true, "Array truthy"], + ["test-result", true, "True truthy"], + ["test-result", false, "False truthy"], + ["test-result", false, "Null truthy"], + ["test-result", false, "Void truthy"], + + ["test-result", false, "Object falsey"], + ["test-result", false, "Array falsey"], + ["test-result", false, "True falsey"], + ["test-result", true, "False falsey"], + ["test-result", true, "Null falsey"], + ["test-result", true, "Void falsey"], + + ["test-eq", true, "Object equality", "[object Object]", "[object Object]"], + ["test-eq", true, "Array equality", "", ""], + ["test-eq", true, "Null equality", "null", "null"], + ["test-eq", true, "Void equality", "undefined", "undefined"], + + ["test-eq", false, "Object reference inequality", "[object Object]", "[object Object] (different)"], + ["test-eq", false, "Array reference inequality", "", " (different)"], + ["test-eq", false, "strict: true and 1 inequality", "true", "1"], + ["test-eq", false, "strict: '1' and 1 inequality", "1", "1 (different)"], + ["test-eq", false, "Null and void inequality", "null", "undefined"], + + ["test-eq", true, "Object deep eq", `{"a":1,"b":1}`, `{"b":1,"a":1}`], + ["test-eq", true, "Array deep eq", "[[2],[1]]", "[[2],[1]]"], + ["test-eq", false, "strict: true and 1 deep ineq", "true", "1"], + ["test-eq", false, "strict: '1' and 1 deep ineq", `"1"`, "1"], + ["test-eq", false, "Null and void deep ineq", "null", "undefined"], + ["test-eq", false, "void+null deep ineq", `{"c":"undefined"}`, `{"c":null}`], + ["test-eq", false, "void/- deep ineq", `{"a":"undefined","b":1}`, `{"b":1}`], + + ["test-eq", true, "NaN deep eq", `NaN`, `NaN`], + ["test-eq", false, "NaN+null deep ineq", `NaN`, `null`], + ["test-eq", true, "Infinity deep eq", `Infinity`, `Infinity`], + ["test-eq", false, "Infinity+null deep ineq", `Infinity`, `null`], + + [ + "test-eq", + true, + "obj with dynamic toString()", + // - Privileged JS API Bindings: the ext-test.js module will get a XrayWrapper and so when + // the object is being stringified the custom `toString()` method will not be called and + // "[object Object]" is the value we expect. + // - WebIDL API Bindngs: the parameter is being serialized into a string on the worker thread, + // the object is stringified using the worker principal and so there is no XrayWrapper + // involved and the value expected is the value returned by the custom toString method the. + // object does provide. + useServiceWorker ? "Dynamic toString" : "[object Object]", + useServiceWorker ? "Dynamic toString" : "[object Object]", + ], + + [ + "test-result", false, + "Function threw, expecting error to match '/dummy2/', got \'Error: dummy\': intentional failure" + ], + [ + "test-result", false, + "Function threw, expecting error to match '/dummy3/', got \'Error: dummy2\'" + ], + [ + "test-result", false, + "Function did not throw, expected error '/dummy/'" + ], + [ + "test-result", true, + "Function threw, expecting error to match '/An unexpected error occurred/', got 'Error: An unexpected error occurred': assertDeepEq obj with function throws", + ], + [ + "test-result", true, + "Function threw, expecting error to match '/An unexpected error occurred/', got 'Error: An unexpected error occurred': assertDeepEq with function throws", + ], + [ + "test-result", true, + "Function threw, expecting error to match '/Unsupported obj type: RegExp/', got 'Error: Unsupported obj type: RegExp': assertDeepEq with RegExp throws", + ], + ]; + + if (!useServiceWorker) { + expectations.push(...[ + ["test-result", true, "Element truthy"], + ["test-result", false, "[object HTMLHtmlElement]"], + ["test-result", false, "Element falsey"], + ["test-result", false, "[object HTMLHeadElement]"], + ["test-eq", true, "Element equality", "[object HTMLBodyElement]", "[object HTMLBodyElement]"], + ["test-eq", false, "Element inequality", "[object HTMLBodyElement]", "[object HTMLBodyElement] (different)"], + ["test-eq", false, "[object HTMLDivElement]", "true", "false"], + ]); + } + + expectations.push(...[ + ["test-message", "Ran test at", expectedProtocol], + ["test-message", "This is the last browser.test call"], + ]); + + expectations.forEach((expectation, i) => { + let msg = expectation.slice(2).join(" - "); + isDeeply(results[i], expectation, `${shortName} (${msg})`); + }); + is(results[expectations.length], undefined, "No more results"); +} + +add_task(async function test_test_in_background() { + let extensionData = { + background: `(${testScript})()`, + // This test case should never run the background script in a worker, + // even if this test file is running when "extensions.backgroundServiceWorker.forceInTest" + // pref is true + useServiceWorker: false, + }; + + let extension = loadExtensionAndInterceptTest(extensionData); + await extension.startup(); + let results = await extension.awaitResults(); + verifyTestResults(results, "background page", "moz-extension:", false); + await extension.unload(); +}); + +add_task(async function test_test_in_background_service_worker() { + if (!ExtensionTestUtils.isInBackgroundServiceWorkerTests()) { + is( + ExtensionTestUtils.getBackgroundServiceWorkerEnabled(), + false, + "This test should only be skipped with background service worker disabled" + ) + info("Test intentionally skipped on 'extensions.backgroundServiceWorker.enabled=false'"); + return; + } + + let extensionData = { + background: `(${testScript})()`, + // This test case should always run the background script in a worker, + // or be skipped if the background service worker is disabled by prefs. + useServiceWorker: true, + }; + + let extension = loadExtensionAndInterceptTest(extensionData); + await extension.startup(); + let results = await extension.awaitResults(); + verifyTestResults(results, "background service worker", "moz-extension:", true); + await extension.unload(); +}); + +add_task(async function test_test_in_content_script() { + let extensionData = { + manifest: { + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html"], + js: ["contentscript.js"], + }], + }, + files: { + "contentscript.js": `(${testScript})()`, + }, + }; + + let extension = loadExtensionAndInterceptTest(extensionData); + await extension.startup(); + let win = window.open("file_sample.html"); + let results = await extension.awaitResults(); + win.close(); + verifyTestResults(results, "content script", "http:", false); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html new file mode 100644 index 0000000000..db0f512ac3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html @@ -0,0 +1,138 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_unlimitedStorage.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + +"use strict"; + +async function test_background_storagePersist(EXTENSION_ID) { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.storageManager.enabled", true], + ["dom.storageManager.prompt.testing", false], + ["dom.storageManager.prompt.testing.allow", false], + ], + }); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + + manifest: { + permissions: ["storage", "unlimitedStorage"], + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }, + + background: async function() { + const PROMISE_RACE_TIMEOUT = 8000; + + browser.test.sendMessage("extension-uuid", window.location.host); + + await browser.storage.local.set({testkey: "testvalue"}); + await browser.test.sendMessage("storage-local-called"); + + const requestStoragePersist = async () => { + const persistAllowed = await navigator.storage.persist(); + if (!persistAllowed) { + throw new Error("navigator.storage.persist() has been denied"); + } + }; + + await Promise.race([ + requestStoragePersist(), + new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("Timeout opening persistent db from background page")); + }, PROMISE_RACE_TIMEOUT); + }), + ]).then( + () => { + browser.test.notifyPass("indexeddb-storagePersistent-unlimitedStorage-done"); + }, + (error) => { + browser.test.fail(`error while testing persistent IndexedDB storage: ${error}`); + browser.test.notifyFail("indexeddb-storagePersistent-unlimitedStorage-done"); + } + ); + }, + }); + + await extension.startup(); + + const uuid = await extension.awaitMessage("extension-uuid"); + + await extension.awaitMessage("storage-local-called"); + + let chromeScript = SpecialPowers.loadChromeScript(function test_country_data() { + /* eslint-env mozilla/chrome-script */ + const {addMessageListener, sendAsyncMessage} = this; + + addMessageListener("getPersistedStatus", (uuid) => { + const { + ExtensionStorageIDB, + } = ChromeUtils.import("resource://gre/modules/ExtensionStorageIDB.jsm"); + + const {WebExtensionPolicy} = Cu.getGlobalForObject(ExtensionStorageIDB); + const policy = WebExtensionPolicy.getByHostname(uuid); + const storagePrincipal = ExtensionStorageIDB.getStoragePrincipal(policy.extension); + const request = Services.qms.persisted(storagePrincipal); + request.callback = () => { + // request.result will be undeinfed if the request failed (request.resultCode !== Cr.NS_OK). + sendAsyncMessage("gotPersistedStatus", request.result); + }; + }); + }); + + const persistedPromise = chromeScript.promiseOneMessage("gotPersistedStatus"); + chromeScript.sendAsyncMessage("getPersistedStatus", uuid); + is(await persistedPromise, true, "Got the expected persist status for the storagePrincipal"); + + await extension.awaitFinish("indexeddb-storagePersistent-unlimitedStorage-done"); + await extension.unload(); + + checkSitePermissions(uuid, Services.perms.UNKNOWN_ACTION, "has been cleared"); +} + +add_task(async function test_unlimitedStorage() { + const EXTENSION_ID = "test-storagePersist@mozilla"; + await SpecialPowers.pushPrefEnv({ + "set": [ + ["extensions.webextensions.ExtensionStorageIDB.enabled", true], + ], + }); + + // Verify persist mode enabled when the storage.local IDB database is opened from + // the main process (from parent/ext-storage.js). + info("Test unlimitedStorage on an extension migrating to the IndexedDB storage.local backend)"); + await test_background_storagePersist(EXTENSION_ID); + + await SpecialPowers.pushPrefEnv({ + "set": [ + [`extensions.webextensions.ExtensionStorageIDB.migrated.` + EXTENSION_ID, true], + ], + }); + + // Verify persist mode enabled when the storage.local IDB database is opened from + // the child process (from child/ext-storage.js). + info("Test unlimitedStorage on an extension migrated to the IndexedDB storage.local backend"); + await test_background_storagePersist(EXTENSION_ID); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html new file mode 100644 index 0000000000..d1c41d2030 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html @@ -0,0 +1,170 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the web_accessible_resources incognito</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); +const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer; + +async function testImageLoading(src, expectedAction) { + let imageLoadingPromise = new Promise((resolve, reject) => { + let cleanupListeners; + let testImage = new window.Image(); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + testImage.wrappedJSObject.setAttribute("src", src); + + let loadListener = () => { + cleanupListeners(); + resolve(expectedAction === "loaded"); + }; + + let errorListener = (event) => { + cleanupListeners(); + resolve(expectedAction === "blocked"); + browser.test.log(`+++ image loading ${event.error}`); + }; + + cleanupListeners = () => { + testImage.removeEventListener("load", loadListener); + testImage.removeEventListener("error", errorListener); + }; + + testImage.addEventListener("load", loadListener); + testImage.addEventListener("error", errorListener); + }); + + let success = await imageLoadingPromise; + browser.runtime.sendMessage({name: "image-loading", expectedAction, success}); +} + +function testScript() { + window.postMessage("test-script-loaded", "*"); +} + +add_task(async function test_web_accessible_resources_incognito() { + // This extension will not have access to private browsing so its + // accessible resources should not be able to load in them. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "web_accessible_resources": [ + "image.png", + "test_script.js", + "accessible.html", + ], + }, + background() { + browser.test.sendMessage("url", browser.runtime.getURL("")); + }, + files: { + "image.png": IMAGE_ARRAYBUFFER, + "test_script.js": testScript, + "accessible.html": `<html><head> + <meta charset="utf-8"> + </head></html>`, + }, + }); + + await extension.startup(); + let baseUrl = await extension.awaitMessage("url"); + + async function content() { + let baseUrl = await browser.runtime.sendMessage({name: "get-url"}); + testImageLoading(`${baseUrl}image.png`, "loaded"); + + let testScriptElement = document.createElement("script"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + testScriptElement.wrappedJSObject.setAttribute("src", `${baseUrl}test_script.js`); + document.head.appendChild(testScriptElement); + + let iframe = document.createElement("iframe"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + iframe.wrappedJSObject.setAttribute("src", `${baseUrl}accessible.html`); + document.body.appendChild(iframe); + + // eslint-disable-next-line mozilla/balanced-listeners + window.addEventListener("message", event => { + browser.runtime.sendMessage({"name": event.data}); + }); + } + + let pb_extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + manifest: { + permissions: ["tabs"], + content_scripts: [{ + "matches": ["*://example.com/*/file_sample.html"], + "run_at": "document_end", + "js": ["content_script_helper.js", "content_script.js"], + }], + }, + files: { + "content_script_helper.js": `${testImageLoading}`, + "content_script.js": content, + }, + background() { + let url = "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + let baseUrl; + let window; + + browser.runtime.onMessage.addListener(async msg => { + switch (msg.name) { + case "image-loading": + browser.test.assertFalse(msg.success, `Image was ${msg.expectedAction}`); + browser.test.sendMessage(`image-${msg.expectedAction}`); + break; + case "get-url": + return baseUrl; + default: + browser.test.fail(`unexepected message ${msg.name}`); + } + }); + + browser.test.onMessage.addListener(async (msg, data) => { + if (msg == "start") { + baseUrl = data; + window = await browser.windows.create({url, incognito: true}); + } + if (msg == "close") { + browser.windows.remove(window.id); + } + }); + }, + }); + await pb_extension.startup(); + + consoleMonitor.start([ + {message: /may not load or link to.*image.png/}, + {message: /may not load or link to.*test_script.js/}, + {message: /\<script\> source URI is not allowed in this document/}, + {message: /may not load or link to.*accessible.html/}, + ]); + + pb_extension.sendMessage("start", baseUrl); + + await pb_extension.awaitMessage("image-loaded"); + + pb_extension.sendMessage("close"); + + await extension.unload(); + await pb_extension.unload(); + + await consoleMonitor.finished(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html new file mode 100644 index 0000000000..c13e40e265 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html @@ -0,0 +1,567 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the web_accessible_resources manifest directive</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// add_setup not available in mochitest +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({set: [["extensions.manifestV3.enabled", true]]}); +}) + +let image = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); +const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)) + .buffer; + +const ANDROID = navigator.userAgent.includes("Android"); + +async function testImageLoading(src, expectedAction) { + let imageLoadingPromise = new Promise((resolve, reject) => { + let cleanupListeners; + let testImage = document.createElement("img"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + testImage.wrappedJSObject.setAttribute("src", src); + + let loadListener = () => { + cleanupListeners(); + resolve(expectedAction === "loaded"); + }; + + let errorListener = () => { + cleanupListeners(); + resolve(expectedAction === "blocked"); + }; + + cleanupListeners = () => { + testImage.removeEventListener("load", loadListener); + testImage.removeEventListener("error", errorListener); + }; + + testImage.addEventListener("load", loadListener); + testImage.addEventListener("error", errorListener); + + document.body.appendChild(testImage); + }); + + let success = await imageLoadingPromise; + browser.runtime.sendMessage({ + name: "image-loading", + expectedAction, + success, + }); +} + +async function _test_web_accessible_resources({ + manifest, + expectShouldLoadByDefault = true, + usePagePrincipal = false, +}) { + function background(shouldLoad, usePagePrincipal) { + let gotURL; + let tabId; + let expectBrowserAPI; + + function loadFrame(url, sandbox = null, srcdoc = false) { + return new Promise(resolve => { + browser.tabs.sendMessage( + tabId, + ["load-iframe", url, sandbox, srcdoc, usePagePrincipal], + reply => { + resolve(reply); + } + ); + }); + } + + // shouldLoad will be true unless we expect all attempts to fail. + let urls = [ + // { url, shouldLoad, sandbox, srcdoc } + { + url: browser.runtime.getURL("accessible.html"), + shouldLoad, + }, + { + url: browser.runtime.getURL("accessible.html") + "?foo=bar", + shouldLoad, + }, + { + url: browser.runtime.getURL("accessible.html") + "#!foo=bar", + shouldLoad, + }, + { + url: browser.runtime.getURL("accessible.html"), + shouldLoad, + sandbox: "allow-scripts", + }, + { + url: browser.runtime.getURL("accessible.html"), + shouldLoad, + sandbox: "allow-same-origin allow-scripts", + }, + { + url: browser.runtime.getURL("accessible.html"), + shouldLoad, + sandbox: "allow-scripts", + srcdoc: true, + }, + { + url: browser.runtime.getURL("inaccessible.html"), + shouldLoad: false, + }, + { + url: browser.runtime.getURL("inaccessible.html"), + shouldLoad: false, + sandbox: "allow-same-origin allow-scripts", + }, + { + url: browser.runtime.getURL("inaccessible.html"), + shouldLoad: false, + sandbox: "allow-same-origin allow-scripts", + srcdoc: true, + }, + { + url: browser.runtime.getURL("wild1.html"), + shouldLoad, + }, + { + url: browser.runtime.getURL("wild2.htm"), + shouldLoad: false, + }, + ]; + + async function runTests() { + for (let { url, shouldLoad, sandbox, srcdoc } of urls) { + // Sandboxed pages with an opaque origin do not get browser api. + expectBrowserAPI = !sandbox || sandbox.includes("allow-same-origin"); + let success = await loadFrame(url, sandbox, srcdoc); + + browser.test.assertEq(shouldLoad, success, "Load was successful"); + if (shouldLoad && !srcdoc) { + browser.test.assertEq(url, gotURL, "Got expected url"); + } else { + browser.test.assertEq(undefined, gotURL, "Got no url"); + } + gotURL = undefined; + } + + browser.test.notifyPass("web-accessible-resources"); + } + + browser.runtime.onMessage.addListener( + ([msg, url, hasBrowserAPI], sender) => { + if (msg == "content-script-ready") { + tabId = sender.tab.id; + runTests(); + } else if (msg == "page-script") { + browser.test.assertEq( + undefined, + gotURL, + "Should have gotten only one message" + ); + browser.test.assertEq("string", typeof url, "URL should be a string"); + browser.test.assertEq( + expectBrowserAPI, + hasBrowserAPI, + "has access to browser api" + ); + gotURL = url; + } + } + ); + + browser.test.sendMessage("ready"); + } + + function contentScript() { + window.addEventListener("message", event => { + // bounce the postmessage to the background script + if (event.data[0] == "page-script") { + browser.runtime.sendMessage(event.data); + } + }); + + browser.runtime.onMessage.addListener( + ([msg, url, sandboxed, srcdoc, usePagePrincipal], sender, respond) => { + if (msg == "load-iframe") { + // construct the frame using srcdoc if requested. + if (srcdoc) { + sandboxed = sandboxed !== null ? `sandbox="${sandboxed}"` : ""; + let frameSrc = `<iframe ${sandboxed} src="${url}" onload="parent.postMessage(true, '*')" onerror="parent.postMessage(false, '*')">`; + let frame = document.createElement("iframe"); + frame.setAttribute("srcdoc", frameSrc); + window.addEventListener("message", function listener(event) { + if (event.source === frame.contentWindow) { + window.removeEventListener("message", listener); + respond(event.data); + } + }); + document.body.appendChild(frame); + return true; + } + + let iframe = document.createElement("iframe"); + if (sandboxed !== null) { + iframe.setAttribute("sandbox", sandboxed); + } + + if (usePagePrincipal) { + // Test using the page principal + iframe.wrappedJSObject.src = url; + } else { + // Test using the expanded principal + iframe.src = url; + } + iframe.addEventListener("load", () => { + respond(true); + }); + iframe.addEventListener("error", () => { + respond(false); + }); + document.body.appendChild(iframe); + return true; + } + } + ); + browser.runtime.sendMessage(["content-script-ready"]); + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + content_scripts: [ + { + matches: ["https://example.com/"], + js: ["content_script.js"], + run_at: "document_idle", + }, + ], + ...manifest, + }, + + background: `(${background})(${expectShouldLoadByDefault}, ${usePagePrincipal})`, + + files: { + "content_script.js": contentScript, + + "accessible.html": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + <script src="pagescript.js"><\/script> + </head></html>`, + + "inaccessible.html": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + <script src="pagescript.js"><\/script> + </head></html>`, + + "wild1.html": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + <script src="pagescript.js"><\/script> + </head></html>`, + + "wild2.htm": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + <script src="pagescript.js"><\/script> + </head></html>`, + + "pagescript.js": + // We postmessage so we can determine when browser is not available + 'window.parent.postMessage(["page-script", location.href, typeof browser !== "undefined"], "*");', + }, + }); + + await extension.startup(); + + await extension.awaitMessage("ready"); + + let win = window.open("https://example.com/"); + + await extension.awaitFinish("web-accessible-resources"); + + win.close(); + + await extension.unload(); +}; + +add_task(async function test_web_accessible_resources_v2() { + await SpecialPowers.pushPrefEnv({set: [["extensions.content_web_accessible.enabled", true]]}); + consoleMonitor.start([ + {message: /Content at https:\/\/example.com\/ may not load or link to.*inaccessible.html/}, + ]); + await _test_web_accessible_resources({ + manifest: { + manifest_version: 2, + web_accessible_resources: ["/accessible.html", "wild*.html"], + } + }); + await consoleMonitor.finished(); + await SpecialPowers.popPrefEnv(); +}); + +// Same test as above, but using only the content principal +add_task(async function test_web_accessible_resources_v2_content() { + await SpecialPowers.pushPrefEnv({set: [["extensions.content_web_accessible.enabled", true]]}); + consoleMonitor.start([ + {message: /Content at https:\/\/example.com\/ may not load or link to.*inaccessible.html/}, + ]); + await _test_web_accessible_resources({ + manifest: { + manifest_version: 2, + web_accessible_resources: ["/accessible.html", "wild*.html"], + }, + usePagePrincipal: true, + }); + await consoleMonitor.finished(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_web_accessible_resources_v3() { + // MV3 always requires this, pref off to ensure it works. + await SpecialPowers.pushPrefEnv({set: [["extensions.content_web_accessible.enabled", false]]}); + consoleMonitor.start([ + {message: /Content at https:\/\/example.com\/ may not load or link to.*inaccessible.html/}, + ]); + await _test_web_accessible_resources({ + manifest: { + manifest_version: 3, + web_accessible_resources: [ + { + resources: ["/accessible.html", "wild*.html"], + matches: ["*://example.com/*"] + }, + ], + host_permissions: ["*://example.com/*"], + granted_host_permissions: true, + } + }); + await consoleMonitor.finished(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_web_accessible_resources_v3_by_id() { + consoleMonitor.start([ + {message: /Content at https:\/\/example.com\/ may not load or link to.*accessible.html/}, + {message: /Content at https:\/\/example.com\/ may not load or link to.*inaccessible.html/}, + ]); + await _test_web_accessible_resources({ + manifest: { + manifest_version: 3, + browser_specific_settings: { + gecko: { + id: "extension_wac@mochitest", + }, + }, + web_accessible_resources: [ + { + resources: ["/accessible.html", "wild*.html"], + extension_ids: ["extension_wac@mochitest"] + }, + ], + host_permissions: ["*://example.com/*"], + // Work-around for bug 1766752 to allow content_scripts to run: + granted_host_permissions: true, + }, + expectShouldLoadByDefault: false, + }); + await consoleMonitor.finished(); +}); + +add_task(async function test_web_accessible_resources_mixed_content() { + function background() { + browser.runtime.onMessage.addListener(msg => { + if (msg.name === "image-loading") { + browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`); + browser.test.sendMessage(`image-${msg.expectedAction}`); + } else { + browser.test.sendMessage(msg); + if (msg === "accessible-script-loaded") { + browser.test.notifyPass("mixed-test"); + } + } + }); + + browser.test.sendMessage("background-ready"); + } + + async function content() { + await testImageLoading( + "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", + "blocked" + ); + await testImageLoading(browser.runtime.getURL("image.png"), "loaded"); + + let testScriptElement = document.createElement("script"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + testScriptElement.wrappedJSObject.setAttribute( + "src", + browser.runtime.getURL("test_script.js") + ); + document.head.appendChild(testScriptElement); + + window.addEventListener("message", event => { + browser.runtime.sendMessage(event.data); + }); + } + + function testScript() { + window.postMessage("accessible-script-loaded", "*"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["https://example.com/*/file_mixed.html"], + run_at: "document_end", + js: ["content_script_helper.js", "content_script.js"], + }, + ], + web_accessible_resources: ["image.png", "test_script.js"], + }, + background, + files: { + "content_script_helper.js": `${testImageLoading}`, + "content_script.js": content, + "test_script.js": testScript, + "image.png": IMAGE_ARRAYBUFFER, + }, + }); + + await SpecialPowers.pushPrefEnv({set: [ + ["security.mixed_content.upgrade_display_content", false], + ["security.mixed_content.block_display_content", true], + ]}); + + await Promise.all([ + extension.startup(), + extension.awaitMessage("background-ready"), + ]); + + let win = window.open( + "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_mixed.html" + ); + + await Promise.all([ + extension.awaitMessage("image-blocked"), + extension.awaitMessage("image-loaded"), + extension.awaitMessage("accessible-script-loaded"), + ]); + await extension.awaitFinish("mixed-test"); + win.close(); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); + +// test that MV2 extensions continue to open other MV2 extension pages +// when they are not listed in web_accessible_resources. This test also +// covers mobile/android tab creation. +add_task(async function test_web_accessible_resources_extensions_MV2() { + function background() { + let newtab; + let win; + let expectUrl; + browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + if (!expectUrl || tab.url != expectUrl || changeInfo.status !== "complete") { + return; + } + expectUrl = undefined; + browser.test.log(`onUpdated ${JSON.stringify(changeInfo)} ${tab.url}`); + browser.test.sendMessage("onUpdated", tab.url); + }); + browser.test.onMessage.addListener(async (msg, url) => { + browser.test.log(`onMessage ${msg} ${url}`); + expectUrl = url; + if (msg == "create") { + newtab = await browser.tabs.create({ url }); + browser.test.assertTrue( + newtab.id !== browser.tabs.TAB_ID_NONE, + "New tab was created." + ); + } else if (msg == "update") { + await browser.tabs.update(newtab.id, { url }); + } else if (msg == "remove") { + await browser.tabs.remove(newtab.id); + newtab = null; + browser.test.sendMessage("completed"); + } else if (msg == "open-window") { + win = await browser.windows.create({ url }); + } else if (msg == "close-window") { + await browser.windows.remove(win.id); + browser.test.sendMessage("completed"); + win = null; + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "this-mv2@mochitest" } }, + }, + background, + files: { + "page.html": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + </head></html>`, + }, + }); + + async function testTabsAction(ext, action, url) { + ext.sendMessage(action, url); + is(await ext.awaitMessage("onUpdated"), url, "extension url was loaded"); + } + + await extension.startup(); + let extensionUrl = `moz-extension://${extension.uuid}/page.html`; + + // Test opening its own pages + await testTabsAction(extension, "create", `${extensionUrl}?q=1`); + await testTabsAction(extension, "update", `${extensionUrl}?q=2`); + extension.sendMessage("remove"); + await extension.awaitMessage("completed"); + if (!ANDROID) { + await testTabsAction(extension, "open-window", `${extensionUrl}?q=3`); + extension.sendMessage("close-window"); + await extension.awaitMessage("completed"); + } + + // Extension used to open the homepage in a new window. + let other = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs", "<all_urls>"], + }, + background, + }); + await other.startup(); + + // Test opening another extensions pages + await testTabsAction(other, "create", `${extensionUrl}?q=4`); + await testTabsAction(other, "update", `${extensionUrl}?q=5`); + other.sendMessage("remove"); + await other.awaitMessage("completed"); + if (!ANDROID) { + await testTabsAction(other, "open-window", `${extensionUrl}?q=6`); + other.sendMessage("close-window"); + await other.awaitMessage("completed"); + } + + await extension.unload(); + await other.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html new file mode 100644 index 0000000000..12c90f8350 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html @@ -0,0 +1,610 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +if (AppConstants.platform === "android") { + SimpleTest.requestLongerTimeout(3); +} + +/* globals sendMouseEvent */ + +function backgroundScript() { + const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + const URL = BASE + "/file_WebNavigation_page1.html"; + + const EVENTS = [ + "onTabReplaced", + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", + "onHistoryStateUpdated", + ]; + + let expectedTabId = -1; + + function gotEvent(event, details) { + if (!details.url.startsWith(BASE)) { + return; + } + browser.test.log(`Got ${event} ${details.url} ${details.frameId} ${details.parentFrameId}`); + + if (expectedTabId == -1) { + browser.test.assertTrue(details.tabId !== undefined, "tab ID defined"); + expectedTabId = details.tabId; + } + + browser.test.assertEq(details.tabId, expectedTabId, "correct tab"); + + browser.test.sendMessage("received", {url: details.url, event}); + + if (details.url == URL) { + browser.test.assertEq(0, details.frameId, "root frame ID correct"); + browser.test.assertEq(-1, details.parentFrameId, "root parent frame ID correct"); + } else { + browser.test.assertEq(0, details.parentFrameId, "parent frame ID correct"); + browser.test.assertTrue(details.frameId != 0, "frame ID probably okay"); + } + + browser.test.assertTrue(details.frameId !== undefined, "frameId != undefined"); + browser.test.assertTrue(details.parentFrameId !== undefined, "parentFrameId != undefined"); + } + + let listeners = {}; + for (let event of EVENTS) { + listeners[event] = gotEvent.bind(null, event); + browser.webNavigation[event].addListener(listeners[event]); + } + + browser.test.sendMessage("ready"); +} + +const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; +const URL = BASE + "/file_WebNavigation_page1.html"; +const FORM_URL = URL + "?"; +const FRAME = BASE + "/file_WebNavigation_page2.html"; +const FRAME2 = BASE + "/file_WebNavigation_page3.html"; +const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html"; +const REDIRECT = BASE + "/redirection.sjs"; +const REDIRECTED = BASE + "/dummy_page.html"; +const CLIENT_REDIRECT = BASE + "/file_webNavigation_clientRedirect.html"; +const CLIENT_REDIRECT_HTTPHEADER = BASE + "/file_webNavigation_clientRedirect_httpHeaders.html"; +const FRAME_CLIENT_REDIRECT = BASE + "/file_webNavigation_frameClientRedirect.html"; +const FRAME_REDIRECT = BASE + "/file_webNavigation_frameRedirect.html"; +const FRAME_MANUAL = BASE + "/file_webNavigation_manualSubframe.html"; +const FRAME_MANUAL_PAGE1 = BASE + "/file_webNavigation_manualSubframe_page1.html"; +const FRAME_MANUAL_PAGE2 = BASE + "/file_webNavigation_manualSubframe_page2.html"; +const INVALID_PAGE = "https://invalid.localhost/"; + +const REQUIRED = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", +]; + +var received = []; +var completedResolve; +var waitingURL, waitingEvent; + +function loadAndWait(win, event, url, script) { + received = []; + waitingEvent = event; + waitingURL = url; + dump(`RUN ${script}\n`); + script(); + return new Promise(resolve => { completedResolve = resolve; }); +} + +add_task(async function webnav_transitions_props() { + function backgroundScriptTransitions() { + const EVENTS = [ + "onCommitted", + "onHistoryStateUpdated", + "onReferenceFragmentUpdated", + "onCompleted", + ]; + + function gotEvent(event, details) { + browser.test.log(`Got ${event} ${details.url} ${details.transitionType} ${details.transitionQualifiers && JSON.stringify(details.transitionQualifiers)}`); + + browser.test.sendMessage("received", {url: details.url, details, event}); + } + + let listeners = {}; + for (let event of EVENTS) { + listeners[event] = gotEvent.bind(null, event); + browser.webNavigation[event].addListener(listeners[event]); + } + + browser.test.sendMessage("ready"); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background: backgroundScriptTransitions, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + extension.onMessage("received", ({url, event, details}) => { + received.push({url, event, details}); + + if (event == waitingEvent && url == waitingURL) { + completedResolve(); + } + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + info("webnavigation extension loaded"); + + let win = window.open(); + + await loadAndWait(win, "onCompleted", URL, () => { win.location = URL; }); + + // transitionType: reload + received = []; + await loadAndWait(win, "onCompleted", URL, () => { win.location.reload(); }); + + let found = received.find((data) => (data.event == "onCommitted" && data.url == URL)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "reload", + "Got the expected 'reload' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers), + "transitionQualifiers found in the OnCommitted events"); + } + + // transitionType: auto_subframe + found = received.find((data) => (data.event == "onCommitted" && data.url == FRAME)); + + ok(found, "Got the sub-frame onCommitted event"); + + if (found) { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers), + "transitionQualifiers found in the OnCommitted events"); + } + + // transitionType: form_submit + received = []; + await loadAndWait(win, "onCompleted", FORM_URL, () => { + win.document.querySelector("form").submit(); + }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == FORM_URL)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "form_submit", + "Got the expected 'form_submit' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers), + "transitionQualifiers found in the OnCommitted events"); + } + + // transitionQualifier: server_redirect + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = REDIRECT; }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "link", + "Got the expected 'link' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "server_redirect"), + "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: forward_back + received = []; + await loadAndWait(win, "onCompleted", FORM_URL, () => { win.history.back(); }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == FORM_URL)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "link", + "Got the expected 'link' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "forward_back"), + "Got the expected 'forward_back' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: client_redirect + // (from meta http-equiv tag) + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { + win.location = CLIENT_REDIRECT; + }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "link", + "Got the expected 'link' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "client_redirect"), + "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: client_redirect + // (from http headers) + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { + win.location = CLIENT_REDIRECT_HTTPHEADER; + }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "link", + "Got the expected 'link' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "client_redirect"), + "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: client_redirect (sub-frame) + // (from meta http-equiv tag) + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { + win.location = FRAME_CLIENT_REDIRECT; + }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "client_redirect"), + "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: server_redirect (sub-frame) + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = FRAME_REDIRECT; }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECT)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); + // TODO BUG 1264936: currently the server_redirect is not detected in sub-frames + // once we fix it we can test it here: + // + // ok(Array.isArray(found.details.transitionQualifiers) && + // found.details.transitionQualifiers.find((q) => q == "server_redirect"), + // "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionType: manual_subframe + received = []; + await loadAndWait(win, "onCompleted", FRAME_MANUAL, () => { win.location = FRAME_MANUAL; }); + found = received.find((data) => (data.event == "onCommitted" && + data.url == FRAME_MANUAL_PAGE1)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); + } + + received = []; + await loadAndWait(win, "onCompleted", FRAME_MANUAL_PAGE2, () => { + let el = win.document.querySelector("iframe") + .contentDocument.querySelector("a"); + sendMouseEvent({type: "click"}, el, win); + }); + + found = received.find((data) => (data.event == "onCommitted" && + data.url == FRAME_MANUAL_PAGE2)); + + ok(found, "Got the onCommitted event"); + + if (found) { + if (AppConstants.MOZ_BUILD_APP === "browser") { + is(found.details.transitionType, "manual_subframe", + "Got the expected 'manual_subframe' transitionType in the OnCommitted event"); + } else { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'manual_subframe' transitionType in the OnCommitted event"); + } + } + + // Test transitions properties on onHistoryStateUpdated events. + + received = []; + await loadAndWait(win, "onCompleted", FRAME2, () => { win.location = FRAME2; }); + + received = []; + await loadAndWait(win, "onHistoryStateUpdated", `${FRAME2}/pushState`, () => { + win.history.pushState({}, "History PushState", `${FRAME2}/pushState`); + }); + + found = received.find((data) => (data.event == "onHistoryStateUpdated" && + data.url == `${FRAME2}/pushState`)); + + ok(found, "Got the onHistoryStateUpdated event"); + + if (found) { + is(typeof found.details.transitionType, "string", + "Got transitionType in the onHistoryStateUpdated event"); + ok(Array.isArray(found.details.transitionQualifiers), + "Got transitionQualifiers in the onHistoryStateUpdated event"); + } + + // Test transitions properties on onReferenceFragmentUpdated events. + + received = []; + await loadAndWait(win, "onReferenceFragmentUpdated", `${FRAME2}/pushState#ref2`, () => { + win.history.pushState({}, "ReferenceFragment Update", `${FRAME2}/pushState#ref2`); + }); + + found = received.find((data) => (data.event == "onReferenceFragmentUpdated" && + data.url == `${FRAME2}/pushState#ref2`)); + + ok(found, "Got the onReferenceFragmentUpdated event"); + + if (found) { + is(typeof found.details.transitionType, "string", + "Got transitionType in the onReferenceFragmentUpdated event"); + ok(Array.isArray(found.details.transitionQualifiers), + "Got transitionQualifiers in the onReferenceFragmentUpdated event"); + } + + // cleanup phase + win.close(); + + await extension.unload(); + info("webnavigation extension unloaded"); +}); + +add_task(async function webnav_ordering() { + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background: backgroundScript, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + extension.onMessage("received", ({url, event}) => { + received.push({url, event}); + + if (event == waitingEvent && url == waitingURL) { + completedResolve(); + } + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + info("webnavigation extension loaded"); + + let win = window.open(); + + await loadAndWait(win, "onCompleted", URL, () => { win.location = URL; }); + + function checkRequired(url) { + for (let event of REQUIRED) { + let found = false; + for (let r of received) { + if (r.url == url && r.event == event) { + found = true; + } + } + ok(found, `Received event ${event} from ${url}`); + } + } + + checkRequired(URL); + checkRequired(FRAME); + + function checkBefore(action1, action2) { + function find(action) { + for (let i = 0; i < received.length; i++) { + if (received[i].url == action.url && received[i].event == action.event) { + return i; + } + } + return -1; + } + + let index1 = find(action1); + let index2 = find(action2); + ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`); + ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`); + ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`); + } + + // As required in the webNavigation API documentation: + // If a navigating frame contains subframes, its onCommitted is fired before any + // of its children's onBeforeNavigate; while onCompleted is fired after + // all of its children's onCompleted. + checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"}); + checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"}); + + // As required in the webNAvigation API documentation, check the event sequence: + // onBeforeNavigate -> onCommitted -> onDOMContentLoaded -> onCompleted + let expectedEventSequence = [ + "onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted", + ]; + + for (let i = 1; i < expectedEventSequence.length; i++) { + let after = expectedEventSequence[i]; + let before = expectedEventSequence[i - 1]; + checkBefore({url: URL, event: before}, {url: URL, event: after}); + checkBefore({url: FRAME, event: before}, {url: FRAME, event: after}); + } + + await loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; }); + + checkRequired(FRAME2); + + let navigationSequence = [ + { + action: () => { win.frames[0].document.getElementById("elt").click(); }, + waitURL: `${FRAME2}#ref`, + expectedEvent: "onReferenceFragmentUpdated", + description: "clicked an anchor link", + }, + { + action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); }, + waitURL: `${FRAME2}#ref2`, + expectedEvent: "onReferenceFragmentUpdated", + description: "history.pushState, same pathname, different hash", + }, + { + action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); }, + waitURL: `${FRAME2}#ref2`, + expectedEvent: "onHistoryStateUpdated", + description: "history.pushState, same pathname, same hash", + }, + { + action: () => { + win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param1=value#ref2`); + }, + waitURL: `${FRAME2}?query_param1=value#ref2`, + expectedEvent: "onHistoryStateUpdated", + description: "history.pushState, same pathname, same hash, different query params", + }, + { + action: () => { + win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param2=value#ref3`); + }, + waitURL: `${FRAME2}?query_param2=value#ref3`, + expectedEvent: "onHistoryStateUpdated", + description: "history.pushState, same pathname, different hash, different query params", + }, + { + action: () => { win.frames[0].history.pushState(null, "History PushState", FRAME_PUSHSTATE); }, + waitURL: FRAME_PUSHSTATE, + expectedEvent: "onHistoryStateUpdated", + description: "history.pushState, different pathname", + }, + ]; + + for (let navigation of navigationSequence) { + let {expectedEvent, waitURL, action, description} = navigation; + info(`Waiting ${expectedEvent} from ${waitURL} - ${description}`); + await loadAndWait(win, expectedEvent, waitURL, action); + info(`Received ${expectedEvent} from ${waitURL} - ${description}`); + } + + for (let i = navigationSequence.length - 1; i > 0; i--) { + let {waitURL: fromURL, expectedEvent} = navigationSequence[i]; + let {waitURL} = navigationSequence[i - 1]; + info(`Waiting ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`); + await loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.back(); }); + info(`Received ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`); + } + + for (let i = 0; i < navigationSequence.length - 1; i++) { + let {waitURL: fromURL} = navigationSequence[i]; + let {waitURL, expectedEvent} = navigationSequence[i + 1]; + info(`Waiting ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`); + await loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); }); + info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`); + } + + win.close(); + + await extension.unload(); + info("webnavigation extension unloaded"); +}); + +add_task(async function webnav_error_event() { + function backgroundScriptErrorEvent() { + browser.webNavigation.onErrorOccurred.addListener((details) => { + browser.test.log(`Got onErrorOccurred ${details.url} ${details.error}`); + + browser.test.sendMessage("received", {url: details.url, details, event: "onErrorOccurred"}); + }); + + browser.test.sendMessage("ready"); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background: backgroundScriptErrorEvent, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + extension.onMessage("received", ({url, event, details}) => { + received.push({url, event, details}); + + if (event == waitingEvent && url == waitingURL) { + completedResolve(); + } + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + info("webnavigation extension loaded"); + + let win = window.open(); + + received = []; + await loadAndWait(win, "onErrorOccurred", INVALID_PAGE, () => { win.location = INVALID_PAGE; }); + + let found = received.find((data) => (data.event == "onErrorOccurred" && + data.url == INVALID_PAGE)); + + ok(found, "Got the onErrorOccurred event"); + + if (found) { + ok(found.details.error.match(/Error code [0-9]+/), + "Got the expected error string in the onErrorOccurred event"); + } + + // cleanup phase + win.close(); + + await extension.unload(); + info("webnavigation extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html new file mode 100644 index 0000000000..19cb6539d7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html @@ -0,0 +1,313 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_webnav_unresolved_uri_on_expected_URI_scheme() { + function background() { + let listeners = []; + + function cleanupTestListeners() { + browser.test.log(`Cleanup previous test event listeners`); + for (let {event, listener} of listeners.splice(0)) { + browser.webNavigation[event].removeListener(listener); + } + } + + function createTestListener(event, fail, urlFilter) { + return new Promise(resolve => { + function listener(details) { + let log = JSON.stringify({url: details.url, urlFilter}); + if (fail) { + browser.test.fail(`Got an unexpected ${event} on the failure listener: ${log}`); + } else { + browser.test.succeed(`Got the expected ${event} on the success listener: ${log}`); + } + + resolve(); + } + + browser.webNavigation[event].addListener(listener, {url: urlFilter}); + listeners.push({event, listener}); + }); + } + + browser.test.onMessage.addListener((msg, events, data) => { + if (msg !== "test-filters") { + return; + } + + let promises = []; + + for (let {okFilter, failFilter} of data.filters) { + for (let event of events) { + promises.push( + Promise.race([ + createTestListener(event, false, okFilter), + createTestListener(event, true, failFilter), + ])); + } + } + + Promise.all(promises).catch(e => { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + }).then(() => { + cleanupTestListeners(); + browser.test.sendMessage("test-filter-next"); + }); + + browser.test.sendMessage("test-filter-ready"); + }); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let win = window.open(); + + let testFilterScenarios = [ + { + url: "https://example.net/browser", + filters: [ + // schemes + { + okFilter: [{schemes: ["https"]}], + failFilter: [{schemes: ["http"]}], + }, + // ports + { + okFilter: [{ports: [80, 22, 443]}], + failFilter: [{ports: [81, 82, 83]}], + }, + { + okFilter: [{ports: [22, 443, [10, 80]]}], + failFilter: [{ports: [22, 23, [81, 100]]}], + }, + // multiple criteria in a single filter: + // if one of the criteria is not verified, the event should not be received. + { + okFilter: [{schemes: ["https"], ports: [80, 22, 443]}], + failFilter: [{schemes: ["https"], ports: [81, 82, 83]}], + }, + { + okFilter: [{hostEquals: "example.net", ports: [80, 22, 443]}], + failFilter: [{hostEquals: "example.org", ports: [80, 22, 443]}], + }, + // multiple urlFilters on the same listener + // if at least one of the criteria is verified, the event should be received. + { + okFilter: [{schemes: ["http"]}, {ports: [80, 22, 443]}], + failFilter: [{schemes: ["http"]}, {ports: [81, 82, 83]}], + }, + ], + }, + { + url: "https://example.net/browser?param=1#ref", + filters: [ + // host: Equals, Contains, Prefix, Suffix + { + okFilter: [{hostEquals: "example.net"}], + failFilter: [{hostEquals: "example.com"}], + }, + { + okFilter: [{hostContains: ".example"}], + failFilter: [{hostContains: ".www"}], + }, + { + okFilter: [{hostPrefix: "example"}], + failFilter: [{hostPrefix: "www"}], + }, + { + okFilter: [{hostSuffix: "net"}], + failFilter: [{hostSuffix: "com"}], + }, + // path: Equals, Contains, Prefix, Suffix + { + okFilter: [{pathEquals: "/browser"}], + failFilter: [{pathEquals: "/"}], + }, + { + okFilter: [{pathContains: "brow"}], + failFilter: [{pathContains: "tool"}], + }, + { + okFilter: [{pathPrefix: "/bro"}], + failFilter: [{pathPrefix: "/tool"}], + }, + { + okFilter: [{pathSuffix: "wser"}], + failFilter: [{pathSuffix: "kit"}], + }, + // query: Equals, Contains, Prefix, Suffix + { + okFilter: [{queryEquals: "param=1"}], + failFilter: [{queryEquals: "wrongparam=2"}], + }, + { + okFilter: [{queryContains: "param"}], + failFilter: [{queryContains: "wrongparam"}], + }, + { + okFilter: [{queryPrefix: "param="}], + failFilter: [{queryPrefix: "wrong"}], + }, + { + okFilter: [{querySuffix: "=1"}], + failFilter: [{querySuffix: "=2"}], + }, + // urlMatches, originAndPathMatches + { + okFilter: [{urlMatches: "example.net/.*\?param=1"}], + failFilter: [{urlMatches: "example.net/.*\?wrongparam=2"}], + }, + { + okFilter: [{originAndPathMatches: "example.net\/browser"}], + failFilter: [{originAndPathMatches: "example.net/.*\?param=1"}], + }, + ], + }, + ]; + + info("WebNavigation event filters test scenarios starting..."); + + const EVENTS = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + ]; + + for (let data of testFilterScenarios) { + info(`Prepare the new test scenario: ${JSON.stringify(data)}`); + + win.location = "about:blank"; + + // Wait for the about:blank load to finish before continuing, in case this + // load is causing a process switch back into our process. + await SimpleTest.promiseWaitForCondition(() => { + try { + return win.location.href == "about:blank" && + win.document.readyState == "complete"; + } catch (e) { + return false; + } + }); + + extension.sendMessage("test-filters", EVENTS, data); + await extension.awaitMessage("test-filter-ready"); + + info(`Loading the test url: ${data.url}`); + win.location = data.url; + + await extension.awaitMessage("test-filter-next"); + + info("Test scenario completed. Moving to the next test scenario."); + } + + info("WebNavigation event filters test onReferenceFragmentUpdated scenario starting..."); + + const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + let url = BASE + "/file_WebNavigation_page3.html"; + + let okFilter = [{urlContains: "_page3.html"}]; + let failFilter = [{ports: [444]}]; + let data = {filters: [{okFilter, failFilter}]}; + let event = "onCompleted"; + + info(`Loading the initial test url: ${url}`); + extension.sendMessage("test-filters", [event], data); + + await extension.awaitMessage("test-filter-ready"); + win.location = url; + await extension.awaitMessage("test-filter-next"); + + event = "onReferenceFragmentUpdated"; + extension.sendMessage("test-filters", [event], data); + + await extension.awaitMessage("test-filter-ready"); + win.location = url + "#ref1"; + await extension.awaitMessage("test-filter-next"); + + info("WebNavigation event filters test onHistoryStateUpdated scenario starting..."); + + event = "onHistoryStateUpdated"; + extension.sendMessage("test-filters", [event], data); + await extension.awaitMessage("test-filter-ready"); + + win.history.pushState({}, "", BASE + "/pushState_page3.html"); + await extension.awaitMessage("test-filter-next"); + + // TODO: add additional specific tests for the other webNavigation events: + // onErrorOccurred (and onCreatedNavigationTarget on supported) + + info("WebNavigation event filters test scenarios completed."); + + await extension.unload(); + + win.close(); +}); + +add_task(async function test_webnav_empty_filter_validation_error() { + function background() { + let catchedException; + + try { + browser.webNavigation.onCompleted.addListener( + // Empty callback (not really used) + () => {}, + // Empty filter (which should raise a validation error exception). + {url: []} + ); + } catch (e) { + catchedException = e; + browser.test.log(`Got an exception`); + } + + if (catchedException && + catchedException.message.includes("Type error for parameter filters") && + catchedException.message.includes("Array requires at least 1 items; you have 0")) { + browser.test.notifyPass("webNav.emptyFilterValidationError"); + } else { + browser.test.notifyFail("webNav.emptyFilterValidationError"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webNavigation", + ], + }, + background, + }); + + await extension.startup(); + + await extension.awaitFinish("webNav.emptyFilterValidationError"); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html new file mode 100644 index 0000000000..45147365ee --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function webnav_test_incognito() { + // Monitor will fail if it gets any event. + let monitor = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["webNavigation", "*://mochi.test/*"], + }, + background() { + const EVENTS = [ + "onTabReplaced", + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", + "onHistoryStateUpdated", + ]; + + function onEvent(event, details) { + browser.test.fail(`not_allowed - Got ${event} ${details.url} ${details.frameId} ${details.parentFrameId}`); + } + + let listeners = {}; + for (let event of EVENTS) { + listeners[event] = onEvent.bind(null, event); + browser.webNavigation[event].addListener(listeners[event]); + } + + browser.test.onMessage.addListener(async (message, tabId) => { + // try to access the private window + await browser.test.assertRejects(browser.webNavigation.getAllFrames({tabId}), + /Invalid tab ID/, + "should not be able to get incognito frames"); + await browser.test.assertRejects(browser.webNavigation.getFrame({tabId, frameId: 0}), + /Invalid tab ID/, + "should not be able to get incognito frames"); + browser.test.notifyPass("completed"); + }); + }, + }); + + // extension loads a private window and waits for the onCompleted event. + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + manifest: { + permissions: ["tabs", "webNavigation", "*://mochi.test/*"], + }, + async background() { + const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + const url = BASE + "/file_WebNavigation_page1.html"; + let window; + + browser.webNavigation.onCompleted.addListener(async (details) => { + if (details.url !== url) { + return; + } + browser.test.log(`spanning - Got onCompleted ${details.url} ${details.frameId} ${details.parentFrameId}`); + browser.test.sendMessage("completed"); + }); + browser.test.onMessage.addListener(async () => { + await browser.windows.remove(window.id); + browser.test.notifyPass("done"); + }); + window = await browser.windows.create({url, incognito: true}); + let tabs = await browser.tabs.query({active: true, windowId: window.id}); + browser.test.sendMessage("tabId", tabs[0].id); + }, + }); + + await monitor.startup(); + await extension.startup(); + + await extension.awaitMessage("completed"); + let tabId = await extension.awaitMessage("tabId"); + + await monitor.sendMessage("tab", tabId); + await monitor.awaitFinish("completed"); + + await extension.sendMessage("close"); + await extension.awaitFinish("done"); + + await extension.unload(); + await monitor.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html new file mode 100644 index 0000000000..b28cbb7635 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +<script> +"use strict"; + +// Check that the windowId and tabId filter work as expected in the webRequest +// and proxy API: +// - A non-matching windowId / tabId listener won't trigger events. +// - A matching tabId from a tab triggers the event. +// - A matching windowId from a tab triggers the event. +// (unlike test_ext_webrequest_filter.html, this also works on Android) +// - Requests from background pages can be matched with windowId and tabId -1. +add_task(async function test_filter_tabId_and_windowId() { + async function tabScript() { + let pendingExpectations = new Set(); + // Helper to detect completion of expected requests. + function watchExpected(filter, desc) { + desc += ` - ${JSON.stringify(filter)}`; + const DESC_PROXY = `${desc} (proxy)`; + const DESC_WEBREQUEST = `${desc} (webRequest)`; + pendingExpectations.add(DESC_PROXY); + pendingExpectations.add(DESC_WEBREQUEST); + browser.proxy.onRequest.addListener(() => { + pendingExpectations.delete(DESC_PROXY); + }, filter); + browser.webRequest.onBeforeRequest.addListener( + () => { + pendingExpectations.delete(DESC_WEBREQUEST); + }, + filter, + ["blocking"] + ); + } + + // Helper to detect unexpected requests. + function watchUnexpected(filter, desc) { + desc += ` - ${JSON.stringify(filter)}`; + browser.proxy.onRequest.addListener(() => { + browser.test.fail(`${desc} - unexpected proxy event`); + }, filter); + browser.webRequest.onBeforeRequest.addListener(() => { + browser.test.fail(`${desc} - unexpected webRequest event`); + }, filter); + } + + function registerExpectations(url, windowId, tabId) { + const urls = [url]; + watchUnexpected({ urls, windowId: 0 }, "non-matching windowId"); + watchUnexpected({ urls, tabId: 0 }, "non-matching tabId"); + + watchExpected({ urls, windowId }, "windowId matches"); + watchExpected({ urls, tabId }, "tabId matches"); + } + + try { + let { windowId, tabId } = await browser.runtime.sendMessage("getIds"); + browser.test.log(`Dummy tab has: tabId=${tabId} windowId=${windowId}`); + registerExpectations("http://example.com/?tab", windowId, tabId); + registerExpectations("http://example.com/?bg", -1, -1); + + // Call an API method implemented in the parent process to ensure that + // the listeners have been registered (workaround for bug 1300234). + // There is a .catch() at the end because the call is rejected on Android. + await browser.proxy.settings.get({}).catch(() => {}); + + browser.test.log("Triggering request from background page."); + await browser.runtime.sendMessage("triggerBackgroundRequest"); + + browser.test.log("Triggering request from tab."); + await fetch("http://example.com/?tab"); + + browser.test.assertEq(0, pendingExpectations.size, "got all events"); + for (let description of pendingExpectations) { + browser.test.fail(`Event not observed: ${description}`); + } + } catch (e) { + browser.test.fail(`Unexpected test failure: ${e} :: ${e.stack}`); + } + browser.runtime.sendMessage("testCompleted"); + } + + function background() { + browser.runtime.onMessage.addListener(async (msg, sender) => { + if (msg === "getIds") { + return { windowId: sender.tab.windowId, tabId: sender.tab.id }; + } + if (msg === "triggerBackgroundRequest") { + await fetch("http://example.com/?bg"); + } + if (msg === "testCompleted") { + await browser.tabs.remove(sender.tab.id); + browser.test.sendMessage("testCompleted"); + } + }); + browser.tabs.create({ + url: browser.runtime.getURL("tab.html"), + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "proxy", + "webRequest", + "webRequestBlocking", + "http://example.com/*", + ], + }, + background, + files: { + "tab.html": `<!DOCTYPE html><script src="tab.js"><\/script>`, + "tab.js": tabScript, + }, + }); + await extension.startup(); + + await extension.awaitMessage("testCompleted"); + await extension.unload(); +}); +</script> +</head> +<body> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html new file mode 100644 index 0000000000..f260f040a1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html @@ -0,0 +1,181 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +// This file defines content scripts. + +let baseUrl = "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/authenticate.sjs"; +function testXHR(url) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.onload = resolve; + xhr.onabort = reject; + xhr.onerror = reject; + xhr.send(); + }); +} + +function getAuthHandler(result, blocking = true) { + function background(result) { + browser.webRequest.onAuthRequired.addListener((details) => { + browser.test.succeed(`authHandler.onAuthRequired called with ${details.requestId} ${details.url} result ${JSON.stringify(result)}`); + browser.test.sendMessage("onAuthRequired"); + return result; + }, {urls: ["*://mochi.test/*"]}, ["blocking"]); + browser.webRequest.onCompleted.addListener((details) => { + browser.test.succeed(`authHandler.onCompleted called with ${details.requestId} ${details.url}`); + browser.test.sendMessage("onCompleted"); + }, {urls: ["*://mochi.test/*"]}); + browser.webRequest.onErrorOccurred.addListener((details) => { + browser.test.succeed(`authHandler.onErrorOccurred called with ${details.requestId} ${details.url}`); + browser.test.sendMessage("onErrorOccurred"); + }, {urls: ["*://mochi.test/*"]}); + } + + let permissions = [ + "webRequest", + "*://mochi.test/*", + ]; + if (blocking) { + permissions.push("webRequestBlocking"); + } + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + }, + background: `(${background})(${JSON.stringify(result)})`, + }); +} + +add_task(async function test_webRequest_auth_nonblocking_forwardAuthProvider() { + // The chrome script sets up a default auth handler on the channel, the + // extension does not return anything in the authRequred call. We should + // get the call in the extension first, then in the chrome code where we + // cancel the request to avoid dealing with the prompt dialog here. The test + // is to ensure that WebRequest calls the previous notificationCallbacks + // if the authorization is not handled by the onAuthRequired handler. + + let chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + let observer = channel => { + if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test" && + channel.URI.spec.includes("authenticate.sjs"))) { + return; + } + Services.obs.removeObserver(observer, "http-on-modify-request"); + channel.notificationCallbacks = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor", + "nsIAuthPromptProvider", + "nsIAuthPrompt2"]), + getInterface: ChromeUtils.generateQI(["nsIAuthPromptProvider", + "nsIAuthPrompt2"]), + promptAuth(channel, level, authInfo) { + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + getAuthPrompt(reason, iid) { + return this; + }, + asyncPromptAuth(channel, callback, context, level, authInfo) { + // We just cancel here, we're only ensuring that non-webrequest + // notificationcallbacks get called if webrequest doesn't handle it. + Promise.resolve().then(() => { + callback.onAuthCancelled(context, false); + channel.cancel(Cr.NS_BINDING_ABORTED); + sendAsyncMessage("callback-complete"); + }); + }, + }; + }; + Services.obs.addObserver(observer, "http-on-modify-request"); + sendAsyncMessage("chrome-ready"); + }); + await chromeScript.promiseOneMessage("chrome-ready"); + let callbackComplete = chromeScript.promiseOneMessage("callback-complete"); + + let handlingExt = getAuthHandler(); + await handlingExt.startup(); + + await Assert.rejects(testXHR(`${baseUrl}?realm=auth_nonblocking_forwardAuth&user=auth_nonblocking_forwardAuth&pass=auth_nonblocking_forwardAuth`), + ProgressEvent, + "caught rejected xhr"); + + await callbackComplete; + await handlingExt.awaitMessage("onAuthRequired"); + // We expect onErrorOccurred because the "default" authprompt above cancelled + // the auth request to avoid a dialog. + await handlingExt.awaitMessage("onErrorOccurred"); + await handlingExt.unload(); + chromeScript.destroy(); +}); + +add_task(async function test_webRequest_auth_nonblocking_forwardAuthPrompt2() { + // The chrome script sets up a default auth handler on the channel, the + // extension does not return anything in the authRequred call. We should + // get the call in the extension first, then in the chrome code where we + // cancel the request to avoid dealing with the prompt dialog here. The test + // is to ensure that WebRequest calls the previous notificationCallbacks + // if the authorization is not handled by the onAuthRequired handler. + + let chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + let observer = channel => { + if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test" && + channel.URI.spec.includes("authenticate.sjs"))) { + return; + } + Services.obs.removeObserver(observer, "http-on-modify-request"); + channel.notificationCallbacks = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor", + "nsIAuthPrompt2"]), + getInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + promptAuth(request, level, authInfo) { + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + asyncPromptAuth(request, callback, context, level, authInfo) { + // We just cancel here, we're only ensuring that non-webrequest + // notificationcallbacks get called if webrequest doesn't handle it. + Promise.resolve().then(() => { + request.cancel(Cr.NS_BINDING_ABORTED); + sendAsyncMessage("callback-complete"); + }); + }, + }; + }; + Services.obs.addObserver(observer, "http-on-modify-request"); + sendAsyncMessage("chrome-ready"); + }); + await chromeScript.promiseOneMessage("chrome-ready"); + let callbackComplete = chromeScript.promiseOneMessage("callback-complete"); + + let handlingExt = getAuthHandler(); + await handlingExt.startup(); + + await Assert.rejects(testXHR(`${baseUrl}?realm=auth_nonblocking_forwardAuthPromptProvider&user=auth_nonblocking_forwardAuth&pass=auth_nonblocking_forwardAuth`), + ProgressEvent, + "caught rejected xhr"); + + await callbackComplete; + await handlingExt.awaitMessage("onAuthRequired"); + // We expect onErrorOccurred because the "default" authprompt above cancelled + // the auth request to avoid a dialog. + await handlingExt.awaitMessage("onErrorOccurred"); + await handlingExt.unload(); + chromeScript.destroy(); +}); +</script> +</head> +<body> +<div id="test">Authorization Test</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html new file mode 100644 index 0000000000..86cec62fb4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_webRequest_serviceworker_events() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "<all_urls>", + ], + }, + background() { + let eventNames = new Set([ + "onBeforeRequest", + "onBeforeSendHeaders", + "onSendHeaders", + "onHeadersReceived", + "onResponseStarted", + "onCompleted", + "onErrorOccurred", + ]); + + function listener(name, details) { + browser.test.assertTrue(eventNames.has(name), `received ${name}`); + eventNames.delete(name); + if (name == "onCompleted") { + eventNames.delete("onErrorOccurred"); + } else if (name == "onErrorOccurred") { + eventNames.delete("onCompleted"); + } + if (eventNames.size == 0) { + browser.test.sendMessage("done"); + } + } + + for (let name of eventNames) { + browser.webRequest[name].addListener( + listener.bind(null, name), + {urls: ["https://example.com/*"]} + ); + } + }, + }); + + await extension.startup(); + let registration = await navigator.serviceWorker.register("webrequest_worker.js", {scope: "."}); + await waitForState(registration.installing, "activated"); + await extension.awaitMessage("done"); + await registration.unregister(); + await extension.unload(); +}); + +add_task(async function test_webRequest_background_events() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "<all_urls>", + ], + }, + background() { + let eventNames = new Set([ + "onBeforeRequest", + "onBeforeSendHeaders", + "onSendHeaders", + "onHeadersReceived", + "onResponseStarted", + "onCompleted", + ]); + + function listener(name, details) { + browser.test.assertTrue(eventNames.has(name), `received ${name}`); + eventNames.delete(name); + + if (eventNames.size === 0) { + browser.test.assertEq("xmlhttprequest", details.type, "correct type for fetch [see bug 1366710]"); + browser.test.assertEq(0, eventNames.size, "messages received"); + browser.test.sendMessage("done"); + } + } + + for (let name of eventNames) { + browser.webRequest[name].addListener( + listener.bind(null, name), + {urls: ["https://example.com/*"]} + ); + } + + fetch("https://example.com/example.txt").then(() => { + browser.test.succeed("Fetch succeeded."); + }, () => { + browser.test.fail("fetch received"); + browser.test.sendMessage("done"); + }); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html new file mode 100644 index 0000000000..9d57d55681 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html @@ -0,0 +1,445 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +const expectedBaseProps = { + // On Desktop builds, if "browse.chrome.guess_favicon" is set to true, + // a favicon requests may be triggered at a random time while the test + // cases are running, we include it the ignore list by default to prevent + // intermittent failures (e.g. see Bug 1733781 and Bug 1633189). + ignore: ["favicon.ico"], +}; + +function promiseWindowEvent(name, accept) { + return new Promise(resolve => { + window.addEventListener(name, function listener(event) { + if (event.data !== accept) { + return; + } + window.removeEventListener(name, listener); + resolve(event); + }); + }); +} + +if (AppConstants.platform === "android") { + SimpleTest.requestLongerTimeout(3); +} + +let extension; +add_task(async function setup() { + // Clear the image cache, since it gets in the way otherwise. + let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); + let cache = imgTools.getImgCacheForDocument(document); + cache.clearCache(false); + await SpecialPowers.spawnChrome([], async () => { + Services.cache2.clear(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [["network.http.rcwn.enabled", false]], + }); + + extension = makeExtension(); + await extension.startup(); +}); + +// expect is a set of test values used by the background script. +// +// type: type of request action +// events: optional, If defined only the events listed are expected for the +// request. If undefined, all events except onErrorOccurred +// and onBeforeRedirect are expected. Must be in order received. +// redirect: url to redirect to during onBeforeSendHeaders +// status: number expected status during onHeadersReceived, 200 default +// cancel: event in which we return cancel=true. cancelled message is sent. +// cached: expected fromCache value, default is false, checked in onCompletion +// headers: request or response headers to modify +// origin: The expected originUrl, a default origin can be passed for all files + +add_task(async function test_webRequest_links() { + let expect = { + "file_style_bad.css": { + type: "stylesheet", + events: ["onBeforeRequest", "onErrorOccurred"], + cancel: "onBeforeRequest", + }, + "file_style_redirect.css": { + type: "stylesheet", + events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], + optional_events: ["onHeadersReceived"], + redirect: "file_style_good.css", + }, + "file_style_good.css": { + type: "stylesheet", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addStylesheet("file_style_bad.css"); + await extension.awaitMessage("cancelled"); + // we redirect to style_good which completes the test + addStylesheet(`file_style_redirect.css?nocache=${Math.random()}`); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_images() { + let expect = { + "file_image_bad.png": { + type: "image", + events: ["onBeforeRequest", "onErrorOccurred"], + cancel: "onBeforeRequest", + }, + "file_image_redirect.png": { + type: "image", + events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], + optional_events: ["onHeadersReceived"], + redirect: "file_image_good.png", + }, + "file_image_good.png": { + type: "image", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addImage("file_image_bad.png"); + await extension.awaitMessage("cancelled"); + // we redirect to image_good which completes the test + addImage("file_image_redirect.png"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_scripts() { + let expect = { + "file_script_bad.js": { + type: "script", + events: ["onBeforeRequest", "onErrorOccurred"], + cancel: "onBeforeRequest", + }, + "file_script_redirect.js": { + type: "script", + events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], + optional_events: ["onHeadersReceived"], + redirect: "file_script_good.js", + }, + "file_script_good.js": { + type: "script", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + let message = promiseWindowEvent("message", "test1"); + addScript("file_script_bad.js"); + await extension.awaitMessage("cancelled"); + // we redirect to script_good which completes the test + addScript("file_script_redirect.js?q=test1"); + await extension.awaitMessage("done"); + + is((await message).data, "test1", "good script ran"); +}); + +add_task(async function test_webRequest_xhr_get() { + let expect = { + "file_script_xhr.js": { + type: "script", + }, + "xhr_resource": { + status: 404, + type: "xmlhttprequest", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addScript("file_script_xhr.js"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_nonexistent() { + let expect = { + "nonexistent_script_url.js": { + status: 404, + type: "script", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addScript("nonexistent_script_url.js"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_checkCached() { + let expect = { + "file_image_good.png": { + type: "image", + cached: true, + }, + "file_script_good.js": { + type: "script", + cached: true, + }, + "file_style_good.css": { + type: "stylesheet", + cached: false, + }, + "nonexistent_script_url.js": { + status: 404, + type: "script", + cached: false, + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + let message = promiseWindowEvent("message", "test1"); + + addImage("file_image_good.png"); + addScript("file_script_good.js?q=test1"); + + is((await message).data, "test1", "good script ran"); + + addStylesheet(`file_style_good.css?nocache=${Math.random()}`); + addScript("nonexistent_script_url.js"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_headers() { + let expect = { + "file_script_nonexistent.js": { + type: "script", + status: 404, + headers: { + request: { + add: { + "X-WebRequest-request": "text", + "X-WebRequest-request-binary": "binary", + }, + modify: { + "user-agent": "WebRequest", + }, + remove: [ + "referer", + ], + }, + response: { + add: { + "X-WebRequest-response": "text", + "X-WebRequest-response-binary": "binary", + }, + modify: { + "server": "WebRequest", + "content-type": "text/html; charset=utf-8", + }, + remove: [ + "connection", + ], + }, + }, + completion: "onCompleted", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addScript("file_script_nonexistent.js"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_tabId() { + function background() { + let tab; + browser.tabs.onCreated.addListener(newTab => { + tab = newTab; + }); + + browser.test.onMessage.addListener(msg => { + if (msg === "close-tab") { + browser.tabs.remove(tab.id); + browser.test.sendMessage("tab-closed"); + } + }); + } + + let tabExt = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "tabs", + ], + }, + background, + }); + await tabExt.startup(); + + let linkUrl = `file_WebRequest_page3.html?trigger=a&nocache=${Math.random()}`; + let expect = { + "file_WebRequest_page3.html": { + type: "main_frame", + }, + }; + + extension.sendMessage("set-expected", { + ...expectedBaseProps, + expect, + origin: location.href, + }); + await extension.awaitMessage("continue"); + let a = addLink(linkUrl); + a.click(); + await extension.awaitMessage("done"); + + let closed = tabExt.awaitMessage("tab-closed"); + tabExt.sendMessage("close-tab"); + await closed; + + await tabExt.unload(); +}); + +add_task(async function test_webRequest_tabId_browser() { + async function background(url) { + let tabId; + browser.test.onMessage.addListener(async (msg, expected) => { + if (msg == "create") { + let tab = await browser.tabs.create({url}); + tabId = tab.id; + return; + } + if (msg == "done") { + await browser.tabs.remove(tabId); + browser.test.sendMessage("done"); + } + }); + browser.test.sendMessage("origin", browser.runtime.getURL("/")); + } + + let pageUrl = `${SimpleTest.getTestFileURL("file_sample.html")}?nocache=${Math.random()}`; + let tabExt = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "tabs", + ], + }, + background: `(${background})('${pageUrl}')`, + }); + + let expect = { + "file_sample.html": { + type: "main_frame", + }, + }; + + await tabExt.startup(); + let origin = await tabExt.awaitMessage("origin"); + + // expecting origin == extension baseUrl + extension.sendMessage("set-expected", { + ...expectedBaseProps, + expect, + origin, + }); + await extension.awaitMessage("continue"); + + // open a tab from an extension principal + tabExt.sendMessage("create"); + await extension.awaitMessage("done"); + tabExt.sendMessage("done"); + await tabExt.awaitMessage("done"); + await tabExt.unload(); +}); + +add_task(async function test_webRequest_frames() { + let expect = { + "redirection.sjs": { + status: 302, + type: "sub_frame", + events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onBeforeRedirect"], + }, + "dummy_page.html": { + type: "sub_frame", + status: 404, + }, + "badrobot": { + type: "sub_frame", + status: 404, + events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onErrorOccurred"], + // When an url's hostname fails to be resolved, an NS_ERROR_NET_ON_RESOLVED/RESOLVING + // onError event may be fired right before the NS_ERROR_UNKNOWN_HOST + // (See Bug 1516862 for a rationale). + optional_events: ["onErrorOccurred"], + error: ["NS_ERROR_UNKNOWN_HOST", "NS_ERROR_NET_ON_RESOLVED", "NS_ERROR_NET_ON_RESOLVING"], + }, + }; + extension.sendMessage("set-expected", { + ...expectedBaseProps, + expect, + origin: location.href + }); + await extension.awaitMessage("continue"); + addFrame("redirection.sjs"); + addFrame("https://nonresolvablehostname.invalid/badrobot"); + await extension.awaitMessage("done"); +}); + +add_task(async function teardown() { + await extension.unload(); +}); + +add_task(async function test_case_preserving() { + const manifest = { + permissions: [ + "webRequest", + "webRequestBlocking", + "http://mochi.test/", + ], + }; + + async function background() { + // This is testing if header names preserve case, + // so the case-sensitive comparison is on purpose. + function ua({url, requestHeaders}) { + if (url.endsWith("?blind-add")) { + requestHeaders.push({name: "user-agent", value: "Blind/Add"}); + return {requestHeaders}; + } + for (const header of requestHeaders) { + if (header.name === "User-Agent") { + header.value = "Case/Sensitive"; + } + } + return {requestHeaders}; + } + + await browser.webRequest.onBeforeSendHeaders.addListener(ua, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]); + browser.test.sendMessage("ready"); + } + + const extension = ExtensionTestUtils.loadExtension({manifest, background}); + + await extension.startup(); + await extension.awaitMessage("ready"); + + const response1 = await fetch(SimpleTest.getTestFileURL("return_headers.sjs")); + const headers1 = JSON.parse(await response1.text()); + + is(headers1["user-agent"], "Case/Sensitive", "User-Agent header matched and changed."); + + const response2 = await fetch(SimpleTest.getTestFileURL("return_headers.sjs?blind-add")); + const headers2 = JSON.parse(await response2.text()); + + is(headers2["user-agent"], "Blind/Add", "User-Agent header blindly added."); + + await extension.unload(); +}); + +</script> +</head> +<body> +<div id="test">Sample text</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html new file mode 100644 index 0000000000..cbfc5c17e7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebRequest errors</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="text/javascript"> +"use strict"; + +async function test_connection_refused(url, expectedError) { + async function background(url, expectedError) { + browser.test.log(`background url is ${url}`); + browser.webRequest.onErrorOccurred.addListener(details => { + if (details.url != url) { + return; + } + browser.test.assertTrue(details.error.startsWith(expectedError), "error correct"); + browser.test.sendMessage("onErrorOccurred"); + }, {urls: ["<all_urls>"]}); + + let tabId; + browser.test.onMessage.addListener(async (msg, expected) => { + await browser.tabs.remove(tabId); + browser.test.sendMessage("done"); + }); + + let tab = await browser.tabs.create({url}); + tabId = tab.id; + } + + let extensionData = { + manifest: { + permissions: ["webRequest", "tabs", "*://badchain.include-subdomains.pinning.example.com/*"], + }, + background: `(${background})("${url}", "${expectedError}")`, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + await extension.awaitMessage("onErrorOccurred"); + extension.sendMessage("close-tab"); + await extension.awaitMessage("done"); + + await extension.unload(); +} + +add_task(function test_bad_cert() { + return test_connection_refused("https://badchain.include-subdomains.pinning.example.com/", "Unable to communicate securely with peer"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html new file mode 100644 index 0000000000..5ccbf761ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html @@ -0,0 +1,226 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +if (AppConstants.platform === "android") { + SimpleTest.requestLongerTimeout(6); +} + +let windowData, testWindow; + +add_task(async function setup() { + await SpecialPowers.spawnChrome([], async () => { + Services.cache2.clear(); + }); + + testWindow = window.open("about:blank", "_blank", "width=100,height=100"); + await waitForLoad(testWindow); + + // Fetch the windowId and tabId we need to filter with WebRequest. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "tabs", + ], + }, + background() { + browser.tabs.query({currentWindow: true}).then(tabs => { + let tab = tabs.find(tab => tab.active); + let {windowId} = tab; + + browser.test.log(`current window ${windowId} tabs: ${JSON.stringify(tabs.map(tab => [tab.id, tab.url]))}`); + browser.test.sendMessage("windowData", {windowId, tabId: tab.id}); + }); + }, + }); + await extension.startup(); + windowData = await extension.awaitMessage("windowData"); + info(`window is ${JSON.stringify(windowData)}`); + await extension.unload(); +}); + +add_task(async function test_webRequest_filter_window() { + if (AppConstants.MOZ_BUILD_APP !== "browser") { + // Android does not support multiple windows. + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true], + ["network.http.rcwn.enabled", false]], + }); + + let events = { + "onBeforeRequest": [{urls: ["<all_urls>"], windowId: windowData.windowId}], + "onBeforeSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]], + "onSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]], + "onBeforeRedirect": [{urls: ["<all_urls>"], windowId: windowData.windowId}], + "onHeadersReceived": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["responseHeaders"]], + "onResponseStarted": [{urls: ["<all_urls>"], windowId: windowData.windowId}], + "onCompleted": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["responseHeaders"]], + "onErrorOccurred": [{urls: ["<all_urls>"], windowId: windowData.windowId}], + }; + let expect = { + "file_image_bad.png": { + optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"], + type: "main_frame", + }, + }; + + if (AppConstants.platform != "android") { + expect["favicon.ico"] = { + // These events only happen in non-e10s. See bug 1472156. + optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"], + type: "image", + origin: SimpleTest.getTestFileURL("file_image_bad.png"), + }; + } + + let extension = makeExtension(events); + await extension.startup(); + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + + // We should not get events for a new window load. + let newWindow = window.open("file_image_good.png", "_blank", "width=100,height=100"); + await waitForLoad(newWindow); + newWindow.close(); + + // We should not get background events. + let registration = await navigator.serviceWorker.register("webrequest_worker.js?test0", {scope: "."}); + await waitForState(registration.installing, "activated"); + + // We should get events for the reload. + testWindow.location = "file_image_bad.png"; + await extension.awaitMessage("done"); + + testWindow.location = "about:blank"; + await registration.unregister(); + await extension.unload(); +}); + +add_task(async function test_webRequest_filter_tab() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true]], + }); + + let img = `file_image_good.png?r=${Math.random()}`; + + let events = { + "onBeforeRequest": [{urls: ["<all_urls>"], tabId: windowData.tabId}], + "onBeforeSendHeaders": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["requestHeaders"]], + "onSendHeaders": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["requestHeaders"]], + "onBeforeRedirect": [{urls: ["<all_urls>"], tabId: windowData.tabId}], + "onHeadersReceived": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["responseHeaders"]], + "onResponseStarted": [{urls: ["<all_urls>"], tabId: windowData.tabId}], + "onCompleted": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["responseHeaders"]], + "onErrorOccurred": [{urls: ["<all_urls>"], tabId: windowData.tabId}], + }; + let expect = { + "file_image_good.png": { + // These events only happen in non-e10s. See bug 1472156. + optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"], + type: "main_frame", + // cached: AppConstants.MOZ_BUILD_APP === "browser", + }, + }; + + if (AppConstants.platform != "android") { + // A favicon request may be initiated, and complete or be aborted. + expect["favicon.ico"] = { + optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onResponseStarted", "onCompleted", "onErrorOccurred"], + type: "image", + origin: SimpleTest.getTestFileURL(img), + }; + } + + let extension = makeExtension(events); + await extension.startup(); + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + + if (AppConstants.MOZ_BUILD_APP === "browser") { + // We should not get events for a new window load. + let newWindow = window.open(img, "_blank", "width=100,height=100"); + await waitForLoad(newWindow); + newWindow.close(); + } + + // We should not get background events. + let registration = await navigator.serviceWorker.register("webrequest_worker.js?test1", {scope: "."}); + await waitForState(registration.installing, "activated"); + + // We should get events for the reload. + testWindow.location = img; + await extension.awaitMessage("done"); + + testWindow.location = "about:blank"; + await registration.unregister(); + await extension.unload(); +}); + + +add_task(async function test_webRequest_filter_background() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true]], + }); + + let events = { + "onBeforeRequest": [{urls: ["<all_urls>"], tabId: -1}], + "onBeforeSendHeaders": [{urls: ["<all_urls>"], tabId: -1}, ["requestHeaders"]], + "onSendHeaders": [{urls: ["<all_urls>"], tabId: -1}, ["requestHeaders"]], + "onBeforeRedirect": [{urls: ["<all_urls>"], tabId: -1}], + "onHeadersReceived": [{urls: ["<all_urls>"], tabId: -1}, ["responseHeaders"]], + "onResponseStarted": [{urls: ["<all_urls>"], tabId: -1}], + "onCompleted": [{urls: ["<all_urls>"], tabId: -1}, ["responseHeaders"]], + "onErrorOccurred": [{urls: ["<all_urls>"], tabId: -1}], + }; + let expect = { + "webrequest_worker.js": { + type: "script", + }, + "example.txt": { + status: 404, + events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onResponseStarted"], + optional_events: ["onCompleted", "onErrorOccurred"], + type: "xmlhttprequest", + origin: SimpleTest.getTestFileURL("webrequest_worker.js?test2"), + }, + }; + + let extension = makeExtension(events); + await extension.startup(); + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + + // We should not get events for a window. + testWindow.location = "file_image_bad.png"; + + // We should get events for the background page. + let registration = await navigator.serviceWorker.register(SimpleTest.getTestFileURL("webrequest_worker.js?test2"), {scope: "."}); + await waitForState(registration.installing, "activated"); + await extension.awaitMessage("done"); + testWindow.location = "about:blank"; + await registration.unregister(); + + await extension.unload(); +}); + +add_task(async function teardown() { + testWindow.close(); +}); +</script> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html new file mode 100644 index 0000000000..76a13be1af --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html @@ -0,0 +1,213 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +let extensionData = { + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>", "tabs"], + }, + background() { + browser.webRequest.onBeforeRequest.addListener(details => { + if (details.url.endsWith("/favicon.ico")) { + // We don't care about favicon.ico in this test. It is hard to control + // whether the request happens. + browser.test.log(`Ignoring favicon request: ${details.url}`); + return; + } + browser.test.sendMessage("onBeforeRequest", details); + }, {urls: ["<all_urls>"]}, ["blocking"]); + + let tab; + browser.tabs.onCreated.addListener(newTab => { + browser.test.sendMessage("tab-created"); + tab = newTab; + }); + + browser.test.onMessage.addListener(msg => { + if (msg === "close-tab") { + browser.tabs.remove(tab.id); + browser.test.sendMessage("tab-closed"); + } + }); + }, +}; + +let expected = { + "file_simple_xhr.html": { + type: "main_frame", + toplevel: true, + }, + "file_image_good.png": { + type: "image", + toplevel: true, + origin: "file_simple_xhr.html", + }, + "example.txt": { + type: "xmlhttprequest", + toplevel: true, + origin: "file_simple_xhr.html", + }, + // sub frames will have the origin and first ancestor is the + // parent document + "file_simple_xhr_frame.html": { + type: "sub_frame", + toplevelParent: true, + origin: "file_simple_xhr.html", + parent: "file_simple_xhr.html", + }, + // a resource in a sub frame will have origin of the subframe, + // but the ancestor chain starts with the parent document + "xhr_resource": { + type: "xmlhttprequest", + origin: "file_simple_xhr_frame.html", + parent: "file_simple_xhr.html", + }, + "file_image_bad.png": { + type: "image", + depth: 2, + origin: "file_simple_xhr_frame.html", + parent: "file_simple_xhr.html", + }, + "file_simple_xhr_frame2.html": { + type: "sub_frame", + depth: 2, + origin: "file_simple_xhr_frame.html", + parent: "file_simple_xhr_frame.html", + }, + "file_image_redirect.png": { + type: "image", + depth: 2, + origin: "file_simple_xhr_frame2.html", + parent: "file_simple_xhr_frame.html", + }, + "xhr_resource_2": { + type: "xmlhttprequest", + depth: 2, + origin: "file_simple_xhr_frame2.html", + parent: "file_simple_xhr_frame.html", + }, + // This is loaded in a sandbox iframe. originUrl is not available for that, + // and requests within a sandboxed iframe will additionally have an empty + // url on their immediate parent/ancestor. + "file_simple_sandboxed_frame.html": { + type: "sub_frame", + depth: 3, + parent: "file_simple_xhr_frame2.html", + }, + "xhr_sandboxed": { + type: "xmlhttprequest", + sandboxed: true, + depth: 3, + parent: "", + }, + "file_image_great.png": { + type: "image", + sandboxed: true, + depth: 3, + parent: "", + }, + "file_simple_sandboxed_subframe.html": { + type: "sub_frame", + depth: 4, + parent: "", + }, +}; + +function checkDetails(details) { + let url = new URL(details.url); + let filename = url.pathname.split("/").pop(); + ok(filename in expected, `Should be expecting a request for ${filename}`); + let expect = expected[filename]; + is(expect.type, details.type, `${details.type} type matches`); + if (details.parentFrameId == -1) { + is(details.frameAncestors.length, 0, "no ancestors for main_frame requests"); + } else if (details.parentFrameId == 0) { + is(details.frameAncestors.length, 1, "one ancestors for sub_frame requests"); + } else { + ok(details.frameAncestors.length > 1, "have multiple ancestors for deep subframe requests"); + is(details.frameAncestors.length, expect.depth, "have multiple ancestors for deep subframe requests"); + } + if (details.parentFrameId > -1) { + ok(!expect.origin || details.originUrl.includes(expect.origin), "origin url is correct"); + is(details.frameAncestors[0].frameId, details.parentFrameId, "first ancestor matches request.parentFrameId"); + ok(details.frameAncestors[0].url.includes(expect.parent), "ancestor parent page correct"); + is(details.frameAncestors[details.frameAncestors.length - 1].frameId, 0, "last ancestor is always zero"); + // All our tests should be somewhere within the frame that we set topframe in the query string. That + // frame will always be the last ancestor. + ok(details.frameAncestors[details.frameAncestors.length - 1].url.includes("topframe=true"), "last ancestor is always topframe"); + } + if (expect.toplevel) { + is(details.frameId, 0, "expect load at top level"); + is(details.parentFrameId, -1, "expect top level frame to have no parent"); + } else if (details.type == "sub_frame") { + ok(details.frameId > 0, "expect sub_frame to load into a new frame"); + if (expect.toplevelParent) { + is(details.parentFrameId, 0, "expect sub_frame to have top level parent"); + is(details.frameAncestors.length, 1, "one ancestor for top sub_frame request"); + } else { + ok(details.parentFrameId > 0, "expect sub_frame to have parent"); + ok(details.frameAncestors.length > 1, "sub_frame has ancestors"); + } + expect.subframeId = details.frameId; + expect.parentId = details.parentFrameId; + } else if (expect.sandboxed) { + is(details.documentUrl, undefined, "null principal documentUrl for sandboxed request"); + } else { + // get the parent frame. + let purl = new URL(details.documentUrl); + let pfilename = purl.pathname.split("/").pop(); + let parent = expected[pfilename]; + is(details.frameId, parent.subframeId, "expect load in subframe"); + is(details.parentFrameId, parent.parentId, "expect subframe parent"); + } + return filename; +} + +add_task(async function test_webRequest_main_frame() { + // Clear the image cache, since it gets in the way otherwise. + let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); + let cache = imgTools.getImgCacheForDocument(document); + cache.clearCache(false); + await SpecialPowers.spawnChrome([], async () => { + Services.cache2.clear(); + }); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let a = addLink(`file_simple_xhr.html?topframe=true&nocache=${Math.random()}`); + a.click(); + + let remaining = new Set(Object.keys(expected)); + let totalExpectedCount = remaining.size; + for (let i = 0; i < totalExpectedCount; i++) { + info(`Waiting for request ${i + 1} out of ${totalExpectedCount}`); + info(`Expecting one of: ${Array.from(remaining)}`); + let details = await extension.awaitMessage("onBeforeRequest"); + info(`Checking details for request ${i}: ${JSON.stringify(details)}`); + let filename = checkDetails(details); + ok(remaining.delete(filename), `Got only one request for ${filename}`); + } + + await extension.awaitMessage("tab-created"); + extension.sendMessage("close-tab"); + await extension.awaitMessage("tab-closed"); + + await extension.unload(); +}); +</script> +</head> +<body> +<div id="test">Sample text</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_getSecurityInfo.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_getSecurityInfo.html new file mode 100644 index 0000000000..5628109483 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_getSecurityInfo.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>browser.webRequest.getSecurityInfo()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script> +"use strict"; + +add_task(async function test_getSecurityInfo() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "<all_urls>" + ], + }, + async background() { + const url = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + + let tab; + browser.webRequest.onHeadersReceived.addListener(async details => { + const securityInfo = await browser.webRequest.getSecurityInfo( + details.requestId, + {} + ); + + // Some properties have dynamic values so let's take them out of the + // `securityInfo` object before asserting all the other props with deep + // equality. + const { + cipherSuite, + secretKeyLength, + keaGroupName, + signatureSchemeName, + protocolVersion, + certificates, + ...otherProps + } = securityInfo; + + browser.test.assertTrue(cipherSuite.length, "expected cipher suite"); + browser.test.assertTrue( + Number.isInteger(secretKeyLength), + "expected secret key length" + ); + browser.test.assertTrue( + keaGroupName.length, + "expected kea group name" + ); + browser.test.assertTrue( + signatureSchemeName.length, + "expected signature scheme name" + ); + browser.test.assertTrue( + protocolVersion.length, + "expected protocol version" + ); + browser.test.assertTrue( + Array.isArray(certificates), + "expected an array of certificates" + ); + + browser.test.assertDeepEq({ + state: "secure", + isExtendedValidation: false, + certificateTransparencyStatus: "not_applicable", + hsts: false, + hpkp: false, + usedEch: false, + usedDelegatedCredentials: false, + usedOcsp: false, + usedPrivateDns: false, + }, otherProps, "expected security info"); + + await browser.tabs.remove(tab.id); + browser.test.notifyPass("success"); + }, { urls: [url] } , ["blocking"]); + + tab = await browser.tabs.create({ url }); + }, + }); + await extension.startup(); + + await extension.awaitFinish("success"); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html new file mode 100644 index 0000000000..e66b5c471a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html @@ -0,0 +1,252 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +function getExtension() { + async function background() { + let expect; + let urls = ["*://*.example.org/tests/*"]; + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.assertEq(expect.shift(), "onBeforeRequest"); + }, {urls}, ["blocking"]); + browser.webRequest.onBeforeSendHeaders.addListener(details => { + browser.test.assertEq(expect.shift(), "onBeforeSendHeaders"); + }, {urls}, ["blocking", "requestHeaders"]); + browser.webRequest.onSendHeaders.addListener(details => { + browser.test.assertEq(expect.shift(), "onSendHeaders"); + }, {urls}, ["requestHeaders"]); + + async function testSecurityInfo(securityInfo, options) { + if (options.certificateChain) { + // Some of the tests here only produce a single cert in the chain. + browser.test.assertTrue(securityInfo.certificates.length >= 1, "have certificate chain"); + } else { + browser.test.assertTrue(securityInfo.certificates.length == 1, "no certificate chain"); + } + let cert = securityInfo.certificates[0]; + let now = Date.now(); + browser.test.assertTrue(Number.isInteger(cert.validity.start), "cert start is integer"); + browser.test.assertTrue(Number.isInteger(cert.validity.end), "cert end is integer"); + browser.test.assertTrue(cert.validity.start < now, "cert start validity is correct"); + browser.test.assertTrue(now < cert.validity.end, "cert end validity is correct"); + if (options.rawDER) { + for (let cert of securityInfo.certificates) { + browser.test.assertTrue(!!cert.rawDER.length, "have rawDER"); + } + } + } + + function stripQuery(url) { + // In this whole test we are not interested in the query part of the URL. + // Most tests include a cache buster (bustcache) param in the URL. + return url.split("?")[0]; + } + + browser.webRequest.onHeadersReceived.addListener(async (details) => { + browser.test.assertEq(expect.shift(), "onHeadersReceived"); + + // We expect all requests to have been upgraded at this point. + browser.test.assertTrue(details.url.startsWith("https"), "connection is https"); + let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {}); + browser.test.assertTrue(securityInfo && securityInfo.state == "secure", + "security info reflects https"); + await testSecurityInfo(securityInfo, {}); + securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {certificateChain: true}); + await testSecurityInfo(securityInfo, {certificateChain: true}); + securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {rawDER: true}); + await testSecurityInfo(securityInfo, {rawDER: true}); + securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {certificateChain: true, rawDER: true}); + await testSecurityInfo(securityInfo, {certificateChain: true, rawDER: true}); + + browser.test.sendMessage("hsts", securityInfo.hsts); + let headers = details.responseHeaders || []; + for (let header of headers) { + if (header.name.toLowerCase() === "strict-transport-security") { + return; + } + } + if (details.url.includes("addHsts")) { + headers.push({ + name: "Strict-Transport-Security", + value: "max-age=31536000000", + }); + } + return {responseHeaders: headers}; + }, {urls}, ["blocking", "responseHeaders"]); + browser.webRequest.onBeforeRedirect.addListener(details => { + browser.test.assertEq(expect.shift(), "onBeforeRedirect"); + }, {urls}); + browser.webRequest.onResponseStarted.addListener(details => { + browser.test.assertEq(expect.shift(), "onResponseStarted"); + }, {urls}); + browser.webRequest.onCompleted.addListener(details => { + browser.test.assertEq(expect.shift(), "onCompleted"); + browser.test.sendMessage("onCompleted", stripQuery(details.url)); + }, {urls}); + browser.webRequest.onErrorOccurred.addListener(details => { + browser.test.notifyFail(`onErrorOccurred ${JSON.stringify(details)}`); + }, {urls}); + + async function onUpdated(tabId, tabInfo, tab) { + if (tabInfo.status !== "complete" || tab.url === "about:blank") { + return; + } + browser.tabs.remove(tabId); + browser.tabs.onUpdated.removeListener(onUpdated); + browser.test.sendMessage("tabs-done", stripQuery(tab.url)); + } + browser.test.onMessage.addListener((url, expected) => { + expect = expected; + browser.tabs.onUpdated.addListener(onUpdated); + browser.tabs.create({url}); + }); + } + + let manifest = { + "permissions": [ + "tabs", + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + }; + return ExtensionTestUtils.loadExtension({ + manifest, + background, + }); +} + +add_setup(async () => { + // In bug 1605515, we repeatedly saw a missing onHeadersReceived event, + // possibly related to bug 1595610. As a workaround, clear the cache. + await SpecialPowers.spawnChrome([], async () => { + Services.cache2.clear(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_HSTS, resolve); + }); + }); +}); + +// This test makes a request against a server that redirects with a 302. +add_task(async function test_hsts_request() { + const testPath = "example.org/tests/toolkit/components/extensions/test/mochitest"; + + let extension = getExtension(); + await extension.startup(); + + // simple redirect + let sample = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + extension.sendMessage( + `https://${testPath}/redirect_auto.sjs?redirect_uri=${sample}?bustcache1=${Math.random()}`, + ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", + "onHeadersReceived", "onBeforeRedirect", "onBeforeRequest", + "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", + "onResponseStarted", "onCompleted"]); + is(await extension.awaitMessage("hsts"), false, "First request to this host, not receiving a hsts header"); + is(await extension.awaitMessage("hsts"), false, "second (redirected) reqiest to the same host, still no knowledge about the hosts hsts preference"); + // Note: stripQuery strips query string added by redirect_auto. + is(await extension.awaitMessage("tabs-done"), sample, "redirection ok"); + is(await extension.awaitMessage("onCompleted"), sample, "redirection ok"); + + // priming hsts + extension.sendMessage( + `https://${testPath}/hsts.sjs`, + ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", + "onHeadersReceived", "onResponseStarted", "onCompleted"]); + is(await extension.awaitMessage("hsts"), false, "First request to this host, receiving hsts header and saving the hosts STS preference for the next request"); + is(await extension.awaitMessage("tabs-done"), + "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs", + "hsts primed"); + is(await extension.awaitMessage("onCompleted"), + "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs"); + + // test upgrade + extension.sendMessage( + `http://${testPath}/hsts.sjs`, + ["onBeforeRequest", "onBeforeRedirect", "onBeforeRequest", + "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", + "onResponseStarted", "onCompleted"]); + is(await extension.awaitMessage("hsts"), true, "second (redirected) reqiest to the same host, we know about the hsts status of the host this time"); + is(await extension.awaitMessage("tabs-done"), + "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs", + "hsts upgraded"); + is(await extension.awaitMessage("onCompleted"), + "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs"); + + await extension.unload(); +}); + +// This test makes a priming request and adds the STS header, then tests the upgrade. +add_task(async function test_hsts_header() { + const testPath = "test1.example.org/tests/toolkit/components/extensions/test/mochitest"; + + let extension = getExtension(); + await extension.startup(); + + // priming hsts, this time there is no STS header, onHeadersReceived adds it. + let completed = extension.awaitMessage("onCompleted"); + let tabdone = extension.awaitMessage("tabs-done"); + extension.sendMessage( + `https://${testPath}/file_sample.html?bustcache2=${Math.random()}&addHsts=true`, + ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", + "onHeadersReceived", "onResponseStarted", "onCompleted"]); + is(await extension.awaitMessage("hsts"), false, "First reqeuest to this host, we don't know about the hosts STS setting yet"); + is(await tabdone, `https://${testPath}/file_sample.html`, "priming request done"); + is(await completed, `https://${testPath}/file_sample.html`, "priming request done"); + + // test upgrade from http to https due to onHeadersReceived adding STS header + completed = extension.awaitMessage("onCompleted"); + tabdone = extension.awaitMessage("tabs-done"); + extension.sendMessage( + `http://${testPath}/file_sample.html?bustcache3=${Math.random()}`, + ["onBeforeRequest", "onBeforeRedirect", "onBeforeRequest", + "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", + "onResponseStarted", "onCompleted"]); + is(await extension.awaitMessage("hsts"), true, "We have received an hsts header last request via oneadersReceived"); + is(await tabdone, `https://${testPath}/file_sample.html`, "hsts upgraded"); + is(await completed, `https://${testPath}/file_sample.html`, "request upgraded"); + + await extension.unload(); +}); + +add_task(async function test_nonBlocking_securityInfo() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": [ + "webRequest", + "<all_urls>", + ], + }, + async background() { + let tab; + browser.webRequest.onHeadersReceived.addListener(async (details) => { + let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {}); + browser.test.assertTrue(!securityInfo, "securityInfo undefined on http request"); + browser.tabs.remove(tab.id); + browser.test.notifyPass("success"); + }, {urls: ["<all_urls>"], types: ["main_frame"]}); + tab = await browser.tabs.create({ + url: `https://example.org/tests/toolkit/components/extensions/test/mochitest/file_sample.html?bustcache4=${Math.random()}`, + }); + }, + }); + await extension.startup(); + + await extension.awaitFinish("success"); + await extension.unload(); +}); +</script> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html new file mode 100644 index 0000000000..87dbbd6598 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450965: Skip Cors Check for Early WebExtention Redirects </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* Description of the test: + * We try to Check if a WebExtention can redirect a request and bypass CORS + * We're redirecting a fetch request in onBeforeRequest + * which should not be blocked, even though we do not have + * the CORS information yet. + */ + +const WIN_URL = + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html"; + + +add_task(async function test_webRequest_redirect_cors_bypass() { + // disable third-party storage isolation so the test works as expected + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + }, + background() { + browser.webRequest.onBeforeRequest.addListener((details) => { + if (details.url.includes("file_cors_blocked.txt")) { + // File_cors_blocked does not need to exist, because we're redirecting anyway. + const testPath = "example.org/tests/toolkit/components/extensions/test/mochitest"; + let redirectUrl = `https://${testPath}/file_sample.txt`; + + // If the WebExtion cant bypass CORS, the fetch will throw a CORS-Exception + // because we do not have the CORS header yet for 'file-cors-blocked.txt' + return {redirectUrl}; + } + }, {urls: ["<all_urls>"]}, ["blocking"]); + }, + + }); + + await extension.startup(); + let win = window.open(WIN_URL); + // Creating a message channel to the new tab. + const channel = new BroadcastChannel("test_bus"); + await new Promise((resolve, reject) => { + channel.onmessage = async function(fetch_result) { + // Fetch result data will either be the text content of file_sample.txt -> 'Sample' + // or a network-Error. + // In case it's 'Sample' the redirect did happen correctly. + ok(fetch_result.data == "Sample", "Cors was Bypassed"); + win.close(); + await extension.unload(); + resolve(); + }; + }); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html new file mode 100644 index 0000000000..5d58549c46 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1434357: Allow Web Request API to redirect to data: URI</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* Description of the test: + * We load a *.js file which gets redirected to a data: URI. + * Since there is no good way to communicate loaded data: URI scripts + * we use updating a divContainer as a detour to verify the data: URI + * script has loaded. + */ + +const WIN_URL = + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html"; + +add_task(async function test_webRequest_redirect_data_uri() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "*://mochi.test/tests/*", + ], + content_scripts: [{ + matches: ["*://mochi.test/tests/*/file_redirect_data_uri.html"], + run_at: "document_end", + js: ["content_script.js"], + "all_frames": true, + }], + }, + + background() { + browser.webRequest.onBeforeRequest.addListener((details) => { + if (details.url.includes("dummy_non_existend_file.js")) { + let redirectUrl = + "data:text/javascript,document.getElementById('testdiv').textContent='loaded'"; + return {redirectUrl}; + } + }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]); + }, + + files: { + "content_script.js": function() { + let scriptEl = document.createElement("script"); + // please note that dummy_non_existend_file.js file does not really need + // to exist because we redirect the load within onBeforeRequest(). + scriptEl.src = "dummy_non_existend_file.js"; + document.body.appendChild(scriptEl); + + scriptEl.onload = function() { + let divContent = document.getElementById("testdiv").textContent; + browser.test.assertEq(divContent, "loaded", + "redirect to data: URI allowed"); + browser.test.sendMessage("finished"); + }; + scriptEl.onerror = function() { + browser.test.fail("script load failure"); + browser.test.sendMessage("finished"); + }; + }, + }, + }); + + await extension.startup(); + let win = window.open(WIN_URL); + await extension.awaitMessage("finished"); + win.close(); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html new file mode 100644 index 0000000000..f086d29d02 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html @@ -0,0 +1,139 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_webRequest_upgrade() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "*://mochi.test/tests/*", + ], + }, + background() { + browser.webRequest.onSendHeaders.addListener((details) => { + // At this point, the request should have been upgraded. + browser.test.assertTrue(details.url.startsWith("https:"), "request is upgraded"); + browser.test.assertTrue(details.url.includes("file_sample"), "redirect after upgrade worked"); + // Note: although not significant for the test assertions, note that + // the requested file won't load - https://mochi.test:8888/ does not + // resolve to anything on the test server. + browser.test.sendMessage("finished"); + }, {urls: ["*://mochi.test/tests/*"]}); + + browser.webRequest.onBeforeRequest.addListener((details) => { + browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`); + let url = new URL(details.url); + if (url.protocol == "http:") { + return {upgradeToSecure: true}; + } + // After the channel is initially upgraded, we get another onBeforeRequest + // call. Here we can redirect again to a new url. + if (details.url.includes("file_mixed.html")) { + let redirectUrl = new URL("file_sample.html", details.url).href; + return {redirectUrl}; + } + }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]); + }, + }); + + await extension.startup(); + let win = window.open("http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_mixed.html"); + await extension.awaitMessage("finished"); + win.close(); + await extension.unload(); +}); + +add_task(async function test_webRequest_redirect_wins() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "*://mochi.test/tests/*", + ], + }, + background() { + browser.webRequest.onSendHeaders.addListener((details) => { + // At this point, the request should have been redirected instead of upgraded. + browser.test.assertTrue(details.url.includes("file_sample"), "request was redirected"); + browser.test.sendMessage("finished"); + }, {urls: ["*://mochi.test/tests/*"]}); + + browser.webRequest.onBeforeRequest.addListener((details) => { + if (details.url.includes("file_mixed.html")) { + let redirectUrl = new URL("file_sample.html", details.url).href; + return {upgradeToSecure: true, redirectUrl}; + } + }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]); + }, + }); + + await extension.startup(); + let win = window.open("http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_mixed.html"); + await extension.awaitMessage("finished"); + win.close(); + await extension.unload(); +}); + +// Test that there is no infinite redirect loop when upgradeToSecure is used on +// https. This test checks that the redirect chain is: http -> https -> done. +add_task(async function upgradeToSecure_for_https_is_noop() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "*://example.com/tests/*", + ], + }, + background() { + let count = 0; + browser.webRequest.onBeforeRequest.addListener( + details => { + browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`); + ++count; + if (details.url.startsWith("http:")) { + browser.test.assertEq(1, count, "Initial request is http:"); + } else { + browser.test.assertEq(2, count, "Second request is https:"); + } + return {upgradeToSecure: true}; + }, + { urls: ["*://example.com/tests/*file_sample.html"] }, + ["blocking"] + ); + browser.webRequest.onCompleted.addListener( + details => { + browser.test.log(`onCompleted ${details.requestId} ${details.url}`); + browser.test.assertTrue(details.url.startsWith("https"), "is https"); + browser.test.assertEq(2, count, "Seen two requests (http + https)"); + browser.test.sendMessage("finished"); + }, + { urls: ["*://example.com/tests/*file_sample.html"] }, + ); + }, + }); + + await extension.startup(); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("finished"); + win.close(); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html new file mode 100644 index 0000000000..30ecb0aa78 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html @@ -0,0 +1,265 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<form method="post" + action="file_WebRequest_page3.html?trigger=form" + target="_blank" + enctype="multipart/form-data" + > +<input type="text" name=""special" 
 ch�rs" value="sp�cial"> +<input type="file" name="testFile"> +<input type="file" name="emptyFile"> +<input type="text" name="textInput1" value="value1"> +</form> + +<form method="post" + action="file_WebRequest_page3.html?trigger=form" + target="_blank" + enctype="multipart/form-data" + > +<input type="text" name="textInput2" value="value2"> +<input type="file" name="testFile"> +<input type="file" name="emptyFile"> +</form> + +</form> +<form method="post" + action="file_WebRequest_page3.html?trigger=form" + target="_blank" + > +<input type="text" name="textInput" value="value1"> +<input type="text" name="textInput" value="value2"> +</form> +<script> +"use strict"; + +let files, testFile, blob, file, uploads; +add_task(async function test_setup() { + files = await new Promise(resolve => { + SpecialPowers.createFiles([{name: "testFile.pdf", data: "Not really a PDF file :)", "type": "application/x-pdf"}], (result) => { + resolve(result); + }); + }); + testFile = files[0]; + blob = { + name: "blobAsFile", + content: new Blob(["A blob sent as a file"], {type: "text/csv"}), + fileName: "blobAsFile.csv", + }; + file = { + name: "testFile", + fileName: testFile.name, + }; + uploads = { + [blob.name]: blob, + [file.name]: file, + "emptyFile": {fileName: ""} + }; +}); + +function background() { + const FILTERS = {urls: ["<all_urls>"]}; + + function onUpload(details) { + let url = new URL(details.url); + let upload = url.searchParams.get("upload"); + if (!upload) { + return; + } + + let requestBody = details.requestBody; + browser.test.log(`onBeforeRequest upload: ${details.url} ${JSON.stringify(details.requestBody)}`); + browser.test.assertTrue(!!requestBody, `Intercepted upload ${details.url} #${details.requestId} ${upload} have a requestBody`); + if (!requestBody) { + return; + } + let byteLength = parseInt(upload, 10); + if (byteLength) { + browser.test.assertTrue(!!requestBody.raw, `Binary upload ${details.url} #${details.requestId} ${upload} have a raw attribute`); + browser.test.assertEq(byteLength, requestBody.raw && requestBody.raw.map(r => r.bytes ? r.bytes.byteLength : 0).reduce((a, b) => a + b), `Binary upload size matches`); + return; + } + if ("raw" in requestBody) { + browser.test.assertEq(upload, JSON.stringify(requestBody.raw).replace(/(\bfile: ")[^"]+/, "$1<file>"), `Upload ${details.url} #${details.requestId} matches raw data`); + } else { + browser.test.assertEq(upload, JSON.stringify(requestBody.formData), `Upload ${details.url} #${details.requestId} matches form data.`); + } + } + + browser.webRequest.onCompleted.addListener( + details => { + browser.test.log(`onCompleted ${details.requestId} ${details.url}`); + // See bug 1471387 + if (details.url.endsWith("/favicon.ico") || details.originUrl == "about:newtab") { + return; + } + + browser.test.sendMessage("done"); + }, + FILTERS); + + let onBeforeRequest = details => { + browser.test.log(`${name} ${details.requestId} ${details.url}`); + // See bug 1471387 + if (details.url.endsWith("/favicon.ico") || details.originUrl == "about:newtab") { + return; + } + + onUpload(details); + }; + + browser.webRequest.onBeforeRequest.addListener( + onBeforeRequest, FILTERS, ["requestBody"]); + + let tab; + browser.tabs.onCreated.addListener(newTab => { + tab = newTab; + }); + + browser.test.onMessage.addListener(msg => { + if (msg === "close-tab") { + browser.tabs.remove(tab.id); + browser.test.sendMessage("tab-closed"); + } + }); +} + +add_task(async function test_xhr_forms() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "tabs", + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + }, + background, + }); + + await extension.startup(); + + async function doneAndTabClosed() { + await extension.awaitMessage("done"); + let closed = extension.awaitMessage("tab-closed"); + extension.sendMessage("close-tab"); + await closed; + } + + for (let form of document.forms) { + if (file.name in form.elements) { + SpecialPowers.wrap(form.elements[file.name]).mozSetFileArray(files); + } + let action = new URL(form.action); + let formData = new FormData(form); + let webRequestFD = {}; + + let updateActionURL = () => { + for (let name of formData.keys()) { + webRequestFD[name] = name in uploads ? [uploads[name].fileName] : formData.getAll(name); + } + action.searchParams.set("upload", JSON.stringify(webRequestFD)); + action.searchParams.set("enctype", form.enctype); + }; + + updateActionURL(); + + form.action = action; + form.submit(); + await doneAndTabClosed(); + + if (form.enctype !== "multipart/form-data") { + continue; + } + + let post = (data) => { + let xhr = new XMLHttpRequest(); + action.searchParams.set("xhr", "1"); + xhr.open("POST", action.href); + xhr.send(data); + action.searchParams.delete("xhr"); + return doneAndTabClosed(); + }; + + formData.append(blob.name, blob.content, blob.fileName); + formData.append("formDataField", "some value"); + updateActionURL(); + await post(formData); + + action.searchParams.set("upload", JSON.stringify([{file: "<file>"}])); + await post(testFile); + + action.searchParams.set("upload", `${blob.content.size} bytes`); + await post(blob.content); + + let byteLength = 16; + action.searchParams.set("upload", `${byteLength} bytes`); + await post(new ArrayBuffer(byteLength)); + } + + // Testing the decoding of percent escapes even in cases where the + // multipart/form-data serializer won't emit them. + { + let boundary = "-".repeat(27); + for (let i = 0; i < 3; i++) { + const randomNumber = Math.floor(Math.random() * (2 ** 32)); + boundary += String(randomNumber); + } + + const formPayload = [ + `--${boundary}`, + 'Content-Disposition: form-data; name="percent escapes other than%20quotes and newlines"', + "", + "", + `--${boundary}`, + 'Content-Disposition: form-data; name="valid UTF-8: %F0%9F%92%A9"', + "", + "", + `--${boundary}`, + 'Content-Disposition: form-data; name="broken UTF-8: %F0%9F %92%A9"', + "", + "", + `--${boundary}`, + 'Content-Disposition: form-data; name="percent escapes aren\'t decoded in filenames"; filename="%0D%0A%22"', + "Content-Type: application/octet-stream", + "", + "", + `--${boundary}--`, + "" + ].join("\r\n"); + + const action = new URL("file_WebRequest_page3.html?trigger=form", document.location.href); + action.searchParams.set("xhr", "1"); + action.searchParams.set("upload", JSON.stringify({ + "percent escapes other than quotes and newlines": [""], + "valid UTF-8: 💩": [""], + "broken UTF-8: � ��": [""], + "percent escapes aren't decoded in filenames": ["%0D%0A%22"] + })); + action.searchParams.set("enctype", "multipart/form-data"); + + await fetch( + action.href, + { + method: "POST", + headers: {"Content-Type": `multipart/form-data; boundary=${boundary}`}, + body: formPayload + }, + ); + await doneAndTabClosed(); + } + + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_worker.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_worker.html new file mode 100644 index 0000000000..9fc3e00f01 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_worker.html @@ -0,0 +1,192 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +let tabId; + +let extensionData = { + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>", "tabs"], + }, + background() { + browser.webRequest.onBeforeRequest.addListener(details => { + if (details.url.endsWith("/favicon.ico")) { + // We don't care about favicon.ico in this test. It is hard to control + // whether the request happens. + browser.test.log(`Ignoring favicon request: ${details.url}`); + return; + } + browser.test.sendMessage("onBeforeRequest", details); + }, {urls: ["<all_urls>"]}, ["blocking"]); + + let tab; + browser.tabs.onCreated.addListener(newTab => { + tab = newTab; + browser.test.sendMessage("tab-created", tab.id); + }); + + browser.test.onMessage.addListener(async (msg) => { + if (msg === "close-tab") { + await browser.tabs.remove(tab.id); + browser.test.sendMessage("tab-closed"); + } + }); + }, +}; + +let expected = { + "file_simple_webrequest_worker.html?topframe=true": { + type: "main_frame", + toplevel: true, + origin: "test_ext_webrequest_worker.html", + tabId: true, + parentFrameId: -1, + }, + "file_simple_iframe_worker.html": { + type: "sub_frame", + toplevel: false, + origin: "file_simple_webrequest_worker.html?topframe=true", + tabId: true, + parentFrameId: 0, + }, + "file_simple_toplevel.txt": { + type: "xmlhttprequest", + toplevel: true, + origin: "file_simple_webrequest_worker.html?topframe=true", + tabId: true, + parentFrameId: -1, + }, + "file_simple_iframe.txt": { + type: "xmlhttprequest", + toplevel: false, + origin: "file_simple_iframe_worker.html", + tabId: true, + parentFrameId: 0, + }, + "file_simple_worker.txt": { + type: "xmlhttprequest", + toplevel: true, + origin: "file_simple_worker.js", + tabId: true, + parentFrameId: -1, + }, + "file_simple_iframe_worker.txt": { + type: "xmlhttprequest", + toplevel: false, + origin: "file_simple_worker.js?iniframe=true", + tabId: true, + parentFrameId: 0, + }, + "file_simple_sharedworker.txt": { + type: "xmlhttprequest", + toplevel: undefined, + origin: "file_simple_sharedworker.js", + tabId: false, + parentFrameId: -1, + }, + "file_simple_iframe_sharedworker.txt": { + type: "xmlhttprequest", + toplevel: undefined, + origin: "file_simple_sharedworker.js?iniframe=true", + tabId: false, + parentFrameId: -1, + }, + "file_simple_worker.js": { + type: "script", + toplevel: true, + origin: "file_simple_webrequest_worker.html?topframe=true", + tabId: true, + parentFrameId: -1, + }, + "file_simple_sharedworker.js": { + type: "script", + toplevel: undefined, + origin: "file_simple_webrequest_worker.html?topframe=true", + tabId: false, + parentFrameId: -1, + }, + "file_simple_worker.js?iniframe=true": { + type: "script", + toplevel: false, + origin: "file_simple_iframe_worker.html", + tabId: true, + parentFrameId: 0, + }, + "file_simple_sharedworker.js?iniframe=true": { + type: "script", + toplevel: undefined, + origin: "file_simple_iframe_worker.html", + tabId: false, + parentFrameId: -1, + }, + + +}; + +function checkDetails(details) { + let filename = details.url.split("/").pop(); + ok(filename in expected, `Should be expecting a request for ${filename}`); + let expect = expected[filename]; + is(expect.type, details.type, `${details.type} type matches`); + const originUrlSuffix = details.originUrl?.split("/").pop(); + ok(expect.origin === originUrlSuffix || originUrlSuffix.startsWith(expect.origin), `origin url is correct`); + is(details.parentFrameId, expect.parentFrameId, "parentFrameId matches"); + is(expect.tabId ? tabId : -1, details.tabId, "tabId matches"); + // TODO: When expect.toplevel is "undefined", the details.frameId is supposed + // to be -1. + // details in https://phabricator.services.mozilla.com/D182705#inline-1030548. + if (expect.toplevel === undefined || expect.toplevel) { + is(details.frameId, 0, "expect zero frameId"); + } else { + ok(details.frameId > 0, "expect non-zero frameId"); + } + return filename; +} + +add_task(async function test_webRequest_worker() { + await SpecialPowers.spawnChrome([], async () => { + Services.cache2.clear(); + }); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let a = addLink(`file_simple_webrequest_worker.html?topframe=true`); + a.click(); + tabId = await extension.awaitMessage("tab-created"); + info(`Get created tab(${tabId}`); + + let remaining = new Set(Object.keys(expected)); + let totalExpectedCount = remaining.size; + let currentExpectedCount = 0; + while (remaining.size !== 0) { + info(`Waiting for request ${currentExpectedCount + 1} out of ${totalExpectedCount}`); + info(`Expecting one of: ${Array.from(remaining)}`); + let details = await extension.awaitMessage("onBeforeRequest"); + info(`Checking details for request: ${JSON.stringify(details)}`); + let filename = checkDetails(details); + ok(remaining.delete(filename), `Got only one request for ${filename}`); + currentExpectedCount = currentExpectedCount + 1; + } + + extension.sendMessage("close-tab"); + await extension.awaitMessage("tab-closed"); + await extension.unload(); +}); + +</script> +</head> +<body> +<div id="test">Sample text</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html new file mode 100644 index 0000000000..53b19d0ead --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(async function test_postMessage() { + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + "all_frames": true, + }, + ], + + web_accessible_resources: ["iframe.html"], + }, + + background() { + browser.test.sendMessage("iframe-url", browser.runtime.getURL("iframe.html")); + }, + + files: { + "content_script.js": function() { + window.addEventListener("message", event => { + if (event.data == "ping") { + event.source.postMessage({pong: location.href}, + event.origin); + } + }); + }, + + "iframe.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="content_script.js"><\/script> + </head> + </html>`, + }, + }; + + let createIframe = url => { + let iframe = document.createElement("iframe"); + return new Promise(resolve => { + iframe.src = url; + iframe.onload = resolve; + document.body.appendChild(iframe); + }).then(() => { + return iframe; + }); + }; + + let awaitMessage = () => { + return new Promise(resolve => { + let listener = event => { + if (event.data.pong) { + window.removeEventListener("message", listener); + resolve(event.data); + } + }; + window.addEventListener("message", listener); + }); + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let iframeURL = await extension.awaitMessage("iframe-url"); + let testURL = SimpleTest.getTestFileURL("file_sample.html"); + + for (let url of [iframeURL, testURL]) { + info(`Testing URL ${url}`); + + let iframe = await createIframe(url); + + iframe.contentWindow.postMessage( + "ping", url); + + let pong = await awaitMessage(); + is(pong.pong, url, "Got expected pong"); + + iframe.remove(); + } + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_startup_canary.html b/toolkit/components/extensions/test/mochitest/test_startup_canary.html new file mode 100644 index 0000000000..1f705940c2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_startup_canary.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Check StartupCache</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// The startup canary file is removed sometime after the startup, with a delay, +// e.g. 30 seconds on desktop: +// https://searchfox.org/mozilla-central/rev/aa46c2dcccbc6fd4265edca05d3d00cccdfc97b9/browser/components/BrowserGlue.jsm#2486-2490 +// e.g. up to 15 seconds (as an idle timeout) on Android: +// https://searchfox.org/mozilla-central/rev/aa46c2dcccbc6fd4265edca05d3d00cccdfc97b9/mobile/android/chrome/geckoview/geckoview.js#510 +// +// This test completes quickly if run sequentially after the many tests in this +// directory. Otherwise the test may wait for up to MAX_DELAY_SEC seconds. +const MAX_DELAY_SEC = 30; +SimpleTest.requestFlakyTimeout("trackStartupCrashEnd() is called with a delay"); + +// This test is not extension-specific, but placed in the extensions/ directory +// because it complements the test_check_startupcache.html test, and because +// the directory has many other tests, to minimize the amount of time wasted on +// waiting. + +add_task(async function check_startup_canary() { + // The ".startup-incomplete" file is created at the startup, and supposedly + // cleared "soon" after startup (when the application knows that the startup + // succeeded without crash). Bug 1624724 and bug 1728461 show that this has + // not always been the case, so this regression test verifies that the file + // is actually non-existent when this test start, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1728461#c12 + + // This test is opened as a web page in the browser, so that should have been + // a point where the startup should have been considered done. + + async function canaryExists() { + let chromeScript = loadChromeScript(async () => { + // This file is called FILE_STARTUP_INCOMPLETE in nsAppRunner.cpp and + // referenced via mozilla::startup::GetIncompleteStartupFile: + let file = Services.dirsvc.get("ProfLD", Ci.nsIFile); + file.append(".startup-incomplete"); + this.sendAsyncMessage("canary_exists", file.exists()); + }); + let exists = await chromeScript.promiseOneMessage("canary_exists"); + chromeScript.destroy(); + return exists; + } + + info("Checking if startup canary exists"); + let i = 0; + while (await canaryExists()) { + if (i++ > MAX_DELAY_SEC) { + info("Canary still exists, giving up on waiting"); + break; + } + info(`Startup canary exists, will retry ${i} / ${MAX_DELAY_SEC}.`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + is( + await canaryExists(), + false, + "Startup canary should have been removed after early startup" + ); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html b/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html new file mode 100644 index 0000000000..3713243c1b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Verify non-remote mode</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; +add_task(async function verify_extensions_in_parent_process() { + // This test ensures we are running with the proper settings. + const { WebExtensionPolicy } = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services); + SimpleTest.ok(!WebExtensionPolicy.useRemoteWebExtensions, "extensions running in-process"); + + let chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + const { WebExtensionPolicy } = Cu.getGlobalForObject(Services); + Assert.ok(WebExtensionPolicy.isExtensionProcess, "parent is extension process"); + this.sendAsyncMessage("checks_done"); + }); + await chromeScript.promiseOneMessage("checks_done"); + chromeScript.destroy(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html b/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html new file mode 100644 index 0000000000..2be0e19179 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Verify remote mode</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + "use strict"; + // This test ensures we are running with the proper settings. + const {WebExtensionPolicy} = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services); + SimpleTest.ok(WebExtensionPolicy.useRemoteWebExtensions, "extensions running remote"); + SimpleTest.ok(!WebExtensionPolicy.isExtensionProcess, "testing from remote process"); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html b/toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html new file mode 100644 index 0000000000..5aea44b62b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Verify WebExtension background service worker mode</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + "use strict"; + // This test ensures we are running with the proper settings. + const {WebExtensionPolicy} = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services); + SimpleTest.ok(WebExtensionPolicy.useRemoteWebExtensions, "extensions running remote"); + SimpleTest.ok(!WebExtensionPolicy.isExtensionProcess, "testing from remote process"); + SimpleTest.ok(WebExtensionPolicy.backgroundServiceWorkerEnabled, "extensions background service worker enabled"); + SimpleTest.ok(AppConstants.MOZ_WEBEXT_WEBIDL_ENABLED, "extensions API webidl bindings enabled"); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js new file mode 100644 index 0000000000..14d3ad2bab --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js @@ -0,0 +1,9 @@ +"use strict"; + +/* eslint-env worker */ + +onmessage = function (event) { + fetch("https://example.com/example.txt").then(() => { + postMessage("Done!"); + }); +}; diff --git a/toolkit/components/extensions/test/mochitest/webrequest_test.sys.mjs b/toolkit/components/extensions/test/mochitest/webrequest_test.sys.mjs new file mode 100644 index 0000000000..33554f3023 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_test.sys.mjs @@ -0,0 +1,16 @@ +export var webrequest_test = { + testFetch(url) { + return fetch(url); + }, + + testXHR(url) { + return new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("HEAD", url); + xhr.onload = () => { + resolve(); + }; + xhr.send(); + }); + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/webrequest_worker.js b/toolkit/components/extensions/test/mochitest/webrequest_worker.js new file mode 100644 index 0000000000..dcffd08578 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +fetch("https://example.com/example.txt"); |