Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
-
CI results over the last 22 days
- Manifest: toolkit/modules/tests/xpcshell/xpcshell.toml
/* Any copyright is dedicated to the Public Domain.
const { BrowserUtils } = ChromeUtils.importESModule(
"resource://gre/modules/BrowserUtils.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
);
add_setup(function initFOG() {
do_get_profile();
Services.fog.initializeFOG();
});
/**
* Verify that category manager calling modules are loaded on-demand,
* and that caching doesn't break adding more modules as category entries
* at runtime.
*/
add_task(async function test_callModulesFromCategory() {
const CATEGORY = "test-modules-from-catman";
const OBSTOPIC1 = CATEGORY + "-notification";
const OBSTOPIC2 = CATEGORY + "-other-notification";
// The two modules both fire different observer topics to allow us to ensure
// they have been called. This helper just makes it easier to get only
// that return value as a result of a promise, as `topicObserved` also
// returns the "subject" of the observer notification, which we don't care about.
let rvFromModule = topic =>
TestUtils.topicObserved(topic).then(([_subj, data]) => data);
// Start off with nothing in a category:
Assert.equal(
Cu.isESModuleLoaded(MODULE1),
false,
"First module should not be loaded."
);
let catEntries = Array.from(Services.catMan.enumerateCategory(CATEGORY));
Assert.deepEqual(catEntries, [], "Should be no entries for this category.");
try {
// There's nothing in this category right now so this should be a no-op.
BrowserUtils.callModulesFromCategory({ categoryName: CATEGORY }, "Hello");
} catch (ex) {
Assert.ok(false, `Should not have thrown but received an exception ${ex}`);
}
// Now add an item, check that calling it now works.
//
// Note that category manager observer notifications are async (they get
// dispatched as runnables) and so we have to wait for it to make sure that
// BrowserUtils has had a chance of being told new entries have arrived.
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
CATEGORY,
MODULE1,
`Module1.test`,
false,
false
);
catEntries = Array.from(Services.catMan.enumerateCategory(CATEGORY));
Assert.equal(catEntries.length, 1);
// See note above.
await catManUpdated;
Assert.equal(
Cu.isESModuleLoaded(MODULE1),
false,
"First module should still not be loaded."
);
// This entry will cause an observer topic to notify, so ensure that happens.
let moduleResult = rvFromModule(OBSTOPIC1);
BrowserUtils.callModulesFromCategory({ categoryName: CATEGORY }, "Hello");
Assert.equal(
Cu.isESModuleLoaded(MODULE1),
true,
"First module should be loaded sync."
);
Assert.equal("Hello", await moduleResult, "Should have been called.");
// Now add another item, check that both are called.
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
CATEGORY,
MODULE2,
`Module2.othertest`,
false,
false
);
await catManUpdated;
moduleResult = Promise.all([
rvFromModule(OBSTOPIC1),
rvFromModule(OBSTOPIC2),
]);
BrowserUtils.callModulesFromCategory({ categoryName: CATEGORY }, "Hello");
Assert.deepEqual(
["Hello", "Hello"],
await moduleResult,
"Both modules should have been called."
);
// Now remove the first module again, check that only the second one notifies.
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-removed");
Services.catMan.deleteCategoryEntry(CATEGORY, MODULE1, false);
await catManUpdated;
let ob = () => Assert.ok(false, "I shouldn't be called.");
Services.obs.addObserver(ob, OBSTOPIC1);
moduleResult = rvFromModule(OBSTOPIC2);
BrowserUtils.callModulesFromCategory({ categoryName: CATEGORY }, "Hello");
Assert.equal(
"Hello",
await moduleResult,
"Second module should still be called."
);
let idleResult = null;
let idlePromise = TestUtils.topicObserved(OBSTOPIC2).then(([_subj, data]) => {
idleResult = data;
return data;
});
BrowserUtils.callModulesFromCategory(
{ categoryName: CATEGORY, idleDispatch: true },
"Hello"
);
Assert.equal(idleResult, null, "Idle calls should not happen immediately.");
Assert.equal("Hello", await idlePromise, "Idle calls should run eventually.");
Services.obs.removeObserver(ob, OBSTOPIC1);
// Now clean up our category for later tests.
Services.catMan.deleteCategory(CATEGORY);
});
// With idleDispatch, a consumer whose jsGlobal is a window that has closed by
// the time the idle task runs should be dropped, so we don't initialize (and
// leak) a closing window.
add_task(
async function test_callModulesFromCategory_idleDispatch_closed_window() {
const CATEGORY = "test-js-global-catman-closed-window";
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
CATEGORY,
MODULE,
"FakeObj.doThing",
false,
false
);
await catManUpdated;
let sandbox = sinon.createSandbox();
// A window that has closed: its consumer should be dropped.
let closedWindow = { closed: true, FakeObj: { doThing: sandbox.spy() } };
await BrowserUtils.callModulesFromCategory(
{ categoryName: CATEGORY, idleDispatch: true, jsGlobal: closedWindow },
"win"
);
sinon.assert.notCalled(closedWindow.FakeObj.doThing);
// A window that is still open: its consumer should run as usual.
let openWindow = { closed: false, FakeObj: { doThing: sandbox.spy() } };
await BrowserUtils.callModulesFromCategory(
{ categoryName: CATEGORY, idleDispatch: true, jsGlobal: openWindow },
"win"
);
sinon.assert.calledOnce(openWindow.FakeObj.doThing);
sinon.assert.calledWithExactly(openWindow.FakeObj.doThing, "win");
sandbox.restore();
Services.catMan.deleteCategory(CATEGORY);
}
);
// Test that errors are reported but do not throw at the callsite,
// and that custom error handlers are invoked.
add_task(async function test_callModulesFromCategory_errors() {
const OTHER_CAT = "someothercat";
// Add an item that doesn't exist, and check that although we report errors,
// the callsite doesn't throw.
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
OTHER_CAT,
MODULE1,
`Module1.nonExistantFunction`,
false,
false
);
await catManUpdated;
let catEntries = Array.from(Services.catMan.enumerateCategory(OTHER_CAT));
Assert.equal(catEntries.length, 1);
let consolePromise = TestUtils.consoleMessageObserved(m => {
let firstArg = m.wrappedJSObject.arguments?.[0]?.message;
return typeof firstArg == "string" && firstArg.includes("not a function");
});
BrowserUtils.callModulesFromCategory(
{
categoryName: OTHER_CAT,
},
"Hello"
);
let reportedError = await consolePromise;
let firstArg = reportedError.wrappedJSObject.arguments?.[0]?.message;
Assert.stringContains(
firstArg,
MODULE1,
"Error message should include module URL."
);
Services.catMan.deleteCategoryEntry(OTHER_CAT, MODULE1, false);
// Check that custom exception handling from extant methods works:
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
OTHER_CAT,
MODULE1,
`Module1.throwingFunction`,
false,
false
);
await catManUpdated;
Assert.equal(catEntries.length, 1);
let exHandler = Promise.withResolvers();
BrowserUtils.callModulesFromCategory({
categoryName: OTHER_CAT,
failureHandler: exHandler.resolve,
});
let caughtException = await exHandler.promise;
Assert.stringContains(
caughtException.message,
"Uh oh",
"Exceptions should be handled."
);
// Now clean up our category for later tests.
Services.catMan.deleteCategory(OTHER_CAT);
});
// A failing consumer should be reported to telemetry via the
// browser_utils.category_dispatch_error event, with the category, module and
// consumer identifying the category manager entry it failed in, on every
// failure.
add_task(async function test_callModulesFromCategory_error_telemetry() {
const TELEMETRY_CAT = "sometelemetrycat";
const CONSUMER = "Module1.throwingFunction";
Services.fog.testResetFOG();
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
TELEMETRY_CAT,
MODULE1,
CONSUMER,
false,
false
);
await catManUpdated;
await BrowserUtils.callModulesFromCategory({ categoryName: TELEMETRY_CAT });
let events = Glean.browserUtils.categoryDispatchError.testGetValue();
Assert.equal(events?.length, 1, "One error event should be recorded.");
Assert.equal(
events[0].extra.category,
TELEMETRY_CAT,
"category extra is recorded."
);
Assert.equal(events[0].extra.module, MODULE1, "module extra is recorded.");
Assert.equal(
events[0].extra.consumer,
CONSUMER,
"consumer extra is the object.method identifier."
);
Assert.equal(
events[0].extra.errorName,
"Error",
"errorName extra is recorded."
);
await BrowserUtils.callModulesFromCategory({ categoryName: TELEMETRY_CAT });
events = Glean.browserUtils.categoryDispatchError.testGetValue();
Assert.equal(
events?.length,
2,
"A repeat failure of the same entry should be recorded again."
);
// TELEMETRY_CAT is not a declared label, so it is counted as __other__.
Assert.equal(
Glean.browserUtils.categoryConsumerResult
.get("__other__", "failure")
.testGetValue(),
2,
"Every failure is counted, including repeats of the same entry."
);
// A second failing consumer in the same category.
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
TELEMETRY_CAT,
MODULE2,
CONSUMER,
false,
false
);
await catManUpdated;
await BrowserUtils.callModulesFromCategory({ categoryName: TELEMETRY_CAT });
Assert.equal(
Glean.browserUtils.categoryDispatchError.testGetValue()?.length,
4,
"Both failing consumers of the dispatch should be recorded."
);
Assert.equal(
Glean.browserUtils.categoryConsumerResult
.get("__other__", "failure")
.testGetValue(),
4,
"Each failing consumer of a dispatch is counted, not each dispatch."
);
Services.catMan.deleteCategory(TELEMETRY_CAT);
});
add_task(async function test_callModulesFromCategory_success_telemetry() {
const SUCCESS_CAT = "somesuccesstelemetrycat";
Services.fog.testResetFOG();
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
SUCCESS_CAT,
"Module1.test",
false,
false
);
await catManUpdated;
await BrowserUtils.callModulesFromCategory({ categoryName: SUCCESS_CAT });
await BrowserUtils.callModulesFromCategory({ categoryName: SUCCESS_CAT });
Assert.equal(
Glean.browserUtils.categoryConsumerResult
.get("__other__", "success")
.testGetValue(),
2,
"Each consumer that returns is counted as a success."
);
Assert.equal(
Glean.browserUtils.categoryConsumerResult
.get("__other__", "failure")
.testGetValue(),
null,
"A consumer that returns is not counted as a failure."
);
Assert.equal(
Glean.browserUtils.categoryDispatchError.testGetValue(),
null,
"No error event is recorded for a consumer that returns."
);
Services.catMan.deleteCategory(SUCCESS_CAT);
});
// An entry registered at runtime can name an arbitrary module URI and object,
// neither of which may be recorded as-is.
add_task(async function test_callModulesFromCategory_error_telemetry_runtime() {
const RUNTIME_CAT = "someruntimetelemetrycat";
const CONSUMER = "/tmp/RuntimeObj.doThing";
Services.fog.testResetFOG();
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(RUNTIME_CAT, MODULE, CONSUMER, false, false);
await catManUpdated;
let jsGlobal = {
"/tmp/RuntimeObj": {
doThing() {
throw new Error("Runtime consumer failed.");
},
},
};
await BrowserUtils.callModulesFromCategory({
categoryName: RUNTIME_CAT,
jsGlobal,
});
let events = Glean.browserUtils.categoryDispatchError.testGetValue();
Assert.equal(events?.length, 1, "One error event should be recorded.");
Assert.equal(
events[0].extra.module,
"other",
"A module URI outside the allowed schemes is not recorded."
);
Assert.equal(
events[0].extra.consumer,
"other",
"A consumer that is not an identifier is not recorded."
);
Services.catMan.deleteCategory(RUNTIME_CAT);
});
// The errorName extra names XPCOM exceptions and bare nsresults by their
// NS_ERROR_* name, and DOMExceptions by their DOM name.
add_task(async function test_callModulesFromCategory_error_telemetry_names() {
const NAMES_CAT = "somenamestelemetrycat";
Services.fog.testResetFOG();
for (let [module, consumer] of [
]) {
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(NAMES_CAT, module, consumer, false, false);
await catManUpdated;
}
let jsGlobal = {
XpcomObj: {
fail() {
Services.prefs.getIntPref("browserutils.test.nonexistent");
},
},
DomObj: {
fail() {
throw new DOMException("Not here.", "NotFoundError");
},
},
NsresultObj: {
fail() {
// eslint-disable-next-line mozilla/no-throw-cr-literal
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
},
};
await BrowserUtils.callModulesFromCategory({
categoryName: NAMES_CAT,
jsGlobal,
});
let errorNames = Object.fromEntries(
Glean.browserUtils.categoryDispatchError
.testGetValue()
.map(e => [e.extra.consumer, e.extra.errorName])
);
Assert.deepEqual(
errorNames,
{
"XpcomObj.fail": "NS_ERROR_UNEXPECTED",
"DomObj.fail": "NotFoundError",
"NsresultObj.fail": "NS_ERROR_NOT_IMPLEMENTED",
},
"Each thrown value is recorded by its error name."
);
Services.catMan.deleteCategory(NAMES_CAT);
});
/**
* Test that callModulesFromCategory returns a Promise that resolves when all
* category tasks have settled.
*/
add_task(async function test_callModulesFromCategory_returns_promise() {
const CATEGORY = "test-modules-from-catman";
const OBSTOPIC1 = CATEGORY + "-notification";
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
CATEGORY,
MODULE1,
`Module1.test`,
false,
false
);
await catManUpdated;
let moduleResult = TestUtils.topicObserved(OBSTOPIC1).then(
([_subj, data]) => data
);
let result = BrowserUtils.callModulesFromCategory(
{ categoryName: CATEGORY },
"Hello"
);
Assert.ok(result.then, "Should return a Promise");
let settledResults = await result;
Assert.ok(Array.isArray(settledResults), "Should return an array of results");
Assert.equal(settledResults.length, 1, "Should have one result");
Assert.equal(
settledResults[0].status,
"fulfilled",
"Task should have been fulfilled"
);
Assert.equal(await moduleResult, "Hello", "Module should have been called");
// Now clean up our category for later tests.
Services.catMan.deleteCategory(CATEGORY);
});
// Test a category with both a plain .js entry and an ESM entry, for both
// single-window and multi-window scenarios.
add_task(async function test_callModulesFromCategory_multiple_window() {
const CATEGORY = "test-js-global-catman";
// A fake .js URL — the file doesn't need to exist since we rely on
// jsGlobal having the object already (via lazy getter in production).
const OBSTOPIC = "test-modules-from-catman-notification";
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
CATEGORY,
MODULE_JS,
"FakeObj.doThing",
false,
false
);
await catManUpdated;
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
CATEGORY,
MODULE_ESM,
"Module1.test",
false,
false
);
await catManUpdated;
let sandbox = sinon.createSandbox();
// Window 1: both the .js and ESM entries should be called.
let fakeGlobal1 = { FakeObj: { doThing: sandbox.spy() } };
let esmResult = TestUtils.topicObserved(OBSTOPIC).then(([, data]) => data);
BrowserUtils.callModulesFromCategory(
{ categoryName: CATEGORY, jsGlobal: fakeGlobal1 },
"window1"
);
sinon.assert.calledOnce(fakeGlobal1.FakeObj.doThing);
sinon.assert.calledWithExactly(fakeGlobal1.FakeObj.doThing, "window1");
Assert.equal(await esmResult, "window1", "ESM entry called for window 1.");
// Window 2 (multi-window): the .js entry must use the new jsGlobal, while
// the ESM entry continues to use the same singleton module instance.
let fakeGlobal2 = { FakeObj: { doThing: sandbox.spy() } };
esmResult = TestUtils.topicObserved(OBSTOPIC).then(([, data]) => data);
BrowserUtils.callModulesFromCategory(
{ categoryName: CATEGORY, jsGlobal: fakeGlobal2 },
"window2"
);
sinon.assert.calledOnce(fakeGlobal2.FakeObj.doThing);
sinon.assert.calledWithExactly(fakeGlobal2.FakeObj.doThing, "window2");
sinon.assert.calledOnce(fakeGlobal1.FakeObj.doThing); // not called again
Assert.equal(await esmResult, "window2", "ESM entry called for window 2.");
sandbox.restore();
Services.catMan.deleteCategory(CATEGORY);
});
// Test error paths for the jsGlobal / plain-.js-script code path.
add_task(async function test_callModulesFromCategory_jsGlobal_errors() {
const CATEGORY = "test-js-global-catman-errors";
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(
CATEGORY,
MODULE,
"FakeObj.doThing",
false,
false
);
await catManUpdated;
// Omitting jsGlobal for a .js entry should log an error.
let consolePromise = TestUtils.consoleMessageObserved(m => {
let firstArg = m.wrappedJSObject.arguments?.[0];
return typeof firstArg == "string" && firstArg.includes(CATEGORY);
});
BrowserUtils.callModulesFromCategory({ categoryName: CATEGORY }, "hello");
await consolePromise;
// Providing a jsGlobal that lacks the expected object should log an error.
consolePromise = TestUtils.consoleMessageObserved(m => {
let firstArg = m.wrappedJSObject.arguments?.[0];
return typeof firstArg == "string" && firstArg.includes(CATEGORY);
});
BrowserUtils.callModulesFromCategory(
{ categoryName: CATEGORY, jsGlobal: {} },
"hello"
);
await consolePromise;
Services.catMan.deleteCategory(CATEGORY);
});