From 1a20b2f1010b4662dad963f176156a46e8df9c1f Mon Sep 17 00:00:00 2001 From: "Mario Alvarado [:marioalv]" Date: Thu, 6 Dec 2012 01:48:46 -0600 Subject: [PATCH] Bug 806739 - Port test_setAndFetchFaviconForPage.js to the new per-tab PB APIs; r=ehsan DONTBUILD since this is NPOTB for global PB builds --HG-- rename : toolkit/components/places/tests/favicons/test_setAndFetchFaviconForPage.js => toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js --- .../places/tests/browser/Makefile.in | 1 + ...owser_favicon_setAndFetchFaviconForPage.js | 145 +++++++++++ .../components/places/tests/browser/head.js | 234 ++++++++++++++++++ 3 files changed, 380 insertions(+) create mode 100644 toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js diff --git a/toolkit/components/places/tests/browser/Makefile.in b/toolkit/components/places/tests/browser/Makefile.in index e65f9c21185..2d3abc8376e 100644 --- a/toolkit/components/places/tests/browser/Makefile.in +++ b/toolkit/components/places/tests/browser/Makefile.in @@ -32,6 +32,7 @@ MOCHITEST_BROWSER_FILES = \ ifdef MOZ_PER_WINDOW_PRIVATE_BROWSING MOCHITEST_BROWSER_FILES += \ browser_bug248970.js \ + browser_favicon_setAndFetchFaviconForPage.js \ $(NULL) else MOCHITEST_BROWSER_FILES += \ diff --git a/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js new file mode 100644 index 00000000000..86021d9d6b8 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js @@ -0,0 +1,145 @@ +/* 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/. */ + +// This file tests the normal operation of setAndFetchFaviconForPage. +function test() { + // Initialization + waitForExplicitFinish(); + let windowsToClose = []; + let testURI = "https://www.mozilla.org/en-US/"; + let favIconLocation = + "http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal32.png"; + let favIconURI = NetUtil.newURI(favIconLocation); + let favIconMimeType= "image/png"; + let pageURI; + let favIconData; + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function(aWin) { + windowsToClose.push(aWin); + executeSoon(function() aCallback(aWin)); + }); + }; + + // This function is called after calling finish() on the test. + registerCleanupFunction(function() { + windowsToClose.forEach(function(aWin) { + aWin.close(); + }); + }); + + function getIconFile(aCallback) { + NetUtil.asyncFetch(favIconLocation, function(inputStream, status) { + if (!Components.isSuccessCode(status)) { + ok(false, "Could not get the icon file"); + // Handle error. + return; + } + + // Check the returned size versus the expected size. + let size = inputStream.available(); + favIconData = NetUtil.readInputStreamToString(inputStream, size); + is(size, favIconData.length, "Check correct icon size"); + // Check that the favicon loaded correctly before starting the actual tests. + is(favIconData.length, 344, "Check correct icon length (344)"); + + if (aCallback) { + aCallback(); + } else { + finish(); + } + }); + } + + function testNormal(aWindow, aCallback) { + pageURI = NetUtil.newURI("http://example.com/normal"); + waitForFaviconChanged(pageURI, favIconURI, aWindow, + function testNormalCallback() { + checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow, + aCallback); + } + ); + + addVisits({uri: pageURI, transition: TRANSITION_TYPED}, aWindow, + function () { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, + favIconURI, true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE); + } + ); + } + + function testAboutURIBookmarked(aWindow, aCallback) { + pageURI = NetUtil.newURI("about:testAboutURI_bookmarked"); + waitForFaviconChanged(pageURI, favIconURI, aWindow, + function testAboutURIBookmarkedCallback() { + checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow, + aCallback); + } + ); + + aWindow.PlacesUtils.bookmarks.insertBookmark( + aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI, + aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec); + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE); + } + + function testPrivateBrowsingBookmarked(aWindow, aCallback) { + pageURI = NetUtil.newURI("http://example.com/privateBrowsing_bookmarked"); + waitForFaviconChanged(pageURI, favIconURI, aWindow, + function testPrivateBrowsingBookmarkedCallback() { + checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow, + aCallback); + } + ); + + aWindow.PlacesUtils.bookmarks.insertBookmark( + aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI, + aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec); + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_PRIVATE); + } + + function testDisabledHistoryBookmarked(aWindow, aCallback) { + pageURI = NetUtil.newURI("http://example.com/disabledHistory_bookmarked"); + waitForFaviconChanged(pageURI, favIconURI, aWindow, + function testDisabledHistoryBookmarkedCallback() { + checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow, + aCallback); + } + ); + + // Disable history while changing the favicon. + aWindow.Services.prefs.setBoolPref("places.history.enabled", false); + + aWindow.PlacesUtils.bookmarks.insertBookmark( + aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI, + aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec); + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE); + + // The setAndFetchFaviconForPage function calls CanAddURI synchronously, thus + // we can set the preference back to true immediately. We don't clear the + // preference because not all products enable Places by default. + aWindow.Services.prefs.setBoolPref("places.history.enabled", true); + } + + getIconFile(function () { + testOnWindow({}, function(aWin) { + testNormal(aWin, function () { + testOnWindow({}, function(aWin) { + testAboutURIBookmarked(aWin, function () { + testOnWindow({private: true}, function(aWin) { + testPrivateBrowsingBookmarked(aWin, function () { + testOnWindow({}, function(aWin) { + testDisabledHistoryBookmarked(aWin, finish); + }); + }); + }); + }); + }); + }); + }); + }); +} diff --git a/toolkit/components/places/tests/browser/head.js b/toolkit/components/places/tests/browser/head.js index c947ec017cc..f1aab4146aa 100644 --- a/toolkit/components/places/tests/browser/head.js +++ b/toolkit/components/places/tests/browser/head.js @@ -1,5 +1,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ +const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK; +const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED; Components.utils.import("resource://gre/modules/NetUtil.jsm"); @@ -112,3 +114,235 @@ function fieldForUrl(aURI, aFieldName, aCallback) }); stmt.finalize(); } + +function whenNewWindowLoaded(aOptions, aCallback) { + let win = OpenBrowserWindow(aOptions); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + aCallback(win); + }, false); +} + +/** + * Generic nsINavHistoryObserver that doesn't implement anything, but provides + * dummy methods to prevent errors about an object not having a certain method. + */ +function NavHistoryObserver() {} + +NavHistoryObserver.prototype = { + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onVisit: function () {}, + onTitleChanged: function () {}, + onBeforeDeleteURI: function () {}, + onDeleteURI: function () {}, + onClearHistory: function () {}, + onPageChanged: function () {}, + onDeleteVisits: function () {}, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavHistoryObserver, + ]) +}; + +/** + * Waits for the first OnPageChanged notification for ATTRIBUTE_FAVICON, and + * verifies that it matches the expected page URI and associated favicon URI. + * + * This function also double-checks the GUID parameter of the notification. + * + * @param aExpectedPageURI + * nsIURI object of the page whose favicon should change. + * @param aExpectedFaviconURI + * nsIURI object of the newly associated favicon. + * @param aCallback + * This function is called after the check finished. + */ +function waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, aWindow, + aCallback) { + let historyObserver = { + __proto__: NavHistoryObserver.prototype, + onPageChanged: function WFFC_onPageChanged(aURI, aWhat, aValue, aGUID) { + if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { + return; + } + aWindow.PlacesUtils.history.removeObserver(this); + + ok(aURI.equals(aExpectedPageURI), + "Check URIs are equal for the page which favicon changed"); + is(aValue, aExpectedFaviconURI.spec, + "Check changed favicon URI is the expected"); + checkGuidForURI(aURI, aGUID); + + if (aCallback) { + aCallback(); + } + } + }; + aWindow.PlacesUtils.history.addObserver(historyObserver, false); +} + +/** + * Asynchronously adds visits to a page, invoking a callback function when done. + * + * @param aPlaceInfo + * Can be an nsIURI, in such a case a single LINK visit will be added. + * Otherwise can be an object describing the visit to add, or an array + * of these objects: + * { uri: nsIURI of the page, + * transition: one of the TRANSITION_* from nsINavHistoryService, + * [optional] title: title of the page, + * [optional] visitDate: visit date in microseconds from the epoch + * [optional] referrer: nsIURI of the referrer for this visit + * } + * @param [optional] aCallback + * Function to be invoked on completion. + * @param [optional] aStack + * The stack frame used to report errors. + */ +function addVisits(aPlaceInfo, aWindow, aCallback, aStack) { + let stack = aStack || Components.stack.caller; + let places = []; + if (aPlaceInfo instanceof Ci.nsIURI) { + places.push({ uri: aPlaceInfo }); + } + else if (Array.isArray(aPlaceInfo)) { + places = places.concat(aPlaceInfo); + } else { + places.push(aPlaceInfo) + } + + // Create mozIVisitInfo for each entry. + let now = Date.now(); + for (let i = 0; i < places.length; i++) { + if (!places[i].title) { + places[i].title = "test visit for " + places[i].uri.spec; + } + places[i].visits = [{ + transitionType: places[i].transition === undefined ? TRANSITION_LINK + : places[i].transition, + visitDate: places[i].visitDate || (now++) * 1000, + referrerURI: places[i].referrer + }]; + } + + aWindow.PlacesUtils.asyncHistory.updatePlaces( + places, + { + handleError: function AAV_handleError() { + throw("Unexpected error in adding visit."); + }, + handleResult: function () {}, + handleCompletion: function UP_handleCompletion() { + if (aCallback) + aCallback(); + } + } + ); +} + +/** + * Checks that the favicon for the given page matches the provided data. + * + * @param aPageURI + * nsIURI object for the page to check. + * @param aExpectedMimeType + * Expected MIME type of the icon, for example "image/png". + * @param aExpectedData + * Expected icon data, expressed as an array of byte values. + * @param aCallback + * This function is called after the check finished. + */ +function checkFaviconDataForPage(aPageURI, aExpectedMimeType, aExpectedData, + aWindow, aCallback) { + aWindow.PlacesUtils.favicons.getFaviconDataForPage(aPageURI, + function (aURI, aDataLen, aData, aMimeType) { + is(aExpectedMimeType, aMimeType, "Check expected MimeType"); + is(aExpectedData.length, aData.length, + "Check favicon data for the given page matches the provided data"); + checkGuidForURI(aPageURI); + aCallback(); + }); +} + +/** + * Tests that a guid was set in moz_places for a given uri. + * + * @param aURI + * The uri to check. + * @param [optional] aGUID + * The expected guid in the database. + */ +function checkGuidForURI(aURI, aGUID) { + let guid = doGetGuidForURI(aURI); + if (aGUID) { + doCheckValidPlacesGuid(aGUID); + is(guid, aGUID, "Check equal guid for URIs"); + } +} + +/** + * Retrieves the guid for a given uri. + * + * @param aURI + * The uri to check. + * @return the associated the guid. + */ +function doGetGuidForURI(aURI) { + let stmt = DBConn().createStatement( + "SELECT guid " + + "FROM moz_places " + + "WHERE url = :url " + ); + stmt.params.url = aURI.spec; + ok(stmt.executeStep(), "Check get guid for uri from moz_places"); + let guid = stmt.row.guid; + stmt.finalize(); + doCheckValidPlacesGuid(guid); + return guid; +} + +/** + * Tests if a given guid is valid for use in Places or not. + * + * @param aGuid + * The guid to test. + */ +function doCheckValidPlacesGuid(aGuid) { + ok(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), "Check guid for valid places"); +} + +/** + * Gets the database connection. If the Places connection is invalid it will + * try to create a new connection. + * + * @param [optional] aForceNewConnection + * Forces creation of a new connection to the database. When a + * connection is asyncClosed it cannot anymore schedule async statements, + * though connectionReady will keep returning true (Bug 726990). + * + * @return The database connection or null if unable to get one. + */ +function DBConn(aForceNewConnection) { + let gDBConn; + if (!aForceNewConnection) { + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; + if (db.connectionReady) + return db; + } + + // If the Places database connection has been closed, create a new connection. + if (!gDBConn || aForceNewConnection) { + let file = Services.dirsvc.get('ProfD', Ci.nsIFile); + file.append("places.sqlite"); + let dbConn = gDBConn = Services.storage.openDatabase(file); + + // Be sure to cleanly close this connection. + Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) { + Services.obs.removeObserver(DBCloseCallback, aTopic); + dbConn.asyncClose(); + }, "profile-before-change", false); + } + + return gDBConn.connectionReady ? gDBConn : null; +}