ChromeUtils.defineESModuleGetters(
this , {
AddonTestUtils:
"resource://testing-common/AddonTestUtils.sys.mjs",
AppProvidedConfigEngine:
"moz-src:///toolkit/components/search/ConfigSearchEngine.sys.mjs",
clearTimeout:
"resource://gre/modules/Timer.sys.mjs",
ConfigSearchEngine:
"moz-src:///toolkit/components/search/ConfigSearchEngine.sys.mjs",
EnterprisePolicyTesting:
"resource://testing-common/EnterprisePolicyTesting.sys.mjs",
ExtensionTestUtils:
"resource://testing-common/ExtensionXPCShellUtils.sys.mjs",
FileUtils:
"resource://gre/modules/FileUtils.sys.mjs",
HttpServer:
"resource://testing-common/httpd.sys.mjs",
Region:
"resource://gre/modules/Region.sys.mjs",
RemoteSettings:
"resource://services-settings/remote-settings.sys.mjs",
RemoteSettingsClient:
"resource://services-settings/RemoteSettingsClient.sys.mjs",
SearchEngineClassification:
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs",
SearchEngineInstallError:
"moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
SearchEngineSelector:
"moz-src:///toolkit/components/search/SearchEngineSelector.sys.mjs",
SearchService:
"moz-src:///toolkit/components/search/SearchService.sys.mjs",
SearchSettings:
"moz-src:///toolkit/components/search/SearchSettings.sys.mjs",
SearchTestUtils:
"resource://testing-common/SearchTestUtils.sys.mjs",
SearchUtils:
"moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
TestUtils:
"resource://testing-common/TestUtils.sys.mjs",
updateAppInfo:
"resource://testing-common/AppInfo.sys.mjs",
Utils:
"resource://services-settings/Utils.sys.mjs",
setTimeout:
"resource://gre/modules/Timer.sys.mjs",
sinon:
"resource://testing-common/Sinon.sys.mjs",
});
// 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(
true );
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 });
}
function promiseSaveSettingsData(data) {
return IOUtils.writeJSON(
PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME),
data,
{ compress:
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 );
return true ;
}
return false ;
}
/**
* 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.
// 150 minutes = 2.5 hours (UTC-2.5), which is
// Newfoundland Daylight Time (http://www.timeanddate.com/time/zones/ndt)
// 600 minutes = 10 hours (UTC-10), which is
// Hawaii-Aleutian Standard Time (http://www.timeanddate.com/time/zones/hast)
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)) {
throw new Error(
`
"${locale}" needs to be included in Services.locales.availableLocales at the start of the t
est.`
);
}
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 hash = hasher.finish(false );
hash = Array.from(hash, (_, i) =>
("0" + hash.charCodeAt(i).toString(16 )).slice(-2 )
).join("" );
// Mapping file extensions to corresponding MIME types.
const mimetypeFromExtension = {
ico: "image/x-icon" ,
svg: "image/svg+xml" ,
png: "image/png" ,
};
const extension = filename.split("." ).pop().toLowerCase();
let record = {
id,
engineIdentifiers,
imageSize,
attachment: {
hash,
location: `${filename}`,
filename,
size: buffer.byteLength,
mimetype: mimetypeFromExtension[extension] || "" ,
},
last_modified: lastModified,
};
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();
});
Services.prefs.setCharPref(
"browser.region.network.url" ,
`http://localhost:${srv.identity.primaryPort}/fetch_region`
);
}
/**
* @ typedef { object } TelemetryDetails
* @ property { string } providerId
* The provider id of the search engine .
* @ property { string } partnerCode
* The partner code of the search engine .
* @ property { boolean } overriddenByThirdParty
* If the engine is overridden by a third party .
* @ 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 .
*/
/**
* 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 { Partial < TelemetryDetails > } expected . normal
* An object with the expected details for the normal search engine .
* @ param { Partial < TelemetryDetails > } [ expected . private ]
* An object with the expected details for the private search engine .
*/
async function assertGleanDefaultEngine(expected) {
for (let property of [
"providerId" ,
"partnerCode" ,
"overriddenByThirdParty" ,
"engineId" ,
"displayName" ,
"loadPath" ,
"submissionUrl" ,
]) {
if (property in expected.normal) {
Assert .equal(
Glean.searchEngineDefault[property].testGetValue(),
expected.normal[property] ?? "" ,
`Should have set ${property} correctly`
);
}
if (!expected.private ) {
let expectedValue;
if (property === "overriddenByThirdParty" ) {
expectedValue = false ;
} else if (property === "submissionUrl" ) {
expectedValue = "blank:" ;
} else {
expectedValue = "" ;
}
Assert .equal(
Glean.searchEnginePrivate[property].testGetValue(),
expectedValue,
`Private engine ${property} should be unset`
);
} else if (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 - initialises 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) {
await this .EnterprisePolicyTesting.setupPolicyEngineWithJsonForSearch(policy);
}
/**
* 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 {
/**
*
* @ param { Array < [ string , string | null ] > } expectedNotifications
* An array of [ notificationType , engineName ] tuples . Use null for
* engineName if we don ' t care which engine triggered the notification .
*/
constructor(expectedNotifications) {
this .observer = this .observer.bind(this );
this .deferred = Promise.withResolvers();
this .expectedNotifications = expectedNotifications.map(([type, name]) => {
return { type, name };
});
this .receivedNotifications = [];
Services.obs.addObserver(this .observer, SearchUtils.TOPIC_ENGINE_MODIFIED);
this .timeout = setTimeout(this .handleTimeout.bind(this ), 5000 );
}
get promise() {
return this .deferred.promise;
}
handleTimeout() {
let stillExpecting = this .expectedNotifications
.map(n => `[type: ${n.type}, name: ${n.name}]`)
.join(", " );
let received = this .receivedNotifications
.map(n => `[type: ${n.type}, name: ${n.engineName}]`)
.join(", " );
this .deferred.reject(
new Error(
`Waiting for Notifications timed out:
still expecting - [${stillExpecting || "(none)" }]
received - [${received || "(none);" }]`
)
);
}
observer(subject, topic, data) {
Assert .greater(
this .expectedNotifications.length,
0 ,
"Should be expecting a notification"
);
let engine = subject.wrappedJSObject;
let engineName = engine.name;
this .receivedNotifications.push({ type: data, engineName });
let matchIndex = this .expectedNotifications.findIndex(
expected =>
expected.type == data &&
(expected.name == null || expected.name == engineName)
);
if (matchIndex == -1 ) {
info(
`SearchObserver received unexpected notification: ${data}, ${engineName}`
);
return ;
}
this .expectedNotifications.splice(matchIndex, 1 );
if (!this .expectedNotifications.length) {
clearTimeout(this .timeout);
delete this .timeout;
this .deferred.resolve(this .engineToReturn);
Services.obs.removeObserver(
this .observer,
SearchUtils.TOPIC_ENGINE_MODIFIED
);
}
}
}
/**
* 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"
);
registerCleanupFunction(async () => {
if (SearchService.isInitialized) {
await updatePromise;
}
});
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
);
}
}
}
});
/**
* This function asserts if the actual engines returned equals the expected
* engines .
*
* @ param { SearchEngineSelector } engineSelector
* The search engine selector to use for the test .
* @ param { object } config
* A fake search config containing engines .
* @ param { object } userEnv
* A fake user ' s environment including locale and region , experiment , etc .
* @ param { Array } expectedEngines
* The array of expected engines to be returned from the fake config .
* @ param { string } message
* The assertion message .
*/
async function assertSelectorEnginesEqualsExpected(
engineSelector,
config,
userEnv,
expectedEngines,
message
) {
engineSelector.clearCachedConfigurationForTests();
SearchTestUtils.setRemoteSettingsConfig(config);
if (expectedEngines.length) {
let { engines } = await engineSelector.fetchEngineConfiguration(userEnv);
// Add default parameters to match the selector output.
for (let i = 0 ; i < expectedEngines.length; i++) {
expectedEngines[i] = {
aliases: [],
charset: "UTF-8" ,
optional: false ,
partnerCode: "" ,
telemetrySuffix: "" ,
orderHint: null ,
clickUrl: null ,
isNewUntil: null ,
...expectedEngines[i],
};
expectedEngines[i].classification =
expectedEngines[i].classification == "general"
? SearchEngineClassification.GENERAL
: SearchEngineClassification.UNKNOWN;
expectedEngines[i].urls = {
suggestions: null ,
trending: null ,
searchForm: null ,
visualSearch: null ,
...expectedEngines[i].urls,
};
expectedEngines[i].urls.search = {
method: "GET" ,
...expectedEngines[i].urls.search,
};
if (!expectedEngines[i].urls.search.params) {
expectedEngines[i].urls.search.params = [];
}
for (let j = 0 ; j < expectedEngines[i].urls.search.params.length; j++) {
expectedEngines[i].urls.search.params[j] = {
enterpriseValue: null ,
experimentConfig: null ,
value: null ,
...expectedEngines[i].urls.search.params[j],
};
}
}
Assert .deepEqual(engines, expectedEngines, message);
} else {
await Assert .rejects(
engineSelector.fetchEngineConfiguration(userEnv),
/Could not find any engines in the filtered configuration/,
message
);
}
}
Messung V0.5 in Prozent C=90 H=97 G=93
¤ Dauer der Verarbeitung: 0.21 Sekunden
(vorverarbeitet am 2026-08-25)
¤
*© Formatika GbR, Deutschland