diff options
Diffstat (limited to 'toolkit/components/search/tests/SearchTestUtils.sys.mjs')
-rw-r--r-- | toolkit/components/search/tests/SearchTestUtils.sys.mjs | 505 |
1 files changed, 505 insertions, 0 deletions
diff --git a/toolkit/components/search/tests/SearchTestUtils.sys.mjs b/toolkit/components/search/tests/SearchTestUtils.sys.mjs new file mode 100644 index 0000000000..a2dc9b9e01 --- /dev/null +++ b/toolkit/components/search/tests/SearchTestUtils.sys.mjs @@ -0,0 +1,505 @@ +import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + ExtensionTestUtils: + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +var gTestScope; + +export var SearchTestUtils = { + init(testScope) { + gTestScope = testScope; + this._isMochitest = !Services.env.exists("XPCSHELL_TEST_PROFILE_DIR"); + if (this._isMochitest) { + this._isMochitest = true; + lazy.AddonTestUtils.initMochitest(testScope); + } else { + this._isMochitest = false; + // This handles xpcshell-tests. + gTestScope.ExtensionTestUtils = lazy.ExtensionTestUtils; + this.initXPCShellAddonManager(testScope); + } + }, + + /** + * Adds an OpenSearch based engine to the search service. It will remove + * the engine at the end of the test. + * + * @param {object} options + * The options for the new search engine. + * @param {string} options.url + * The URL of the engine to add. + * @param {boolean} [options.setAsDefault] + * Whether or not to set the engine as default automatically. If this is + * true, the engine will be set as default, and the previous default engine + * will be restored when the test exits. + * @param {boolean} [options.setAsDefaultPrivate] + * Whether or not to set the engine as default automatically for private mode. + * If this is true, the engine will be set as default, and the previous default + * engine will be restored when the test exits. + * @returns {Promise} Returns a promise that is resolved with the new engine + * or rejected if it fails. + */ + async promiseNewSearchEngine({ + url, + setAsDefault = false, + setAsDefaultPrivate = false, + }) { + // OpenSearch engines can only be added via http protocols. + url = url.replace("chrome://mochitests/content", "https://example.com"); + let engine = await Services.search.addOpenSearchEngine(url, ""); + let previousEngine = Services.search.defaultEngine; + let previousPrivateEngine = Services.search.defaultPrivateEngine; + if (setAsDefault) { + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + if (setAsDefaultPrivate) { + await Services.search.setDefaultPrivate( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + gTestScope.registerCleanupFunction(async () => { + if (setAsDefault) { + await Services.search.setDefault( + previousEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + if (setAsDefaultPrivate) { + await Services.search.setDefaultPrivate( + previousPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + try { + await Services.search.removeEngine(engine); + } catch (ex) { + // Don't throw if the test has already removed it. + } + }); + return engine; + }, + + /** + * Returns a promise that is resolved when an observer notification from the + * search service fires with the specified data. + * + * @param {*} expectedData + * The value the observer notification sends that causes us to resolve + * the promise. + * @param {string} topic + * The notification topic to observe. Defaults to 'browser-search-service'. + * @returns {Promise} + * Returns a promise that is resolved with the subject of the + * topic once the topic with the data has been observed. + */ + promiseSearchNotification(expectedData, topic = "browser-search-service") { + return new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + if (aData != expectedData) { + return; + } + + Services.obs.removeObserver(observer, topic); + // Let the stack unwind. + Services.tm.dispatchToMainThread(() => resolve(aSubject)); + }, topic); + }); + }, + + /** + * Load engines from test data located in particular folders. + * + * @param {string} [folder] + * The folder name to use. + * @param {string} [subFolder] + * The subfolder to use, if any. + * @param {Array} [config] + * An array which contains the configuration to set. + * @returns {object} + * An object that is a sinon stub for the configuration getter. + */ + async useTestEngines(folder = "data", subFolder = null, config = null) { + let url = `resource://test/${folder}/`; + if (subFolder) { + url += `${subFolder}/`; + } + let resProt = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + resProt.setSubstitution("search-extensions", Services.io.newURI(url)); + + const settings = await lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY); + if (config) { + return lazy.sinon.stub(settings, "get").returns(config); + } + + let response = await fetch(`resource://search-extensions/engines.json`); + let json = await response.json(); + return lazy.sinon.stub(settings, "get").returns(json.data); + }, + + async useMochitestEngines(testDir) { + // Replace the path we load search engines from with + // the path to our test data. + let resProt = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + let originalSubstitution = resProt.getSubstitution("search-extensions"); + resProt.setSubstitution( + "search-extensions", + Services.io.newURI("file://" + testDir.path) + ); + gTestScope.registerCleanupFunction(() => { + resProt.setSubstitution("search-extensions", originalSubstitution); + }); + }, + + /** + * Convert a list of engine configurations into engine objects. + * + * @param {Array} engineConfigurations + * An array of engine configurations. + */ + async searchConfigToEngines(engineConfigurations) { + let engines = []; + for (let config of engineConfigurations) { + let engine = await Services.search.wrappedJSObject._makeEngineFromConfig( + config + ); + engines.push(engine); + } + return engines; + }, + + /** + * Provides various setup for xpcshell-tests installing WebExtensions. Should + * be called from the global scope of the test. + * + * @param {object} scope + * The global scope of the test being run. + * @param {*} usePrivilegedSignatures + * How to sign created addons. + */ + initXPCShellAddonManager(scope, usePrivilegedSignatures = false) { + let scopes = + lazy.AddonManager.SCOPE_PROFILE | lazy.AddonManager.SCOPE_APPLICATION; + Services.prefs.setIntPref("extensions.enabledScopes", scopes); + // Only do this once. + try { + gTestScope.ExtensionTestUtils.init(scope); + } catch (ex) { + // This can happen if init is called twice. + if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) { + throw ex; + } + } + lazy.AddonTestUtils.usePrivilegedSignatures = usePrivilegedSignatures; + lazy.AddonTestUtils.overrideCertDB(); + }, + + /** + * Add a search engine as a WebExtension. + * + * Note: for tests, the extension must generally be unloaded before + * `registerCleanupFunction`s are triggered. See bug 1694409. + * + * This function automatically registers an unload for the extension, this + * may be skipped with the skipUnload argument. + * + * @param {object} [manifest] + * See {@link createEngineManifest} + * @param {object} [options] + * Options for how the engine is installed and uninstalled. + * @param {boolean} [options.setAsDefault] + * Whether or not to set the engine as default automatically. If this is + * true, the engine will be set as default, and the previous default engine + * will be restored when the test exits. + * @param {boolean} [options.setAsDefaultPrivate] + * Whether or not to set the engine as default automatically for private mode. + * If this is true, the engine will be set as default, and the previous default + * engine will be restored when the test exits. + * @param {boolean} [options.skipUnload] + * If true, this will skip the automatic unloading of the extension. + * @param {object} [files] + * A key value object where the keys are the filenames and their contents are + * the values. Used for simulating locales and other files in the WebExtension. + * @returns {object} + * The loaded extension. + */ + async installSearchExtension( + manifest = {}, + { + setAsDefault = false, + setAsDefaultPrivate = false, + skipUnload = false, + } = {}, + files = {} + ) { + await Services.search.init(); + + let extensionInfo = { + useAddonManager: "permanent", + files, + manifest: this.createEngineManifest(manifest), + }; + + let extension; + + let previousEngine = Services.search.defaultEngine; + let previousPrivateEngine = Services.search.defaultPrivateEngine; + + async function cleanup() { + if (setAsDefault) { + await Services.search.setDefault( + previousEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + if (setAsDefaultPrivate) { + await Services.search.setDefaultPrivate( + previousPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + await extension.unload(); + } + + // Cleanup must be registered before loading the extension to avoid + // failures for mochitests. + if (!skipUnload && this._isMochitest) { + gTestScope.registerCleanupFunction(cleanup); + } + + extension = gTestScope.ExtensionTestUtils.loadExtension(extensionInfo); + await extension.startup(); + await lazy.AddonTestUtils.waitForSearchProviderStartup(extension); + let engine = Services.search.getEngineByName(manifest.name); + + if (setAsDefault) { + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + if (setAsDefaultPrivate) { + await Services.search.setDefaultPrivate( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + + // For xpcshell-tests we must register the unload after adding the extension. + // See bug 1694409 for why this is. + if (!skipUnload && !this._isMochitest) { + gTestScope.registerCleanupFunction(cleanup); + } + + return extension; + }, + + /** + * Install a search engine as a system extension to simulate + * Normandy updates. For xpcshell-tests only. + * + * @param {object} [options] + * See {@link createEngineManifest} + */ + async installSystemSearchExtension(options = {}) { + options.id = (options.id ?? "example") + "@search.mozilla.org"; + let xpi = await lazy.AddonTestUtils.createTempWebExtensionFile({ + manifest: this.createEngineManifest(options), + background() { + // eslint-disable-next-line no-undef + browser.test.sendMessage("started"); + }, + }); + let wrapper = gTestScope.ExtensionTestUtils.expectExtension(options.id); + + const install = await lazy.AddonManager.getInstallForURL( + `file://${xpi.path}`, + { + useSystemLocation: true, + } + ); + + install.install(); + + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + + return wrapper; + }, + + /** + * Create a search engine extension manifest. + * + * @param {object} [options] + * The options for the manifest. + * @param {string} [options.id] + * The id to use for the WebExtension. + * @param {string} [options.name] + * The display name to use for the WebExtension. + * @param {string} [options.version] + * The version to use for the WebExtension. + * @param {string} [options.favicon_url] + * The favicon to use for the search engine in the WebExtension. + * @param {string} [options.keyword] + * The keyword to use for the search engine. + * @param {string} [options.encoding] + * The encoding to use for the search engine. + * @param {string} [options.search_url] + * The search URL to use for the search engine. + * @param {string} [options.search_url_get_params] + * The GET search URL parameters to use for the search engine + * @param {string} [options.search_url_post_params] + * The POST search URL parameters to use for the search engine + * @param {string} [options.suggest_url] + * The suggestion URL to use for the search engine. + * @param {string} [options.suggest_url_get_params] + * The suggestion URL parameters to use for the search engine. + * @param {string} [options.search_form] + * The search form to use for the search engine. + * @returns {object} + * The generated manifest. + */ + createEngineManifest(options = {}) { + options.name = options.name ?? "Example"; + options.id = options.id ?? options.name.toLowerCase().replaceAll(" ", ""); + if (!options.id.includes("@")) { + options.id += "@tests.mozilla.org"; + } + options.version = options.version ?? "1.0"; + let manifest = { + version: options.version, + browser_specific_settings: { + gecko: { + id: options.id, + }, + }, + chrome_settings_overrides: { + search_provider: { + name: options.name, + search_url: options.search_url ?? "https://example.com/", + }, + }, + }; + + if (options.default_locale) { + manifest.default_locale = options.default_locale; + } + + if (options.search_url_post_params) { + manifest.chrome_settings_overrides.search_provider.search_url_post_params = + options.search_url_post_params; + } else { + manifest.chrome_settings_overrides.search_provider.search_url_get_params = + options.search_url_get_params ?? "?q={searchTerms}"; + } + + if (options.favicon_url) { + manifest.chrome_settings_overrides.search_provider.favicon_url = + options.favicon_url; + } + if (options.encoding) { + manifest.chrome_settings_overrides.search_provider.encoding = + options.encoding; + } + if (options.keyword) { + manifest.chrome_settings_overrides.search_provider.keyword = + options.keyword; + } + if (options.suggest_url) { + manifest.chrome_settings_overrides.search_provider.suggest_url = + options.suggest_url; + } + if (options.suggest_url) { + manifest.chrome_settings_overrides.search_provider.suggest_url_get_params = + options.suggest_url_get_params; + } + if (options.search_form) { + manifest.chrome_settings_overrides.search_provider.search_form = + options.search_form; + } + if (options.favicon_url) { + manifest.chrome_settings_overrides.search_provider.favicon_url = + options.favicon_url; + } + return manifest; + }, + + /** + * A mock idleService that allows us to simulate RemoteSettings + * configuration updates. + */ + idleService: { + _observers: new Set(), + + _reset() { + this._observers.clear(); + }, + + _fireObservers(state) { + for (let observer of this._observers.values()) { + observer.observe(observer, state, null); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIUserIdleService"]), + idleTime: 19999, + + addIdleObserver(observer, time) { + this._observers.add(observer); + }, + + removeIdleObserver(observer, time) { + this._observers.delete(observer); + }, + }, + + /** + * Register the mock idleSerice. + */ + useMockIdleService() { + let fakeIdleService = MockRegistrar.register( + "@mozilla.org/widget/useridleservice;1", + SearchTestUtils.idleService + ); + gTestScope.registerCleanupFunction(() => { + MockRegistrar.unregister(fakeIdleService); + }); + }, + + /** + * Simulates an update to the RemoteSettings configuration. + * + * @param {object} [config] + * The new configuration. + */ + async updateRemoteSettingsConfig(config) { + if (!config) { + let settings = lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY); + config = await settings.get(); + } + const reloadObserved = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + await lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY).emit("sync", { + data: { current: config }, + }); + + this.idleService._fireObservers("idle"); + await reloadObserved; + }, +}; |