Source code
Revision control
Copy as Markdown
Other Tools
/* Any copyright is dedicated to the Public Domain.
const { PermissionTestUtils } = ChromeUtils.importESModule(
);
ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
);
module.init(this);
return module;
});
const kDefaultWait = 2000;
function is_element_visible(aElement, aMsg) {
isnot(aElement, null, "Element should not be null, when checking visibility");
ok(!BrowserTestUtils.isHidden(aElement), aMsg);
}
function is_element_hidden(aElement, aMsg) {
isnot(aElement, null, "Element should not be null, when checking visibility");
ok(BrowserTestUtils.isHidden(aElement), aMsg);
}
function open_preferences(aCallback) {
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:preferences");
let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
newTabBrowser.addEventListener(
"Initialized",
function () {
aCallback(gBrowser.contentWindow);
},
{ capture: true, once: true }
);
}
function openAndLoadSubDialog(
aURL,
aFeatures = null,
aParams = null,
aClosingCallback = null
) {
let promise = promiseLoadSubDialog(aURL);
content.gSubDialog.open(
aURL,
{ features: aFeatures, closingCallback: aClosingCallback },
aParams
);
return promise;
}
function promiseLoadSubDialog(aURL) {
return new Promise(resolve => {
content.gSubDialog._dialogStack.addEventListener(
"dialogopen",
function dialogopen(aEvent) {
if (
aEvent.detail.dialog._frame.contentWindow.location == "about:blank"
) {
return;
}
content.gSubDialog._dialogStack.removeEventListener(
"dialogopen",
dialogopen
);
is(
aEvent.detail.dialog._frame.contentWindow.location.toString(),
aURL,
"Check the proper URL is loaded"
);
// Check visibility
is_element_visible(aEvent.detail.dialog._overlay, "Overlay is visible");
// Check that stylesheets were injected
let expectedStyleSheetURLs =
aEvent.detail.dialog._injectedStyleSheets.slice(0);
for (let styleSheet of aEvent.detail.dialog._frame.contentDocument
.styleSheets) {
let i = expectedStyleSheetURLs.indexOf(styleSheet.href);
if (i >= 0) {
info("found " + styleSheet.href);
expectedStyleSheetURLs.splice(i, 1);
}
}
is(
expectedStyleSheetURLs.length,
0,
"All expectedStyleSheetURLs should have been found"
);
// Wait for the next event tick to make sure the remaining part of the
// testcase runs after the dialog gets ready for input.
executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow));
}
);
});
}
async function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) {
let finalPaneEvent = Services.prefs.getBoolPref("identity.fxaccounts.enabled")
? "sync-pane-loaded"
: "privacy-pane-loaded";
let finalPrefPaneLoaded = TestUtils.topicObserved(finalPaneEvent, () => true);
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
openPreferences(aPane, aOptions);
let newTabBrowser = gBrowser.selectedBrowser;
if (!newTabBrowser.contentWindow) {
await BrowserTestUtils.waitForEvent(newTabBrowser, "Initialized", true);
await BrowserTestUtils.waitForEvent(newTabBrowser.contentWindow, "load");
await finalPrefPaneLoaded;
}
let win = gBrowser.contentWindow;
let selectedPane = win.history.state;
if (!aOptions || !aOptions.leaveOpen) {
gBrowser.removeCurrentTab();
}
return { selectedPane };
}
async function runSearchInput(input) {
let searchInput = gBrowser.contentDocument.getElementById("searchInput");
searchInput.focus();
let searchCompletedPromise = BrowserTestUtils.waitForEvent(
gBrowser.contentWindow,
"PreferencesSearchCompleted",
evt => evt.detail == input
);
EventUtils.sendString(input);
await searchCompletedPromise;
}
async function evaluateSearchResults(
keyword,
searchResults,
includeExperiments = false
) {
searchResults = Array.isArray(searchResults)
? searchResults
: [searchResults];
searchResults.push("header-searchResults");
await runSearchInput(keyword);
let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane");
for (let i = 0; i < mainPrefTag.childElementCount; i++) {
let child = mainPrefTag.children[i];
if (!includeExperiments && child.id?.startsWith("pane-experimental")) {
continue;
}
if (searchResults.includes(child.id)) {
is_element_visible(child, `${child.id} should be in search results`);
} else if (child.id) {
is_element_hidden(child, `${child.id} should not be in search results`);
}
}
}
function waitForMutation(target, opts, cb) {
return new Promise(resolve => {
let observer = new MutationObserver(() => {
if (!cb || cb(target)) {
observer.disconnect();
resolve();
}
});
observer.observe(target, opts);
});
}
// Used to add sample experimental features for testing. To use, create
// a DefinitionServer, then call addDefinition as needed.
class DefinitionServer {
constructor(definitionOverrides = []) {
let { HttpServer } = ChromeUtils.importESModule(
);
this.server = new HttpServer();
this.server.registerPathHandler("/definitions.json", this);
this.definitions = {};
for (const override of definitionOverrides) {
this.addDefinition(override);
}
this.server.start();
registerCleanupFunction(
() => new Promise(resolve => this.server.stop(resolve))
);
}
// for nsIHttpRequestHandler
handle(request, response) {
response.write(JSON.stringify(this.definitions));
}
get definitionsUrl() {
const { primaryScheme, primaryHost, primaryPort } = this.server.identity;
return `${primaryScheme}://${primaryHost}:${primaryPort}/definitions.json`;
}
addDefinition(overrides = {}) {
const definition = {
id: "test-feature",
// These l10n IDs are just random so we have some text to display
title: "experimental-features-media-jxl",
group: "experimental-features-group-customize-browsing",
description: "pane-experimental-description3",
restartRequired: false,
type: "boolean",
preference: "test.feature",
defaultValue: false,
isPublic: false,
...overrides,
};
// convert targeted values, used by fromId
definition.isPublic = { default: definition.isPublic };
definition.defaultValue = { default: definition.defaultValue };
this.definitions[definition.id] = definition;
return definition;
}
}
/**
* Creates observer that waits for and then compares all perm-changes with the observances in order.
* @param {Array} observances permission changes to observe (order is important)
* @returns {Promise} Promise object that resolves once all permission changes have been observed
*/
function createObserveAllPromise(observances) {
// Create new promise that resolves once all items
// in observances array have been observed.
return new Promise(resolve => {
let permObserver = {
observe(aSubject, aTopic, aData) {
if (aTopic != "perm-changed") {
return;
}
if (!observances.length) {
return;
}
let permission = aSubject.QueryInterface(Ci.nsIPermission);
let expected = observances.shift();
info(
`observed perm-changed for ${permission.principal.origin} (remaining ${observances.length})`
);
is(aData, expected.data, "type of message should be the same");
for (let prop of ["type", "capability", "expireType"]) {
if (expected[prop]) {
is(
permission[prop],
expected[prop],
`property: "${prop}" should be equal (${permission.principal.origin})`
);
}
}
if (expected.origin) {
is(
permission.principal.origin,
expected.origin,
`property: "origin" should be equal (${permission.principal.origin})`
);
}
if (!observances.length) {
Services.obs.removeObserver(permObserver, "perm-changed");
executeSoon(resolve);
}
},
};
Services.obs.addObserver(permObserver, "perm-changed");
});
}
/**
* Waits for preference to be set and asserts the value.
* @param {string} pref - Preference key.
* @param {*} expectedValue - Expected value of the preference.
* @param {string} message - Assertion message.
*/
async function waitForAndAssertPrefState(pref, expectedValue, message) {
await TestUtils.waitForPrefChange(pref, value => {
if (value != expectedValue) {
return false;
}
is(value, expectedValue, message);
return true;
});
}
/**
* The Relay promo is not shown for distributions with a custom FxA instance,
* since Relay requires an account on our own server. These prefs are set to a
* dummy address by the test harness, filling the prefs with a "user value."
* This temporarily sets the default value equal to the dummy value, so that
* Firefox thinks we've configured the correct FxA server.
* @returns {Promise<MockFxAUtilityFunctions>} { mock, unmock }
*/
async function mockDefaultFxAInstance() {
/**
* @typedef {Object} MockFxAUtilityFunctions
* @property {function():void} mock - Makes the dummy values default, creating
* the illusion of a production FxA instance.
* @property {function():void} unmock - Restores the true defaults, creating
* the illusion of a custom FxA instance.
*/
const defaultPrefs = Services.prefs.getDefaultBranch("");
const userPrefs = Services.prefs.getBranch("");
const realAuth = defaultPrefs.getCharPref("identity.fxaccounts.auth.uri");
const realRoot = defaultPrefs.getCharPref("identity.fxaccounts.remote.root");
const mockAuth = userPrefs.getCharPref("identity.fxaccounts.auth.uri");
const mockRoot = userPrefs.getCharPref("identity.fxaccounts.remote.root");
const mock = () => {
defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth);
defaultPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot);
userPrefs.clearUserPref("identity.fxaccounts.auth.uri");
userPrefs.clearUserPref("identity.fxaccounts.remote.root");
};
const unmock = () => {
defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", realAuth);
defaultPrefs.setCharPref("identity.fxaccounts.remote.root", realRoot);
userPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth);
userPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot);
};
mock();
registerCleanupFunction(unmock);
return { mock, unmock };
}
/**
* Runs a test that checks the visibility of the Firefox Suggest preferences UI.
* An initial Suggest scenario is set and visibility is checked. Then a Nimbus
* experiment is installed and visibility is checked again. Finally the page is
* reopened and visibility is checked again.
*
* @param {array} initialScenarios
* Array of Suggest scenario names. The test will be run once per scenario,
* with each test starting with a given scenario.
* @param {object} initialExpected
* The expected visibility after setting the initial scenario. It should be an
* object that can be passed to `assertSuggestVisibility()`.
* @param {object} nimbusVariables
* An object mapping Nimbus variable names to values.
* @param {object} newExpected
* The expected visibility after installing the Nimbus experiment. It should
* be an object that can be passed to `assertSuggestVisibility()`.
* @param {string} pane
* The pref pane to open.
*/
async function doSuggestVisibilityTest({
initialScenarios,
initialExpected,
nimbusVariables,
newExpected = initialExpected,
pane = "search",
}) {
for (let scenario of initialScenarios) {
info(
"Running Suggest visibility test: " +
JSON.stringify(
{
scenario,
initialExpected,
nimbusVariables,
newExpected,
},
null,
2
)
);
// Set the initial scenario.
await QuickSuggestTestUtils.setScenario(scenario);
// Open prefs and check the initial visibility.
await openPreferencesViaOpenPreferencesAPI(pane, { leaveOpen: true });
assertSuggestVisibility(initialExpected);
// Install a Nimbus experiment.
await QuickSuggestTestUtils.withExperiment({
valueOverrides: nimbusVariables,
callback: async () => {
// Check visibility again.
assertSuggestVisibility(newExpected);
// To make sure visibility is properly updated on load, close the tab,
// open the prefs again, and check visibility.
gBrowser.removeCurrentTab();
await openPreferencesViaOpenPreferencesAPI(pane, { leaveOpen: true });
assertSuggestVisibility(newExpected);
},
});
gBrowser.removeCurrentTab();
}
await QuickSuggestTestUtils.setScenario(null);
}
/**
* Checks the visibility of the Suggest UI.
*
* @param {object} expectedByElementId
* An object that maps IDs of elements in the current tab to objects with the
* following properties:
*
* {bool} isVisible
* Whether the element is expected to be visible.
* {string} l10nId
* The expected l10n ID of the element. Optional.
*/
function assertSuggestVisibility(expectedByElementId) {
let doc = gBrowser.selectedBrowser.contentDocument;
for (let [elementId, { isVisible, l10nId }] of Object.entries(
expectedByElementId
)) {
let element = doc.getElementById(elementId);
Assert.strictEqual(
BrowserTestUtils.isVisible(element),
isVisible,
"The element should be visible as expected"
);
if (l10nId) {
Assert.equal(
element.dataset.l10nId,
l10nId,
"The l10n ID should be correct for element " + elementId
);
}
}
}