зеркало из https://github.com/mozilla/gecko-dev.git
Bug 820834 - Abstract about:home storage and make it async-ready.
r=gavin
This commit is contained in:
Родитель
4c60c029bd
Коммит
5987d821e9
|
@ -86,7 +86,7 @@ let gObserver = new MutationObserver(function (mutations) {
|
|||
if (mutation.attributeName == "searchEngineURL") {
|
||||
gObserver.disconnect();
|
||||
setupSearchEngine();
|
||||
loadSnippets();
|
||||
ensureSnippetsMapThen(loadSnippets);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,69 @@ window.addEventListener("load", function () {
|
|||
window.addEventListener("resize", fitToWidth);
|
||||
});
|
||||
|
||||
// This object has the same interface as Map and is used to store and retrieve
|
||||
// the snippets data. It is lazily initialized by ensureSnippetsMapThen(), so
|
||||
// be sure its callback returned before trying to use it.
|
||||
let gSnippetsMap;
|
||||
let gSnippetsMapCallbacks = [];
|
||||
|
||||
/**
|
||||
* Ensure the snippets map is properly initialized.
|
||||
*
|
||||
* @param aCallback
|
||||
* Invoked once the map has been initialized, gets the map as argument.
|
||||
* @note Snippets should never directly manage the underlying storage, since
|
||||
* it may change inadvertently.
|
||||
*/
|
||||
function ensureSnippetsMapThen(aCallback)
|
||||
{
|
||||
if (gSnippetsMap) {
|
||||
aCallback(gSnippetsMap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle multiple requests during the async initialization.
|
||||
gSnippetsMapCallbacks.push(aCallback);
|
||||
if (gSnippetsMapCallbacks.length > 1) {
|
||||
// We are already updating, the callbacks will be invoked when done.
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO (bug 789348): use a real asynchronous storage here. This setTimeout
|
||||
// is done just to catch bugs with the asynchronous behavior.
|
||||
setTimeout(function() {
|
||||
// Populate the cache from the persistent storage.
|
||||
let cache = new Map();
|
||||
for (let key of [ "snippets-last-update",
|
||||
"snippets" ]) {
|
||||
cache.set(key, localStorage[key]);
|
||||
}
|
||||
|
||||
gSnippetsMap = Object.freeze({
|
||||
get: function (aKey) cache.get(aKey),
|
||||
set: function (aKey, aValue) {
|
||||
localStorage[aKey] = aValue;
|
||||
return cache.set(aKey, aValue);
|
||||
},
|
||||
has: function(aKey) cache.has(aKey),
|
||||
delete: function(aKey) {
|
||||
delete localStorage[aKey];
|
||||
return cache.delete(aKey);
|
||||
},
|
||||
clear: function() {
|
||||
localStorage.clear();
|
||||
return cache.clear();
|
||||
},
|
||||
get size() cache.size
|
||||
});
|
||||
|
||||
for (let callback of gSnippetsMapCallbacks) {
|
||||
callback(gSnippetsMap);
|
||||
}
|
||||
gSnippetsMapCallbacks.length = 0;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function onSearchSubmit(aEvent)
|
||||
{
|
||||
let searchTerms = document.getElementById("searchText").value;
|
||||
|
@ -157,13 +220,21 @@ function setupSearchEngine()
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the local snippets from the remote storage, then show them through
|
||||
* showSnippets.
|
||||
*/
|
||||
function loadSnippets()
|
||||
{
|
||||
if (!gSnippetsMap)
|
||||
throw new Error("Snippets map has not properly been initialized");
|
||||
|
||||
// Check last snippets update.
|
||||
let lastUpdate = localStorage["snippets-last-update"];
|
||||
let lastUpdate = gSnippetsMap.get("snippets-last-update");
|
||||
let updateURL = document.documentElement.getAttribute("snippetsURL");
|
||||
if (updateURL && (!lastUpdate ||
|
||||
Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS)) {
|
||||
let shouldUpdate = !lastUpdate ||
|
||||
Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS;
|
||||
if (updateURL && shouldUpdate) {
|
||||
// Try to update from network.
|
||||
let xhr = new XMLHttpRequest();
|
||||
try {
|
||||
|
@ -174,14 +245,14 @@ function loadSnippets()
|
|||
}
|
||||
// Even if fetching should fail we don't want to spam the server, thus
|
||||
// set the last update time regardless its results. Will retry tomorrow.
|
||||
localStorage["snippets-last-update"] = Date.now();
|
||||
gSnippetsMap.set("snippets-last-update", Date.now());
|
||||
xhr.onerror = function (event) {
|
||||
showSnippets();
|
||||
};
|
||||
xhr.onload = function (event)
|
||||
{
|
||||
if (xhr.status == 200) {
|
||||
localStorage["snippets"] = xhr.responseText;
|
||||
gSnippetsMap.set("snippets", xhr.responseText);
|
||||
}
|
||||
showSnippets();
|
||||
};
|
||||
|
@ -191,10 +262,27 @@ function loadSnippets()
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows locally cached remote snippets, or default ones when not available.
|
||||
*
|
||||
* @note: snippets should never invoke showSnippets(), or they may cause
|
||||
* a "too much recursion" exception.
|
||||
*/
|
||||
let _snippetsShown = false;
|
||||
function showSnippets()
|
||||
{
|
||||
if (!gSnippetsMap)
|
||||
throw new Error("Snippets map has not properly been initialized");
|
||||
if (_snippetsShown) {
|
||||
// There's something wrong with the remote snippets, just in case fall back
|
||||
// to the default snippets.
|
||||
showDefaultSnippets();
|
||||
throw new Error("showSnippets should never be invoked multiple times");
|
||||
}
|
||||
_snippetsShown = true;
|
||||
|
||||
let snippetsElt = document.getElementById("snippets");
|
||||
let snippets = localStorage["snippets"];
|
||||
let snippets = gSnippetsMap.get("snippets");
|
||||
// If there are remotely fetched snippets, try to to show them.
|
||||
if (snippets) {
|
||||
// Injecting snippets can throw if they're invalid XML.
|
||||
|
@ -214,7 +302,19 @@ function showSnippets()
|
|||
}
|
||||
}
|
||||
|
||||
// Show default snippets otherwise.
|
||||
showDefaultSnippets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear snippets element contents and show default snippets.
|
||||
*/
|
||||
function showDefaultSnippets()
|
||||
{
|
||||
// Clear eventual contents...
|
||||
let snippetsElt = document.getElementById("snippets");
|
||||
snippetsElt.innerHTML = "";
|
||||
|
||||
// ...then show default snippets.
|
||||
let defaultSnippetsElt = document.getElementById("defaultSnippets");
|
||||
let entries = defaultSnippetsElt.querySelectorAll("span");
|
||||
// Choose a random snippet. Assume there is always at least one.
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/commonjs/sdk/core/promise.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
// Ensure we don't pollute prefs for next tests.
|
||||
try {
|
||||
|
@ -22,19 +27,15 @@ let gTests = [
|
|||
.getService(Ci.nsIObserver)
|
||||
.observe(null, "cookie-changed", "cleared");
|
||||
},
|
||||
run: function ()
|
||||
run: function (aSnippetsMap)
|
||||
{
|
||||
let storage = getStorage();
|
||||
isnot(storage.getItem("snippets-last-update"), null);
|
||||
executeSoon(runNextTest);
|
||||
isnot(aSnippetsMap.get("snippets-last-update"), null);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Check default snippets are shown",
|
||||
setup: function ()
|
||||
{
|
||||
},
|
||||
setup: function () { },
|
||||
run: function ()
|
||||
{
|
||||
let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
|
||||
|
@ -42,19 +43,17 @@ let gTests = [
|
|||
ok(snippetsElt, "Found snippets element")
|
||||
is(snippetsElt.getElementsByTagName("span").length, 1,
|
||||
"A default snippet is visible.");
|
||||
executeSoon(runNextTest);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Check default snippets are shown if snippets are invalid xml",
|
||||
setup: function ()
|
||||
setup: function (aSnippetsMap)
|
||||
{
|
||||
let storage = getStorage();
|
||||
// This must be some incorrect xhtml code.
|
||||
storage.setItem("snippets", "<p><b></p></b>");
|
||||
aSnippetsMap.set("snippets", "<p><b></p></b>");
|
||||
},
|
||||
run: function ()
|
||||
run: function (aSnippetsMap)
|
||||
{
|
||||
let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
|
||||
|
||||
|
@ -62,16 +61,14 @@ let gTests = [
|
|||
ok(snippetsElt, "Found snippets element");
|
||||
is(snippetsElt.getElementsByTagName("span").length, 1,
|
||||
"A default snippet is visible.");
|
||||
let storage = getStorage();
|
||||
storage.removeItem("snippets");
|
||||
executeSoon(runNextTest);
|
||||
|
||||
aSnippetsMap.delete("snippets");
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Check that search engine logo has alt text",
|
||||
setup: function ()
|
||||
{
|
||||
},
|
||||
setup: function () { },
|
||||
run: function ()
|
||||
{
|
||||
let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
|
||||
|
@ -85,27 +82,29 @@ let gTests = [
|
|||
|
||||
isnot(altText, "undefined",
|
||||
"Search engine logo's alt text shouldn't be the string 'undefined'");
|
||||
|
||||
executeSoon(runNextTest);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Check that performing a search fires a search event.",
|
||||
setup: function () { },
|
||||
run: function () {
|
||||
let deferred = Promise.defer();
|
||||
let doc = gBrowser.contentDocument;
|
||||
|
||||
doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) {
|
||||
is(e.detail, doc.documentElement.getAttribute("searchEngineName"), "Detail is search engine name");
|
||||
|
||||
gBrowser.stop();
|
||||
executeSoon(runNextTest);
|
||||
deferred.resolve();
|
||||
}, true, true);
|
||||
|
||||
doc.getElementById("searchText").value = "it works";
|
||||
doc.getElementById("searchSubmit").click();
|
||||
},
|
||||
return deferred.promise;
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Check that performing a search records to Firefox Health Report.",
|
||||
setup: function () { },
|
||||
|
@ -115,10 +114,10 @@ let gTests = [
|
|||
cm.getCategoryEntry("healthreport-js-provider", "SearchesProvider");
|
||||
} catch (ex) {
|
||||
// Health Report disabled, or no SearchesProvider.
|
||||
runNextTest();
|
||||
return;
|
||||
}
|
||||
|
||||
let deferred = Promise.defer();
|
||||
let doc = gBrowser.contentDocument;
|
||||
|
||||
// We rely on the listener in browser.js being installed and fired before
|
||||
|
@ -149,7 +148,7 @@ let gTests = [
|
|||
// Note the search from the previous test.
|
||||
is(day.get(field), 2, "Have searches recorded.");
|
||||
|
||||
executeSoon(runNextTest);
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -157,62 +156,118 @@ let gTests = [
|
|||
|
||||
doc.getElementById("searchText").value = "a search";
|
||||
doc.getElementById("searchSubmit").click();
|
||||
},
|
||||
return deferred.promise;
|
||||
}
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
function test()
|
||||
{
|
||||
waitForExplicitFinish();
|
||||
|
||||
// Ensure that by default we don't try to check for remote snippets since that
|
||||
// could be tricky due to network bustages or slowness.
|
||||
let storage = getStorage();
|
||||
storage.setItem("snippets-last-update", Date.now());
|
||||
storage.removeItem("snippets");
|
||||
Task.spawn(function () {
|
||||
for (let test of gTests) {
|
||||
info(test.desc);
|
||||
|
||||
executeSoon(runNextTest);
|
||||
}
|
||||
let tab = yield promiseNewTabLoadEvent("about:home", "DOMContentLoaded");
|
||||
|
||||
function runNextTest()
|
||||
{
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
// Must wait for both the snippets map and the browser attributes, since
|
||||
// can't guess the order they will happen.
|
||||
// So, start listening now, but verify the promise is fulfilled only
|
||||
// after the snippets map setup.
|
||||
let promise = promiseBrowserAttributes(tab);
|
||||
// Prepare the snippets map with default values, then run the test setup.
|
||||
let snippetsMap = yield promiseSetupSnippetsMap(tab, test.setup);
|
||||
// Ensure browser has set attributes already, or wait for them.
|
||||
yield promise;
|
||||
|
||||
if (gTests.length) {
|
||||
let test = gTests.shift();
|
||||
info(test.desc);
|
||||
test.setup();
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab("about:home");
|
||||
tab.linkedBrowser.addEventListener("load", function load(event) {
|
||||
tab.linkedBrowser.removeEventListener("load", load, true);
|
||||
yield test.run(snippetsMap);
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
|
||||
let observer = new MutationObserver(function (mutations) {
|
||||
for (let mutation of mutations) {
|
||||
if (mutation.attributeName == "searchEngineURL") {
|
||||
observer.disconnect();
|
||||
executeSoon(test.run);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
let docElt = tab.linkedBrowser.contentDocument.documentElement;
|
||||
observer.observe(docElt, { attributes: true });
|
||||
}, true);
|
||||
}
|
||||
else {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getStorage()
|
||||
/**
|
||||
* Creates a new tab and waits for a load event.
|
||||
*
|
||||
* @param aUrl
|
||||
* The url to load in a new tab.
|
||||
* @param aEvent
|
||||
* The load event type to wait for. Defaults to "load".
|
||||
* @return {Promise} resolved when the event is handled. Gets the new tab.
|
||||
*/
|
||||
function promiseNewTabLoadEvent(aUrl, aEventType="load")
|
||||
{
|
||||
let aboutHomeURI = Services.io.newURI("moz-safe-about:home", null, null);
|
||||
let principal = Components.classes["@mozilla.org/scriptsecuritymanager;1"].
|
||||
getService(Components.interfaces.nsIScriptSecurityManager).
|
||||
getNoAppCodebasePrincipal(Services.io.newURI("about:home", null, null));
|
||||
let dsm = Components.classes["@mozilla.org/dom/storagemanager;1"].
|
||||
getService(Components.interfaces.nsIDOMStorageManager);
|
||||
return dsm.getLocalStorageForPrincipal(principal, "");
|
||||
};
|
||||
let deferred = Promise.defer();
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
|
||||
tab.linkedBrowser.addEventListener(aEventType, function load(event) {
|
||||
tab.linkedBrowser.removeEventListener(aEventType, load, true);
|
||||
deferred.resolve(tab);
|
||||
}, true);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up snippets and ensures that by default we don't try to check for
|
||||
* remote snippets since that may cause network bustage or slowness.
|
||||
*
|
||||
* @param aTab
|
||||
* The tab containing about:home.
|
||||
* @param aSetupFn
|
||||
* The setup function to be run.
|
||||
* @return {Promise} resolved when the snippets are ready. Gets the snippets map.
|
||||
*/
|
||||
function promiseSetupSnippetsMap(aTab, aSetupFn)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let cw = aTab.linkedBrowser.contentWindow.wrappedJSObject;
|
||||
cw.ensureSnippetsMapThen(function (aSnippetsMap) {
|
||||
// Don't try to update.
|
||||
aSnippetsMap.set("snippets-last-update", Date.now());
|
||||
// Clear snippets.
|
||||
aSnippetsMap.delete("snippets");
|
||||
aSetupFn(aSnippetsMap);
|
||||
// Must be sure to continue after the page snippets map setup.
|
||||
executeSoon(function() deferred.resolve(aSnippetsMap));
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the attributes being set by browser.js and overwrites snippetsURL
|
||||
* to ensure we won't try to hit the network and we can force xhr to throw.
|
||||
*
|
||||
* @param aTab
|
||||
* The tab containing about:home.
|
||||
* @return {Promise} resolved when the attributes are ready.
|
||||
*/
|
||||
function promiseBrowserAttributes(aTab)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let docElt = aTab.linkedBrowser.contentDocument.documentElement;
|
||||
//docElt.setAttribute("snippetsURL", "nonexistent://test");
|
||||
let observer = new MutationObserver(function (mutations) {
|
||||
for (let mutation of mutations) {
|
||||
if (mutation.attributeName == "snippetsURL" &&
|
||||
docElt.getAttribute("snippetsURL") != "nonexistent://test") {
|
||||
docElt.setAttribute("snippetsURL", "nonexistent://test");
|
||||
}
|
||||
|
||||
// Now we just have to wait for the last attribute.
|
||||
if (mutation.attributeName == "searchEngineURL") {
|
||||
observer.disconnect();
|
||||
// Must be sure to continue after the page mutation observer.
|
||||
executeSoon(function() deferred.resolve());
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(docElt, { attributes: true });
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ Components.utils.import("resource://gre/modules/Services.jsm");
|
|||
const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
|
||||
|
||||
// Should be bumped up if the snippets content format changes.
|
||||
const STARTPAGE_VERSION = 3;
|
||||
const STARTPAGE_VERSION = 4;
|
||||
|
||||
this.AboutHomeUtils = new Object();
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче