The browser window startup path initializes components sequentially. A failure in any single component causes the remaining startup tasks to abort, leaving the browser window in an unusable state. This has affected release users in the past. In Bug 1962056, the application startup path was made more resilient by introducing a category-manager-based mechanism with safe dispatch, ensuring failures in individual components do not prevent the rest of startup from completing. This patch introduces the same pattern for browser window initialization. BrowserUtils.callModulesFromCategory() dispatches to modules registered via browser-window.manifest, isolating per-component failures. Unlike the application startup path, browser window initialization also involves plain JS scripts loaded into the window scope, so the mechanism is extended to support these via an optional jsGlobal parameter. The onBeforeInitialXULLayout handlers are the first to be ported to this system. Differential Revision: https://phabricator.services.mozilla.com/D293463
607 lines
20 KiB
JavaScript
607 lines
20 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
const { BrowserUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/BrowserUtils.sys.mjs"
|
|
);
|
|
|
|
const { sinon } = ChromeUtils.importESModule(
|
|
"resource://testing-common/Sinon.sys.mjs"
|
|
);
|
|
|
|
const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
|
|
"resource://testing-common/EnterprisePolicyTesting.sys.mjs"
|
|
);
|
|
|
|
const { Region } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/Region.sys.mjs"
|
|
);
|
|
|
|
const { updateAppInfo } = ChromeUtils.importESModule(
|
|
"resource://testing-common/AppInfo.sys.mjs"
|
|
);
|
|
|
|
// Helper to run tests for specific regions
|
|
function setupRegions(home, current) {
|
|
Region._setHomeRegion(home || "");
|
|
Region._setCurrentRegion(current || "");
|
|
}
|
|
|
|
function setLanguage(language) {
|
|
Services.locale.availableLocales = [language];
|
|
Services.locale.requestedLocales = [language];
|
|
}
|
|
|
|
/**
|
|
* Calls to this need to revert these changes by undoing them at the end of the test,
|
|
* using:
|
|
*
|
|
* await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
|
|
*/
|
|
async function setupEnterprisePolicy() {
|
|
// set app info name as it's needed to set a policy and is not defined by default
|
|
// in xpcshell tests
|
|
updateAppInfo({
|
|
name: "XPCShell",
|
|
});
|
|
|
|
// set up an arbitrary enterprise policy
|
|
await EnterprisePolicyTesting.setupPolicyEngineWithJson({
|
|
policies: {
|
|
EnableTrackingProtection: {
|
|
Value: true,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
add_task(async function test_shouldShowVPNPromo() {
|
|
function setPromoEnabled(enabled) {
|
|
Services.prefs.setBoolPref("browser.vpn_promo.enabled", enabled);
|
|
}
|
|
|
|
const allowedRegion = "US";
|
|
const disallowedRegion = "SY";
|
|
const illegalRegion = "CN";
|
|
const unsupportedRegion = "LY";
|
|
const regionNotInDefaultPref = "QQ";
|
|
|
|
// Show promo when enabled in allowed regions
|
|
setupRegions(allowedRegion, allowedRegion);
|
|
Assert.ok(BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// Don't show when not enabled
|
|
setPromoEnabled(false);
|
|
Assert.ok(!BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// Don't show in disallowed home regions, even when enabled
|
|
setPromoEnabled(true);
|
|
setupRegions(disallowedRegion);
|
|
Assert.ok(!BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// Don't show in illegal home regions, even when enabled
|
|
setupRegions(illegalRegion);
|
|
Assert.ok(!BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// Don't show in disallowed current regions, even when enabled
|
|
setupRegions(allowedRegion, disallowedRegion);
|
|
Assert.ok(!BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// Don't show in illegal current regions, even when enabled
|
|
setupRegions(allowedRegion, illegalRegion);
|
|
Assert.ok(!BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// Show if home region is supported, even if current region is not supported (but isn't disallowed or illegal)
|
|
setupRegions(allowedRegion, unsupportedRegion);
|
|
Assert.ok(BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// Show VPN if current region is supported, even if home region is unsupported (but isn't disallowed or illegal)
|
|
setupRegions(unsupportedRegion, allowedRegion); // revert changes to regions
|
|
Assert.ok(BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// Make sure we are getting the list of allowed regions from the right
|
|
// place.
|
|
setupRegions(regionNotInDefaultPref);
|
|
Services.prefs.setStringPref(
|
|
"browser.contentblocking.report.vpn_regions",
|
|
"qq"
|
|
);
|
|
Assert.ok(BrowserUtils.shouldShowVPNPromo());
|
|
Services.prefs.clearUserPref("browser.contentblocking.report.vpn_regions");
|
|
|
|
if (AppConstants.platform !== "android") {
|
|
// Services.policies isn't shipped on Android
|
|
// Don't show VPN if there's an active enterprise policy
|
|
setupRegions(allowedRegion, allowedRegion);
|
|
await setupEnterprisePolicy();
|
|
|
|
Assert.ok(!BrowserUtils.shouldShowVPNPromo());
|
|
|
|
// revert policy changes made earlier
|
|
await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
|
|
}
|
|
});
|
|
|
|
add_task(async function test_sendToDeviceEmailsSupported() {
|
|
const allowedLanguage = "en-US";
|
|
const disallowedLanguage = "ar";
|
|
|
|
// Return true if language is en-US
|
|
setLanguage(allowedLanguage);
|
|
Assert.ok(BrowserUtils.sendToDeviceEmailsSupported());
|
|
|
|
// Return false if language is ar
|
|
setLanguage(disallowedLanguage);
|
|
Assert.ok(!BrowserUtils.sendToDeviceEmailsSupported());
|
|
});
|
|
|
|
add_task(async function test_shouldShowFocusPromo() {
|
|
const allowedRegion = "US";
|
|
const disallowedRegion = "CN";
|
|
|
|
// Show promo when neither region is disallowed
|
|
setupRegions(allowedRegion, allowedRegion);
|
|
Assert.ok(BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.FOCUS));
|
|
|
|
// Don't show when home region is disallowed
|
|
setupRegions(disallowedRegion);
|
|
Assert.ok(!BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.FOCUS));
|
|
|
|
setupRegions(allowedRegion, allowedRegion);
|
|
|
|
// Don't show when there is an enterprise policy active
|
|
if (AppConstants.platform !== "android") {
|
|
// Services.policies isn't shipped on Android
|
|
await setupEnterprisePolicy();
|
|
|
|
Assert.ok(!BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.FOCUS));
|
|
|
|
// revert policy changes made earlier
|
|
await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
|
|
}
|
|
|
|
// Don't show when promo disabled by pref
|
|
Preferences.set("browser.promo.focus.enabled", false);
|
|
Assert.ok(!BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.FOCUS));
|
|
|
|
Preferences.resetBranch("browser.promo.focus");
|
|
});
|
|
|
|
add_task(async function test_shouldShowPinPromo() {
|
|
Preferences.set("browser.promo.pin.enabled", true);
|
|
// Show pin promo type by default when promo is enabled
|
|
Assert.ok(BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.PIN));
|
|
|
|
// Don't show when there is an enterprise policy active
|
|
if (AppConstants.platform !== "android") {
|
|
// Services.policies isn't shipped on Android
|
|
await setupEnterprisePolicy();
|
|
|
|
Assert.ok(!BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.PIN));
|
|
|
|
// revert policy changes made earlier
|
|
await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
|
|
}
|
|
|
|
// Don't show when promo disabled by pref
|
|
Preferences.set("browser.promo.pin.enabled", false);
|
|
Assert.ok(!BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.PIN));
|
|
|
|
Preferences.resetBranch("browser.promo.pin");
|
|
});
|
|
|
|
add_task(async function test_shouldShowRelayPromo() {
|
|
// This test assumes by default no uri is configured.
|
|
Preferences.set("identity.fxaccounts.autoconfig.uri", "");
|
|
Assert.ok(BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.RELAY));
|
|
|
|
// Don't show when there is an enterprise policy active
|
|
if (AppConstants.platform !== "android") {
|
|
// Services.policies isn't shipped on Android
|
|
await setupEnterprisePolicy();
|
|
|
|
Assert.ok(!BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.RELAY));
|
|
|
|
// revert policy changes made earlier
|
|
await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
|
|
}
|
|
|
|
// Don't show if a custom FxA instance is configured
|
|
Preferences.set("identity.fxaccounts.autoconfig.uri", "https://x");
|
|
Assert.ok(!BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.RELAY));
|
|
|
|
Preferences.reset("identity.fxaccounts.autoconfig.uri");
|
|
});
|
|
|
|
add_task(async function test_shouldShowCookieBannersPromo() {
|
|
Preferences.set("browser.promo.cookiebanners.enabled", true);
|
|
// Show cookie banners promo type by default when promo is enabled
|
|
Assert.ok(
|
|
BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.COOKIE_BANNERS)
|
|
);
|
|
|
|
// Don't show when promo disabled by pref
|
|
Preferences.set("browser.promo.cookiebanners.enabled", false);
|
|
Assert.ok(
|
|
!BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.COOKIE_BANNERS)
|
|
);
|
|
|
|
Preferences.resetBranch("browser.promo.cookiebanners");
|
|
});
|
|
|
|
add_task(function test_getShareableURL() {
|
|
// Some test suites, specifically android, don't have this setup properly -- so we add it manually
|
|
if (!Preferences.get("services.sync.engine.tabs.filteredSchemes")) {
|
|
Preferences.set(
|
|
"services.sync.engine.tabs.filteredSchemes",
|
|
"about|resource|chrome|file|blob|moz-extension|data"
|
|
);
|
|
}
|
|
// Empty shouldn't be sendable
|
|
Assert.ok(!BrowserUtils.getShareableURL(""));
|
|
// Valid
|
|
let good = Services.io.newURI("https://mozilla.org");
|
|
Assert.ok(BrowserUtils.getShareableURL(good).equals(good));
|
|
// Invalid
|
|
Assert.ok(
|
|
!BrowserUtils.getShareableURL(Services.io.newURI("file://path/to/pdf.pdf"))
|
|
);
|
|
|
|
// Invalid
|
|
Assert.ok(
|
|
!BrowserUtils.getShareableURL(
|
|
Services.io.newURI(
|
|
"data:application/json;base64,ewogICJ0eXBlIjogIm1haW4i=="
|
|
)
|
|
)
|
|
);
|
|
|
|
// Reader mode:
|
|
if (AppConstants.platform !== "android") {
|
|
let readerUrl = Services.io.newURI(
|
|
"about:reader?url=" + encodeURIComponent("http://foo.com/")
|
|
);
|
|
Assert.equal(
|
|
BrowserUtils.getShareableURL(readerUrl).spec,
|
|
"http://foo.com/"
|
|
);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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 MODULE1 = "resource://test/my_catman_1.sys.mjs";
|
|
const MODULE2 = "resource://test/my_catman_2.sys.mjs";
|
|
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);
|
|
});
|
|
|
|
// 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";
|
|
const MODULE1 = "resource://test/my_catman_1.sys.mjs";
|
|
|
|
// 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);
|
|
});
|
|
|
|
/**
|
|
* 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 MODULE1 = "resource://test/my_catman_1.sys.mjs";
|
|
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 MODULE_JS = "chrome://browser/content/fake-catman-test.js";
|
|
const MODULE_ESM = "resource://test/my_catman_1.sys.mjs";
|
|
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";
|
|
const MODULE = "chrome://browser/content/fake-catman-test.js";
|
|
|
|
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);
|
|
});
|