summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/browser/browser_queryContextCache.js
blob: 758043233defa48bf72c8ba2ada863bf909a11a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

// Tests the view's QueryContextCache. When the view opens and a context is
// cached for the search, the view should *synchronously* open and update.

"use strict";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.sys.mjs",
});

const TEST_URLS = [];
const TEST_URLS_COUNT = 5;
const TOP_SITES_VISIT_COUNT = 5;
const SEARCH_STRING = "example";

// Allow more time for Mac machines so they don't time out in verify mode.
if (AppConstants.platform == "macosx") {
  requestLongerTimeout(3);
}

add_setup(async function () {
  // Clear history and bookmarks to make sure the URLs we add below are truly
  // the top sites. If any existing history or bookmarks were the top sites,
  // which is likely but not guaranteed, one or more "newtab-top-sites-changed"
  // notifications will be sent, potentially interfering with the rest of the
  // test. Waiting for Places updates to finish and then an extra tick should be
  // enough to make sure no more notfications occur.
  await PlacesUtils.history.clear();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesTestUtils.promiseAsyncUpdates();
  await TestUtils.waitForTick();

  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.suggest.quickactions", false]],
  });

  // Add some URLs to populate both history and top sites. Each URL needs to
  // match `SEARCH_STRING`.
  for (let i = 0; i < TEST_URLS_COUNT; i++) {
    let url = `https://${i}.example.com/${SEARCH_STRING}`;
    TEST_URLS.unshift(url);
    // Each URL needs to be added several times to boost its frecency enough to
    // qualify as a top site.
    for (let j = 0; j < TOP_SITES_VISIT_COUNT; j++) {
      await PlacesTestUtils.addVisits(url);
    }
  }
  await updateTopSitesAndAwaitChanged(TEST_URLS_COUNT);

  registerCleanupFunction(async () => {
    await PlacesUtils.history.clear();
  });
});

add_task(async function search() {
  await withNewBrowserWindow(async win => {
    // Do a search and then close the view.
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window: win,
      value: SEARCH_STRING,
    });
    await UrlbarTestUtils.promisePopupClose(win);

    // Open the view. It should open synchronously and the cached search context
    // should be used.
    await openViewAndAssertCached({
      win,
      searchString: SEARCH_STRING,
      cached: true,
    });
  });
});

add_task(async function topSites_simple() {
  await withNewBrowserWindow(async win => {
    // Open the view to show top sites and then close it.
    await openViewAndAssertCached({ win, cached: false });

    // Open the view again. It should open synchronously and the cached
    // top-sites context should be used.
    await openViewAndAssertCached({ win, cached: true });
  });
});

add_task(async function topSites_nonEmptySearch() {
  await withNewBrowserWindow(async win => {
    // Open the view to show top sites and then close it.
    await openViewAndAssertCached({ win, cached: false });

    // Do a search, close the view, and revert the input.
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window: win,
      value: "test",
    });
    await UrlbarTestUtils.promisePopupClose(win);
    win.gURLBar.handleRevert();

    // Open the view. It should open synchronously and the cached top-sites
    // context should be used.
    await openViewAndAssertCached({ win, cached: true });
  });
});

add_task(async function topSites_otherEmptySearch() {
  await withNewBrowserWindow(async win => {
    // Open the view to show top sites and then close it.
    await openViewAndAssertCached({ win, cached: false });

    // Enter search mode with an empty search string (by pressing accel+K),
    // starting a new search. The view should *not* open synchronously and the
    // cached top-sites context should not be used.
    let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
    EventUtils.synthesizeKey("k", { accelKey: true }, win);
    Assert.ok(!win.gURLBar.view.isOpen, "View is not open");
    await searchPromise;
    await UrlbarTestUtils.assertSearchMode(win, {
      engineName: Services.search.defaultEngine.name,
      isGeneralPurposeEngine: true,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      isPreview: false,
      entry: "shortcut",
    });

    // Close the view and revert the input.
    await UrlbarTestUtils.promisePopupClose(win);
    win.gURLBar.handleRevert();
    await UrlbarTestUtils.assertSearchMode(win, null);

    // Open the view. It should open synchronously and the cached top-sites
    // context should be used.
    await openViewAndAssertCached({ win, cached: true });
  });
});

add_task(async function topSites_changed() {
  await withNewBrowserWindow(async win => {
    // Open the view to show top sites and then close it.
    await openViewAndAssertCached({ win, cached: false });

    // Change the top sites by adding visits to a new URL.
    let newURL = "https://changed.example.com/";
    for (let j = 0; j < TOP_SITES_VISIT_COUNT; j++) {
      await PlacesTestUtils.addVisits(newURL);
    }
    await updateTopSitesAndAwaitChanged(TEST_URLS_COUNT + 1);

    // Open the view. It should *not* open synchronously and the cached
    // top-sites context should not be used.
    await openViewAndAssertCached({ win, cached: false });

    // Open the view again. It should open synchronously and the new cached
    // top-sites context with the new URL should be used.
    await openViewAndAssertCached({
      win,
      cached: true,
      urls: [newURL, ...TEST_URLS],
      // The new URL is sometimes at the end of the list of top sites instead of
      // the start, so ignore the order of the results.
      ignoreOrder: true,
    });

    // Remove the new URL. The top sites will update themselves automatically,
    // so we only need to wait for newtab-top-sites-changed.
    info("Removing new URL and awaiting newtab-top-sites-changed");
    let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed");
    await PlacesUtils.history.remove([newURL]);
    await changedPromise;

    // Open the view. It should *not* open synchronously and the cached
    // top-sites context should not be used.
    await openViewAndAssertCached({ win, cached: false });

    // Open the view again. It should open synchronously and the new cached
    // top-sites context with the new URL should be used.
    await openViewAndAssertCached({ win, cached: true });
  });
});

add_task(async function topSites_nonTopSitesResults() {
  await withNewBrowserWindow(async win => {
    // Open the view to show top sites and then close it.
    await openViewAndAssertCached({ win, cached: false });

    // Add a provider that returns a result with a suggested index of zero so
    // that the first result in the view is not from the top-sites provider.
    let suggestedIndexURL = "https://example.com/suggested-index-0";
    let provider = new UrlbarTestUtils.TestProvider({
      priority: lazy.UrlbarProviderTopSites.PRIORITY,
      results: [
        Object.assign(
          new UrlbarResult(
            UrlbarUtils.RESULT_TYPE.URL,
            UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
            {
              url: suggestedIndexURL,
            }
          ),
          { suggestedIndex: 0 }
        ),
      ],
    });
    UrlbarProvidersManager.registerProvider(provider);

    // Open the view. It should open synchronously and the cached top-sites
    // context should be used. The suggested-index result should not be
    // immediately present in the view since it's not in the cached context.
    await openViewAndAssertCached({ win, cached: true, keepOpen: true });

    // After the search has finished, the suggested-index result should be in
    // the first row. The search's context should become the newly cached
    // top-sites context and it should include the suggested-index result.
    Assert.equal(
      UrlbarTestUtils.getResultCount(win),
      TEST_URLS.length + 1,
      "Should be one more result after search finishes"
    );
    let details = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
    Assert.equal(
      details.url,
      suggestedIndexURL,
      "First result after search finishes should be the suggested index result"
    );

    // At this point, the search's context should have become the newly cached
    // top-sites context and it should include the suggested-index result.

    await UrlbarTestUtils.promisePopupClose(win);

    // Open the view again. It should open synchronously and the new cached
    // top-sites context with the suggested-index URL should be used.
    await openViewAndAssertCached({
      win,
      cached: true,
      urls: [suggestedIndexURL, ...TEST_URLS],
    });

    UrlbarProvidersManager.unregisterProvider(provider);
  });
});

add_task(async function topSites_disabled_1() {
  await withNewBrowserWindow(async win => {
    // Open the view to show top sites and then close it.
    await openViewAndAssertCached({ win, cached: false });

    // Disable `browser.urlbar.suggest.topsites`.
    UrlbarPrefs.set("suggest.topsites", false);

    // Open the view. It should *not* open synchronously and the cached
    // top-sites context should not be used.
    await openViewAndAssertCached({
      win,
      cached: false,
      cachedAfterOpen: false,
    });

    // Clear the pref, open the view to show top sites, and close it.
    UrlbarPrefs.clear("suggest.topsites");
    await openViewAndAssertCached({ win, cached: false });

    // Open the view. It should open synchronously and the cached top-sites
    // context should be used.
    await openViewAndAssertCached({ win, cached: true });
  });
});

add_task(async function topSites_disabled_2() {
  await withNewBrowserWindow(async win => {
    // Open the view to show top sites and then close it.
    await openViewAndAssertCached({ win, cached: false });

    // Disable `browser.newtabpage.activity-stream.feeds.system.topsites`.
    Services.prefs.setBoolPref(
      "browser.newtabpage.activity-stream.feeds.system.topsites",
      false
    );

    // Open the view. It should *not* open synchronously and the cached
    // top-sites context should not be used.
    await openViewAndAssertCached({
      win,
      cached: false,
      cachedAfterOpen: false,
    });

    // Clear the pref, open the view to show top sites, and close it.
    Services.prefs.clearUserPref(
      "browser.newtabpage.activity-stream.feeds.system.topsites"
    );
    await openViewAndAssertCached({ win, cached: false });

    // Open the view. It should open synchronously and the cached top-sites
    // context should be used.
    await openViewAndAssertCached({ win, cached: true });
  });
});

add_task(async function evict() {
  await withNewBrowserWindow(async win => {
    let cache = win.gURLBar.view.queryContextCache;
    Assert.equal(
      typeof cache.size,
      "number",
      "Sanity check: queryContextCache.size is a number"
    );

    // Open the view to show top sites and then close it.
    await openViewAndAssertCached({ win, cached: false });

    // Do `cache.size` + 1 searches.
    for (let i = 0; i < cache.size + 1; i++) {
      let searchString = "test" + i;
      await UrlbarTestUtils.promiseAutocompleteResultPopup({
        window: win,
        value: searchString,
      });
      await UrlbarTestUtils.promisePopupClose(win);
      Assert.ok(
        cache.get(searchString),
        "Cache includes search string: " + searchString
      );
    }

    // The first search string should have been evicted from the cache, but the
    // one after that should still be cached.
    Assert.ok(!cache.get("test0"), "test0 has been evicted from the cache");
    Assert.ok(cache.get("test1"), "Cache includes test1");

    // Revert the input and open the view to show the top sites. It should open
    // synchronously and the cached top-sites context should be used.
    win.gURLBar.handleRevert();
    Assert.equal(win.gURLBar.value, "", "Input is empty after reverting");
    await openViewAndAssertCached({ win, cached: true });
  });
});

/**
 * Opens the view and checks that it is or is not synchronously opened and
 * populated as specified.
 *
 * @param {object} options
 *   Options object.
 * @param {window} options.win
 *   The window to open the view in.
 * @param {boolean} options.cached
 *   Whether a query context is expected to already be cached for the search
 *   that's performed when the view opens. If true, then the view should
 *   synchronously open and populate using the cached context. If false, then
 *   the view should asynchronously open once the first results are fetched.
 * @param {boolean} [options.cachedAfterOpen]
 *   Whether the context is expected to be cached after the view opens and the
 *   query finishes.
 * @param {string} [options.searchString]
 *   The search string for which the context should or should not be cached. If
 *   falsey, then the relevant context is assumed to be the top-sites context.
 * @param {Array} [options.urls]
 *   Array of URLs that are expected to be shown in the view.
 * @param {boolean} [options.ignoreOrder]
 *   Whether to treat `urls` as an unordered set instead of an array. When true,
 *   the order of results is ignored.
 * @param {boolean} [options.keepOpen]
 *   Whether to keep the view open when the function returns.
 */
async function openViewAndAssertCached({
  win,
  cached,
  cachedAfterOpen = true,
  searchString = "",
  urls = TEST_URLS,
  ignoreOrder = false,
  keepOpen = false,
}) {
  let cache = win.gURLBar.view.queryContextCache;
  let getContext = () =>
    searchString ? cache.get(searchString) : cache.topSitesContext;

  Assert.equal(
    !!getContext(),
    cached,
    "Context is present or not in cache as expected for search string: " +
      JSON.stringify(searchString)
  );

  // Open the view by performing the accel+L command.
  await SimpleTest.promiseFocus(win);
  win.document.getElementById("Browser:OpenLocation").doCommand();

  Assert.equal(
    win.gURLBar.view.isOpen,
    cached,
    "View is open or not as expected"
  );

  if (!cached && cachedAfterOpen) {
    // Wait for the search to finish and the context to be cached since callers
    // generally expect it.
    await TestUtils.waitForCondition(
      getContext,
      "Waiting for context to be cached for search string: " +
        JSON.stringify(searchString)
    );
  } else if (cached) {
    // The view is expected to open synchronously. Check the results. We don't
    // do this in the `!cached` case, when the view is expected to open
    // asynchronously, because there are plenty of other tests for that. Here we
    // want to make sure results are correct before the new search finishes in
    // order to avoid any flicker.
    let startIndex = 0;
    let resultCount = urls.length;
    if (searchString) {
      // Plus heuristic
      startIndex++;
      resultCount++;
    }

    // In all the checks below, check the rows container directly instead of
    // relying on `UrlbarTestUtils` functions that wait for the search to
    // finish. Here we're specifically checking cached results that should be
    // used before the search finishes.
    let rows = UrlbarTestUtils.getResultsContainer(win).children;
    Assert.equal(rows.length, resultCount, "View has expected row count");

    // Check the search heuristic row.
    if (searchString) {
      let result = rows[0].result;
      Assert.ok(result.heuristic, "First row should be a heuristic");
      Assert.equal(
        result.payload.query,
        searchString,
        "First row's query should be the search string"
      );
    }

    // Check the URL rows.
    let actualURLs = [];
    let urlRows = Array.from(rows).slice(startIndex);
    for (let row of urlRows) {
      actualURLs.push(row.result.payload.url);
    }
    if (ignoreOrder) {
      urls.sort();
      actualURLs.sort();
    }
    Assert.deepEqual(actualURLs, urls, "View should contain the expected URLs");
  }

  // Now wait for the search to finish before returning. We await
  // `lastQueryContextPromise` instead of the promise returned from
  // `UrlbarTestUtils.promiseSearchComplete()` because the latter assumes the
  // view will open, which isn't the case for every task here.
  await win.gURLBar.lastQueryContextPromise;
  if (!keepOpen) {
    await UrlbarTestUtils.promisePopupClose(win);
  }
}

/**
 * Updates the top sites and waits for the "newtab-top-sites-changed"
 * notification. Note that this notification is not sent if the sites don't
 * actually change. In that case, use only `updateTopSites()` instead.
 *
 * @param {number} expectedCount
 *   The new expected number of top sites.
 */
async function updateTopSitesAndAwaitChanged(expectedCount) {
  info("Updating top sites and awaiting newtab-top-sites-changed");
  let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then(
    () => info("Observed newtab-top-sites-changed")
  );
  await updateTopSites(sites => sites?.length == expectedCount);
  await changedPromise;
}

async function withNewBrowserWindow(callback) {
  let win = await BrowserTestUtils.openNewBrowserWindow();
  await callback(win);
  await BrowserTestUtils.closeWindow(win);
}