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
This commit is contained in:
Luca Greco 2022-01-12 11:33:13 +00:00
parent 804f0d3fde
commit 63324905ef
8 changed files with 225 additions and 40 deletions

View File

@ -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.

View File

@ -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;
}

View File

@ -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}`
);
}
},

View File

@ -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"

View File

@ -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]

View File

@ -3,6 +3,7 @@
<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>
@ -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();
});
</script>

View File

@ -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>

View File

@ -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);
}