tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

head.js (38451B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 const { XPCOMUtils } = ChromeUtils.importESModule(
      5  "resource://gre/modules/XPCOMUtils.sys.mjs"
      6 );
      7 const { AppConstants } = ChromeUtils.importESModule(
      8  "resource://gre/modules/AppConstants.sys.mjs"
      9 );
     10 
     11 var { UrlbarMuxer, UrlbarProvider, UrlbarQueryContext, UrlbarUtils } =
     12  ChromeUtils.importESModule(
     13    "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"
     14  );
     15 
     16 ChromeUtils.defineESModuleGetters(this, {
     17  HttpServer: "resource://testing-common/httpd.sys.mjs",
     18  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
     19  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     20  SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
     21  TestUtils: "resource://testing-common/TestUtils.sys.mjs",
     22  UrlbarController:
     23    "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs",
     24  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     25  UrlbarProviderOpenTabs:
     26    "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
     27  UrlbarProvidersManager:
     28    "moz-src:///browser/components/urlbar/UrlbarProvidersManager.sys.mjs",
     29  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     30  UrlbarTokenizer:
     31    "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
     32  sinon: "resource://testing-common/Sinon.sys.mjs",
     33 });
     34 
     35 ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
     36  const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
     37    "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
     38  );
     39  module.init(this);
     40  return module;
     41 });
     42 
     43 ChromeUtils.defineLazyGetter(this, "MerinoTestUtils", () => {
     44  const { MerinoTestUtils: module } = ChromeUtils.importESModule(
     45    "resource://testing-common/MerinoTestUtils.sys.mjs"
     46  );
     47  module.init(this);
     48  return module;
     49 });
     50 
     51 ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
     52  const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
     53    "resource://testing-common/UrlbarTestUtils.sys.mjs"
     54  );
     55  module.init(this);
     56  return module;
     57 });
     58 
     59 ChromeUtils.defineLazyGetter(this, "GeolocationTestUtils", () => {
     60  const { GeolocationTestUtils: module } = ChromeUtils.importESModule(
     61    "resource://testing-common/GeolocationTestUtils.sys.mjs"
     62  );
     63  module.init(this);
     64  return module;
     65 });
     66 
     67 ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => {
     68  return Cc["@mozilla.org/places/frecency-recalculator;1"].getService(
     69    Ci.nsIObserver
     70  ).wrappedJSObject;
     71 });
     72 
     73 // Most tests need a profile to be setup, so do that here.
     74 do_get_profile();
     75 
     76 SearchTestUtils.init(this);
     77 
     78 const SUGGESTIONS_ENGINE_NAME = "Suggestions";
     79 const TAIL_SUGGESTIONS_ENGINE_NAME = "Tail Suggestions";
     80 
     81 const SEARCH_GLASS_ICON = "chrome://global/skin/icons/search-glass.svg";
     82 
     83 /**
     84 * Gets the database connection.  If the Places connection is invalid it will
     85 * try to create a new connection.
     86 *
     87 * @param [optional] aForceNewConnection
     88 *        Forces creation of a new connection to the database.  When a
     89 *        connection is asyncClosed it cannot anymore schedule async statements,
     90 *        though connectionReady will keep returning true (Bug 726990).
     91 *
     92 * @returns The database connection or null if unable to get one.
     93 */
     94 var gDBConn;
     95 function DBConn(aForceNewConnection) {
     96  if (!aForceNewConnection) {
     97    let db = PlacesUtils.history.DBConnection;
     98    if (db.connectionReady) {
     99      return db;
    100    }
    101  }
    102 
    103  // If the Places database connection has been closed, create a new connection.
    104  if (!gDBConn || aForceNewConnection) {
    105    let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
    106    file.append("places.sqlite");
    107    let dbConn = (gDBConn = Services.storage.openDatabase(file));
    108 
    109    TestUtils.topicObserved("profile-before-change").then(() =>
    110      dbConn.asyncClose()
    111    );
    112  }
    113 
    114  return gDBConn.connectionReady ? gDBConn : null;
    115 }
    116 
    117 /**
    118 * @param {string} searchString The search string to insert into the context.
    119 * @param {object} properties Overrides for the default values.
    120 * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled
    121 *          required options.
    122 */
    123 function createContext(searchString = "foo", properties = {}) {
    124  info(`Creating new queryContext with searchString: ${searchString}`);
    125  let context = new UrlbarQueryContext(
    126    Object.assign(
    127      {
    128        allowAutofill: UrlbarPrefs.get("autoFill"),
    129        isPrivate: true,
    130        maxResults: UrlbarPrefs.get("maxRichResults"),
    131        sapName: "urlbar",
    132        searchString,
    133      },
    134      properties
    135    )
    136  );
    137  let tokens = UrlbarTokenizer.tokenize(context);
    138  context.tokens = tokens;
    139  return context;
    140 }
    141 
    142 /**
    143 * Waits for the given notification from the supplied controller.
    144 *
    145 * @param {UrlbarController} controller The controller to wait for a response from.
    146 * @param {string} notification The name of the notification to wait for.
    147 * @param {boolean} expected Wether the notification is expected.
    148 * @returns {Promise} A promise that is resolved with the arguments supplied to
    149 *   the notification.
    150 */
    151 function promiseControllerNotification(
    152  controller,
    153  notification,
    154  expected = true
    155 ) {
    156  return new Promise((resolve, reject) => {
    157    let proxifiedObserver = new Proxy(
    158      {},
    159      {
    160        get: (target, name) => {
    161          if (name == notification) {
    162            return (...args) => {
    163              controller.removeListener(proxifiedObserver);
    164              if (expected) {
    165                resolve(args);
    166              } else {
    167                reject();
    168              }
    169            };
    170          }
    171          return () => false;
    172        },
    173      }
    174    );
    175    controller.addListener(proxifiedObserver);
    176  });
    177 }
    178 
    179 function convertToUtf8(str) {
    180  return String.fromCharCode(...new TextEncoder().encode(str));
    181 }
    182 
    183 /**
    184 * Helper function to clear the existing providers and register a basic provider
    185 * that returns only the results given.
    186 *
    187 * @param {Array} results The results for the provider to return.
    188 * @param {Function} [onCancel] Optional, called when the query provider
    189 *                              receives a cancel instruction.
    190 * @param {UrlbarUtils.PROVIDER_TYPE} type The provider type.
    191 * @param {string} [name] Optional, use as the provider name.
    192 *                        If none, a default name is chosen.
    193 * @returns {UrlbarProvider} The provider
    194 */
    195 function registerBasicTestProvider(results = [], onCancel, type, name) {
    196  let provider = new UrlbarTestUtils.TestProvider({
    197    results,
    198    onCancel,
    199    type,
    200    name,
    201  });
    202  UrlbarProvidersManager.registerProvider(provider);
    203  registerCleanupFunction(() =>
    204    UrlbarProvidersManager.unregisterProvider(provider)
    205  );
    206  return provider;
    207 }
    208 
    209 // Creates an HTTP server for the test.
    210 function makeTestServer(port = -1) {
    211  let httpServer = new HttpServer();
    212  httpServer.start(port);
    213  registerCleanupFunction(() => httpServer.stop(() => {}));
    214  return httpServer;
    215 }
    216 
    217 /**
    218 * Sets up a search engine that provides some suggestions by appending strings
    219 * onto the search query.
    220 *
    221 * @param {Function} suggestionsFn
    222 *   A function that returns an array of suggestion strings given a
    223 *   search string.  If not given, a default function is used.
    224 * @param {object} options
    225 *   Options for the check.
    226 * @param {string} [options.name]
    227 *   The name of the engine to install.
    228 * @returns {nsISearchEngine} The new engine.
    229 */
    230 async function addTestSuggestionsEngine(
    231  suggestionsFn = null,
    232  { name = SUGGESTIONS_ENGINE_NAME } = {}
    233 ) {
    234  // This port number should match the number in engine-suggestions.xml.
    235  let server = makeTestServer();
    236  server.registerPathHandler("/suggest", (req, resp) => {
    237    let params = new URLSearchParams(req.queryString);
    238    let searchStr = params.get("q");
    239    let suggestions = suggestionsFn
    240      ? suggestionsFn(searchStr)
    241      : [searchStr].concat(["foo", "bar"].map(s => searchStr + " " + s));
    242    let data = [searchStr, suggestions];
    243    resp.setHeader("Content-Type", "application/json", false);
    244    resp.write(JSON.stringify(data));
    245  });
    246  await SearchTestUtils.installSearchExtension({
    247    name,
    248    search_url: `http://localhost:${server.identity.primaryPort}/search`,
    249    suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`,
    250    suggest_url_get_params: "?q={searchTerms}",
    251  });
    252  let engine = Services.search.getEngineByName(name);
    253  return engine;
    254 }
    255 
    256 /**
    257 * Sets up a search engine that provides some tail suggestions by creating an
    258 * array that mimics Google's tail suggestion responses.
    259 *
    260 * @param {Function} suggestionsFn
    261 *        A function that returns an array that mimics Google's tail suggestion
    262 *        responses. See bug 1626897.
    263 *        NOTE: Consumers specifying suggestionsFn must include searchStr as a
    264 *              part of the array returned by suggestionsFn.
    265 * @returns {nsISearchEngine} The new engine.
    266 */
    267 async function addTestTailSuggestionsEngine(suggestionsFn = null) {
    268  // This port number should match the number in engine-tail-suggestions.xml.
    269  let server = makeTestServer();
    270  server.registerPathHandler("/suggest", (req, resp) => {
    271    let params = new URLSearchParams(req.queryString);
    272    let searchStr = params.get("q");
    273    let suggestions = suggestionsFn
    274      ? suggestionsFn(searchStr)
    275      : [
    276          "what time is it in t",
    277          ["what is the time today texas"].concat(
    278            ["toronto", "tunisia"].map(s => searchStr + s.slice(1))
    279          ),
    280          [],
    281          {
    282            "google:irrelevantparameter": [],
    283            "google:suggestdetail": [{}].concat(
    284              ["toronto", "tunisia"].map(s => ({
    285                mp: "… ",
    286                t: s,
    287              }))
    288            ),
    289          },
    290        ];
    291    let data = suggestions;
    292    let jsonString = JSON.stringify(data);
    293    // This script must be evaluated as UTF-8 for this to write out the bytes of
    294    // the string in UTF-8.  If it's evaluated as Latin-1, the written bytes
    295    // will be the result of UTF-8-encoding the result-string *twice*, which
    296    // will break the "… " match prefixes.
    297    let stringOfUtf8Bytes = convertToUtf8(jsonString);
    298    resp.setHeader("Content-Type", "application/json", false);
    299    resp.write(stringOfUtf8Bytes);
    300  });
    301  await SearchTestUtils.installSearchExtension({
    302    name: TAIL_SUGGESTIONS_ENGINE_NAME,
    303    search_url: `http://localhost:${server.identity.primaryPort}/search`,
    304    suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`,
    305    suggest_url_get_params: "?q={searchTerms}",
    306  });
    307  let engine = Services.search.getEngineByName("Tail Suggestions");
    308  return engine;
    309 }
    310 
    311 /**
    312 * Creates a function that can be provided to the new engine
    313 * utility function to mimic a search engine that returns
    314 * rich suggestions.
    315 *
    316 * @param {string} searchStr
    317 *        The string being searched for.
    318 *
    319 * @returns {object}
    320 *        A JSON object mimicing the data format returned by
    321 *        a search engine.
    322 */
    323 function defaultRichSuggestionsFn(searchStr) {
    324  let suffixes = ["toronto", "tunisia", "tacoma", "taipei"];
    325  return [
    326    "what time is it in t",
    327    suffixes.map(s => searchStr + s.slice(1)),
    328    [],
    329    {
    330      "google:irrelevantparameter": [],
    331      "google:suggestdetail": suffixes.map((suffix, i) => {
    332        // Set every other suggestion as a rich suggestion so we can
    333        // test how they are handled and ordered when interleaved.
    334        if (i % 2) {
    335          return {};
    336        }
    337        return {
    338          a: "description",
    339          dc: "#FFFFFF",
    340          i: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",
    341          t: "Title",
    342        };
    343      }),
    344    },
    345  ];
    346 }
    347 
    348 async function addOpenPages(
    349  uri,
    350  count = 1,
    351  userContextId = 0,
    352  tabGroupId = null
    353 ) {
    354  for (let i = 0; i < count; i++) {
    355    await UrlbarProviderOpenTabs.registerOpenTab(
    356      uri.spec,
    357      userContextId,
    358      tabGroupId,
    359      false
    360    );
    361  }
    362 }
    363 
    364 async function removeOpenPages(
    365  aUri,
    366  aCount = 1,
    367  aUserContextId = 0,
    368  tabGroupId = null
    369 ) {
    370  for (let i = 0; i < aCount; i++) {
    371    await UrlbarProviderOpenTabs.unregisterOpenTab(
    372      aUri.spec,
    373      aUserContextId,
    374      tabGroupId,
    375      false
    376    );
    377  }
    378 }
    379 
    380 /**
    381 * Helper for tests that generate search results but aren't interested in
    382 * suggestions, such as autofill tests. Installs a test engine and disables
    383 * suggestions.
    384 */
    385 function testEngine_setup() {
    386  add_setup(async () => {
    387    await cleanupPlaces();
    388    let engine = await addTestSuggestionsEngine();
    389    let oldDefaultEngine = await Services.search.getDefault();
    390 
    391    registerCleanupFunction(async () => {
    392      Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
    393      Services.prefs.clearUserPref("browser.urlbar.contextualSearch.enabled");
    394      Services.prefs.clearUserPref(
    395        "browser.search.separatePrivateDefault.ui.enabled"
    396      );
    397      Services.search.setDefault(
    398        oldDefaultEngine,
    399        Ci.nsISearchService.CHANGE_REASON_UNKNOWN
    400      );
    401    });
    402 
    403    Services.search.setDefault(
    404      engine,
    405      Ci.nsISearchService.CHANGE_REASON_UNKNOWN
    406    );
    407    Services.prefs.setBoolPref(
    408      "browser.search.separatePrivateDefault.ui.enabled",
    409      false
    410    );
    411    Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
    412    Services.prefs.setBoolPref(
    413      "browser.urlbar.scotchBonnet.enableOverride",
    414      false
    415    );
    416  });
    417 }
    418 
    419 async function cleanupPlaces() {
    420  Services.prefs.clearUserPref("browser.urlbar.autoFill");
    421 
    422  await PlacesUtils.bookmarks.eraseEverything();
    423  await PlacesUtils.history.clear();
    424 }
    425 
    426 /**
    427 * Creates a UrlbarResult for a bookmark result.
    428 *
    429 * @param {UrlbarQueryContext} queryContext
    430 *   The context that this result will be displayed in.
    431 * @param {object} options
    432 *   Options for the result.
    433 * @param {string} [options.title]
    434 *   The page title.
    435 * @param {string} options.uri
    436 *   The page URI.
    437 * @param {string} [options.iconUri]
    438 *   A URI for the page's icon.
    439 * @param {Array} [options.tags]
    440 *   An array of string tags. Defaults to an empty array.
    441 * @param {boolean} [options.heuristic]
    442 *   True if this is a heuristic result. Defaults to false.
    443 * @param {number} [options.source]
    444 *   Where the results should be sourced from. See {@link UrlbarUtils.RESULT_SOURCE}.
    445 * @returns {UrlbarResult}
    446 */
    447 function makeBookmarkResult(
    448  queryContext,
    449  {
    450    title,
    451    uri,
    452    iconUri,
    453    tags = [],
    454    heuristic = false,
    455    source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
    456  }
    457 ) {
    458  return new UrlbarResult({
    459    type: UrlbarUtils.RESULT_TYPE.URL,
    460    source,
    461    heuristic,
    462    payload: {
    463      url: uri,
    464      title,
    465      tags,
    466      // Check against undefined so consumers can pass in the empty string.
    467      icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
    468      isBlockable:
    469        source == UrlbarUtils.RESULT_SOURCE.HISTORY ? true : undefined,
    470      blockL10n:
    471        source == UrlbarUtils.RESULT_SOURCE.HISTORY
    472          ? { id: "urlbar-result-menu-remove-from-history" }
    473          : undefined,
    474      helpUrl:
    475        source == UrlbarUtils.RESULT_SOURCE.HISTORY
    476          ? Services.urlFormatter.formatURLPref("app.support.baseURL") +
    477            "awesome-bar-result-menu"
    478          : undefined,
    479    },
    480    highlights: {
    481      url: UrlbarUtils.HIGHLIGHT.TYPED,
    482      title: UrlbarUtils.HIGHLIGHT.TYPED,
    483      tags: UrlbarUtils.HIGHLIGHT.TYPED,
    484    },
    485  });
    486 }
    487 
    488 /**
    489 * Creates a UrlbarResult for a form history result.
    490 *
    491 * @param {UrlbarQueryContext} queryContext
    492 *   The context that this result will be displayed in.
    493 * @param {object} options
    494 *   Options for the result.
    495 * @param {string} options.suggestion
    496 *   The form history suggestion.
    497 * @param {string} options.engineName
    498 *   The name of the engine that will do the search when the result is picked.
    499 * @returns {UrlbarResult}
    500 */
    501 function makeFormHistoryResult(queryContext, { suggestion, engineName }) {
    502  return new UrlbarResult({
    503    type: UrlbarUtils.RESULT_TYPE.SEARCH,
    504    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    505    payload: {
    506      engine: engineName,
    507      suggestion,
    508      title: suggestion,
    509      lowerCaseSuggestion: suggestion.toLocaleLowerCase(),
    510      isBlockable: true,
    511      blockL10n: { id: "urlbar-result-menu-remove-from-history" },
    512      helpUrl:
    513        Services.urlFormatter.formatURLPref("app.support.baseURL") +
    514        "awesome-bar-result-menu",
    515    },
    516    highlights: {
    517      suggestion: UrlbarUtils.HIGHLIGHT.SUGGESTED,
    518      title: UrlbarUtils.HIGHLIGHT.SUGGESTED,
    519    },
    520  });
    521 }
    522 
    523 /**
    524 * Creates a UrlbarResult for an omnibox extension result. For more information,
    525 * see the documentation for omnibox.SuggestResult:
    526 * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/omnibox/SuggestResult
    527 *
    528 * @param {UrlbarQueryContext} queryContext
    529 *   The context that this result will be displayed in.
    530 * @param {object} options
    531 *   Options for the result.
    532 * @param {string} options.content
    533 *   The string displayed when the result is highlighted.
    534 * @param {string} options.description
    535 *   The string displayed in the address bar dropdown.
    536 * @param {string} options.keyword
    537 *   The keyword associated with the extension returning the result.
    538 * @param {boolean} [options.heuristic]
    539 *   True if this is a heuristic result. Defaults to false.
    540 * @returns {UrlbarResult}
    541 */
    542 function makeOmniboxResult(
    543  queryContext,
    544  { content, description, keyword, heuristic = false }
    545 ) {
    546  return new UrlbarResult({
    547    type: UrlbarUtils.RESULT_TYPE.OMNIBOX,
    548    source: UrlbarUtils.RESULT_SOURCE.ADDON,
    549    heuristic,
    550    payload: {
    551      title: description,
    552      content,
    553      keyword,
    554      icon: UrlbarUtils.ICON.EXTENSION,
    555    },
    556    highlights: {
    557      title: UrlbarUtils.HIGHLIGHT.TYPED,
    558      content: UrlbarUtils.HIGHLIGHT.TYPED,
    559      keyword: UrlbarUtils.HIGHLIGHT.TYPED,
    560    },
    561  });
    562 }
    563 
    564 /**
    565 * Creates a UrlbarResult for an switch-to-tab result.
    566 *
    567 * @param {UrlbarQueryContext} queryContext
    568 *   The context that this result will be displayed in.
    569 * @param {object} options
    570 *   Options for the result.
    571 * @param {string} options.uri
    572 *   The page URI.
    573 * @param {string} [options.title]
    574 *   The page title.
    575 * @param {string} [options.iconUri]
    576 *   A URI for the page icon.
    577 * @param {number} [options.userContextId]
    578 *   An id of the userContext in which the tab is located.
    579 * @param {string} [options.tabGroup]
    580 *   An id of the tab group in which the tab is located.
    581 * @returns {UrlbarResult}
    582 */
    583 function makeTabSwitchResult(
    584  queryContext,
    585  { uri, title, iconUri, userContextId, tabGroup }
    586 ) {
    587  return new UrlbarResult({
    588    type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
    589    source: UrlbarUtils.RESULT_SOURCE.TABS,
    590    payload: {
    591      url: uri,
    592      title,
    593      // Check against undefined so consumers can pass in the empty string.
    594      icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
    595      userContextId: userContextId || 0,
    596      tabGroup,
    597    },
    598    highlights: {
    599      url: UrlbarUtils.HIGHLIGHT.TYPED,
    600      title: UrlbarUtils.HIGHLIGHT.TYPED,
    601    },
    602  });
    603 }
    604 
    605 /**
    606 * Creates a UrlbarResult for a keyword search result.
    607 *
    608 * @param {UrlbarQueryContext} queryContext
    609 *   The context that this result will be displayed in.
    610 * @param {object} options
    611 *   Options for the result.
    612 * @param {string} options.uri
    613 *   The page URI.
    614 * @param {string} options.keyword
    615 *   The page's search keyword.
    616 * @param {string} [options.title]
    617 *   The title for the bookmarked keyword page.
    618 * @param {string} [options.iconUri]
    619 *   A URI for the engine's icon.
    620 * @param {string} [options.postData]
    621 *   The search POST data.
    622 * @param {boolean} [options.heuristic]
    623 *   True if this is a heuristic result. Defaults to false.
    624 * @returns {UrlbarResult}
    625 */
    626 function makeKeywordSearchResult(
    627  queryContext,
    628  { uri, keyword, title, iconUri, postData, heuristic = false }
    629 ) {
    630  return new UrlbarResult({
    631    type: UrlbarUtils.RESULT_TYPE.KEYWORD,
    632    source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
    633    heuristic,
    634    payload: {
    635      title: title || uri,
    636      url: uri,
    637      keyword,
    638      input: queryContext.searchString,
    639      postData: postData || null,
    640      icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
    641    },
    642    highlights: {
    643      title: UrlbarUtils.HIGHLIGHT.TYPED,
    644      url: UrlbarUtils.HIGHLIGHT.TYPED,
    645      keyword: UrlbarUtils.HIGHLIGHT.TYPED,
    646      input: UrlbarUtils.HIGHLIGHT.TYPED,
    647    },
    648  });
    649 }
    650 
    651 /**
    652 * Creates a UrlbarResult for a remote tab result.
    653 *
    654 * @param {UrlbarQueryContext} queryContext
    655 *   The context that this result will be displayed in.
    656 * @param {object} options
    657 *   Options for the result.
    658 * @param {string} options.uri
    659 *   The page URI.
    660 * @param {string} options.device
    661 *   The name of the device that the remote tab comes from.
    662 * @param {string} [options.title]
    663 *   The page title.
    664 * @param {number} [options.lastUsed]
    665 *   The last time the remote tab was visited, in epoch seconds. Defaults
    666 *   to 0.
    667 * @param {string} [options.iconUri]
    668 *   A URI for the page's icon.
    669 * @returns {UrlbarResult}
    670 */
    671 function makeRemoteTabResult(
    672  queryContext,
    673  { uri, device, title, iconUri, lastUsed = 0 }
    674 ) {
    675  let payload = {
    676    url: uri,
    677    device,
    678    // Check against undefined so consumers can pass in the empty string.
    679    icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
    680    lastUsed: lastUsed * 1000,
    681  };
    682 
    683  // Check against undefined so consumers can pass in the empty string.
    684  if (typeof title != "undefined") {
    685    payload.title = title;
    686  } else {
    687    payload.title = uri;
    688  }
    689 
    690  return new UrlbarResult({
    691    type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
    692    source: UrlbarUtils.RESULT_SOURCE.TABS,
    693    payload,
    694    highlights: {
    695      url: UrlbarUtils.HIGHLIGHT.TYPED,
    696      title: UrlbarUtils.HIGHLIGHT.TYPED,
    697      device: UrlbarUtils.HIGHLIGHT.TYPED,
    698    },
    699  });
    700 }
    701 
    702 /**
    703 * Creates a UrlbarResult for a search result.
    704 *
    705 * @param {UrlbarQueryContext} queryContext
    706 *   The context that this result will be displayed in.
    707 * @param {object} options
    708 *   Options for the result.
    709 * @param {string} [options.suggestion]
    710 *   The suggestion offered by the search engine.
    711 * @param {string} [options.tailPrefix]
    712 *   The characters placed at the end of a Google "tail" suggestion. See
    713 *   {@link https://firefox-source-docs.mozilla.org/browser/urlbar/nontechnical-overview.html#search-suggestions}
    714 * @param {*} [options.tail]
    715 *   The details of the URL bar tail
    716 * @param {number} [options.tailOffsetIndex]
    717 *   The index of the first character in the tail suggestion that should be
    718 * @param {string} [options.engineName]
    719 *   The name of the engine providing the suggestion. Leave blank if there
    720 *   is no suggestion.
    721 * @param {string} [options.uri]
    722 *   The URI that the search result will navigate to.
    723 * @param {string} [options.query]
    724 *   The query that started the search. This overrides
    725 *   `queryContext.searchString`. This is useful when the query that will show
    726 *   up in the result object will be different from what was typed. For example,
    727 *   if a leading restriction token will be used.
    728 * @param {string} [options.alias]
    729 *   The alias for the search engine, if the search is an alias search.
    730 * @param {string} [options.engineIconUri]
    731 *   A URI for the engine's icon.
    732 * @param {boolean} [options.heuristic]
    733 *   True if this is a heuristic result. Defaults to false.
    734 * @param {boolean} [options.providesSearchMode]
    735 *   Whether search mode is entered when this result is selected.
    736 * @param {string} [options.providerName]
    737 *   The name of the provider offering this result. The test suite will not
    738 *   check which provider offered a result unless this option is specified.
    739 * @param {boolean} [options.inPrivateWindow]
    740 *   If the window to test is a private window.
    741 * @param {boolean} [options.isPrivateEngine]
    742 *   If the engine is a private engine.
    743 * @param {string} [options.searchUrlDomainWithoutSuffix]
    744 *   For tab-to-search results, the search engine domain without the public
    745 *   suffix.
    746 * @param {number} [options.type]
    747 *   The type of the search result. Defaults to UrlbarUtils.RESULT_TYPE.SEARCH.
    748 * @param {number} [options.source]
    749 *   The source of the search result. Defaults to UrlbarUtils.RESULT_SOURCE.SEARCH.
    750 * @param {boolean} [options.satisfiesAutofillThreshold]
    751 *   If this search should appear in the autofill section of the box
    752 * @param {boolean} [options.trending]
    753 *    If the search result is a trending result. `Defaults to false`.
    754 * @param {boolean} [options.isRichSuggestion]
    755 *    If the search result is a rich result. `Defaults to false`.
    756 * @returns {UrlbarResult}
    757 */
    758 function makeSearchResult(
    759  queryContext,
    760  {
    761    suggestion,
    762    tailPrefix,
    763    tail,
    764    tailOffsetIndex,
    765    engineName,
    766    alias,
    767    uri,
    768    query,
    769    engineIconUri,
    770    providesSearchMode,
    771    providerName,
    772    inPrivateWindow,
    773    isPrivateEngine,
    774    searchUrlDomainWithoutSuffix,
    775    heuristic = false,
    776    trending = false,
    777    isRichSuggestion = false,
    778    type = UrlbarUtils.RESULT_TYPE.SEARCH,
    779    source = UrlbarUtils.RESULT_SOURCE.SEARCH,
    780    satisfiesAutofillThreshold = false,
    781  }
    782 ) {
    783  // Tail suggestion common cases, handled here to reduce verbosity in tests.
    784  if (tail) {
    785    if (!tailPrefix && !isRichSuggestion) {
    786      tailPrefix = "… ";
    787    }
    788    if (!tailOffsetIndex) {
    789      tailOffsetIndex = suggestion.indexOf(tail);
    790    }
    791  }
    792 
    793  let payload = {
    794    engine: engineName,
    795    suggestion,
    796    tailPrefix,
    797    tail,
    798    tailOffsetIndex,
    799    keyword: alias,
    800    // Check against undefined so consumers can pass in the empty string.
    801    query:
    802      typeof query != "undefined" ? query : queryContext.trimmedSearchString,
    803    icon: engineIconUri,
    804    providesSearchMode,
    805    inPrivateWindow,
    806    isPrivateEngine,
    807  };
    808 
    809  if (providesSearchMode) {
    810    // No title.
    811  } else if (payload.tail && payload.tailOffsetIndex >= 0) {
    812    payload.title = payload.tail;
    813  } else if (payload.suggestion != undefined) {
    814    payload.title = payload.suggestion;
    815  } else if (payload.query != undefined) {
    816    payload.title = payload.query;
    817  }
    818 
    819  if (uri) {
    820    payload.url = uri;
    821  }
    822 
    823  if (providerName == "UrlbarProviderTabToSearch") {
    824    if (searchUrlDomainWithoutSuffix.startsWith("www.")) {
    825      searchUrlDomainWithoutSuffix = searchUrlDomainWithoutSuffix.substring(4);
    826    }
    827    payload.searchUrlDomainWithoutSuffix = searchUrlDomainWithoutSuffix;
    828    payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold;
    829    payload.isGeneralPurposeEngine = false;
    830  }
    831 
    832  if (providerName == "UrlbarProviderTokenAliasEngines") {
    833    payload.keywords = alias?.toLowerCase();
    834  }
    835 
    836  if (typeof suggestion == "string") {
    837    payload.lowerCaseSuggestion = suggestion.toLocaleLowerCase();
    838    payload.trending = trending;
    839  }
    840 
    841  if (isRichSuggestion) {
    842    payload.icon =
    843      "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
    844    payload.description = "description";
    845  }
    846 
    847  return new UrlbarResult({
    848    type,
    849    source,
    850    heuristic,
    851    isRichSuggestion,
    852    providerName,
    853    payload,
    854    highlights: {
    855      engine: UrlbarUtils.HIGHLIGHT.TYPED,
    856      suggestion: UrlbarUtils.HIGHLIGHT.SUGGESTED,
    857      tail: UrlbarUtils.HIGHLIGHT.SUGGESTED,
    858      keyword: providesSearchMode ? UrlbarUtils.HIGHLIGHT.TYPED : undefined,
    859      query: UrlbarUtils.HIGHLIGHT.TYPED,
    860    },
    861  });
    862 }
    863 
    864 /**
    865 * Creates a UrlbarResult for a history result.
    866 *
    867 * @param {UrlbarQueryContext} queryContext
    868 *   The context that this result will be displayed in.
    869 * @param {object} options Options for the result.
    870 * @param {string} options.title
    871 *   The page title.
    872 * @param {string} options.uri
    873 *   The page URI.
    874 * @param {Array} [options.tags]
    875 *   An array of string tags. Defaults to an empty array.
    876 * @param {string} [options.iconUri]
    877 *   A URI for the page's icon.
    878 * @param {boolean} [options.heuristic]
    879 *   True if this is a heuristic result. Defaults to false.
    880 * @param {string} options.providerName
    881 *   The name of the provider offering this result. The test suite will not
    882 *   check which provider offered a result unless this option is specified.
    883 * @param {number} [options.source]
    884 *   The source of the result
    885 * @returns {UrlbarResult}
    886 */
    887 function makeVisitResult(
    888  queryContext,
    889  {
    890    title,
    891    uri,
    892    iconUri,
    893    providerName,
    894    tags = [],
    895    heuristic = false,
    896    source = UrlbarUtils.RESULT_SOURCE.HISTORY,
    897  }
    898 ) {
    899  let payload = {
    900    url: uri,
    901  };
    902 
    903  if (title != undefined) {
    904    payload.title = title;
    905  }
    906 
    907  if (
    908    !heuristic &&
    909    providerName != "UrlbarProviderAboutPages" &&
    910    source == UrlbarUtils.RESULT_SOURCE.HISTORY
    911  ) {
    912    payload.isBlockable = true;
    913    payload.blockL10n = { id: "urlbar-result-menu-remove-from-history" };
    914    payload.helpUrl =
    915      Services.urlFormatter.formatURLPref("app.support.baseURL") +
    916      "awesome-bar-result-menu";
    917  }
    918 
    919  if (iconUri) {
    920    payload.icon = iconUri;
    921  } else if (
    922    iconUri === undefined &&
    923    source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL
    924  ) {
    925    payload.icon = `page-icon:${uri}`;
    926  }
    927 
    928  if (!heuristic && tags) {
    929    payload.tags = tags;
    930  }
    931 
    932  return new UrlbarResult({
    933    type: UrlbarUtils.RESULT_TYPE.URL,
    934    source,
    935    heuristic,
    936    providerName,
    937    payload,
    938    highlights: {
    939      url: UrlbarUtils.HIGHLIGHT.TYPED,
    940      title: UrlbarUtils.HIGHLIGHT.TYPED,
    941      fallbackTitle: UrlbarUtils.HIGHLIGHT.TYPED,
    942      tags: UrlbarUtils.HIGHLIGHT.TYPED,
    943    },
    944  });
    945 }
    946 
    947 /**
    948 * Creates a UrlbarResult for a calculator result.
    949 *
    950 * @param {UrlbarQueryContext} queryContext
    951 *   The context that this result will be displayed in.
    952 * @param {object} options
    953 *   Options for the result.
    954 * @param {string} options.value
    955 *   The value of the calculator result.
    956 * @returns {UrlbarResult}
    957 */
    958 function makeCalculatorResult(queryContext, { value }) {
    959  return new UrlbarResult({
    960    type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
    961    source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
    962    payload: {
    963      value,
    964      input: queryContext.searchString,
    965      dynamicType: "calculator",
    966    },
    967  });
    968 }
    969 
    970 /**
    971 * Creates a UrlbarResult for a global action result.
    972 *
    973 * @param {object} options
    974 * @param {Array} [options.actionsResults]
    975 *   An array of action descriptors.
    976 * @param {string} [options.query]
    977 *  The query passed to actions when they run.
    978 *  It is "" for contextual search, otherwise it is queryContext.searchString.
    979 * @param {number} [options.inputLength]
    980 *  Original input length.
    981 * @param {boolean} [options.showOnboardingLabel]
    982 *   Whether the “press Tab” hint should appear.
    983 * @returns {UrlbarResult}
    984 */
    985 function makeGlobalActionsResult({
    986  actionsResults,
    987  query,
    988  inputLength,
    989  showOnboardingLabel = false,
    990 }) {
    991  const payload = {
    992    actionsResults,
    993    dynamicType: "actions",
    994    query,
    995    input: query,
    996    inputLength,
    997    showOnboardingLabel,
    998  };
    999 
   1000  return new UrlbarResult({
   1001    type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
   1002    source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
   1003    providerName: "UrlbarProviderGlobalActions",
   1004    payload,
   1005  });
   1006 }
   1007 
   1008 /**
   1009 * Checks that the results returned by a UrlbarController match those in
   1010 * the param `matches`.
   1011 *
   1012 * @param {object} options Options for the check.
   1013 * @param {UrlbarQueryContext} options.context
   1014 *   The context for this query.
   1015 * @param {string} [options.incompleteSearch]
   1016 *   A search will be fired for this string and then be immediately canceled by
   1017 *   the query in `context`.
   1018 * @param {string} [options.autofilled]
   1019 *   The autofilled value in the first result.
   1020 * @param {string} [options.completed]
   1021 *   The value that would be filled if the autofill result was confirmed.
   1022 *   Has no effect if `autofilled` is not specified.
   1023 * @param {object} [options.conditionalPayloadProperties]
   1024 *   An object mapping payload property names to objects { optional, ignore }.
   1025 *   See the code below.
   1026 * @param {Array} options.matches
   1027 *   An array of UrlbarResults.
   1028 */
   1029 async function check_results({
   1030  context,
   1031  incompleteSearch,
   1032  autofilled,
   1033  completed,
   1034  matches = [],
   1035  conditionalPayloadProperties = {},
   1036 } = {}) {
   1037  if (!context) {
   1038    return;
   1039  }
   1040 
   1041  // At this point frecency could still be updating due to latest pages
   1042  // updates.
   1043  // This is not a problem in real life, but autocomplete tests should
   1044  // return reliable resultsets, thus we have to wait.
   1045  await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
   1046 
   1047  const controller = UrlbarTestUtils.newMockController({
   1048    input: {
   1049      isPrivate: context.isPrivate,
   1050      onFirstResult() {
   1051        return false;
   1052      },
   1053      getSearchSource() {
   1054        return "dummy-search-source";
   1055      },
   1056      window: {
   1057        location: {
   1058          href: AppConstants.BROWSER_CHROME_URL,
   1059        },
   1060      },
   1061    },
   1062  });
   1063  controller.setView({
   1064    get visibleResults() {
   1065      return context.results;
   1066    },
   1067    controller: {
   1068      removeResult() {},
   1069    },
   1070  });
   1071 
   1072  if (incompleteSearch) {
   1073    let incompleteContext = createContext(incompleteSearch, {
   1074      isPrivate: context.isPrivate,
   1075    });
   1076    controller.startQuery(incompleteContext);
   1077  }
   1078  await controller.startQuery(context);
   1079 
   1080  if (autofilled) {
   1081    Assert.ok(context.results[0], "There is a first result.");
   1082    Assert.ok(
   1083      context.results[0].autofill,
   1084      "The first result is an autofill result"
   1085    );
   1086    Assert.equal(
   1087      context.results[0].autofill.value,
   1088      autofilled,
   1089      "The correct value was autofilled."
   1090    );
   1091    if (completed) {
   1092      Assert.equal(
   1093        context.results[0].payload.url,
   1094        completed,
   1095        "The completed autofill value is correct."
   1096      );
   1097    }
   1098  }
   1099  if (context.results.length != matches.length) {
   1100    info("Actual results: " + JSON.stringify(context.results));
   1101  }
   1102  Assert.equal(
   1103    context.results.length,
   1104    matches.length,
   1105    "Found the expected number of results."
   1106  );
   1107 
   1108  let propertiesToCheck = {
   1109    type: {},
   1110    source: {},
   1111    heuristic: {},
   1112    isBestMatch: { map: v => !!v },
   1113    providerName: { optional: true },
   1114    suggestedIndex: { optional: true },
   1115    isSuggestedIndexRelativeToGroup: { optional: true, map: v => !!v },
   1116    exposureTelemetry: { optional: true },
   1117    isRichSuggestion: { optional: true },
   1118    richSuggestionIconVariation: { optional: true },
   1119  };
   1120 
   1121  // Payload properties to conditionally check. Properties not specified here
   1122  // will always be checked.
   1123  //
   1124  // ignore:
   1125  //   Always ignore the property.
   1126  // optional:
   1127  //   Ignore the property if it's not in the expected result.
   1128  conditionalPayloadProperties = {
   1129    frecency: { optional: true },
   1130    lastVisit: { optional: true },
   1131    // `suggestionObject` is only used for dismissing Suggest Rust results, and
   1132    // important properties in this object are reflected in the top-level
   1133    // payload object, so ignore it. There are Suggest tests specifically for
   1134    // dismissals that indirectly test the important aspects of this property.
   1135    suggestionObject: { ignore: true },
   1136    ...conditionalPayloadProperties,
   1137  };
   1138 
   1139  for (let i = 0; i < matches.length; i++) {
   1140    let actual = context.results[i];
   1141    let expected = matches[i];
   1142    info(
   1143      `Comparing results at index ${i}:` +
   1144        " actual=" +
   1145        JSON.stringify(actual) +
   1146        " expected=" +
   1147        JSON.stringify(expected)
   1148    );
   1149 
   1150    for (let [key, { optional, map }] of Object.entries(propertiesToCheck)) {
   1151      if (!optional || expected[key] !== undefined) {
   1152        map ??= v => v;
   1153        Assert.equal(
   1154          map(actual[key]),
   1155          map(expected[key]),
   1156          `result.${key} at result index ${i}`
   1157        );
   1158      }
   1159    }
   1160 
   1161    if (
   1162      actual.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
   1163      actual.source == UrlbarUtils.RESULT_SOURCE.SEARCH &&
   1164      actual.providerName == "UrlbarProviderHeuristicFallback"
   1165    ) {
   1166      expected.payload.icon = SEARCH_GLASS_ICON;
   1167    }
   1168 
   1169    if (actual.payload?.url) {
   1170      try {
   1171        const payloadUrlProtocol = new URL(actual.payload.url).protocol;
   1172        if (
   1173          !UrlbarUtils.PROTOCOLS_WITH_ICONS.includes(payloadUrlProtocol) &&
   1174          actual.source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL
   1175        ) {
   1176          expected.payload.icon = UrlbarUtils.ICON.DEFAULT;
   1177        }
   1178      } catch (e) {
   1179        console.error(e);
   1180      }
   1181    }
   1182 
   1183    if (expected.payload) {
   1184      let expectedKeys = new Set(Object.keys(expected.payload));
   1185      let actualKeys = new Set(Object.keys(actual.payload));
   1186 
   1187      for (let key of actualKeys.union(expectedKeys)) {
   1188        let condition = conditionalPayloadProperties[key];
   1189        if (
   1190          condition?.ignore ||
   1191          (condition?.optional && !expected.hasOwnProperty(key))
   1192        ) {
   1193          continue;
   1194        }
   1195 
   1196        Assert.deepEqual(
   1197          actual.payload[key],
   1198          expected.payload[key],
   1199          `result.payload.${key} at result index ${i}`
   1200        );
   1201      }
   1202    }
   1203  }
   1204 }
   1205 
   1206 /**
   1207 * Returns the frecency of an origin.
   1208 *
   1209 * @param   {string} prefix
   1210 *          The origin's prefix, e.g., "http://".
   1211 * @param   {string} aHost
   1212 *          The origin's host.
   1213 * @returns {number} The origin's frecency.
   1214 */
   1215 async function getOriginFrecency(prefix, aHost) {
   1216  let db = await PlacesUtils.promiseDBConnection();
   1217  let rows = await db.execute(
   1218    `
   1219    SELECT frecency
   1220    FROM moz_origins
   1221    WHERE prefix = :prefix AND host = :host
   1222  `,
   1223    { prefix, host: aHost }
   1224  );
   1225  Assert.equal(rows.length, 1);
   1226  return rows[0].getResultByIndex(0);
   1227 }
   1228 
   1229 /**
   1230 * Returns the origin autofill frecency threshold.
   1231 *
   1232 * @returns {number}
   1233 *          The threshold.
   1234 */
   1235 async function getOriginAutofillThreshold() {
   1236  return PlacesUtils.metadata.get("origin_frecency_threshold", 2.0);
   1237 }
   1238 
   1239 /**
   1240 * Checks that origins appear in a given order in the database.
   1241 *
   1242 * @param {string} host The "fixed" host, without "www."
   1243 * @param {Array} prefixOrder The prefixes (scheme + www.) sorted appropriately.
   1244 */
   1245 async function checkOriginsOrder(host, prefixOrder) {
   1246  await PlacesUtils.withConnectionWrapper("checkOriginsOrder", async db => {
   1247    let prefixes = (
   1248      await db.execute(
   1249        `SELECT prefix || iif(instr(host, "www.") = 1, "www.", "")
   1250         FROM moz_origins
   1251         WHERE host = :host OR host = "www." || :host
   1252         ORDER BY ROWID ASC
   1253        `,
   1254        { host }
   1255      )
   1256    ).map(r => r.getResultByIndex(0));
   1257    Assert.deepEqual(prefixes, prefixOrder);
   1258  });
   1259 }
   1260 
   1261 function daysAgo(days) {
   1262  let date = new Date();
   1263  date.setDate(date.getDate() - days);
   1264  return date;
   1265 }