mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 911307 - Reflect changes to top sites immediately in about:newtab (part 2, front-end patch). r=ttaubert
This commit is contained in:
parent
fd1bdd522f
commit
1d7cde8400
@ -65,10 +65,13 @@ let gPage = {
|
||||
|
||||
/**
|
||||
* Updates the whole page and the grid when the storage has changed.
|
||||
* @param aOnlyIfHidden If true, the page is updated only if it's hidden in
|
||||
* the preloader.
|
||||
*/
|
||||
update: function Page_update() {
|
||||
update: function Page_update(aOnlyIfHidden=false) {
|
||||
let skipUpdate = aOnlyIfHidden && this.allowBackgroundCaptures;
|
||||
// The grid might not be ready yet as we initialize it asynchronously.
|
||||
if (gGrid.ready) {
|
||||
if (gGrid.ready && !skipUpdate) {
|
||||
gGrid.refresh();
|
||||
}
|
||||
},
|
||||
|
@ -24,3 +24,4 @@ skip-if = os == "mac" # Intermittent failures, bug 898317
|
||||
[browser_newtab_tabsync.js]
|
||||
[browser_newtab_undo.js]
|
||||
[browser_newtab_unpin.js]
|
||||
[browser_newtab_update.js]
|
||||
|
52
browser/base/content/test/newtab/browser_newtab_update.js
Normal file
52
browser/base/content/test/newtab/browser_newtab_update.js
Normal file
@ -0,0 +1,52 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Checks that newtab is updated as its links change.
|
||||
*/
|
||||
|
||||
function runTests() {
|
||||
if (NewTabUtils.allPages.updateScheduledForHiddenPages) {
|
||||
// Wait for dynamic updates triggered by the previous test to finish.
|
||||
yield whenPagesUpdated(null, true);
|
||||
}
|
||||
|
||||
// First, start with an empty page. setLinks will trigger a hidden page
|
||||
// update because it calls clearHistory. We need to wait for that update to
|
||||
// happen so that the next time we wait for a page update below, we catch the
|
||||
// right update and not the one triggered by setLinks.
|
||||
//
|
||||
// Why this weird way of yielding? First, these two functions don't return
|
||||
// promises, they call TestRunner.next when done. Second, the point at which
|
||||
// setLinks is done is independent of when the page update will happen, so
|
||||
// calling whenPagesUpdated cannot wait until that time.
|
||||
setLinks([]);
|
||||
whenPagesUpdated(null, true);
|
||||
yield null;
|
||||
yield null;
|
||||
|
||||
// Strategy: Add some visits, open a new page, check the grid, repeat.
|
||||
fillHistory([link(1)]);
|
||||
yield whenPagesUpdated(null, true);
|
||||
yield addNewTabPageTab();
|
||||
checkGrid("1,,,,,,,,");
|
||||
|
||||
fillHistory([link(2)]);
|
||||
yield whenPagesUpdated(null, true);
|
||||
yield addNewTabPageTab();
|
||||
checkGrid("2,1,,,,,,,");
|
||||
|
||||
fillHistory([link(1)]);
|
||||
yield whenPagesUpdated(null, true);
|
||||
yield addNewTabPageTab();
|
||||
checkGrid("1,2,,,,,,,");
|
||||
|
||||
fillHistory([link(2), link(3), link(4)]);
|
||||
yield whenPagesUpdated(null, true);
|
||||
yield addNewTabPageTab();
|
||||
checkGrid("2,1,3,4,,,,,");
|
||||
}
|
||||
|
||||
function link(id) {
|
||||
return { url: "http://example.com/#" + id, title: "site#" + id };
|
||||
}
|
@ -159,20 +159,34 @@ function clearHistory(aCallback) {
|
||||
|
||||
function fillHistory(aLinks, aCallback) {
|
||||
let numLinks = aLinks.length;
|
||||
if (!numLinks) {
|
||||
if (aCallback)
|
||||
executeSoon(aCallback);
|
||||
return;
|
||||
}
|
||||
|
||||
let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK;
|
||||
|
||||
for (let link of aLinks.reverse()) {
|
||||
// Important: To avoid test failures due to clock jitter on Windows XP, call
|
||||
// Date.now() once here, not each time through the loop.
|
||||
let now = Date.now() * 1000;
|
||||
|
||||
for (let i = 0; i < aLinks.length; i++) {
|
||||
let link = aLinks[i];
|
||||
let place = {
|
||||
uri: makeURI(link.url),
|
||||
title: link.title,
|
||||
visits: [{visitDate: Date.now() * 1000, transitionType: transitionLink}]
|
||||
// Links are secondarily sorted by visit date descending, so decrease the
|
||||
// visit date as we progress through the array so that links appear in the
|
||||
// grid in the order they're present in the array.
|
||||
visits: [{visitDate: now - i, transitionType: transitionLink}]
|
||||
};
|
||||
|
||||
PlacesUtils.asyncHistory.updatePlaces(place, {
|
||||
handleError: function () ok(false, "couldn't add visit to history"),
|
||||
handleResult: function () {},
|
||||
handleCompletion: function () {
|
||||
if (--numLinks == 0)
|
||||
if (--numLinks == 0 && aCallback)
|
||||
aCallback();
|
||||
}
|
||||
});
|
||||
@ -503,12 +517,18 @@ function createDragEvent(aEventType, aData) {
|
||||
|
||||
/**
|
||||
* Resumes testing when all pages have been updated.
|
||||
* @param aCallback Called when done. If not specified, TestRunner.next is used.
|
||||
* @param aOnlyIfHidden If true, this resumes testing only when an update that
|
||||
* applies to pre-loaded, hidden pages is observed. If
|
||||
* false, this resumes testing when any update is observed.
|
||||
*/
|
||||
function whenPagesUpdated(aCallback) {
|
||||
function whenPagesUpdated(aCallback, aOnlyIfHidden=false) {
|
||||
let page = {
|
||||
update: function () {
|
||||
NewTabUtils.allPages.unregister(this);
|
||||
executeSoon(aCallback || TestRunner.next);
|
||||
update: function (onlyIfHidden=false) {
|
||||
if (onlyIfHidden == aOnlyIfHidden) {
|
||||
NewTabUtils.allPages.unregister(this);
|
||||
executeSoon(aCallback || TestRunner.next);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
74
toolkit/modules/BinarySearch.jsm
Normal file
74
toolkit/modules/BinarySearch.jsm
Normal file
@ -0,0 +1,74 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"BinarySearch",
|
||||
];
|
||||
|
||||
this.BinarySearch = Object.freeze({
|
||||
|
||||
/**
|
||||
* Returns the index of the given target in the given array or -1 if the
|
||||
* target is not found.
|
||||
*
|
||||
* See search() for a description of this function's parameters.
|
||||
*
|
||||
* @return The index of `target` in `array` or -1 if `target` is not found.
|
||||
*/
|
||||
indexOf: function (array, target, comparator) {
|
||||
let [found, idx] = this.search(array, target, comparator);
|
||||
return found ? idx : -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the index within the given array where the given target may be
|
||||
* inserted to keep the array ordered.
|
||||
*
|
||||
* See search() for a description of this function's parameters.
|
||||
*
|
||||
* @return The index in `array` where `target` may be inserted to keep `array`
|
||||
* ordered.
|
||||
*/
|
||||
insertionIndexOf: function (array, target, comparator) {
|
||||
return this.search(array, target, comparator)[1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Searches for the given target in the given array.
|
||||
*
|
||||
* @param array
|
||||
* An array whose elements are ordered by `comparator`.
|
||||
* @param target
|
||||
* The value to search for.
|
||||
* @param comparator
|
||||
* A function that takes two arguments and compares them, returning a
|
||||
* negative number if the first should be ordered before the second,
|
||||
* zero if the first and second have the same ordering, or a positive
|
||||
* number if the second should be ordered before the first. The first
|
||||
* argument is always `target`, and the second argument is a value
|
||||
* from the array.
|
||||
* @return An array with two elements. If `target` is found, the first
|
||||
* element is true, and the second element is its index in the array.
|
||||
* If `target` is not found, the first element is false, and the
|
||||
* second element is the index where it may be inserted to keep the
|
||||
* array ordered.
|
||||
*/
|
||||
search: function (array, target, comparator) {
|
||||
let low = 0;
|
||||
let high = array.length - 1;
|
||||
while (low <= high) {
|
||||
let mid = Math.floor((low + high) / 2);
|
||||
let cmp = comparator(target, array[mid]);
|
||||
if (cmp == 0)
|
||||
return [true, mid];
|
||||
if (cmp < 0)
|
||||
high = mid - 1;
|
||||
else
|
||||
low = mid + 1;
|
||||
}
|
||||
return [false, low];
|
||||
},
|
||||
});
|
@ -19,6 +19,13 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
|
||||
"resource://gre/modules/PageThumbs.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
|
||||
"resource://gre/modules/BinarySearch.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "Timer", () => {
|
||||
return Cu.import("resource://gre/modules/Timer.jsm", {});
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gPrincipal", function () {
|
||||
let uri = Services.io.newURI("about:newtab", null, null);
|
||||
return Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
|
||||
@ -44,12 +51,18 @@ const PREF_NEWTAB_ROWS = "browser.newtabpage.rows";
|
||||
// The preference that tells the number of columns of the newtab grid.
|
||||
const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
|
||||
|
||||
// The maximum number of results we want to retrieve from history.
|
||||
// The maximum number of results PlacesProvider retrieves from history.
|
||||
const HISTORY_RESULTS_LIMIT = 100;
|
||||
|
||||
// The maximum number of links Links.getLinks will return.
|
||||
const LINKS_GET_LINKS_LIMIT = 100;
|
||||
|
||||
// The gather telemetry topic.
|
||||
const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
|
||||
|
||||
// The amount of time we wait while coalescing updates for hidden pages.
|
||||
const SCHEDULE_UPDATE_TIMEOUT_MS = 1000;
|
||||
|
||||
/**
|
||||
* Calculate the MD5 hash for a string.
|
||||
* @param aValue
|
||||
@ -244,14 +257,34 @@ let AllPages = {
|
||||
/**
|
||||
* Updates all currently active pages but the given one.
|
||||
* @param aExceptPage The page to exclude from updating.
|
||||
* @param aHiddenPagesOnly If true, only pages hidden in the preloader are
|
||||
* updated.
|
||||
*/
|
||||
update: function AllPages_update(aExceptPage) {
|
||||
update: function AllPages_update(aExceptPage, aHiddenPagesOnly=false) {
|
||||
this._pages.forEach(function (aPage) {
|
||||
if (aExceptPage != aPage)
|
||||
aPage.update();
|
||||
aPage.update(aHiddenPagesOnly);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Many individual link changes may happen in a small amount of time over
|
||||
* multiple turns of the event loop. This method coalesces updates by waiting
|
||||
* a small amount of time before updating hidden pages.
|
||||
*/
|
||||
scheduleUpdateForHiddenPages: function AllPages_scheduleUpdateForHiddenPages() {
|
||||
if (!this._scheduleUpdateTimeout) {
|
||||
this._scheduleUpdateTimeout = Timer.setTimeout(() => {
|
||||
delete this._scheduleUpdateTimeout;
|
||||
this.update(null, true);
|
||||
}, SCHEDULE_UPDATE_TIMEOUT_MS);
|
||||
}
|
||||
},
|
||||
|
||||
get updateScheduledForHiddenPages() {
|
||||
return !!this._scheduleUpdateTimeout;
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements the nsIObserver interface to get notified when the preference
|
||||
* value changes or when a new copy of a page thumbnail is available.
|
||||
@ -504,13 +537,25 @@ let BlockedLinks = {
|
||||
* the history to retrieve the most frequently visited sites.
|
||||
*/
|
||||
let PlacesProvider = {
|
||||
/**
|
||||
* Set this to change the maximum number of links the provider will provide.
|
||||
*/
|
||||
maxNumLinks: HISTORY_RESULTS_LIMIT,
|
||||
|
||||
/**
|
||||
* Must be called before the provider is used.
|
||||
*/
|
||||
init: function PlacesProvider_init() {
|
||||
PlacesUtils.history.addObserver(this, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the current set of links delivered by this provider.
|
||||
* @param aCallback The function that the array of links is passed to.
|
||||
*/
|
||||
getLinks: function PlacesProvider_getLinks(aCallback) {
|
||||
let options = PlacesUtils.history.getNewQueryOptions();
|
||||
options.maxResults = HISTORY_RESULTS_LIMIT;
|
||||
options.maxResults = this.maxNumLinks;
|
||||
|
||||
// Sort by frecency, descending.
|
||||
options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING
|
||||
@ -525,7 +570,14 @@ let PlacesProvider = {
|
||||
let url = row.getResultByIndex(1);
|
||||
if (LinkChecker.checkLoadURI(url)) {
|
||||
let title = row.getResultByIndex(2);
|
||||
links.push({url: url, title: title});
|
||||
let frecency = row.getResultByIndex(12);
|
||||
let lastVisitDate = row.getResultByIndex(5);
|
||||
links.push({
|
||||
url: url,
|
||||
title: title,
|
||||
frecency: frecency,
|
||||
lastVisitDate: lastVisitDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -536,6 +588,26 @@ let PlacesProvider = {
|
||||
},
|
||||
|
||||
handleCompletion: function (aReason) {
|
||||
// The Places query breaks ties in frecency by place ID descending, but
|
||||
// that's different from how Links.compareLinks breaks ties, because
|
||||
// compareLinks doesn't have access to place IDs. It's very important
|
||||
// that the initial list of links is sorted in the same order imposed by
|
||||
// compareLinks, because Links uses compareLinks to perform binary
|
||||
// searches on the list. So, ensure the list is so ordered.
|
||||
let i = 1;
|
||||
let outOfOrder = [];
|
||||
while (i < links.length) {
|
||||
if (Links.compareLinks(links[i - 1], links[i]) > 0)
|
||||
outOfOrder.push(links.splice(i, 1)[0]);
|
||||
else
|
||||
i++;
|
||||
}
|
||||
for (let link of outOfOrder) {
|
||||
i = BinarySearch.insertionIndexOf(links, link,
|
||||
Links.compareLinks.bind(Links));
|
||||
links.splice(i, 0, link);
|
||||
}
|
||||
|
||||
aCallback(links);
|
||||
}
|
||||
};
|
||||
@ -544,28 +616,116 @@ let PlacesProvider = {
|
||||
let query = PlacesUtils.history.getNewQuery();
|
||||
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
|
||||
db.asyncExecuteLegacyQueries([query], 1, options, callback);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers an object that will be notified when the provider's links change.
|
||||
* @param aObserver An object with the following optional properties:
|
||||
* * onLinkChanged: A function that's called when a single link
|
||||
* changes. It's passed the provider and the link object. Only the
|
||||
* link's `url` property is guaranteed to be present. If its `title`
|
||||
* property is present, then its title has changed, and the
|
||||
* property's value is the new title. If any sort properties are
|
||||
* present, then its position within the provider's list of links may
|
||||
* have changed, and the properties' values are the new sort-related
|
||||
* values. Note that this link may not necessarily have been present
|
||||
* in the lists returned from any previous calls to getLinks.
|
||||
* * onManyLinksChanged: A function that's called when many links
|
||||
* change at once. It's passed the provider. You should call
|
||||
* getLinks to get the provider's new list of links.
|
||||
*/
|
||||
addObserver: function PlacesProvider_addObserver(aObserver) {
|
||||
this._observers.push(aObserver);
|
||||
},
|
||||
|
||||
_observers: [],
|
||||
|
||||
/**
|
||||
* Called by the history service.
|
||||
*/
|
||||
onFrecencyChanged: function PlacesProvider_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) {
|
||||
// The implementation of the query in getLinks excludes hidden and
|
||||
// unvisited pages, so it's important to exclude them here, too.
|
||||
if (!aHidden && aLastVisitDate) {
|
||||
this._callObservers("onLinkChanged", {
|
||||
url: aURI.spec,
|
||||
frecency: aNewFrecency,
|
||||
lastVisitDate: aLastVisitDate,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by the history service.
|
||||
*/
|
||||
onManyFrecenciesChanged: function PlacesProvider_onManyFrecenciesChanged() {
|
||||
this._callObservers("onManyLinksChanged");
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by the history service.
|
||||
*/
|
||||
onTitleChanged: function PlacesProvider_onTitleChanged(aURI, aNewTitle, aGUID) {
|
||||
this._callObservers("onLinkChanged", {
|
||||
url: aURI.spec,
|
||||
title: aNewTitle
|
||||
});
|
||||
},
|
||||
|
||||
_callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
|
||||
for (let obs of this._observers) {
|
||||
if (obs[aMethodName]) {
|
||||
try {
|
||||
obs[aMethodName](this, aArg);
|
||||
} catch (err) {
|
||||
Cu.reportError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
|
||||
Ci.nsISupportsWeakReference]),
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton that provides access to all links contained in the grid (including
|
||||
* the ones that don't fit on the grid). A link is a plain object with title
|
||||
* and url properties.
|
||||
* the ones that don't fit on the grid). A link is a plain object that looks
|
||||
* like this:
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* {url: "http://www.mozilla.org/", title: "Mozilla"}
|
||||
* {
|
||||
* url: "http://www.mozilla.org/",
|
||||
* title: "Mozilla",
|
||||
* frecency: 1337,
|
||||
* lastVisitDate: 1394678824766431,
|
||||
* }
|
||||
*/
|
||||
let Links = {
|
||||
/**
|
||||
* The links cache.
|
||||
* The maximum number of links returned by getLinks.
|
||||
*/
|
||||
_links: null,
|
||||
maxNumLinks: LINKS_GET_LINKS_LIMIT,
|
||||
|
||||
/**
|
||||
* The default provider for links.
|
||||
* The link providers.
|
||||
*/
|
||||
_provider: PlacesProvider,
|
||||
_providers: new Set(),
|
||||
|
||||
/**
|
||||
* A mapping from each provider to an object { sortedLinks, linkMap }.
|
||||
* sortedLinks is the cached, sorted array of links for the provider. linkMap
|
||||
* is a Map from link URLs to link objects.
|
||||
*/
|
||||
_providerLinks: new Map(),
|
||||
|
||||
/**
|
||||
* The properties of link objects used to sort them.
|
||||
*/
|
||||
_sortProperties: [
|
||||
"frecency",
|
||||
"lastVisitDate",
|
||||
"url",
|
||||
],
|
||||
|
||||
/**
|
||||
* List of callbacks waiting for the cache to be populated.
|
||||
@ -573,7 +733,26 @@ let Links = {
|
||||
_populateCallbacks: [],
|
||||
|
||||
/**
|
||||
* Populates the cache with fresh links from the current provider.
|
||||
* Adds a link provider.
|
||||
* @param aProvider The link provider.
|
||||
*/
|
||||
addProvider: function Links_addProvider(aProvider) {
|
||||
this._providers.add(aProvider);
|
||||
aProvider.addObserver(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a link provider.
|
||||
* @param aProvider The link provider.
|
||||
*/
|
||||
removeProvider: function Links_removeProvider(aProvider) {
|
||||
if (!this._providers.delete(aProvider))
|
||||
throw new Error("Unknown provider");
|
||||
this._providerLinks.delete(aProvider);
|
||||
},
|
||||
|
||||
/**
|
||||
* Populates the cache with fresh links from the providers.
|
||||
* @param aCallback The callback to call when finished (optional).
|
||||
* @param aForce When true, populates the cache even when it's already filled.
|
||||
*/
|
||||
@ -601,16 +780,15 @@ let Links = {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._links && !aForce) {
|
||||
executeCallbacks();
|
||||
} else {
|
||||
this._provider.getLinks(function (aLinks) {
|
||||
this._links = aLinks;
|
||||
executeCallbacks();
|
||||
}.bind(this));
|
||||
|
||||
this._addObserver();
|
||||
let numProvidersRemaining = this._providers.size;
|
||||
for (let provider of this._providers) {
|
||||
this._populateProviderCache(provider, () => {
|
||||
if (--numProvidersRemaining == 0)
|
||||
executeCallbacks();
|
||||
}, aForce);
|
||||
}
|
||||
|
||||
this._addObserver();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -619,9 +797,10 @@ let Links = {
|
||||
*/
|
||||
getLinks: function Links_getLinks() {
|
||||
let pinnedLinks = Array.slice(PinnedLinks.links);
|
||||
let links = this._getMergedProviderLinks();
|
||||
|
||||
// Filter blocked and pinned links.
|
||||
let links = (this._links || []).filter(function (link) {
|
||||
links = links.filter(function (link) {
|
||||
return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
|
||||
});
|
||||
|
||||
@ -641,7 +820,186 @@ let Links = {
|
||||
* Resets the links cache.
|
||||
*/
|
||||
resetCache: function Links_resetCache() {
|
||||
this._links = null;
|
||||
this._providerLinks.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Compares two links.
|
||||
* @param aLink1 The first link.
|
||||
* @param aLink2 The second link.
|
||||
* @return A negative number if aLink1 is ordered before aLink2, zero if
|
||||
* aLink1 and aLink2 have the same ordering, or a positive number if
|
||||
* aLink1 is ordered after aLink2.
|
||||
*/
|
||||
compareLinks: function Links_compareLinks(aLink1, aLink2) {
|
||||
for (let prop of this._sortProperties) {
|
||||
if (!(prop in aLink1) || !(prop in aLink2))
|
||||
throw new Error("Comparable link missing required property: " + prop);
|
||||
}
|
||||
return aLink2.frecency - aLink1.frecency ||
|
||||
aLink2.lastVisitDate - aLink1.lastVisitDate ||
|
||||
aLink1.url.localeCompare(aLink2.url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls getLinks on the given provider and populates our cache for it.
|
||||
* @param aProvider The provider whose cache will be populated.
|
||||
* @param aCallback The callback to call when finished.
|
||||
* @param aForce When true, populates the provider's cache even when it's
|
||||
* already filled.
|
||||
*/
|
||||
_populateProviderCache: function Links_populateProviderCache(aProvider, aCallback, aForce) {
|
||||
if (this._providerLinks.has(aProvider) && !aForce) {
|
||||
aCallback();
|
||||
} else {
|
||||
aProvider.getLinks(links => {
|
||||
// Filter out null and undefined links so we don't have to deal with
|
||||
// them in getLinks when merging links from providers.
|
||||
links = links.filter((link) => !!link);
|
||||
this._providerLinks.set(aProvider, {
|
||||
sortedLinks: links,
|
||||
linkMap: links.reduce((map, link) => {
|
||||
map.set(link.url, link);
|
||||
return map;
|
||||
}, new Map()),
|
||||
});
|
||||
aCallback();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Merges the cached lists of links from all providers whose lists are cached.
|
||||
* @return The merged list.
|
||||
*/
|
||||
_getMergedProviderLinks: function Links__getMergedProviderLinks() {
|
||||
// Build a list containing a copy of each provider's sortedLinks list.
|
||||
let linkLists = [];
|
||||
for (let links of this._providerLinks.values()) {
|
||||
linkLists.push(links.sortedLinks.slice());
|
||||
}
|
||||
|
||||
function getNextLink() {
|
||||
let minLinks = null;
|
||||
for (let links of linkLists) {
|
||||
if (links.length &&
|
||||
(!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0))
|
||||
minLinks = links;
|
||||
}
|
||||
return minLinks ? minLinks.shift() : null;
|
||||
}
|
||||
|
||||
let finalLinks = [];
|
||||
for (let nextLink = getNextLink();
|
||||
nextLink && finalLinks.length < this.maxNumLinks;
|
||||
nextLink = getNextLink()) {
|
||||
finalLinks.push(nextLink);
|
||||
}
|
||||
|
||||
return finalLinks;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by a provider to notify us when a single link changes.
|
||||
* @param aProvider The provider whose link changed.
|
||||
* @param aLink The link that changed. If the link is new, it must have all
|
||||
* of the _sortProperties. Otherwise, it may have as few or as
|
||||
* many as is convenient.
|
||||
*/
|
||||
onLinkChanged: function Links_onLinkChanged(aProvider, aLink) {
|
||||
if (!("url" in aLink))
|
||||
throw new Error("Changed links must have a url property");
|
||||
|
||||
let links = this._providerLinks.get(aProvider);
|
||||
if (!links)
|
||||
// This is not an error, it just means that between the time the provider
|
||||
// was added and the future time we call getLinks on it, it notified us of
|
||||
// a change.
|
||||
return;
|
||||
|
||||
let { sortedLinks, linkMap } = links;
|
||||
|
||||
// Nothing to do if the list is full and the link isn't in it and shouldn't
|
||||
// be in it.
|
||||
if (!linkMap.has(aLink.url) &&
|
||||
sortedLinks.length &&
|
||||
sortedLinks.length == aProvider.maxNumLinks) {
|
||||
let lastLink = sortedLinks[sortedLinks.length - 1];
|
||||
if (this.compareLinks(lastLink, aLink) < 0)
|
||||
return;
|
||||
}
|
||||
|
||||
let updatePages = false;
|
||||
|
||||
// Update the title in O(1).
|
||||
if ("title" in aLink) {
|
||||
let link = linkMap.get(aLink.url);
|
||||
if (link && link.title != aLink.title) {
|
||||
link.title = aLink.title;
|
||||
updatePages = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the link's position in O(lg n).
|
||||
if (this._sortProperties.some((prop) => prop in aLink)) {
|
||||
let link = linkMap.get(aLink.url);
|
||||
if (link) {
|
||||
// The link is already in the list.
|
||||
let idx = this._indexOf(sortedLinks, link);
|
||||
if (idx < 0)
|
||||
throw new Error("Link should be in _sortedLinks if in _linkMap");
|
||||
sortedLinks.splice(idx, 1);
|
||||
for (let prop of this._sortProperties) {
|
||||
if (prop in aLink)
|
||||
link[prop] = aLink[prop];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The link is new.
|
||||
for (let prop of this._sortProperties) {
|
||||
if (!(prop in aLink))
|
||||
throw new Error("New link missing required sort property: " + prop);
|
||||
}
|
||||
// Copy the link object so that if the caller changes it, it doesn't
|
||||
// screw up our bookkeeping.
|
||||
link = {};
|
||||
for (let [prop, val] of Iterator(aLink)) {
|
||||
link[prop] = val;
|
||||
}
|
||||
linkMap.set(link.url, link);
|
||||
}
|
||||
let idx = this._insertionIndexOf(sortedLinks, link);
|
||||
sortedLinks.splice(idx, 0, link);
|
||||
if (sortedLinks.length > aProvider.maxNumLinks) {
|
||||
let lastLink = sortedLinks.pop();
|
||||
linkMap.delete(lastLink.url);
|
||||
}
|
||||
updatePages = true;
|
||||
}
|
||||
|
||||
if (updatePages)
|
||||
AllPages.scheduleUpdateForHiddenPages();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by a provider to notify us when many links change.
|
||||
*/
|
||||
onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
|
||||
this._populateProviderCache(aProvider, () => {
|
||||
AllPages.scheduleUpdateForHiddenPages();
|
||||
}, true);
|
||||
},
|
||||
|
||||
_indexOf: function Links__indexOf(aArray, aLink) {
|
||||
return this._binsearch(aArray, aLink, "indexOf");
|
||||
},
|
||||
|
||||
_insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
|
||||
return this._binsearch(aArray, aLink, "insertionIndexOf");
|
||||
},
|
||||
|
||||
_binsearch: function Links__binsearch(aArray, aLink, aMethod) {
|
||||
return BinarySearch[aMethod](aArray, aLink, this.compareLinks.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
@ -654,7 +1012,7 @@ let Links = {
|
||||
if (AllPages.length && AllPages.enabled)
|
||||
this.populateCache(function () { AllPages.update() }, true);
|
||||
else
|
||||
this._links = null;
|
||||
this.resetCache();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -774,11 +1132,20 @@ this.NewTabUtils = {
|
||||
_initialized: false,
|
||||
|
||||
init: function NewTabUtils_init() {
|
||||
if (this.initWithoutProviders()) {
|
||||
PlacesProvider.init();
|
||||
Links.addProvider(PlacesProvider);
|
||||
}
|
||||
},
|
||||
|
||||
initWithoutProviders: function NewTabUtils_initWithoutProviders() {
|
||||
if (!this._initialized) {
|
||||
this._initialized = true;
|
||||
ExpirationFilter.init();
|
||||
Telemetry.init();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,7 @@ MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'AsyncShutdown.jsm',
|
||||
'BinarySearch.jsm',
|
||||
'BrowserUtils.jsm',
|
||||
'CharsetMenu.jsm',
|
||||
'debug.js',
|
||||
|
81
toolkit/modules/tests/xpcshell/test_BinarySearch.js
Normal file
81
toolkit/modules/tests/xpcshell/test_BinarySearch.js
Normal file
@ -0,0 +1,81 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Components.utils.import("resource://gre/modules/BinarySearch.jsm");
|
||||
|
||||
function run_test() {
|
||||
// empty array
|
||||
ok([], 1, false, 0);
|
||||
|
||||
// one-element array
|
||||
ok([2], 2, true, 0);
|
||||
ok([2], 1, false, 0);
|
||||
ok([2], 3, false, 1);
|
||||
|
||||
// two-element array
|
||||
ok([2, 4], 2, true, 0);
|
||||
ok([2, 4], 4, true, 1);
|
||||
ok([2, 4], 1, false, 0);
|
||||
ok([2, 4], 3, false, 1);
|
||||
ok([2, 4], 5, false, 2);
|
||||
|
||||
// three-element array
|
||||
ok([2, 4, 6], 2, true, 0);
|
||||
ok([2, 4, 6], 4, true, 1);
|
||||
ok([2, 4, 6], 6, true, 2);
|
||||
ok([2, 4, 6], 1, false, 0);
|
||||
ok([2, 4, 6], 3, false, 1);
|
||||
ok([2, 4, 6], 5, false, 2);
|
||||
ok([2, 4, 6], 7, false, 3);
|
||||
|
||||
// duplicates
|
||||
ok([2, 2], 2, true, 0);
|
||||
ok([2, 2], 1, false, 0);
|
||||
ok([2, 2], 3, false, 2);
|
||||
|
||||
// duplicates on the left
|
||||
ok([2, 2, 4], 2, true, 1);
|
||||
ok([2, 2, 4], 4, true, 2);
|
||||
ok([2, 2, 4], 1, false, 0);
|
||||
ok([2, 2, 4], 3, false, 2);
|
||||
ok([2, 2, 4], 5, false, 3);
|
||||
|
||||
// duplicates on the right
|
||||
ok([2, 4, 4], 2, true, 0);
|
||||
ok([2, 4, 4], 4, true, 1);
|
||||
ok([2, 4, 4], 1, false, 0);
|
||||
ok([2, 4, 4], 3, false, 1);
|
||||
ok([2, 4, 4], 5, false, 3);
|
||||
|
||||
// duplicates in the middle
|
||||
ok([2, 4, 4, 6], 2, true, 0);
|
||||
ok([2, 4, 4, 6], 4, true, 1);
|
||||
ok([2, 4, 4, 6], 6, true, 3);
|
||||
ok([2, 4, 4, 6], 1, false, 0);
|
||||
ok([2, 4, 4, 6], 3, false, 1);
|
||||
ok([2, 4, 4, 6], 5, false, 3);
|
||||
ok([2, 4, 4, 6], 7, false, 4);
|
||||
|
||||
// duplicates all around
|
||||
ok([2, 2, 4, 4, 6, 6], 2, true, 0);
|
||||
ok([2, 2, 4, 4, 6, 6], 4, true, 2);
|
||||
ok([2, 2, 4, 4, 6, 6], 6, true, 4);
|
||||
ok([2, 2, 4, 4, 6, 6], 1, false, 0);
|
||||
ok([2, 2, 4, 4, 6, 6], 3, false, 2);
|
||||
ok([2, 2, 4, 4, 6, 6], 5, false, 4);
|
||||
ok([2, 2, 4, 4, 6, 6], 7, false, 6);
|
||||
}
|
||||
|
||||
function ok(array, target, expectedFound, expectedIdx) {
|
||||
let [found, idx] = BinarySearch.search(array, target, cmp);
|
||||
do_check_eq(found, expectedFound);
|
||||
do_check_eq(idx, expectedIdx);
|
||||
|
||||
idx = expectedFound ? expectedIdx : -1;
|
||||
do_check_eq(BinarySearch.indexOf(array, target, cmp), idx);
|
||||
do_check_eq(BinarySearch.insertionIndexOf(array, target, cmp), expectedIdx);
|
||||
}
|
||||
|
||||
function cmp(num1, num2) {
|
||||
return num1 - num2;
|
||||
}
|
176
toolkit/modules/tests/xpcshell/test_NewTabUtils.js
Normal file
176
toolkit/modules/tests/xpcshell/test_NewTabUtils.js
Normal file
@ -0,0 +1,176 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// See also browser/base/content/test/newtab/.
|
||||
|
||||
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
||||
Cu.import("resource://gre/modules/NewTabUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_test(function multipleProviders() {
|
||||
// Make each provider generate NewTabUtils.links.maxNumLinks links to check
|
||||
// that no more than maxNumLinks are actually returned in the merged list.
|
||||
let evenLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks, 2);
|
||||
let evenProvider = new TestProvider(done => done(evenLinks));
|
||||
let oddLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks - 1, 2);
|
||||
let oddProvider = new TestProvider(done => done(oddLinks));
|
||||
|
||||
NewTabUtils.initWithoutProviders();
|
||||
NewTabUtils.links.addProvider(evenProvider);
|
||||
NewTabUtils.links.addProvider(oddProvider);
|
||||
|
||||
// This is sync since the providers' getLinks are sync.
|
||||
NewTabUtils.links.populateCache(function () {}, false);
|
||||
|
||||
let links = NewTabUtils.links.getLinks();
|
||||
let expectedLinks = makeLinks(NewTabUtils.links.maxNumLinks,
|
||||
2 * NewTabUtils.links.maxNumLinks,
|
||||
1);
|
||||
do_check_eq(links.length, NewTabUtils.links.maxNumLinks);
|
||||
do_check_links(links, expectedLinks);
|
||||
|
||||
NewTabUtils.links.removeProvider(evenProvider);
|
||||
NewTabUtils.links.removeProvider(oddProvider);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function changeLinks() {
|
||||
let expectedLinks = makeLinks(0, 20, 2);
|
||||
let provider = new TestProvider(done => done(expectedLinks));
|
||||
|
||||
NewTabUtils.initWithoutProviders();
|
||||
NewTabUtils.links.addProvider(provider);
|
||||
|
||||
// This is sync since the provider's getLinks is sync.
|
||||
NewTabUtils.links.populateCache(function () {}, false);
|
||||
|
||||
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
|
||||
|
||||
// Notify of a new link.
|
||||
let newLink = {
|
||||
url: "http://example.com/19",
|
||||
title: "My frecency is 19",
|
||||
frecency: 19,
|
||||
lastVisitDate: 0,
|
||||
};
|
||||
expectedLinks.splice(1, 0, newLink);
|
||||
provider.notifyLinkChanged(newLink);
|
||||
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
|
||||
|
||||
// Notify of a link that's changed sort criteria.
|
||||
newLink.frecency = 17;
|
||||
expectedLinks.splice(1, 1);
|
||||
expectedLinks.splice(2, 0, newLink);
|
||||
provider.notifyLinkChanged({
|
||||
url: newLink.url,
|
||||
frecency: 17,
|
||||
});
|
||||
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
|
||||
|
||||
// Notify of a link that's changed title.
|
||||
newLink.title = "My frecency is now 17";
|
||||
provider.notifyLinkChanged({
|
||||
url: newLink.url,
|
||||
title: newLink.title,
|
||||
});
|
||||
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
|
||||
|
||||
// Notify of a new link again, but this time make it overflow maxNumLinks.
|
||||
provider.maxNumLinks = expectedLinks.length;
|
||||
newLink = {
|
||||
url: "http://example.com/21",
|
||||
frecency: 21,
|
||||
lastVisitDate: 0,
|
||||
};
|
||||
expectedLinks.unshift(newLink);
|
||||
expectedLinks.pop();
|
||||
do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check.
|
||||
provider.notifyLinkChanged(newLink);
|
||||
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
|
||||
|
||||
// Notify of many links changed.
|
||||
expectedLinks = makeLinks(0, 3, 1);
|
||||
provider.notifyManyLinksChanged();
|
||||
// NewTabUtils.links will now repopulate its cache, which is sync since
|
||||
// the provider's getLinks is sync.
|
||||
do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
|
||||
|
||||
NewTabUtils.links.removeProvider(provider);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function oneProviderAlreadyCached() {
|
||||
let links1 = makeLinks(0, 10, 1);
|
||||
let provider1 = new TestProvider(done => done(links1));
|
||||
|
||||
NewTabUtils.initWithoutProviders();
|
||||
NewTabUtils.links.addProvider(provider1);
|
||||
|
||||
// This is sync since the provider's getLinks is sync.
|
||||
NewTabUtils.links.populateCache(function () {}, false);
|
||||
do_check_links(NewTabUtils.links.getLinks(), links1);
|
||||
|
||||
let links2 = makeLinks(10, 20, 1);
|
||||
let provider2 = new TestProvider(done => done(links2));
|
||||
NewTabUtils.links.addProvider(provider2);
|
||||
|
||||
NewTabUtils.links.populateCache(function () {}, false);
|
||||
do_check_links(NewTabUtils.links.getLinks(), links2.concat(links1));
|
||||
|
||||
NewTabUtils.links.removeProvider(provider1);
|
||||
NewTabUtils.links.removeProvider(provider2);
|
||||
});
|
||||
|
||||
function TestProvider(getLinksFn) {
|
||||
this.getLinks = getLinksFn;
|
||||
this._observers = new Set();
|
||||
}
|
||||
|
||||
TestProvider.prototype = {
|
||||
addObserver: function (observer) {
|
||||
this._observers.add(observer);
|
||||
},
|
||||
notifyLinkChanged: function (link) {
|
||||
this._notifyObservers("onLinkChanged", link);
|
||||
},
|
||||
notifyManyLinksChanged: function () {
|
||||
this._notifyObservers("onManyLinksChanged");
|
||||
},
|
||||
_notifyObservers: function (observerMethodName, arg) {
|
||||
for (let obs of this._observers) {
|
||||
if (obs[observerMethodName])
|
||||
obs[observerMethodName](this, arg);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function do_check_links(actualLinks, expectedLinks) {
|
||||
do_check_true(Array.isArray(actualLinks));
|
||||
do_check_eq(actualLinks.length, expectedLinks.length);
|
||||
for (let i = 0; i < expectedLinks.length; i++) {
|
||||
let expected = expectedLinks[i];
|
||||
let actual = actualLinks[i];
|
||||
do_check_eq(actual.url, expected.url);
|
||||
do_check_eq(actual.title, expected.title);
|
||||
do_check_eq(actual.frecency, expected.frecency);
|
||||
do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
|
||||
}
|
||||
}
|
||||
|
||||
function makeLinks(frecRangeStart, frecRangeEnd, step) {
|
||||
let links = [];
|
||||
// Remember, links are ordered by frecency descending.
|
||||
for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
|
||||
links.push({
|
||||
url: "http://example.com/" + i,
|
||||
title: "My frecency is " + i,
|
||||
frecency: i,
|
||||
lastVisitDate: 0,
|
||||
});
|
||||
}
|
||||
return links;
|
||||
}
|
@ -8,12 +8,14 @@ support-files =
|
||||
zips/zen.zip
|
||||
|
||||
[test_AsyncShutdown.js]
|
||||
[test_BinarySearch.js]
|
||||
[test_DeferredTask.js]
|
||||
[test_dict.js]
|
||||
[test_DirectoryLinksProvider.js]
|
||||
[test_FileUtils.js]
|
||||
[test_Http.js]
|
||||
[test_Log.js]
|
||||
[test_NewTabUtils.js]
|
||||
[test_PermissionsUtils.js]
|
||||
[test_Preferences.js]
|
||||
[test_Promise.js]
|
||||
|
Loading…
Reference in New Issue
Block a user