From 63324905ef84efa849c54b5e3d2d452c12927dc2 Mon Sep 17 00:00:00 2001 From: Luca Greco Date: Wed, 12 Jan 2022 11:33:13 +0000 Subject: [PATCH] Bug 1724026 - Support running existing Extension API mochitests with a background service worker. r=mixedpuppy Provide a way to use service workers as the background script in existing tests, possibly by only requiring minimal changes to the existing test cases. This patch includes: - changes needed to detect when a test extension is being created for a test running in the "background service worker mode" and automatically turn the background script into a background service worker (instead of a background page) when not explicitly listed in the test extension manifest - a new mochitest-serviceworker.ini manifest where new or existing test files meant to be run on a background service worker can be added to run them automatically in the "background service worker mode" - a new test_verify_sw_mode.html smoke test that make sure the mochitest-serviceworker.ini manifest is running in the expected mode. - a new `sw-webextension` tag, which can be used locally to run a test file only in the "background service worker mode" - changes to test_ext_test.html to make it able to run in both background pages and background workers - small tweaks to `test` API (both the WebIDL binding and the current bindings injected from privileged js code, to better match each other behavior) Differential Revision: https://phabricator.services.mozilla.com/D122536 --- .../tests/SimpleTest/ExtensionTestUtils.js | 16 ++- .../extensions/ExtensionTestCommon.jsm | 45 ++++++- .../components/extensions/child/ext-test.js | 36 +++-- toolkit/components/extensions/moz.build | 2 + .../mochitest/mochitest-serviceworker.ini | 15 +++ .../test/mochitest/test_ext_test.html | 125 ++++++++++++++---- .../test/mochitest/test_verify_sw_mode.html | 24 ++++ .../extensions/webidl-api/ExtensionTest.cpp | 2 +- 8 files changed, 225 insertions(+), 40 deletions(-) create mode 100644 toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini create mode 100644 toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html diff --git a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js index f75b09b9318a..b725707d8801 100644 --- a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js +++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js @@ -1,10 +1,22 @@ -var ExtensionTestUtils = {}; - const { ExtensionTestCommon } = SpecialPowers.Cu.import( "resource://testing-common/ExtensionTestCommon.jsm", {} ); +var ExtensionTestUtils = { + // Shortcut to more easily access WebExtensionPolicy.backgroundServiceWorkerEnabled + // from mochitest-plain tests. + getBackgroundServiceWorkerEnabled() { + return ExtensionTestCommon.getBackgroundServiceWorkerEnabled(); + }, + + // A test helper used to check if the pref "extension.backgroundServiceWorker.forceInTestExtension" + // is set to true. + isInBackgroundServiceWorkerTests() { + return ExtensionTestCommon.isInBackgroundServiceWorkerTests(); + }, +}; + ExtensionTestUtils.loadExtension = function(ext) { // Cleanup functions need to be registered differently depending on // whether we're in browser chrome or plain mochitests. diff --git a/toolkit/components/extensions/ExtensionTestCommon.jsm b/toolkit/components/extensions/ExtensionTestCommon.jsm index 1ab2390a43e0..0ead28bca915 100644 --- a/toolkit/components/extensions/ExtensionTestCommon.jsm +++ b/toolkit/components/extensions/ExtensionTestCommon.jsm @@ -236,6 +236,37 @@ function provide(obj, keys, value, override = false) { } ExtensionTestCommon = class ExtensionTestCommon { + /** + * Shortcut to more easily access WebExtensionPolicy.backgroundServiceWorkerEnabled + * from mochitest-plain tests. + * + * @returns {boolean} true if the background service worker are enabled. + */ + static getBackgroundServiceWorkerEnabled() { + return WebExtensionPolicy.backgroundServiceWorkerEnabled; + } + + /** + * A test helper mainly used to skip test tasks if running in "backgroundServiceWorker" test mode + * (e.g. while running test files shared across multiple test modes: e.g. in-process-webextensions, + * remote-webextensions, sw-webextensions etc.). + * + * The underlying pref "extension.backgroundServiceWorker.forceInTestExtension": + * - is set to true in the xpcshell-serviceworker.ini and mochitest-serviceworker.ini manifests + * (and so it is going to be set to true while running the test files listed in those manifests) + * - when set to true, all test extension using a background script without explicitly listing it + * in the test extension manifest will be automatically executed as background service workers + * (instead of background scripts loaded in a background page) + * + * @returns {boolean} true if the test is running in "background service worker mode" + */ + static isInBackgroundServiceWorkerTests() { + return Services.prefs.getBoolPref( + "extensions.backgroundServiceWorker.forceInTestExtension", + false + ); + } + /** * This code is designed to make it easy to test a WebExtension * without creating a bunch of files. Everything is contained in a @@ -284,10 +315,22 @@ ExtensionTestCommon = class ExtensionTestCommon { delete manifest.host_permissions; } + if (data.useServiceWorker === undefined) { + // If we're force-testing service workers we will turn the background + // script part of ExtensionTestUtils test extensions into a background + // service worker. + data.useServiceWorker = ExtensionTestCommon.isInBackgroundServiceWorkerTests(); + } + if (data.background) { let bgScript = Services.uuid.generateUUID().number + ".js"; - provide(manifest, ["background", "scripts"], [bgScript], true); + let scriptKey = data.useServiceWorker + ? ["background", "service_worker"] + : ["background", "scripts"]; + let scriptVal = data.useServiceWorker ? bgScript : [bgScript]; + provide(manifest, scriptKey, scriptVal, true); + files[bgScript] = data.background; } diff --git a/toolkit/components/extensions/child/ext-test.js b/toolkit/components/extensions/child/ext-test.js index 893bb97f0bbd..acb358cc7669 100644 --- a/toolkit/components/extensions/child/ext-test.js +++ b/toolkit/components/extensions/child/ext-test.js @@ -231,16 +231,24 @@ this.test = class extends ExtensionAPI { return promise.then( result => { - assertTrue(false, `Promise resolved, expected rejection: ${msg}`); + let message = `Promise resolved, expected rejection '${toSource( + expectedError + )}'`; + if (msg) { + message += `: ${msg}`; + } + assertTrue(false, message); }, error => { - let errorMessage = toSource(error && error.message); + let expected = toSource(expectedError); + let message = `got '${toSource(error)}'`; + if (msg) { + message += `: ${msg}`; + } assertTrue( errorMatches(error, expectedError, context), - `Promise rejected, expecting rejection to match ${toSource( - expectedError - )}, got ${errorMessage}: ${msg}` + `Promise rejected, expecting rejection to match '${expected}', ${message}` ); } ); @@ -250,15 +258,23 @@ this.test = class extends ExtensionAPI { try { func(); - assertTrue(false, `Function did not throw, expected error: ${msg}`); + let message = `Function did not throw, expected error '${toSource( + expectedError + )}'`; + if (msg) { + message += `: ${msg}`; + } + assertTrue(false, message); } catch (error) { - let errorMessage = toSource(error && error.message); + let expected = toSource(expectedError); + let message = `got '${toSource(error)}'`; + if (msg) { + message += `: ${msg}`; + } assertTrue( errorMatches(error, expectedError, context), - `Function threw, expecting error to match ${toSource( - expectedError - )}, got ${errorMessage}: ${msg}` + `Function threw, expecting error to match '${expected}', ${message}` ); } }, diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build index 2c5e91a2b4e2..5229f2ddae11 100755 --- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -127,6 +127,8 @@ XPCSHELL_TESTS_MANIFESTS += [ # in builds where they are enabled (currently only on Nightly builds). if CONFIG["MOZ_WEBEXT_WEBIDL_ENABLED"]: XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/webidl-api/xpcshell.ini"] + MOCHITEST_MANIFESTS += ["test/mochitest/mochitest-serviceworker.ini"] + SPHINX_TREES["webextensions"] = "docs" 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 000000000000..78ea3d27057b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-serviceworker.ini @@ -0,0 +1,15 @@ +[DEFAULT] +tags = webextensions sw-webextensions +skip-if = + !e10s # Thunderbird does still run in non e10s mode (and so also with in-process-webextensions mode) + (os == 'android') # Bug 1620091: disable on android until extension process is done + +prefs = + extensions.webextensions.remote=true + extensions.backgroundServiceWorker.enabled=true + extensions.backgroundServiceWorker.forceInTestExtension=true + +dupe-manifest = true + +[test_verify_sw_mode.html] +[test_ext_test.html] diff --git a/toolkit/components/extensions/test/mochitest/test_ext_test.html b/toolkit/components/extensions/test/mochitest/test_ext_test.html index 82aadcabfdf7..609e22fb4de7 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_test.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html @@ -3,6 +3,7 @@ Testing test + @@ -43,9 +44,15 @@ function loadExtensionAndInterceptTest(extensionData) { 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() { - // Note: The result of these browser.test calls are intercepted by the test. - // See verifyTestResults for the expectations of each browser.test call. browser.test.notifyPass("dot notifyPass"); browser.test.notifyFail("dot notifyFail"); browser.test.log("dot log"); @@ -57,43 +64,36 @@ function testScript() { let obj = {}; let arr = []; - let dom = document.createElement("body"); browser.test.assertTrue(obj, "Object truthy"); browser.test.assertTrue(arr, "Array truthy"); - browser.test.assertTrue(dom, "Element truthy"); browser.test.assertTrue(true, "True truthy"); browser.test.assertTrue(false, "False truthy"); browser.test.assertTrue(null, "Null truthy"); browser.test.assertTrue(undefined, "Void truthy"); - browser.test.assertTrue(false, document.createElement("html")); browser.test.assertFalse(obj, "Object falsey"); browser.test.assertFalse(arr, "Array falsey"); - browser.test.assertFalse(dom, "Element falsey"); browser.test.assertFalse(true, "True falsey"); browser.test.assertFalse(false, "False falsey"); browser.test.assertFalse(null, "Null falsey"); browser.test.assertFalse(undefined, "Void falsey"); - browser.test.assertFalse(true, document.createElement("head")); browser.test.assertEq(obj, obj, "Object equality"); browser.test.assertEq(arr, arr, "Array equality"); - browser.test.assertEq(dom, dom, "Element equality"); browser.test.assertEq(null, null, "Null equality"); browser.test.assertEq(undefined, undefined, "Void equality"); browser.test.assertEq({}, {}, "Object reference ineqality"); browser.test.assertEq([], [], "Array reference ineqality"); - browser.test.assertEq(dom, document.createElement("body"), "Element ineqality"); browser.test.assertEq(null, undefined, "Null and void ineqality"); - browser.test.assertEq(true, false, document.createElement("div")); obj = { toString() { - return "Dynamic toString forbidden"; + return "Dynamic toString"; }, }; browser.test.assertEq(obj, obj, "obj with dynamic toString()"); + browser.test.assertThrows( () => { throw new Error("dummy"); }, /dummy2/, @@ -107,11 +107,25 @@ function testScript() { () => {}, /dummy/ ); + + // 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 ineqality"); + 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) { +function verifyTestResults(results, shortName, expectedProtocol, useServiceWorker) { let expectations = [ ["test-done", true, "dot notifyPass"], ["test-done", false, "dot notifyFail"], @@ -124,42 +138,72 @@ function verifyTestResults(results, shortName, expectedProtocol) { ["test-result", true, "Object truthy"], ["test-result", true, "Array truthy"], - ["test-result", true, "Element truthy"], ["test-result", true, "True truthy"], ["test-result", false, "False truthy"], ["test-result", false, "Null truthy"], ["test-result", false, "Void truthy"], - ["test-result", false, "[object HTMLHtmlElement]"], ["test-result", false, "Object falsey"], ["test-result", false, "Array falsey"], - ["test-result", false, "Element falsey"], ["test-result", false, "True falsey"], ["test-result", true, "False falsey"], ["test-result", true, "Null falsey"], ["test-result", true, "Void falsey"], - ["test-result", false, "[object HTMLHeadElement]"], ["test-eq", true, "Object equality", "[object Object]", "[object Object]"], ["test-eq", true, "Array equality", "", ""], - ["test-eq", true, "Element equality", "[object HTMLBodyElement]", "[object HTMLBodyElement]"], ["test-eq", true, "Null equality", "null", "null"], ["test-eq", true, "Void equality", "undefined", "undefined"], ["test-eq", false, "Object reference ineqality", "[object Object]", "[object Object] (different)"], ["test-eq", false, "Array reference ineqality", "", " (different)"], - ["test-eq", false, "Element ineqality", "[object HTMLBodyElement]", "[object HTMLBodyElement] (different)"], ["test-eq", false, "Null and void ineqality", "null", "undefined"], - ["test-eq", false, "[object HTMLDivElement]", "true", "false"], - ["test-eq", true, "obj with dynamic toString()", "[object Object]", "[object Object]"], - ["test-result", false, "Function threw, expecting error to match /dummy2/, got \"dummy\": intentional failure"], - ["test-result", false, "Function threw, expecting error to match /dummy3/, got \"dummy2\": null"], - ["test-result", false, "Function did not throw, expected error: 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/'" + ], + ]; + + 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 ineqality", "[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(" - "); @@ -171,12 +215,41 @@ function verifyTestResults(results, shortName, expectedProtocol) { 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:"); + 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(); }); @@ -198,7 +271,7 @@ add_task(async function test_test_in_content_script() { let win = window.open("file_sample.html"); let results = await extension.awaitResults(); win.close(); - verifyTestResults(results, "content script", "http:"); + verifyTestResults(results, "content script", "http:", false); await extension.unload(); }); 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 000000000000..5aea44b62b84 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_sw_mode.html @@ -0,0 +1,24 @@ + + + + Verify WebExtension background service worker mode + + + + + + + + + + + + diff --git a/toolkit/components/extensions/webidl-api/ExtensionTest.cpp b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp index a9e108c3af00..919d4aa76cad 100644 --- a/toolkit/components/extensions/webidl-api/ExtensionTest.cpp +++ b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp @@ -322,7 +322,7 @@ MOZ_CAN_RUN_SCRIPT void ExtensionTest::AssertThrows( } if (NS_WARN_IF(!AssertMatchInternal(aCx, exn, aExpectedError, - u"Function did throw, expected error"_ns, + u"Function threw, expecting error"_ns, aMessage, nullptr, aRv))) { ThrowUnexpectedError(aCx, aRv); }