Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
// 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, {
});
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.
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;
let cachedContext = getContext();
Assert.equal(
!!cachedContext,
cached,
"Context is present or not in cache as expected for search string: " +
JSON.stringify(searchString)
);
// Our payload schema validator allows for explicit undefined properties,
// thus we must transform them for stringify.
Assert.deepEqual(
cachedContext,
JSON.parse(JSON.stringify(cachedContext, (k, v) => v ?? null)),
"The query context should be made of serializable properties"
);
// 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);
}