summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js709
1 files changed, 709 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
new file mode 100644
index 0000000000..3108c7b9b4
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
@@ -0,0 +1,709 @@
+"use strict";
+
+const PROCESS_COUNT_PREF = "dom.ipc.processCount";
+
+const { createAppInfo } = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
+
+add_task(async function setup_test_environment() {
+ // Start with one content process so that we can increase the number
+ // later and test the behavior of a fresh content process.
+ Services.prefs.setIntPref(PROCESS_COUNT_PREF, 1);
+
+ // Grant the optional permissions requested, without prompting.
+ Services.prefs.setBoolPref(
+ "extensions.webextOptionalPermissionPrompts",
+ false
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webextOptionalPermissionPrompts");
+ });
+});
+
+// Test that there is no userScripts API namespace when the manifest doesn't include a user_scripts
+// property.
+add_task(async function test_userScripts_manifest_property_required() {
+ function background() {
+ browser.test.assertEq(
+ undefined,
+ browser.userScripts,
+ "userScripts API namespace should be undefined in the extension page"
+ );
+ browser.test.sendMessage("background-page:done");
+ }
+
+ async function contentScript() {
+ browser.test.assertEq(
+ undefined,
+ browser.userScripts,
+ "userScripts API namespace should be undefined in the content script"
+ );
+ browser.test.sendMessage("content-script:done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["http://*/*/file_sample.html"],
+ content_scripts: [
+ {
+ matches: ["http://*/*/file_sample.html"],
+ js: ["content_script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+ files: {
+ "content_script.js": contentScript,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-page:done");
+
+ let url = `${BASE_URL}/file_sample.html`;
+ let contentPage = await ExtensionTestUtils.loadContentPage(url);
+
+ await extension.awaitMessage("content-script:done");
+
+ await extension.unload();
+ await contentPage.close();
+});
+
+// Test that userScripts can only matches origins that are subsumed by the extension permissions,
+// and that more origins can be allowed by requesting an optional permission.
+add_task(async function test_userScripts_matches_denied() {
+ async function background() {
+ async function registerUserScriptWithMatches(matches) {
+ const scripts = await browser.userScripts.register({
+ js: [{ code: "" }],
+ matches,
+ });
+ await scripts.unregister();
+ }
+
+ // These matches are supposed to be denied until the extension has been granted the
+ // <all_urls> origin permission.
+ const testMatches = [
+ "<all_urls>",
+ "file://*/*",
+ "https://localhost/*",
+ "http://example.com/*",
+ ];
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "test-denied-matches") {
+ for (let testMatch of testMatches) {
+ await browser.test.assertRejects(
+ registerUserScriptWithMatches([testMatch]),
+ /Permission denied to register a user script for/,
+ "Got the expected rejection when the extension permission does not subsume the userScript matches"
+ );
+ }
+ } else if (msg === "grant-all-urls") {
+ await browser.permissions.request({ origins: ["<all_urls>"] });
+ } else if (msg === "test-allowed-matches") {
+ for (let testMatch of testMatches) {
+ try {
+ await registerUserScriptWithMatches([testMatch]);
+ } catch (err) {
+ browser.test.fail(
+ `Unexpected rejection ${err} on matching ${JSON.stringify(
+ testMatch
+ )}`
+ );
+ }
+ }
+ } else {
+ browser.test.fail(`Received an unexpected ${msg} test message`);
+ }
+
+ browser.test.sendMessage(`${msg}:done`);
+ });
+
+ browser.test.sendMessage("background-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://localhost/*"],
+ optional_permissions: ["<all_urls>"],
+ user_scripts: {},
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("background-ready");
+
+ // Test that the matches not subsumed by the extension permissions are being denied.
+ extension.sendMessage("test-denied-matches");
+ await extension.awaitMessage("test-denied-matches:done");
+
+ // Grant the optional <all_urls> permission.
+ await withHandlingUserInput(extension, async () => {
+ extension.sendMessage("grant-all-urls");
+ await extension.awaitMessage("grant-all-urls:done");
+ });
+
+ // Test that all the matches are now subsumed by the extension permissions.
+ extension.sendMessage("test-allowed-matches");
+ await extension.awaitMessage("test-allowed-matches:done");
+
+ await extension.unload();
+});
+
+// Test that userScripts sandboxes:
+// - can be registered/unregistered from an extension page (and they are registered on both new and
+// existing processes).
+// - have no WebExtensions APIs available
+// - are able to access the target window and document
+add_task(async function test_userScripts_no_webext_apis() {
+ async function background() {
+ const matches = ["http://localhost/*/file_sample.html*"];
+
+ const sharedCode = {
+ code: 'console.log("js code shared by multiple userScripts");',
+ };
+
+ const userScriptOptions = {
+ js: [
+ sharedCode,
+ {
+ code: `
+ window.addEventListener("load", () => {
+ const webextAPINamespaces = this.browser ? Object.keys(this.browser) : undefined;
+ document.body.innerHTML = "userScript loaded - " + JSON.stringify(webextAPINamespaces);
+ }, {once: true});
+ `,
+ },
+ ],
+ runAt: "document_start",
+ matches,
+ scriptMetadata: {
+ name: "test-user-script",
+ arrayProperty: ["el1"],
+ objectProperty: { nestedProp: "nestedValue" },
+ nullProperty: null,
+ },
+ };
+
+ let script = await browser.userScripts.register(userScriptOptions);
+
+ // Unregister and then register the same js code again, to verify that the last registered
+ // userScript doesn't get assigned a revoked blob url (otherwise Extensioncontent.jsm
+ // ScriptCache raises an error because it fails to compile the revoked blob url and the user
+ // script will never be loaded).
+ script.unregister();
+ script = await browser.userScripts.register(userScriptOptions);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "register-new-script") {
+ return;
+ }
+
+ await script.unregister();
+ await browser.userScripts.register({
+ ...userScriptOptions,
+ scriptMetadata: { name: "test-new-script" },
+ js: [
+ sharedCode,
+ {
+ code: `
+ window.addEventListener("load", () => {
+ const webextAPINamespaces = this.browser ? Object.keys(this.browser) : undefined;
+ document.body.innerHTML = "new userScript loaded - " + JSON.stringify(webextAPINamespaces);
+ }, {once: true});
+ `,
+ },
+ ],
+ });
+
+ browser.test.sendMessage("script-registered");
+ });
+
+ const scriptToRemove = await browser.userScripts.register({
+ js: [
+ sharedCode,
+ {
+ code: `
+ window.addEventListener("load", () => {
+ document.body.innerHTML = "unexpected unregistered userScript loaded";
+ }, {once: true});
+ `,
+ },
+ ],
+ runAt: "document_start",
+ matches,
+ scriptMetadata: {
+ name: "user-script-to-remove",
+ },
+ });
+
+ browser.test.assertTrue(
+ "unregister" in script,
+ "Got an unregister method on the userScript API object"
+ );
+
+ // Remove the last registered user script.
+ await scriptToRemove.unregister();
+
+ browser.test.sendMessage("background-ready");
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["http://localhost/*/file_sample.html"],
+ user_scripts: {},
+ },
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+
+ await extension.awaitMessage("background-ready");
+
+ let url = `${BASE_URL}/file_sample.html?testpage=1`;
+ let contentPage = await ExtensionTestUtils.loadContentPage(url);
+ let result = await contentPage.spawn([], async () => {
+ return {
+ textContent: this.content.document.body.textContent,
+ url: this.content.location.href,
+ readyState: this.content.document.readyState,
+ };
+ });
+ Assert.deepEqual(
+ result,
+ {
+ textContent: "userScript loaded - undefined",
+ url,
+ readyState: "complete",
+ },
+ "The userScript executed on the expected url and no access to the WebExtensions APIs"
+ );
+
+ info("Test content script are correctly created on a newly created process");
+
+ await extension.sendMessage("register-new-script");
+ await extension.awaitMessage("script-registered");
+
+ // Update the process count preference, so that we can test that the newly registered user script
+ // is propagated as expected into the newly created process.
+ Services.prefs.setIntPref(PROCESS_COUNT_PREF, 2);
+
+ const url2 = `${BASE_URL}/file_sample.html?testpage=2`;
+ let contentPage2 = await ExtensionTestUtils.loadContentPage(url2, {
+ remote: true,
+ });
+ let result2 = await contentPage2.spawn([], async () => {
+ return {
+ textContent: this.content.document.body.textContent,
+ url: this.content.location.href,
+ readyState: this.content.document.readyState,
+ };
+ });
+ Assert.deepEqual(
+ result2,
+ {
+ textContent: "new userScript loaded - undefined",
+ url: url2,
+ readyState: "complete",
+ },
+ "The userScript executed on the expected url and no access to the WebExtensions APIs"
+ );
+
+ await contentPage.close();
+
+ await contentPage2.close();
+
+ await extension.unload();
+});
+
+// This test verify that a cached script is still able to catch the document
+// while it is still loading (when we do not block the document parsing as
+// we do for a non cached script).
+add_task(async function test_cached_userScript_on_document_start() {
+ function apiScript() {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ sendTestMessage(name, params) {
+ return browser.test.sendMessage(name, params);
+ },
+ });
+ });
+ }
+
+ async function background() {
+ function userScript() {
+ this.sendTestMessage("user-script-loaded", {
+ url: window.location.href,
+ documentReadyState: document.readyState,
+ });
+ }
+
+ await browser.userScripts.register({
+ js: [
+ {
+ code: `(${userScript})();`,
+ },
+ ],
+ runAt: "document_start",
+ matches: ["http://localhost/*/file_sample.html"],
+ });
+
+ browser.test.sendMessage("user-script-registered");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://localhost/*/file_sample.html"],
+ user_scripts: {
+ api_script: "api-script.js",
+ // The following is an unexpected manifest property, that we expect to be ignored and
+ // to not prevent the test extension from being installed and run as expected.
+ unexpected_manifest_key: "test-unexpected-key",
+ },
+ },
+ background,
+ files: {
+ "api-script.js": apiScript,
+ },
+ });
+
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ await extension.awaitMessage("user-script-registered");
+
+ let url = `${BASE_URL}/file_sample.html`;
+ let contentPage = await ExtensionTestUtils.loadContentPage(url);
+
+ let msg = await extension.awaitMessage("user-script-loaded");
+ Assert.deepEqual(
+ msg,
+ {
+ url,
+ documentReadyState: "loading",
+ },
+ "Got the expected url and document.readyState from a non cached user script"
+ );
+
+ // Reload the page and check that the cached content script is still able to
+ // run on document_start.
+ await contentPage.loadURL(url);
+
+ let msgFromCached = await extension.awaitMessage("user-script-loaded");
+ Assert.deepEqual(
+ msgFromCached,
+ {
+ url,
+ documentReadyState: "loading",
+ },
+ "Got the expected url and document.readyState from a cached user script"
+ );
+
+ await contentPage.close();
+ await extension.unload();
+});
+
+add_task(async function test_userScripts_pref_disabled() {
+ async function run_userScript_on_pref_disabled_test() {
+ async function background() {
+ let promise = (async () => {
+ await browser.userScripts.register({
+ js: [
+ {
+ code: "throw new Error('This userScripts should not be registered')",
+ },
+ ],
+ runAt: "document_start",
+ matches: ["<all_urls>"],
+ });
+ })();
+
+ await browser.test.assertRejects(
+ promise,
+ /userScripts APIs are currently experimental/,
+ "Got the expected error from userScripts.register when the userScripts API is disabled"
+ );
+
+ browser.test.sendMessage("background-page:done");
+ }
+
+ async function contentScript() {
+ let promise = (async () => {
+ browser.userScripts.onBeforeScript.addListener(() => {});
+ })();
+ await browser.test.assertRejects(
+ promise,
+ /userScripts APIs are currently experimental/,
+ "Got the expected error from userScripts.onBeforeScript when the userScripts API is disabled"
+ );
+
+ browser.test.sendMessage("content-script:done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["http://*/*/file_sample.html"],
+ user_scripts: { api_script: "" },
+ content_scripts: [
+ {
+ matches: ["http://*/*/file_sample.html"],
+ js: ["content_script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+ files: {
+ "content_script.js": contentScript,
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("background-page:done");
+
+ let url = `${BASE_URL}/file_sample.html`;
+ let contentPage = await ExtensionTestUtils.loadContentPage(url);
+
+ await extension.awaitMessage("content-script:done");
+
+ await extension.unload();
+ await contentPage.close();
+ }
+
+ await runWithPrefs(
+ [["extensions.webextensions.userScripts.enabled", false]],
+ run_userScript_on_pref_disabled_test
+ );
+});
+
+// This test verify that userScripts.onBeforeScript API Event is not available without
+// a "user_scripts.api_script" property in the manifest.
+add_task(async function test_user_script_api_script_required() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://localhost/*/file_sample.html"],
+ js: ["content_script.js"],
+ run_at: "document_start",
+ },
+ ],
+ user_scripts: {},
+ },
+ files: {
+ "content_script.js": function () {
+ browser.test.assertEq(
+ undefined,
+ browser.userScripts && browser.userScripts.onBeforeScript,
+ "Got an undefined onBeforeScript property as expected"
+ );
+ browser.test.sendMessage("no-onBeforeScript:done");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let url = `${BASE_URL}/file_sample.html`;
+ let contentPage = await ExtensionTestUtils.loadContentPage(url);
+
+ await extension.awaitMessage("no-onBeforeScript:done");
+
+ await extension.unload();
+ await contentPage.close();
+});
+
+add_task(async function test_scriptMetaData() {
+ function getTestCases(isUserScriptsRegister) {
+ return [
+ // When scriptMetadata is not set (or undefined), it is treated as if it were null.
+ // In the API script, the metadata is then expected to be null.
+ isUserScriptsRegister ? undefined : null,
+
+ // Falsey
+ null,
+ "",
+ false,
+ 0,
+
+ // Truthy
+ true,
+ 1,
+ "non-empty string",
+
+ // Objects
+ ["some array with value"],
+ { "some object": "with value" },
+ ];
+ }
+
+ async function background() {
+ for (let scriptMetadata of getTestCases(true)) {
+ await browser.userScripts.register({
+ js: [{ file: "userscript.js" }],
+ runAt: "document_end",
+ matches: ["http://localhost/*/file_sample.html"],
+ scriptMetadata,
+ });
+ }
+
+ browser.test.sendMessage("background-page:done");
+ }
+
+ function apiScript() {
+ let testCases = getTestCases(false);
+ let i = 0;
+
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ checkMetadata() {
+ let expectation = testCases[i];
+ let metadata = script.metadata;
+ if (typeof expectation === "object" && expectation !== null) {
+ // Non-primitive values cannot be compared with assertEq,
+ // so serialize both and just verify that they are equal.
+ expectation = JSON.stringify(expectation);
+ metadata = JSON.stringify(script.metadata);
+ }
+
+ browser.test.assertEq(
+ expectation,
+ metadata,
+ `Expected metadata at call ${i}`
+ );
+ if (++i === testCases.length) {
+ browser.test.sendMessage("apiscript:done");
+ }
+ },
+ });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `${getTestCases};(${background})()`,
+ manifest: {
+ permissions: ["http://*/*/file_sample.html"],
+ user_scripts: {
+ api_script: "apiscript.js",
+ },
+ },
+ files: {
+ "apiscript.js": `${getTestCases};(${apiScript})()`,
+ "userscript.js": "checkMetadata();",
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("background-page:done");
+
+ const pageUrl = `${BASE_URL}/file_sample.html`;
+ info(`Load content page: ${pageUrl}`);
+ const page = await ExtensionTestUtils.loadContentPage(pageUrl);
+
+ await extension.awaitMessage("apiscript:done");
+
+ await page.close();
+
+ await extension.unload();
+});
+
+add_task(async function test_userScriptOptions_js_property_required() {
+ function background() {
+ const userScriptOptions = {
+ runAt: "document_start",
+ matches: ["http://*/*/file_sample.html"],
+ };
+
+ browser.test.assertThrows(
+ () => browser.userScripts.register(userScriptOptions),
+ /Type error for parameter userScriptOptions \(Property \"js\" is required\)/,
+ "Got the expected error from userScripts.register when js property is missing"
+ );
+
+ browser.test.sendMessage("done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["http://*/*/file_sample.html"],
+ user_scripts: {},
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function test_userScripts_are_unregistered_on_unload() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://*/*/file_sample.html"],
+ user_scripts: {
+ api_script: "api_script.js",
+ },
+ },
+ files: {
+ "userscript.js": "",
+ "extpage.html": `<!DOCTYPE html><script src="extpage.js"></script>`,
+ "extpage.js": async function extPage() {
+ await browser.userScripts.register({
+ js: [{ file: "userscript.js" }],
+ matches: ["http://localhost/*/file_sample.html"],
+ });
+
+ browser.test.sendMessage("user-script-registered");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ equal(
+ // In order to read the `registeredContentScripts` map, we need to access
+ // the extension embedded in the `ExtensionWrapper` first.
+ extension.extension.registeredContentScripts.size,
+ 0,
+ "no user scripts registered yet"
+ );
+
+ const url = `moz-extension://${extension.uuid}/extpage.html`;
+ info(`loading extension page: ${url}`);
+ const page = await ExtensionTestUtils.loadContentPage(url);
+
+ info("waiting for the user script to be registered");
+ await extension.awaitMessage("user-script-registered");
+
+ equal(
+ extension.extension.registeredContentScripts.size,
+ 1,
+ "got registered user scripts in the extension content scripts map"
+ );
+
+ await page.close();
+
+ equal(
+ extension.extension.registeredContentScripts.size,
+ 0,
+ "user scripts unregistered from the extension content scripts map"
+ );
+
+ await extension.unload();
+});