Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  SearchTestUtils.sys.mjs   Sprache: unbekannt

 
import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
  AppProvidedSearchEngine:
    "resource://gre/modules/AppProvidedSearchEngine.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",
});

/**
 * A class containing useful testing functions for Search based tests.
 */
class _SearchTestUtils {
  /**
   * The test scope that the test is running in.
   *
   * @type {object}
   */
  #testScope = null;

  /**
   * True if we are in a mochitest scope, false for xpcshell-tests.
   *
   * @type {boolean?}
   */
  #isMochitest = null;

  /**
   * Whether the fake idle service is registered and needs to be cleaned up.
   *
   * @type {boolean}
   */
  #idleServiceCID = null;

  /**
   * All stubs of remote settings that will be cleaned up by their key.
   *
   * @type {object}
   */
  #stubs = new Map();

  /**
   * Initialises the test utils, setting up the scope and working out if these
   * are mochitest or xpcshell-test.
   *
   * @param {object} testScope
   *   The global scope for the test.
   */
  init(testScope) {
    this.#testScope = testScope;
    this.#isMochitest = !Services.env.exists("XPCSHELL_TEST_PROFILE_DIR");
    if (this.#isMochitest) {
      lazy.AddonTestUtils.initMochitest(testScope);

      testScope.registerCleanupFunction(async () => {
        this.#stubs.forEach(stub => stub.restore());

        if (this.#stubs.size) {
          this.#stubs = new Map();

          let settingsWritten = SearchTestUtils.promiseSearchNotification(
            "write-settings-to-disk-complete"
          );
          await this.updateRemoteSettingsConfig();
          await settingsWritten;
        }

        if (this.#idleServiceCID) {
          MockRegistrar.unregister(this.#idleServiceCID);
          this.#idleServiceCID = null;
        }
      });
    }
  }

  /**
   * 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 {string} [options.faviconURL]
   *   The icon to be used for the open search engine, if not specified in the
   *   OpenSearch engine data.
   * @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.skipReset]
   *   Skips resetting the default engine at the end of the test.
   * @returns {Promise} Returns a promise that is resolved with the new engine
   *                    or rejected if it fails.
   */
  async installOpenSearchEngine({
    url,
    faviconURL,
    setAsDefault = false,
    setAsDefaultPrivate = false,
    skipReset = 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, faviconURL);
    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
      );
    }
    this.#testScope.registerCleanupFunction(async () => {
      if (setAsDefault && !skipReset) {
        await Services.search.setDefault(
          previousEngine,
          Ci.nsISearchService.CHANGE_REASON_UNKNOWN
        );
      }
      if (setAsDefaultPrivate && !skipReset) {
        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);
    });
  }

  /**
   * Stubs the get property of the remote settings client with a
   * given Key. Configuration does not get expanded.
   *
   * @param {string} [key]
   *   The remote settings key of the configuration to be stubbed.
   * @param {object[]} [config]
   *   The configuration that will be returned by the stub.
   */
  #stubConfig(key, config) {
    if (!config) {
      if (this.#stubs.has(key)) {
        this.#stubs.get(key).restore();
        this.#stubs.delete(key);
      }
      return;
    }

    if (!this.#stubs.has(key)) {
      let settings = lazy.RemoteSettings(key);
      this.#stubs.set(key, lazy.sinon.stub(settings, "get").returns(config));
    } else {
      this.#stubs.get(key).returns(config);
    }
  }

  /**
   * Expands a partial search config by the minumum number of properties
   * to be a valid config and loads it to make test cases less verbose.
   * Does not modify the input.
   *
   * defaultEngines and engineOrders are not required. The first
   * engine will be set to default if defaultEngines is not specified.
   *
   * Engine objects only require an identifier and there needs to be at least
   * one engine. The name defaults to the identifier, the classification
   * to general, the search url to https://www.example.com/search?q=query
   * and the environment to allRegionsAndLocales.
   *
   * The recordType is detected automatically and thus optional.
   *
   * @param {object[]} [partialConfig]
   *  The partial search config that will be expanded.
   * @returns {object[]}
   *  The expanded search config.
   */
  expandPartialConfig(partialConfig) {
    if (!partialConfig) {
      return partialConfig;
    }
    let fullConfig = structuredClone(partialConfig);
    let numEngines = 0;
    let defaultEngines;
    let engineOrders;

    for (let obj of fullConfig) {
      obj.recordType = this.#detectRecordType(obj);

      switch (obj.recordType) {
        case "engine":
          if (!obj.base) {
            obj.base = {};
          }
          if (!obj.base.name) {
            obj.base.name = obj.identifier;
          }
          if (!obj.base.classification) {
            obj.base.classification = "general";
          }
          if (!obj.base.urls) {
            obj.base.urls = {};
          }
          if (!obj.base.urls.search) {
            obj.base.urls.search = {
              base: "https://www.example.com/search",
              searchTermParamName: "q",
            };
          }

          if (!obj.variants) {
            obj.variants = [{ environment: { allRegionsAndLocales: true } }];
          }
          numEngines++;
          break;
        case "defaultEngines":
          defaultEngines = obj;
          break;
        case "engineOrders":
          engineOrders = obj;
          break;
      }
    }

    if (!numEngines) {
      throw new Error("One engine is required.");
    }

    if (!engineOrders) {
      engineOrders = {
        recordType: "engineOrders",
        orders: [],
      };
      fullConfig.push(engineOrders);
    }

    if (!defaultEngines) {
      defaultEngines = { recordType: "defaultEngines" };
      fullConfig.push(defaultEngines);
    }

    if (!defaultEngines.globalDefault) {
      let firstEngine = fullConfig.find(r => r.recordType == "engine");
      defaultEngines.globalDefault = firstEngine.identifier;
    }
    if (!defaultEngines.specificDefaults) {
      defaultEngines.specificDefaults = [];
    }

    return fullConfig;
  }

  /**
   * Detects the recordType of a partial search config object based
   * on its properties.
   *
   * @param {object} [partialObject]
   *  The partial search config object whose recordType will be detected.
   * @returns {string}
   *   The detected recordType.
   */
  #detectRecordType(partialObject) {
    const identifyingProperties = {
      engine: ["identifier"],
      defaultEngines: ["specificDefaults", "globalDefault"],
      engineOrders: ["orders"],
    };

    let detectedType = partialObject.recordType;
    for (let recordType in identifyingProperties) {
      if (identifyingProperties[recordType].some(p => p in partialObject)) {
        if (detectedType && detectedType != recordType) {
          throw new Error("Ambiguous recordType");
        }
        detectedType = recordType;
      }
    }

    if (!detectedType) {
      throw new Error(
        "Could not detect recordType. Identifier is mandatory for engine recordTypes."
      );
    }

    return detectedType;
  }

  /**
   * Convert a list of engine configurations into engine objects.
   *
   * @param {Array} engineConfigurations
   *   An array of engine configurations.
   * @returns {AppProvidedSearchEngine[]}
   *   An array of app provided search engine objects.
   */
  async searchConfigToEngines(engineConfigurations) {
    return engineConfigurations.map(
      config => new lazy.AppProvidedSearchEngine({ config })
    );
  }

  /**
   * Sets up the add-on manager so that it is ready for loading WebExtension
   * in xpcshell-tests.
   */
  async initXPCShellAddonManager() {
    this.#testScope.ExtensionTestUtils = lazy.ExtensionTestUtils;

    if (
      lazy.ExtensionTestUtils.addonManagerStarted ||
      lazy.AddonTestUtils.addonIntegrationService
    ) {
      // We have already started the add-on manager, and the following functions
      // may throw if they are called twice.
      return;
    }

    lazy.ExtensionTestUtils.init(this.#testScope);
    lazy.AddonTestUtils.overrideCertDB();
    lazy.AddonTestUtils.init(this.#testScope, false);

    await lazy.ExtensionTestUtils.startAddonManager();
  }

  /**
   * 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 = {}
  ) {
    if (!this.#isMochitest) {
      await this.initXPCShellAddonManager();
    }

    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) {
      this.#testScope.registerCleanupFunction(cleanup);
    }

    extension = this.#testScope.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) {
      this.#testScope.registerCleanupFunction(cleanup);
    }

    return extension;
  }

  /**
   * Create a search engine extension manifest.
   *
   * @param {object} [options]
   *   The options for the manifest.
   * @param {object} [options.icons]
   *   The icons to use for the WebExtension.
   * @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 {boolean} [options.is_default]
   *   Whether or not to ask for the search engine in the WebExtension to be
   *   attempted to set as default.
   * @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.
   * @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,
          is_default: !!options.is_default,
          search_url: options.search_url ?? "https://example.com/",
        },
      },
    };

    if (options.icons) {
      manifest.icons = options.icons;
    }

    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.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) {
      this._observers.add(observer);
    },

    removeIdleObserver(observer) {
      this._observers.delete(observer);
    },
  };

  /**
   * Register the mock idleSerice.
   */
  useMockIdleService() {
    let fakeIdleService = MockRegistrar.register(
      "@mozilla.org/widget/useridleservice;1",
      this.idleService
    );
    this.#idleServiceCID = fakeIdleService;
  }

  /**
   * Sets the search configuration and search overrides configuration without
   * reloading the engines.
   * If parameters are not specified, the appropriate configuration is
   * reset to the data stored in remote settings.
   *
   * This is useful for example in xpcshell-tests before the search service
   * is initialized.
   *
   * @param {object[]} [partialConfig]
   *   The replacement configuration. Will be expanded via `expandPartialConfig`.
   * @param {object[]} [overridesConfig]
   *   The replacement overrides configuration.
   */
  setRemoteSettingsConfig(partialConfig, overridesConfig) {
    let config = this.expandPartialConfig(partialConfig);
    this.#stubConfig(lazy.SearchUtils.SETTINGS_KEY, config);
    this.#stubConfig(lazy.SearchUtils.SETTINGS_OVERRIDES_KEY, overridesConfig);
  }

  /**
   * Simulates an update to the RemoteSettings configuration.
   * If parameters are not specified, the appropriate configuration is
   * reset to the data stored in remote settings.
   *
   * This is useful if the search service has already been initialized.
   * Note that the search service is always initialized in mochitests.
   *
   * @param {object[]} [partialConfig]
   *   The replacement configuration. Will be expanded via `expandPartialConfig`.
   * @param {object[]} [overridesConfig]
   *   The replacement overrides configuration.
   */
  async updateRemoteSettingsConfig(partialConfig, overridesConfig) {
    if (!this.#idleServiceCID) {
      this.useMockIdleService();
    }
    let config = this.expandPartialConfig(partialConfig);
    this.#stubConfig(lazy.SearchUtils.SETTINGS_KEY, config);
    this.#stubConfig(lazy.SearchUtils.SETTINGS_OVERRIDES_KEY, overridesConfig);

    if (!config) {
      let settings = lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY);
      config = await settings.get();
    }
    if (!overridesConfig) {
      let settings = lazy.RemoteSettings(
        lazy.SearchUtils.SETTINGS_OVERRIDES_KEY
      );
      overridesConfig = await settings.get();
    }
    const reloadObserved = this.promiseSearchNotification("engines-reloaded");
    await lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY).emit("sync", {
      data: { current: config },
    });
    await lazy
      .RemoteSettings(lazy.SearchUtils.SETTINGS_OVERRIDES_KEY)
      .emit("sync", {
        data: { current: overridesConfig },
      });

    this.idleService._fireObservers("idle");
    await reloadObserved;
  }

  /**
   * Fetches a URL and converts the content to a data URL.
   *
   * @param {string} url
   *   The URL of the file that should be fetched.
   * @returns {Promise<string>}
   *   The content of the file as a data URL.
   */
  async fetchAsDataUrl(url) {
    let res = await fetch(url);
    let blob = await res.blob();
    let reader = new FileReader();
    return new Promise((resolve, reject) => {
      reader.onload = () => {
        resolve(reader.result);
      };
      reader.onerror = () => {
        reject(new Error("Failed to read blob."));
      };
      reader.readAsDataURL(blob);
    });
  }
}

export const SearchTestUtils = new _SearchTestUtils();

[ Dauer der Verarbeitung: 0.33 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge