tor-browser

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

head.js (18903B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 // The files in this directory test UrlbarView.#updateResults().
      6 
      7 "use strict";
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
     11  UrlbarProvidersManager:
     12    "moz-src:///browser/components/urlbar/UrlbarProvidersManager.sys.mjs",
     13  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     14  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     15  UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs",
     16 });
     17 
     18 ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
     19  const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
     20    "resource://testing-common/UrlbarTestUtils.sys.mjs"
     21  );
     22  module.init(this);
     23  return module;
     24 });
     25 
     26 // How long to wait for view-update mutations to settle (i.e., to finish
     27 // happening) before assuming they're done and moving on with the test.
     28 const MUTATION_SETTLE_TIME_MS = 500;
     29 
     30 const MAX_RESULTS = 10;
     31 
     32 add_setup(async function headInit() {
     33  await PlacesUtils.history.clear();
     34  await PlacesUtils.bookmarks.eraseEverything();
     35 
     36  await SpecialPowers.pushPrefEnv({
     37    set: [
     38      // Make absolutely sure the panel stays open during the test. There are
     39      // spurious blurs on WebRender TV tests as the test starts that cause the
     40      // panel to close and the query to be canceled, resulting in intermittent
     41      // failures without this.
     42      ["ui.popup.disable_autohide", true],
     43 
     44      // Make sure maxRichResults is 10 for sanity.
     45      ["browser.urlbar.maxRichResults", MAX_RESULTS],
     46    ],
     47  });
     48 
     49  // Increase the timeout of the remove-stale-rows timer so that it doesn't
     50  // interfere with the tests.
     51  let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout;
     52  UrlbarView.removeStaleRowsTimeout = 30000;
     53  registerCleanupFunction(() => {
     54    UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
     55  });
     56 });
     57 
     58 /**
     59 * A test provider that doesn't finish startQuery() until `finishQueryPromise`
     60 * is resolved.
     61 */
     62 class DelayingTestProvider extends UrlbarTestUtils.TestProvider {
     63  finishQueryPromise = null;
     64  async startQuery(context, addCallback) {
     65    for (let result of this.results) {
     66      addCallback(this, result);
     67    }
     68    await this.finishQueryPromise;
     69  }
     70 }
     71 
     72 /**
     73 * Makes a result with a suggested index.
     74 *
     75 * @param {number} suggestedIndex
     76 *   The preferred index of the result.
     77 * @param {number} resultSpan
     78 *   The result will have this span.
     79 * @returns {UrlbarResult}
     80 */
     81 function makeSuggestedIndexResult(suggestedIndex, resultSpan = 1) {
     82  return new UrlbarResult({
     83    type: UrlbarUtils.RESULT_TYPE.URL,
     84    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
     85    suggestedIndex,
     86    resultSpan,
     87    payload: {
     88      url: "http://example.com/si",
     89      title: "suggested index",
     90      helpUrl: "http://example.com/",
     91      isBlockable: true,
     92      blockL10n: { id: "urlbar-result-menu-remove-from-history" },
     93    },
     94  });
     95 }
     96 
     97 /**
     98 * Makes an array of results for the suggestedIndex tests. The array will
     99 * include a heuristic followed by the specified results.
    100 *
    101 * @param {object} options
    102 *   The options object
    103 * @param {number} [options.count]
    104 *   The number of results to return other than the heuristic. This and
    105 *   `type` must be given together.
    106 * @param {UrlbarUtils.RESULT_TYPE} [options.type]
    107 *   The type of results to return other than the heuristic. This and `count`
    108 *   must be given together.
    109 * @param {Array} [options.specs]
    110 *   If you want a mix of result types instead of only one type, then use this
    111 *   param instead of `count` and `type`. Each item in this array must be an
    112 *   object with the following properties:
    113 *   {number} count
    114 *     The number of results to return for the given `type`.
    115 *   {UrlbarUtils.RESULT_TYPE} type
    116 *     The type of results.
    117 * @returns {Array}
    118 *   An array of results.
    119 */
    120 function makeProviderResults({ count = 0, type = undefined, specs = [] }) {
    121  if (count) {
    122    specs.push({ count, type });
    123  }
    124 
    125  let query = "test";
    126  let results = [
    127    new UrlbarResult({
    128      type: UrlbarUtils.RESULT_TYPE.SEARCH,
    129      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    130      heuristic: true,
    131      payload: {
    132        query,
    133        engine: Services.search.defaultEngine.name,
    134      },
    135    }),
    136  ];
    137 
    138  for (let { count: specCount, type: specType } of specs) {
    139    for (let i = 0; i < specCount; i++) {
    140      let str = `${query} ${results.length}`;
    141      switch (specType) {
    142        case UrlbarUtils.RESULT_TYPE.SEARCH:
    143          results.push(
    144            new UrlbarResult({
    145              type: UrlbarUtils.RESULT_TYPE.SEARCH,
    146              source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    147              payload: {
    148                query,
    149                suggestion: str,
    150                lowerCaseSuggestion: str.toLowerCase(),
    151                engine: Services.search.defaultEngine.name,
    152              },
    153            })
    154          );
    155          break;
    156        case UrlbarUtils.RESULT_TYPE.URL:
    157          results.push(
    158            new UrlbarResult({
    159              type: UrlbarUtils.RESULT_TYPE.URL,
    160              source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    161              payload: {
    162                url: "http://example.com/" + i,
    163                title: str,
    164                helpUrl: "http://example.com/",
    165                isBlockable: true,
    166                blockL10n: { id: "urlbar-result-menu-remove-from-history" },
    167              },
    168            })
    169          );
    170          break;
    171        default:
    172          throw new Error(`Unsupported makeProviderResults type: ${specType}`);
    173      }
    174    }
    175  }
    176 
    177  return results;
    178 }
    179 
    180 let gSuggestedIndexTaskIndex = 0;
    181 
    182 /**
    183 * Adds a suggestedIndex test task. See doSuggestedIndexTest() for params.
    184 *
    185 * @param {object} options
    186 *   See doSuggestedIndexTest().
    187 */
    188 function add_suggestedIndex_task(options) {
    189  if (!gSuggestedIndexTaskIndex) {
    190    initSuggestedIndexTest();
    191  }
    192  let testIndex = gSuggestedIndexTaskIndex++;
    193  let testName = "test_" + testIndex;
    194  let testDesc = JSON.stringify(options);
    195  let func = async () => {
    196    info(`Running task at index ${testIndex}: ${testDesc}`);
    197    await doSuggestedIndexTest(options);
    198  };
    199  Object.defineProperty(func, "name", { value: testName });
    200  add_task(func);
    201 }
    202 
    203 /**
    204 * Initializes suggestedIndex tests. You don't normally need to call this from
    205 * your test because add_suggestedIndex_task() calls it automatically.
    206 */
    207 function initSuggestedIndexTest() {
    208  // These tests can time out on Mac TV WebRender just because they do so much,
    209  // so request a longer timeout.
    210  if (AppConstants.platform == "macosx") {
    211    requestLongerTimeout(3);
    212  }
    213  registerCleanupFunction(() => {
    214    gSuggestedIndexTaskIndex = 0;
    215  });
    216 }
    217 
    218 /**
    219 * @typedef {object} SuggestedIndexTestOptions
    220 * @property {number} [otherCount]
    221 *   The number of results other than the heuristic and suggestedIndex results
    222 *   that the provider should return for search 1. This and `otherType` must be
    223 *   given together.
    224 * @property {UrlbarUtils.RESULT_TYPE} [otherType]
    225 *   The type of results other than the heuristic and suggestedIndex results
    226 *   that the provider should return for search 1. This and `otherCount` must be
    227 *   given together.
    228 * @property {Array} [other]
    229 *   If you want the provider to return a mix of result types instead of only
    230 *   one type, then use this param instead of `otherCount` and `otherType`. Each
    231 *   item in this array must be an object with the following properties:
    232 *   {number} count
    233 *     The number of results to return for the given `type`.
    234 *   {UrlbarUtils.RESULT_TYPE} type
    235 *     The type of results.
    236 * @property {number} viewCount
    237 *   The total number of results expected in the view after search 1 finishes,
    238 *   including the heuristic and suggestedIndex results.
    239 * @param {number} [suggestedIndex]
    240 *   If given, the provider will return a result with this suggested index for
    241 *   search 1.
    242 * @property {number} [resultSpan]
    243 *   If this and `search1.suggestedIndex` are given, then the suggestedIndex
    244 *   result for search 1 will have this resultSpan.
    245 * @property {Array} [suggestedIndexes]
    246 *   If you want the provider to return more than one suggestedIndex result for
    247 *   search 1, then use this instead of `search1.suggestedIndex`. Each item in
    248 *   this array must be one of the following:
    249 *     suggestedIndex value
    250 *     [suggestedIndex, resultSpan] tuple
    251 */
    252 
    253 /**
    254 * Runs a suggestedIndex test. Performs two searches and checks the results just
    255 * after the view update and after the second search finishes. The caller is
    256 * responsible for passing in a description of what the rows should look like
    257 * just after the view update finishes but before the second search finishes,
    258 * i.e., before stale rows are removed and hidden rows are shown -- this is the
    259 * `duringUpdate` param. The important thing this checks is that the rows with
    260 * suggested indexes don't move around or appear in the wrong places.
    261 *
    262 * @param {object} options
    263 *   The options object
    264 * @param {SuggestedIndexTestOptions} options.search1
    265 *   The first search options object
    266 * @param {SuggestedIndexTestOptions} options.search2
    267 *   This object has the same properties as the `search1` object but it applies
    268 *   to the second search.
    269 * @param {Array<{ count: number, type: UrlbarUtils.RESULT_TYPE, suggestedIndex: ?number, stale: ?boolean, hidden: ?boolean }>} options.duringUpdate
    270 *   An array of expected row states during the view update. Each item in the
    271 *   array must be an object with the following properties:
    272 *   {number} count
    273 *     The number of rows in the view to which this row state object applies.
    274 *   {UrlbarUtils.RESULT_TYPE} type
    275 *     The expected type of the rows.
    276 *   {number} [suggestedIndex]
    277 *     The expected suggestedIndex of the row.
    278 *   {boolean} [stale]
    279 *     Whether the rows are expected to be stale. Defaults to false.
    280 *   {boolean} [hidden]
    281 *     Whether the rows are expected to be hidden. Defaults to false.
    282 */
    283 async function doSuggestedIndexTest({ search1, search2, duringUpdate }) {
    284  // We use this test provider to test specific results. It has an Infinity
    285  // priority so that it provides all results in our test searches, including
    286  // the heuristic. That lets us avoid any potential races with the built-in
    287  // providers; testing them is not important here.
    288  let provider = new DelayingTestProvider({ priority: Infinity });
    289  UrlbarProvidersManager.registerProvider(provider);
    290  registerCleanupFunction(() => {
    291    UrlbarProvidersManager.unregisterProvider(provider);
    292  });
    293 
    294  // Set up the first search. First, add the non-suggestedIndex results to the
    295  // provider.
    296  provider.results = makeProviderResults({
    297    specs: search1.other,
    298    count: search1.otherCount,
    299    type: search1.otherType,
    300  });
    301 
    302  // Set up `suggestedIndexes`. It's an array with [suggestedIndex, resultSpan]
    303  // tuples.
    304  if (!search1.suggestedIndexes) {
    305    search1.suggestedIndexes = [];
    306  }
    307  search1.suggestedIndexes = search1.suggestedIndexes.map(value =>
    308    typeof value == "number" ? [value, 1] : value
    309  );
    310  if (typeof search1.suggestedIndex == "number") {
    311    search1.suggestedIndexes.push([
    312      search1.suggestedIndex,
    313      search1.resultSpan || 1,
    314    ]);
    315  }
    316 
    317  // Add the suggestedIndex results to the provider.
    318  for (let [suggestedIndex, resultSpan] of search1.suggestedIndexes) {
    319    provider.results.push(makeSuggestedIndexResult(suggestedIndex, resultSpan));
    320  }
    321 
    322  // Do the first search.
    323  provider.finishQueryPromise = Promise.resolve();
    324  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    325    window,
    326    value: "test",
    327  });
    328 
    329  // Sanity check the results.
    330  Assert.equal(
    331    UrlbarTestUtils.getResultCount(window),
    332    search1.viewCount,
    333    "Row count after first search"
    334  );
    335  for (let [suggestedIndex, resultSpan] of search1.suggestedIndexes) {
    336    let index =
    337      suggestedIndex >= 0
    338        ? Math.min(search1.viewCount - 1, suggestedIndex)
    339        : Math.max(0, search1.viewCount + suggestedIndex);
    340    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
    341    Assert.equal(
    342      result.element.row.result.suggestedIndex,
    343      suggestedIndex,
    344      "suggestedIndex after first search"
    345    );
    346    Assert.equal(
    347      UrlbarUtils.getSpanForResult(result.element.row.result),
    348      resultSpan,
    349      "resultSpan after first search"
    350    );
    351  }
    352 
    353  // Set up the second search. First, add the non-suggestedIndex results to the
    354  // provider.
    355  provider.results = makeProviderResults({
    356    specs: search2.other,
    357    count: search2.otherCount,
    358    type: search2.otherType,
    359  });
    360 
    361  // Set up `suggestedIndexes`. It's an array with [suggestedIndex, resultSpan]
    362  // tuples.
    363  if (!search2.suggestedIndexes) {
    364    search2.suggestedIndexes = [];
    365  }
    366  search2.suggestedIndexes = search2.suggestedIndexes.map(value =>
    367    typeof value == "number" ? [value, 1] : value
    368  );
    369  if (typeof search2.suggestedIndex == "number") {
    370    search2.suggestedIndexes.push([
    371      search2.suggestedIndex,
    372      search2.resultSpan || 1,
    373    ]);
    374  }
    375 
    376  // Add the suggestedIndex results to the provider.
    377  for (let [suggestedIndex, resultSpan] of search2.suggestedIndexes) {
    378    provider.results.push(makeSuggestedIndexResult(suggestedIndex, resultSpan));
    379  }
    380 
    381  let rowCountDuringUpdate = duringUpdate.reduce(
    382    (count, rowState) => count + rowState.count,
    383    0
    384  );
    385 
    386  // Don't allow the search to finish until we check the updated rows. We'll
    387  // accomplish that by adding a mutation observer to observe completion of the
    388  // update and delaying resolving the provider's finishQueryPromise.
    389  //
    390  // This promise works like this: We add a mutation observer that observes the
    391  // view's entire subtree. Every time we observe a mutation, we set
    392  // `lastMutationTime` to the current time. Meanwhile, we run an interval that
    393  // compares `now` to `lastMutationTime` every time it fires. When the
    394  // difference between `now` and `lastMutationTime` is sufficiently large, we
    395  // assume the view update is done, and we resolve the promise.
    396  let mutationPromise = new Promise(resolve => {
    397    let lastMutationTime = ChromeUtils.now();
    398    let observer = new MutationObserver(() => {
    399      info("Observed mutation");
    400      lastMutationTime = ChromeUtils.now();
    401    });
    402    observer.observe(UrlbarTestUtils.getResultsContainer(window), {
    403      attributes: true,
    404      characterData: true,
    405      childList: true,
    406      subtree: true,
    407    });
    408 
    409    let interval = setInterval(
    410      () => {
    411        if (MUTATION_SETTLE_TIME_MS < ChromeUtils.now() - lastMutationTime) {
    412          info("No further mutations observed, stopping");
    413          clearInterval(interval);
    414          observer.disconnect();
    415          resolve();
    416        }
    417      },
    418      Math.ceil(MUTATION_SETTLE_TIME_MS / 10)
    419    );
    420  });
    421 
    422  // Now do the second search but don't wait for it to finish.
    423  let resolveQuery;
    424  provider.finishQueryPromise = new Promise(
    425    resolve => (resolveQuery = resolve)
    426  );
    427  let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({
    428    window,
    429    value: "test",
    430  });
    431 
    432  // Wait for the update to finish.
    433  info("Waiting for mutations to settle");
    434  await mutationPromise;
    435 
    436  // Check the rows. We can't use UrlbarTestUtils.getDetailsOfResultAt() here
    437  // because it waits for the search to finish.
    438  Assert.equal(
    439    UrlbarTestUtils.getResultCount(window),
    440    rowCountDuringUpdate,
    441    "Row count during update"
    442  );
    443  let rows = UrlbarTestUtils.getResultsContainer(window).children;
    444  let rowIndex = 0;
    445  for (let rowState of duringUpdate) {
    446    for (let i = 0; i < rowState.count; i++) {
    447      let row = rows[rowIndex];
    448 
    449      // type
    450      if ("type" in rowState) {
    451        Assert.equal(
    452          row.result.type,
    453          rowState.type,
    454          `Type at index ${rowIndex} during update`
    455        );
    456      }
    457 
    458      // suggestedIndex
    459      if ("suggestedIndex" in rowState) {
    460        Assert.ok(
    461          row.result.hasSuggestedIndex,
    462          `Row at index ${rowIndex} has suggestedIndex during update`
    463        );
    464        Assert.equal(
    465          row.result.suggestedIndex,
    466          rowState.suggestedIndex,
    467          `suggestedIndex at index ${rowIndex} during update`
    468        );
    469      } else {
    470        Assert.ok(
    471          !row.result.hasSuggestedIndex,
    472          `Row at index ${rowIndex} does not have suggestedIndex during update`
    473        );
    474      }
    475 
    476      // resultSpan
    477      Assert.equal(
    478        UrlbarUtils.getSpanForResult(row.result),
    479        rowState.resultSpan || 1,
    480        `resultSpan at index ${rowIndex} during update`
    481      );
    482 
    483      // stale
    484      if (rowState.stale) {
    485        Assert.equal(
    486          row.getAttribute("stale"),
    487          "true",
    488          `Row at index ${rowIndex} is stale during update`
    489        );
    490      } else {
    491        Assert.ok(
    492          !row.hasAttribute("stale"),
    493          `Row at index ${rowIndex} is not stale during update`
    494        );
    495      }
    496 
    497      // visible
    498      Assert.equal(
    499        BrowserTestUtils.isVisible(row),
    500        !rowState.hidden,
    501        `Visible at index ${rowIndex} during update`
    502      );
    503 
    504      rowIndex++;
    505    }
    506  }
    507 
    508  // Finish the search.
    509  resolveQuery();
    510  await queryPromise;
    511 
    512  // Check the rows now that the second search is done. First, build a map from
    513  // real indexes to suggested index. e.g., if a suggestedIndex = -1, then the
    514  // real index = the result count - 1.
    515  let suggestedIndexesByRealIndex = new Map();
    516  for (let [suggestedIndex, resultSpan] of search2.suggestedIndexes) {
    517    let realIndex =
    518      suggestedIndex >= 0
    519        ? Math.min(suggestedIndex, search2.viewCount - 1)
    520        : Math.max(0, search2.viewCount + suggestedIndex);
    521    suggestedIndexesByRealIndex.set(realIndex, [suggestedIndex, resultSpan]);
    522  }
    523 
    524  Assert.equal(
    525    UrlbarTestUtils.getResultCount(window),
    526    search2.viewCount,
    527    "Row count after update"
    528  );
    529  for (let i = 0; i < search2.viewCount; i++) {
    530    let result = rows[i].result;
    531    let tuple = suggestedIndexesByRealIndex.get(i);
    532    if (tuple) {
    533      let [suggestedIndex, resultSpan] = tuple;
    534      Assert.ok(
    535        result.hasSuggestedIndex,
    536        `Row at index ${i} has suggestedIndex after update`
    537      );
    538      Assert.equal(
    539        result.suggestedIndex,
    540        suggestedIndex,
    541        `suggestedIndex at index ${i} after update`
    542      );
    543      Assert.equal(
    544        UrlbarUtils.getSpanForResult(result),
    545        resultSpan,
    546        `resultSpan at index ${i} after update`
    547      );
    548    } else {
    549      Assert.ok(
    550        !result.hasSuggestedIndex,
    551        `Row at index ${i} does not have suggestedIndex after update`
    552      );
    553    }
    554  }
    555 
    556  await UrlbarTestUtils.promisePopupClose(window);
    557  gURLBar.handleRevert();
    558  UrlbarProvidersManager.unregisterProvider(provider);
    559 }