Backout ac2581b910b6 (bug 823547) and f50b8afa272f (bug 820834) for bc failures in browser_aboutHome.js

This commit is contained in:
Marco Bonardo 2013-02-27 19:54:15 +01:00
parent 91b8dd7606
commit 9b0066cba7
4 changed files with 78 additions and 294 deletions

View File

@ -86,7 +86,7 @@ let gObserver = new MutationObserver(function (mutations) {
if (mutation.attributeName == "searchEngineURL") { if (mutation.attributeName == "searchEngineURL") {
gObserver.disconnect(); gObserver.disconnect();
setupSearchEngine(); setupSearchEngine();
ensureSnippetsMapThen(loadSnippets); loadSnippets();
return; return;
} }
} }
@ -100,70 +100,6 @@ window.addEventListener("load", function () {
window.addEventListener("resize", fitToWidth); 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-cached-version",
"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) function onSearchSubmit(aEvent)
{ {
let searchTerms = document.getElementById("searchText").value; let searchTerms = document.getElementById("searchText").value;
@ -221,29 +157,13 @@ function setupSearchEngine()
} }
/**
* Update the local snippets from the remote storage, then show them through
* showSnippets.
*/
function loadSnippets() function loadSnippets()
{ {
if (!gSnippetsMap)
throw new Error("Snippets map has not properly been initialized");
// Check cached snippets version.
let cachedVersion = gSnippetsMap.get("snippets-cached-version") || 0;
let currentVersion = document.documentElement.getAttribute("snippetsVersion");
if (cachedVersion < currentVersion) {
// The cached snippets are old and unsupported, restart from scratch.
gSnippetsMap.clear();
}
// Check last snippets update. // Check last snippets update.
let lastUpdate = gSnippetsMap.get("snippets-last-update"); let lastUpdate = localStorage["snippets-last-update"];
let updateURL = document.documentElement.getAttribute("snippetsURL"); let updateURL = document.documentElement.getAttribute("snippetsURL");
let shouldUpdate = !lastUpdate || if (updateURL && (!lastUpdate ||
Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS; Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS)) {
if (updateURL && shouldUpdate) {
// Try to update from network. // Try to update from network.
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
try { try {
@ -254,15 +174,14 @@ function loadSnippets()
} }
// Even if fetching should fail we don't want to spam the server, thus // 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. // set the last update time regardless its results. Will retry tomorrow.
gSnippetsMap.set("snippets-last-update", Date.now()); localStorage["snippets-last-update"] = Date.now();
xhr.onerror = function (event) { xhr.onerror = function (event) {
showSnippets(); showSnippets();
}; };
xhr.onload = function (event) xhr.onload = function (event)
{ {
if (xhr.status == 200) { if (xhr.status == 200) {
gSnippetsMap.set("snippets", xhr.responseText); localStorage["snippets"] = xhr.responseText;
gSnippetsMap.set("snippets-cached-version", currentVersion);
} }
showSnippets(); showSnippets();
}; };
@ -272,27 +191,10 @@ 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() 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 snippetsElt = document.getElementById("snippets");
let snippets = gSnippetsMap.get("snippets"); let snippets = localStorage["snippets"];
// If there are remotely fetched snippets, try to to show them. // If there are remotely fetched snippets, try to to show them.
if (snippets) { if (snippets) {
// Injecting snippets can throw if they're invalid XML. // Injecting snippets can throw if they're invalid XML.
@ -312,19 +214,7 @@ function showSnippets()
} }
} }
showDefaultSnippets(); // Show default snippets otherwise.
}
/**
* 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 defaultSnippetsElt = document.getElementById("defaultSnippets");
let entries = defaultSnippetsElt.querySelectorAll("span"); let entries = defaultSnippetsElt.querySelectorAll("span");
// Choose a random snippet. Assume there is always at least one. // Choose a random snippet. Assume there is always at least one.

View File

@ -2635,7 +2635,6 @@ function BrowserOnAboutPageLoad(doc) {
// Inject search engine and snippets URL. // Inject search engine and snippets URL.
let docElt = doc.documentElement; let docElt = doc.documentElement;
docElt.setAttribute("snippetsURL", AboutHomeUtils.snippetsURL); docElt.setAttribute("snippetsURL", AboutHomeUtils.snippetsURL);
docElt.setAttribute("snippetsVersion", AboutHomeUtils.snippetsVersion);
docElt.setAttribute("searchEngineName", docElt.setAttribute("searchEngineName",
AboutHomeUtils.defaultSearchEngine.name); AboutHomeUtils.defaultSearchEngine.name);
docElt.setAttribute("searchEngineURL", docElt.setAttribute("searchEngineURL",

View File

@ -2,13 +2,6 @@
* http://creativecommons.org/publicdomain/zero/1.0/ * 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");
XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
"resource:///modules/AboutHomeUtils.jsm");
registerCleanupFunction(function() { registerCleanupFunction(function() {
// Ensure we don't pollute prefs for next tests. // Ensure we don't pollute prefs for next tests.
try { try {
@ -29,15 +22,19 @@ let gTests = [
.getService(Ci.nsIObserver) .getService(Ci.nsIObserver)
.observe(null, "cookie-changed", "cleared"); .observe(null, "cookie-changed", "cleared");
}, },
run: function (aSnippetsMap) run: function ()
{ {
isnot(aSnippetsMap.get("snippets-last-update"), null); let storage = getStorage();
isnot(storage.getItem("snippets-last-update"), null);
executeSoon(runNextTest);
} }
}, },
{ {
desc: "Check default snippets are shown", desc: "Check default snippets are shown",
setup: function () { }, setup: function ()
{
},
run: function () run: function ()
{ {
let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
@ -45,17 +42,19 @@ let gTests = [
ok(snippetsElt, "Found snippets element") ok(snippetsElt, "Found snippets element")
is(snippetsElt.getElementsByTagName("span").length, 1, is(snippetsElt.getElementsByTagName("span").length, 1,
"A default snippet is visible."); "A default snippet is visible.");
executeSoon(runNextTest);
} }
}, },
{ {
desc: "Check default snippets are shown if snippets are invalid xml", desc: "Check default snippets are shown if snippets are invalid xml",
setup: function (aSnippetsMap) setup: function ()
{ {
let storage = getStorage();
// This must be some incorrect xhtml code. // This must be some incorrect xhtml code.
aSnippetsMap.set("snippets", "<p><b></p></b>"); storage.setItem("snippets", "<p><b></p></b>");
}, },
run: function (aSnippetsMap) run: function ()
{ {
let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
@ -63,14 +62,16 @@ let gTests = [
ok(snippetsElt, "Found snippets element"); ok(snippetsElt, "Found snippets element");
is(snippetsElt.getElementsByTagName("span").length, 1, is(snippetsElt.getElementsByTagName("span").length, 1,
"A default snippet is visible."); "A default snippet is visible.");
let storage = getStorage();
aSnippetsMap.delete("snippets"); storage.removeItem("snippets");
executeSoon(runNextTest);
} }
}, },
{ {
desc: "Check that search engine logo has alt text", desc: "Check that search engine logo has alt text",
setup: function () { }, setup: function ()
{
},
run: function () run: function ()
{ {
let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
@ -84,38 +85,37 @@ let gTests = [
isnot(altText, "undefined", isnot(altText, "undefined",
"Search engine logo's alt text shouldn't be the string '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.", desc: "Check that performing a search fires a search event.",
setup: function () { }, setup: function () { },
run: function () { run: function () {
let deferred = Promise.defer();
let doc = gBrowser.contentDocument; let doc = gBrowser.contentDocument;
doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) { doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) {
is(e.detail, doc.documentElement.getAttribute("searchEngineName"), "Detail is search engine name"); is(e.detail, doc.documentElement.getAttribute("searchEngineName"), "Detail is search engine name");
gBrowser.stop(); gBrowser.stop();
deferred.resolve(); executeSoon(runNextTest);
}, true, true); }, true, true);
doc.getElementById("searchText").value = "it works"; doc.getElementById("searchText").value = "it works";
doc.getElementById("searchSubmit").click(); doc.getElementById("searchSubmit").click();
return deferred.promise; },
}
}, },
{ {
desc: "Check that performing a search records to Firefox Health Report.", desc: "Check that performing a search records to Firefox Health Report.",
setup: function () { }, setup: function () { },
run: function () { run: function () {
if (!("@mozilla.org/datareporting/service;1" in Components.classes)) { if (!("@mozilla.org/datareporting/service;1" in Components.classes)) {
runNextTest();
return; return;
} }
let deferred = Promise.defer();
let doc = gBrowser.contentDocument; let doc = gBrowser.contentDocument;
// We rely on the listener in browser.js being installed and fired before // We rely on the listener in browser.js being installed and fired before
@ -146,7 +146,7 @@ let gTests = [
// Note the search from the previous test. // Note the search from the previous test.
is(day.get(field), 2, "Have searches recorded."); is(day.get(field), 2, "Have searches recorded.");
deferred.resolve(); executeSoon(runNextTest);
}); });
}); });
@ -154,165 +154,62 @@ let gTests = [
doc.getElementById("searchText").value = "a search"; doc.getElementById("searchText").value = "a search";
doc.getElementById("searchSubmit").click(); doc.getElementById("searchSubmit").click();
return deferred.promise;
}
},
{
desc: "Check default snippets are shown if cached version is old and fetch fails",
setup: function (aSnippetsMap)
{
aSnippetsMap.set("snippets", "test");
aSnippetsMap.set("snippets-cached-version", 0);
}, },
run: function (aSnippetsMap)
{
let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
let snippetsElt = doc.getElementById("snippets");
ok(snippetsElt, "Found snippets element");
is(snippetsElt.getElementsByTagName("span").length, 1,
"A default snippet is present.");
ok(!aSnippetsMap.has("snippets"), "snippets have been properly cleared");
ok(!aSnippetsMap.has("snippets-cached-version"),
"cached-version has been properly cleared");
ok(!aSnippetsMap.has("snippets-last-update"),
"last-update has been properly cleared");
}
}, },
{
desc: "Check cached snippets are shown if cached version is current",
setup: function (aSnippetsMap)
{
aSnippetsMap.set("snippets", "test");
aSnippetsMap.set("snippets-cached-version", AboutHomeUtils.snippetsVersion);
},
run: function (aSnippetsMap)
{
let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
let snippetsElt = doc.getElementById("snippets");
ok(snippetsElt, "Found snippets element");
is(snippetsElt.innerHTML, "test", "Cached snippet is present.");
is(aSnippetsMap.get("snippets"), "test", "snippets still cached");
is(aSnippetsMap.get("snippets-cached-version"),
AboutHomeUtils.snippetsVersion,
"cached-version is correct");
ok(aSnippetsMap.has("snippets-last-update"), "last-update still exists");
}
},
]; ];
function test() function test()
{ {
waitForExplicitFinish(); waitForExplicitFinish();
Task.spawn(function () { // Ensure that by default we don't try to check for remote snippets since that
for (let test of gTests) { // could be tricky due to network bustages or slowness.
info(test.desc); let storage = getStorage();
storage.setItem("snippets-last-update", Date.now());
storage.removeItem("snippets");
let tab = yield promiseNewTabLoadEvent("about:home", "DOMContentLoaded"); executeSoon(runNextTest);
}
// Must wait for both the snippets map and the browser attributes, since function runNextTest()
// can't guess the order they will happen. {
// So, start listening now, but verify the promise is fulfilled only while (gBrowser.tabs.length > 1) {
// after the snippets map setup. gBrowser.removeCurrentTab();
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;
yield test.run(snippetsMap); if (gTests.length) {
let test = gTests.shift();
gBrowser.removeCurrentTab(); 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);
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(); 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 deferred = Promise.defer(); let aboutHomeURI = Services.io.newURI("moz-safe-about:home", null, null);
let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl); let principal = Components.classes["@mozilla.org/scriptsecuritymanager;1"].
tab.linkedBrowser.addEventListener(aEventType, function load(event) { getService(Components.interfaces.nsIScriptSecurityManager).
tab.linkedBrowser.removeEventListener(aEventType, load, true); getNoAppCodebasePrincipal(Services.io.newURI("about:home", null, null));
deferred.resolve(tab); let dsm = Components.classes["@mozilla.org/dom/storagemanager;1"].
}, true); getService(Components.interfaces.nsIDOMStorageManager);
return deferred.promise; return dsm.getLocalStorageForPrincipal(principal, "");
} };
/**
* 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;
}

View File

@ -13,11 +13,9 @@ Components.utils.import("resource://gre/modules/Services.jsm");
const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl"; const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
// Should be bumped up if the snippets content format changes. // Should be bumped up if the snippets content format changes.
const STARTPAGE_VERSION = 4; const STARTPAGE_VERSION = 3;
this.AboutHomeUtils = { this.AboutHomeUtils = new Object();
get snippetsVersion() STARTPAGE_VERSION
};
/** /**
* Returns an object containing the name and searchURL of the original default * Returns an object containing the name and searchURL of the original default