Bug 1950639 - Implement browser.test changes required for WPT r=robwu

Differential Revision: https://phabricator.services.mozilla.com/D244091
This commit is contained in:
zombie
2025-04-17 13:16:14 +00:00
parent 2a246a1405
commit 7a87b55356
8 changed files with 210 additions and 3 deletions

View File

@@ -19,6 +19,10 @@ user_pref("dom.testing.testutils.enabled", true);
user_pref("extensions.autoDisableScopes", 10);
// Don't open a dialog to show available add-on updates
user_pref("extensions.update.notifyUser", false);
// For cross-browser tests, don't fail on manifest warnings like unknown keys.
user_pref("extensions.webextensions.warnings-as-errors", false);
// Adjust behavior of browser.test API to be compatible across engines.
user_pref("extensions.wpt.enabled", true);
// Enable test mode to run multiple tests in parallel
user_pref("focusmanager.testmode", true);
// Enable fake media streams for getUserMedia

View File

@@ -1261,6 +1261,9 @@ export class SpecialPowersParent extends JSWindowActorParent {
extension.on("test-eq", resultListener);
extension.on("test-log", resultListener);
extension.on("test-done", resultListener);
// Web Platform Test subtest started and finished events.
extension.on("test-task-start", resultListener);
extension.on("test-task-done", resultListener);
extension.on("test-message", messageListener);

View File

@@ -24,6 +24,10 @@ const lazy = XPCOMUtils.declareLazy({
service: "@mozilla.org/content/style-sheet-service;1",
iid: Ci.nsIStyleSheetService,
},
wptEnabled: {
pref: "extensions.wpt.enabled",
default: false,
},
});
const ScriptError = Components.Constructor(
@@ -3150,4 +3154,10 @@ export var ExtensionCommon = {
MultiAPIManager,
LazyAPIManager,
// Whether we're running under Web Platform Tests mode,
// required to adjust some cross-browser behaviors.
get isInWPT() {
return Cu.isInAutomation && lazy.wptEnabled;
},
};

View File

@@ -2476,6 +2476,13 @@ class ArrayType extends Type {
}
value = v.value;
// eslint-disable-next-line no-use-before-define
if (value && this.itemType instanceof FunctionType) {
// This needs special handling if we're expecting an array of functions,
// because iterating over (wrapped) callable items fails otherwise.
value = this.extractItems(value, context);
}
let result = [];
for (let [i, element] of value.entries()) {
element = context.withPath(String(i), () =>
@@ -2509,6 +2516,29 @@ class ArrayType extends Type {
return this.postprocess({ value: result }, context);
}
/**
* Extracts all items of the given array, including callable
* ones which would normally be omitted by X-ray wrappers.
*
* @see ObjectType.extractProperties for more details.
*
* @param {Array} value
* @param {Context} context
* @returns {Array}
*/
extractItems(value, context) {
let klass = ChromeUtils.getClassName(value, true);
if (klass !== "Array") {
throw context.error(
`Expected a plain JavaScript array, got a ${klass}`,
`be a plain JavaScript array`
);
}
let obj = ChromeUtils.shallowClone(value);
obj.length = value.length;
return Array.from(obj);
}
checkBaseType(baseType) {
return baseType == "array";
}

View File

@@ -131,7 +131,53 @@ const toSource = value => {
this.test = class extends ExtensionAPI {
getAPI(context) {
const CONTEXT_DESTROYED = "Test context destroyed.";
const { extension } = context;
let running = false;
let testTasks = [];
let unnamed = 0;
async function runTasks(tests) {
testTasks.push(...tests);
// If still running tasks from a previous call, queue new ones and bail.
if (running) {
return;
}
let onClosed = Promise.withResolvers();
onClosed.close = () => onClosed.reject(CONTEXT_DESTROYED);
context.callOnClose(onClosed);
try {
running = true;
while (testTasks.length) {
let task = testTasks.shift();
let name = task.name || `unnamed_test_${++unnamed}`;
let stack = getStack(context.getCaller());
extension.emit("test-task-start", name, stack);
try {
await Promise.race([task(), onClosed.promise]);
if (!context.active) {
assertTrue(false, CONTEXT_DESTROYED);
throw new ExtensionUtils.ExtensionError(CONTEXT_DESTROYED);
}
} catch (e) {
let err = `Exception running ${name}: ${e.message}`;
assertTrue(false, err);
Cu.reportError(err);
throw e;
} finally {
extension.emit("test-task-done", testTasks.length, name, stack);
}
}
} finally {
context.forgetOnClose(onClosed);
testTasks.length = 0;
running = false;
}
}
function getStack(savedFrame = null) {
if (savedFrame) {
@@ -327,6 +373,15 @@ this.test = class extends ExtensionAPI {
},
assertThrows(func, expectedError, msg) {
if (!expectedError) {
if (ExtensionCommon.isInWPT) {
expectedError = /.*/;
} else {
throw new ExtensionUtils.ExtensionError(
"Missing required expectedError"
);
}
}
try {
func();
@@ -351,6 +406,10 @@ this.test = class extends ExtensionAPI {
}
},
runTests(tests) {
return runTasks(tests);
},
onMessage: new TestEventManager({
context,
name: "test.onMessage",

View File

@@ -137,6 +137,7 @@
{
"name": "assertThrows",
"type": "function",
"allowAmbiguousOptionalArguments": true,
"parameters": [
{
"name": "func",
@@ -144,7 +145,9 @@
},
{
"name": "expectedError",
"$ref": "ExpectedError"
"$ref": "ExpectedError",
"optional": true,
"description": "Required, unless running with extensions.wpt.enabled"
},
{
"name": "message",
@@ -152,6 +155,21 @@
"optional": true
}
]
},
{
"name": "runTests",
"type": "function",
"async": true,
"parameters": [
{
"name": "tests",
"type": "array",
"minItems": 1,
"items": {
"type": "function"
}
}
]
}
],
"types": [

View File

@@ -32,3 +32,4 @@ skip-if = ["true"] # Bug 1748318 - Add WebIDL bindings for `tabs`
skip-if = ["true"] # Bug 1748318 - Add WebIDL bindings for `tabs`
["test_ext_test.html"]
skip-if = ["true"] # Bug 1950639 - Fix webidl version once WPT design stabilizes.

View File

@@ -52,7 +52,7 @@ function loadExtensionAndInterceptTest(extensionData) {
//
// All browser.test calls results are intercepted by the test itself, see verifyTestResults for
// the expectations of each browser.test call.
function testScript() {
async function testScript() {
browser.test.notifyPass("dot notifyPass");
browser.test.notifyFail("dot notifyFail");
browser.test.log("dot log");
@@ -124,6 +124,13 @@ function testScript() {
/dummy/
);
browser.test.assertThrows(
() => { throw new Error("dummy4"); },
"dummy4"
// allowAmbiguousOptionalArguments: with two optional arguments,
// passing a string is interpreted as `expectedError`, not `message`.
);
// 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.sys.mjs-generated API bindings.
@@ -156,11 +163,38 @@ function testScript() {
browser.test.assertEq(true, false, document.createElement("div"));
}
await browser.test.runTests([
async () => {
await new Promise(r => setTimeout(r, 100));
browser.test.assertTrue(true, "After await.");
},
function subTest2() {
// Normally, assertThrows() requires the second `expectedError` argument,
// so the inner assertThrows() will throw, producing one success result.
// In WPT mode, the inner assertThrows() will not throw,
// producing two failure results.
browser.test.assertThrows(
() => browser.test.assertThrows(() => "no-throw"),
/Missing required expectedError/
);
throw new Error("Sub2");
},
() => {
// This task will not run because previous task throws.
browser.test.assertTrue(false, "Never runs.");
},
]).catch(e => {
// We could use assertRejects, but the flow would be harder to follow.
browser.test.assertEq(e.message, "Sub2", "Expected exception.");
});
browser.test.sendMessage("Ran test at", location.protocol);
browser.test.sendMessage("This is the last browser.test call");
}
function verifyTestResults(results, shortName, expectedProtocol, useServiceWorker) {
function verifyTestResults(results, shortName, expectedProtocol, useServiceWorker, wptMode) {
let expectations = [
["test-done", true, "dot notifyPass"],
["test-done", false, "dot notifyFail"],
@@ -236,6 +270,10 @@ function verifyTestResults(results, shortName, expectedProtocol, useServiceWorke
"test-result", false,
"Function did not throw, expected error '/dummy/'"
],
[
"test-result", true,
"Function threw, expecting error to match '\"dummy4\"', got 'Error: dummy4'"
],
[
"test-result", true,
"Function threw, expecting error to match '/An unexpected error occurred/', got 'Error: An unexpected error occurred': assertDeepEq obj with function throws",
@@ -262,6 +300,33 @@ function verifyTestResults(results, shortName, expectedProtocol, useServiceWorke
]);
}
expectations.push(
["test-task-start", "unnamed_test_1"],
["test-result", true, "After await."],
["test-task-done", 2, "unnamed_test_1"],
);
if (wptMode) {
expectations.push(
["test-task-start", "subTest2"],
["test-result", false, "Function did not throw, expected error '/.*/'"],
["test-result", false, "Function did not throw, expected error '/Missing required expectedError/'"],
["test-result", false, "Exception running subTest2: Sub2"],
["test-task-done", 1, "subTest2"],
);
} else {
expectations.push(
["test-task-start", "subTest2"],
[
"test-result", true,
"Function threw, expecting error to match '/Missing required expectedError/', got 'Error: Missing required expectedError'"
],
["test-result", false, "Exception running subTest2: Sub2"],
["test-task-done", 1, "subTest2"],
);
}
expectations.push(["test-eq", true, "Expected exception.", "Sub2", "Sub2"]);
expectations.push(...[
["test-message", "Ran test at", expectedProtocol],
["test-message", "This is the last browser.test call"],
@@ -290,6 +355,23 @@ add_task(async function test_test_in_background() {
await extension.unload();
});
add_task(async function test_test_in_wpt_mode() {
let extensionData = {
background: `(${testScript})()`,
useServiceWorker: false,
};
await SpecialPowers.pushPrefEnv({ set: [["extensions.wpt.enabled", true]] });
let extension = loadExtensionAndInterceptTest(extensionData);
await extension.startup();
let results = await extension.awaitResults();
verifyTestResults(results, "wpt mode", "moz-extension:", false, true);
await extension.unload();
await SpecialPowers.popPrefEnv();
});
add_task(async function test_test_in_background_service_worker() {
if (!ExtensionTestUtils.isInBackgroundServiceWorkerTests()) {
is(