/* 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);
});
add_task(async
function test_willLoadInBackground() {
const TEST_DATA = [
// where: "tab" and loadInBackgroundPref is on.
{
loadInBackgroundPref:
false,
where:
"tab",
expected:
false,
},
{
loadInBackgroundPref:
false,
where:
"tab",
params: { inBackground:
true },
expected:
true,
},
{
loadInBackgroundPref:
false,
where:
"tab",
params: { inBackground:
false },
expected:
false,
},
{
loadInBackgroundPref:
false,
where:
"tab",
params: { forceForeground:
true },
expected:
false,
},
{
loadInBackgroundPref:
false,
where:
"tab",
params: { forceForeground:
false },
expected:
false,
},
// where: "tab" and loadInBackgroundPref is off.
{
loadInBackgroundPref:
true,
where:
"tab",
expected:
true,
},
{
loadInBackgroundPref:
true,
where:
"tab",
params: { inBackground:
true },
expected:
true,
},
{
loadInBackgroundPref:
true,
where:
"tab",
params: { inBackground:
false },
expected:
false,
},
{
loadInBackgroundPref:
true,
where:
"tab",
params: { forceForeground:
true },
expected:
false,
},
{
loadInBackgroundPref:
true,
where:
"tab",
params: { forceForeground:
false },
expected:
true,
},
// where: "tabshifted" and loadInBackgroundPref is on.
{
loadInBackgroundPref:
false,
where:
"tabshifted",
expected:
true,
},
{
loadInBackgroundPref:
false,
where:
"tabshifted",
params: { inBackground:
true },
expected:
false,
},
{
loadInBackgroundPref:
false,
where:
"tabshifted",
params: { inBackground:
false },
expected:
true,
},
{
loadInBackgroundPref:
false,
where:
"tabshifted",
params: { forceForeground:
true },
expected:
true,
},
{
loadInBackgroundPref:
false,
where:
"tabshifted",
params: { forceForeground:
false },
expected:
true,
},
// where: "tabshifted" and loadInBackgroundPref is off.
{
loadInBackgroundPref:
true,
where:
"tabshifted",
expected:
false,
},
{
loadInBackgroundPref:
true,
where:
"tabshifted",
params: { inBackground:
true },
expected:
false,
},
{
loadInBackgroundPref:
true,
where:
"tabshifted",
params: { inBackground:
false },
expected:
true,
},
{
loadInBackgroundPref:
true,
where:
"tabshifted",
params: { forceForeground:
true },
expected:
true,
},
{
loadInBackgroundPref:
true,
where:
"tabshifted",
params: { forceForeground:
false },
expected:
false,
},
// where: other,
{
loadInBackgroundPref:
true,
where:
"current",
params: { inBackground:
true },
expected:
false,
},
];
for (
const { loadInBackgroundPref, where, params, expected } of TEST_DATA) {
info(
`Test
for ${JSON.stringify({ loadInBackgroundPref, where, params, expected })}`
);
Services.prefs.setBoolPref(
"browser.tabs.loadInBackground",
loadInBackgroundPref
);
Assert.equal(BrowserUtils.willLoadInBackground(where, params), expected);
Services.prefs.clearUserPref(
"browser.tabs.loadInBackground");
}
});