// Expose Remote Settings utils with an explicit name. const RemoteSettingsUtils = Utils;
// We need Services.appinfo.name set up to allow the hashes to work with a // consistent name. // Note: the name and versions here match those in ExtensionXPCShellUtils.sys.mjs.
updateAppInfo({ name: "XPCShell", version: "48", platformVersion: "48" });
// We generally also need a profile set-up, for saving search settings etc.
do_get_profile();
SearchTestUtils.init(this);
const SETTINGS_FILENAME = "search.json.mozlz4";
// Expand the amount of information available in error logs
Services.prefs.setBoolPref("browser.search.log", true);
Services.prefs.setBoolPref("browser.region.log", true);
// Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird)
Services.prefs.setBoolPref( "toolkit.telemetry.testing.overrideProductsCheck", true
);
// For tests, allow the settings to write sooner than it would do normally so that // the tests that need to wait for it can run a bit faster.
SearchSettings.SETTNGS_INVALIDATION_DELAY = 250;
async function promiseSettingsData() {
let path = PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME); return IOUtils.readJSON(path, { decompress: true });
}
async function promiseEngineMetadata() {
let settings = await promiseSettingsData();
let data = {}; for (let engine of settings.engines) {
data[engine._name] = engine._metaData;
} return data;
}
async function promiseGlobalMetadata() { return (await promiseSettingsData()).metaData;
}
async function promiseSaveGlobalMetadata(globalData) {
let data = await promiseSettingsData();
data.metaData = globalData;
await promiseSaveSettingsData(data);
}
function promiseDefaultNotification(type = "normal") { return SearchTestUtils.promiseSearchNotification(
SearchUtils.MODIFIED_TYPE[
type == "private" ? "DEFAULT_PRIVATE" : "DEFAULT"
],
SearchUtils.TOPIC_ENGINE_MODIFIED
);
}
/** * Clean the profile of any settings file left from a previous run. * * @returns {boolean} * Indicates if the settings file existed.
*/ function removeSettingsFile() {
let file = do_get_profile().clone();
file.append(SETTINGS_FILENAME); if (file.exists()) {
file.remove(false); returntrue;
} returnfalse;
}
/** * isUSTimezone taken from nsSearchService.js * * @returns {boolean}
*/ function isUSTimezone() { // Timezone assumptions! We assume that if the system clock's timezone is // between Newfoundland and Hawaii, that the user is in North America.
// This includes all of South America as well, but we have relatively few // en-US users there, so that's OK.
let UTCOffset = new Date().getTimezoneOffset(); return UTCOffset >= 150 && UTCOffset <= 600;
}
const kTestEngineName = "Test search engine";
/** * Waits for the settings file to be saved. * * @returns {Promise} Resolved when the settings file is saved.
*/ function promiseAfterSettings() { return SearchTestUtils.promiseSearchNotification( "write-settings-to-disk-complete"
);
}
/** * Sets the home region, and waits for the search service to reload the engines. * * @param {string} region * The region to set.
*/
async function promiseSetHomeRegion(region) {
let promise = SearchTestUtils.promiseSearchNotification("engines-reloaded");
Region._setHomeRegion(region);
await promise;
}
/** * Sets the requested/available locales and waits for the search service to * reload the engines. * * @param {string} locale * The locale to set.
*/
async function promiseSetLocale(locale) { if (!Services.locale.availableLocales.includes(locale)) { thrownew Error(
`"${locale}" needs to be included in Services.locales.availableLocales at the start of the test.`
);
}
let promise = SearchTestUtils.promiseSearchNotification("engines-reloaded");
Services.locale.requestedLocales = [locale];
await promise;
}
/** * Read a JSON file and return the JS object * * @param {nsIFile} file * The file to read. * @returns {object} * Returns the JSON object if the file was successfully read, * false otherwise.
*/
async function readJSONFile(file) { return JSON.parse(await IOUtils.readUTF8(file.path));
}
/** * Recursively compare two objects and check that every property of expectedObj has the same value * on actualObj. * * @param {object} expectedObj * The source object that we expect to match * @param {object} actualObj * The object to check against the source * @param {Function} skipProp * A function that is called with the property name and its value, to see if * testing that property should be skipped or not.
*/ function isSubObjectOf(expectedObj, actualObj, skipProp) { for (let prop in expectedObj) { if (skipProp && skipProp(prop, expectedObj[prop])) { continue;
} if (expectedObj[prop] instanceof Object) { Assert.equal(
actualObj[prop]?.length,
expectedObj[prop].length,
`Should have the correct length for property ${prop}`
);
isSubObjectOf(expectedObj[prop], actualObj[prop], skipProp);
} else { Assert.equal(
actualObj[prop],
expectedObj[prop],
`Should have the correct value for property ${prop}`
);
}
}
}
/** * After useHttpServer() is called, this string contains the URL test directory, * excluding the final slash.
*/ var gHttpURL;
/** * Initializes the HTTP server and ensures that it is terminated when tests end. * * @returns {HttpServer} * The HttpServer object in case further customization is needed.
*/ function useHttpServer() {
let httpServer = new HttpServer();
httpServer.start(-1);
httpServer.registerDirectory("/", do_get_cwd());
gHttpURL = `http://localhost:${httpServer.identity.primaryPort}`;
registerCleanupFunction(async function cleanup_httpServer() {
await new Promise(resolve => {
httpServer.stop(resolve);
});
}); return httpServer;
}
// This "enum" from nsSearchService.js const TELEMETRY_RESULT_ENUM = {
SUCCESS: 0,
SUCCESS_WITHOUT_DATA: 1,
TIMEOUT: 2,
ERROR: 3,
};
/** * Checks the value of the SEARCH_SERVICE_COUNTRY_FETCH_RESULT probe. * * @param {string|null} aExpectedValue * If a value from TELEMETRY_RESULT_ENUM, we expect to see this value * recorded exactly once in the probe. If |null|, we expect to see * nothing recorded in the probe at all.
*/ function checkCountryResultTelemetry(aExpectedValue) {
let histogram = Services.telemetry.getHistogramById( "SEARCH_SERVICE_COUNTRY_FETCH_RESULT"
);
let snapshot = histogram.snapshot(); if (aExpectedValue != null) {
equal(snapshot.values[aExpectedValue], 1);
} else {
deepEqual(snapshot.values, {});
}
}
/** * Reads the specified file from the data directory and returns its contents as * an Uint8Array. * * @param {string} filename * The name of the file to read. * @returns {Promise<Uint8Array>} * The contents of the file in an Uint8Array.
*/
async function getFileDataBuffer(filename) { return IOUtils.read(PathUtils.join(do_get_cwd().path, "icons", filename));
}
/** * Creates a mock attachment record for use in remote settings related tests. * * @param {object} item * An object containing the details of the attachment. * @param {string} item.filename * The name of the attachmnet file in the data directory. * @param {string[]} item.engineIdentifiers * The engine identifiers for the attachment. * @param {number} item.imageSize * The size of the image. * @param {string} [item.id] * The ID to use for the record. If not provided, a new UUID will be generated. * @param {number} [item.lastModified] * The last modified time for the record. Defaults to the current time. * @returns {object} * An object containing the record and attachment.
*/
async function mockRecordWithAttachment({
filename,
engineIdentifiers,
imageSize,
id = Services.uuid.generateUUID().toString(),
lastModified = Date.now(),
}) {
let buffer = await getFileDataBuffer(filename);
// Generate a hash.
let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
Ci.nsICryptoHash
);
hasher.init(Ci.nsICryptoHash.SHA256);
hasher.update(buffer, buffer.length);
let attachment = {
record,
blob: new Blob([buffer]),
};
return { record, attachment };
}
/** * Inserts an attachment record into the remote settings collection. * * @param {RemoteSettingsClient} client * The remote settings client to use. * @param {object} item * An object containing the details of the attachment - see mockRecordWithAttachment. * @param {boolean} [addAttachmentToCache] * Whether to add the attachment file to the cache. Defaults to true.
*/
async function insertRecordIntoCollection(
client,
item,
addAttachmentToCache = true
) {
let { record, attachment } = await mockRecordWithAttachment(item);
await client.db.create(record); if (addAttachmentToCache) {
await client.attachments.cacheImpl.set(record.id, attachment);
}
await client.db.importChanges({}, record.last_modified);
}
/** * Helper function that sets up a server and respnds to region * fetch requests. * * @param {string} region * The region that the server will respond with. * @param {Promise|null} waitToRespond * A promise that the server will await on to delay responding * to the request.
*/ function useCustomGeoServer(region, waitToRespond = Promise.resolve()) {
let srv = useHttpServer();
srv.registerPathHandler("/fetch_region", async (req, res) => {
res.processAsync();
await waitToRespond;
res.setStatusLine("1.1", 200, "OK");
res.write(JSON.stringify({ country_code: region }));
res.finish();
});
/** * @typedef {object} TelemetryDetails * @property {string} engineId * The telemetry ID for the search engine. * @property {string} [displayName] * The search engine's display name. * @property {string} [loadPath] * The load path for the search engine. * @property {string} [submissionUrl] * The submission URL for the search engine. * @property {string} [verified] * Whether the search engine is verified.
*/
/** * Asserts that default search engine telemetry has been correctly reported * to Glean. * * @param {object} expected * An object containing telemetry details for normal and private engines. * @param {TelemetryDetails} expected.normal * An object with the expected details for the normal search engine. * @param {TelemetryDetails} [expected.private] * An object with the expected details for the private search engine.
*/
async function assertGleanDefaultEngine(expected) {
await TestUtils.waitForCondition(
() =>
Glean.searchEngineDefault.engineId.testGetValue() ==
(expected.normal.engineId ?? ""), "Should have set the correct telemetry id for the normal engine"
);
await TestUtils.waitForCondition(
() =>
Glean.searchEnginePrivate.engineId.testGetValue() ==
(expected.private?.engineId ?? ""), "Should have set the correct telemetry id for the private engine"
);
for (let property of [ "displayName", "loadPath", "submissionUrl", "verified",
]) { if (property in expected.normal) { Assert.equal(
Glean.searchEngineDefault[property].testGetValue(),
expected.normal[property] ?? "",
`Should have set ${property} correctly`
);
} if (expected.private && property in expected.private) { Assert.equal(
Glean.searchEnginePrivate[property].testGetValue(),
expected.private[property] ?? "",
`Should have set ${property} correctly`
);
}
}
}
/** * Loads a new enterprise policy, and re-initialise the search service * with the new policy. Also waits for the search service to write the settings * file to disk. * * @param {object} policy * The enterprise policy to use.
*/
async function setupPolicyEngineWithJson(policy) {
Services.search.wrappedJSObject.reset();
/** * Makes Services.policies.isEnterprise return true by loading an enterprise * policy and re-initialise the search service with the new policy. Also waits * for the search service to write the settings file to disk.
*/
async function enableEnterprise() {
await setupPolicyEngineWithJson({ // Use any policy.
policies: {
BlockAboutSupport: true,
},
}); Assert.ok(Services.policies.isEnterprise, "isEnterprise");
}
/** * A simple observer to ensure we get only the expected notifications.
*/ class SearchObserver {
constructor(expectedNotifications, returnEngineForNotification = false) { this.observer = this.observer.bind(this); this.deferred = Promise.withResolvers(); this.expectedNotifications = expectedNotifications; this.returnEngineForNotification = returnEngineForNotification;
handleTimeout() { this.deferred.reject( new Error( "Waiting for Notifications timed out, only received: " + this.expectedNotifications.join(",")
)
);
}
observer(subject, topic, data) { Assert.greater( this.expectedNotifications.length,
0, "Should be expecting a notification"
); Assert.equal(
data, this.expectedNotifications[0], "Should have received the next expected notification"
);
if ( this.returnEngineForNotification &&
data == this.returnEngineForNotification
) { this.engineToReturn = subject.QueryInterface(Ci.nsISearchEngine);
}
/** * Some tests might trigger initialisation which will trigger the search settings * update. We need to make sure we wait for that to finish before we exit, otherwise * it may cause shutdown issues.
*/
let updatePromise = SearchTestUtils.promiseSearchNotification( "settings-update-complete"
);
let consoleAllowList = [ // Harness issues. 'property "localProfileDir" is non-configurable and can\'t be deleted', 'property "profileDir" is non-configurable and can\'t be deleted',
];
let endConsoleListening = TestUtils.listenForConsoleMessages();
registerCleanupFunction(async () => {
let msgs = await endConsoleListening(); for (let msg of msgs) {
msg = msg.wrappedJSObject; if (msg.level != "error") { continue;
}
if (!msg.arguments?.length) { Assert.ok( false, "Unexpected console message received during test: " + msg
);
} else {
let firstArg = msg.arguments[0]; // Use the appropriate message depending on the object supplied to // the first argument.
let message = firstArg.messageContents ?? firstArg.message ?? firstArg; if (!consoleAllowList.some(e => message.includes(e))) { Assert.ok( false, "Unexpected console message received during test: " + message
);
}
}
}
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.