michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK; michael@0: const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED; michael@0: const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK; michael@0: const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT; michael@0: const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY; michael@0: const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED; michael@0: const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK; michael@0: const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD; michael@0: michael@0: Components.utils.import("resource://gre/modules/NetUtil.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: /** michael@0: * Allows waiting for an observer notification once. michael@0: * michael@0: * @param aTopic michael@0: * Notification topic to observe. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The array [aSubject, aData] from the observed notification. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseTopicObserved(aTopic) michael@0: { michael@0: let deferred = Promise.defer(); michael@0: michael@0: Services.obs.addObserver( michael@0: function PTO_observe(aSubject, aTopic, aData) { michael@0: Services.obs.removeObserver(PTO_observe, aTopic); michael@0: deferred.resolve([aSubject, aData]); michael@0: }, aTopic, false); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Clears history asynchronously. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When history has been cleared. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseClearHistory() { michael@0: let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); michael@0: PlacesUtils.bhistory.removeAllPages(); michael@0: return promise; michael@0: } michael@0: michael@0: /** michael@0: * Waits for all pending async statements on the default connection. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When all pending async statements finished. michael@0: * @rejects Never. michael@0: * michael@0: * @note The result is achieved by asynchronously executing a query requiring michael@0: * a write lock. Since all statements on the same connection are michael@0: * serialized, the end of this write operation means that all writes are michael@0: * complete. Note that WAL makes so that writers don't block readers, but michael@0: * this is a problem only across different connections. michael@0: */ michael@0: function promiseAsyncUpdates() michael@0: { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); michael@0: begin.executeAsync(); michael@0: begin.finalize(); michael@0: michael@0: let commit = db.createAsyncStatement("COMMIT"); michael@0: commit.executeAsync({ michael@0: handleResult: function() {}, michael@0: handleError: function() {}, michael@0: handleCompletion: function(aReason) michael@0: { michael@0: deferred.resolve(); michael@0: } michael@0: }); michael@0: commit.finalize(); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Returns a moz_places field value for a url. michael@0: * michael@0: * @param aURI michael@0: * The URI or spec to get field for. michael@0: * param aCallback michael@0: * Callback function that will get the property value. michael@0: */ michael@0: function fieldForUrl(aURI, aFieldName, aCallback) michael@0: { michael@0: let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; michael@0: let stmt = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection.createAsyncStatement( michael@0: "SELECT " + aFieldName + " FROM moz_places WHERE url = :page_url" michael@0: ); michael@0: stmt.params.page_url = url; michael@0: stmt.executeAsync({ michael@0: _value: -1, michael@0: handleResult: function(aResultSet) { michael@0: let row = aResultSet.getNextRow(); michael@0: if (!row) michael@0: ok(false, "The page should exist in the database"); michael@0: this._value = row.getResultByName(aFieldName); michael@0: }, michael@0: handleError: function() {}, michael@0: handleCompletion: function(aReason) { michael@0: if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) michael@0: ok(false, "The statement should properly succeed"); michael@0: aCallback(this._value); michael@0: } michael@0: }); michael@0: stmt.finalize(); michael@0: } michael@0: michael@0: /** michael@0: * Generic nsINavHistoryObserver that doesn't implement anything, but provides michael@0: * dummy methods to prevent errors about an object not having a certain method. michael@0: */ michael@0: function NavHistoryObserver() {} michael@0: michael@0: NavHistoryObserver.prototype = { michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: onVisit: function () {}, michael@0: onTitleChanged: function () {}, michael@0: onDeleteURI: function () {}, michael@0: onClearHistory: function () {}, michael@0: onPageChanged: function () {}, michael@0: onDeleteVisits: function () {}, michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsINavHistoryObserver, michael@0: ]) michael@0: }; michael@0: michael@0: /** michael@0: * Waits for the first OnPageChanged notification for ATTRIBUTE_FAVICON, and michael@0: * verifies that it matches the expected page URI and associated favicon URI. michael@0: * michael@0: * This function also double-checks the GUID parameter of the notification. michael@0: * michael@0: * @param aExpectedPageURI michael@0: * nsIURI object of the page whose favicon should change. michael@0: * @param aExpectedFaviconURI michael@0: * nsIURI object of the newly associated favicon. michael@0: * @param aCallback michael@0: * This function is called after the check finished. michael@0: */ michael@0: function waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, aWindow, michael@0: aCallback) { michael@0: let historyObserver = { michael@0: __proto__: NavHistoryObserver.prototype, michael@0: onPageChanged: function WFFC_onPageChanged(aURI, aWhat, aValue, aGUID) { michael@0: if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { michael@0: return; michael@0: } michael@0: aWindow.PlacesUtils.history.removeObserver(this); michael@0: michael@0: ok(aURI.equals(aExpectedPageURI), michael@0: "Check URIs are equal for the page which favicon changed"); michael@0: is(aValue, aExpectedFaviconURI.spec, michael@0: "Check changed favicon URI is the expected"); michael@0: checkGuidForURI(aURI, aGUID); michael@0: michael@0: if (aCallback) { michael@0: aCallback(); michael@0: } michael@0: } michael@0: }; michael@0: aWindow.PlacesUtils.history.addObserver(historyObserver, false); michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously adds visits to a page, invoking a callback function when done. michael@0: * michael@0: * @param aPlaceInfo michael@0: * Either an nsIURI, in such a case a single LINK visit will be added. michael@0: * Or can be an object describing the visit to add, or an array michael@0: * of these objects: michael@0: * { uri: nsIURI of the page, michael@0: * transition: one of the TRANSITION_* from nsINavHistoryService, michael@0: * [optional] title: title of the page, michael@0: * [optional] visitDate: visit date in microseconds from the epoch michael@0: * [optional] referrer: nsIURI of the referrer for this visit michael@0: * } michael@0: * @param [optional] aCallback michael@0: * Function to be invoked on completion. michael@0: * @param [optional] aStack michael@0: * The stack frame used to report errors. michael@0: */ michael@0: function addVisits(aPlaceInfo, aWindow, aCallback, aStack) { michael@0: let stack = aStack || Components.stack.caller; michael@0: let places = []; michael@0: if (aPlaceInfo instanceof Ci.nsIURI) { michael@0: places.push({ uri: aPlaceInfo }); michael@0: } michael@0: else if (Array.isArray(aPlaceInfo)) { michael@0: places = places.concat(aPlaceInfo); michael@0: } else { michael@0: places.push(aPlaceInfo) michael@0: } michael@0: michael@0: // Create mozIVisitInfo for each entry. michael@0: let now = Date.now(); michael@0: for (let place of places) { michael@0: if (!place.title) { michael@0: place.title = "test visit for " + place.uri.spec; michael@0: } michael@0: place.visits = [{ michael@0: transitionType: place.transition === undefined ? TRANSITION_LINK michael@0: : place.transition, michael@0: visitDate: place.visitDate || (now++) * 1000, michael@0: referrerURI: place.referrer michael@0: }]; michael@0: } michael@0: michael@0: aWindow.PlacesUtils.asyncHistory.updatePlaces( michael@0: places, michael@0: { michael@0: handleError: function AAV_handleError() { michael@0: throw("Unexpected error in adding visit."); michael@0: }, michael@0: handleResult: function () {}, michael@0: handleCompletion: function UP_handleCompletion() { michael@0: if (aCallback) michael@0: aCallback(); michael@0: } michael@0: } michael@0: ); michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously adds visits to a page. michael@0: * michael@0: * @param aPlaceInfo michael@0: * Can be an nsIURI, in such a case a single LINK visit will be added. michael@0: * Otherwise can be an object describing the visit to add, or an array michael@0: * of these objects: michael@0: * { uri: nsIURI of the page, michael@0: * transition: one of the TRANSITION_* from nsINavHistoryService, michael@0: * [optional] title: title of the page, michael@0: * [optional] visitDate: visit date in microseconds from the epoch michael@0: * [optional] referrer: nsIURI of the referrer for this visit michael@0: * } michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When all visits have been added successfully. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: function promiseAddVisits(aPlaceInfo) michael@0: { michael@0: let deferred = Promise.defer(); michael@0: let places = []; michael@0: if (aPlaceInfo instanceof Ci.nsIURI) { michael@0: places.push({ uri: aPlaceInfo }); michael@0: } michael@0: else if (Array.isArray(aPlaceInfo)) { michael@0: places = places.concat(aPlaceInfo); michael@0: } else { michael@0: places.push(aPlaceInfo) michael@0: } michael@0: michael@0: // Create mozIVisitInfo for each entry. michael@0: let now = Date.now(); michael@0: for (let i = 0; i < places.length; i++) { michael@0: if (!places[i].title) { michael@0: places[i].title = "test visit for " + places[i].uri.spec; michael@0: } michael@0: places[i].visits = [{ michael@0: transitionType: places[i].transition === undefined ? TRANSITION_LINK michael@0: : places[i].transition, michael@0: visitDate: places[i].visitDate || (now++) * 1000, michael@0: referrerURI: places[i].referrer michael@0: }]; michael@0: } michael@0: michael@0: PlacesUtils.asyncHistory.updatePlaces( michael@0: places, michael@0: { michael@0: handleError: function AAV_handleError(aResultCode, aPlaceInfo) { michael@0: let ex = new Components.Exception("Unexpected error in adding visits.", michael@0: aResultCode); michael@0: deferred.reject(ex); michael@0: }, michael@0: handleResult: function () {}, michael@0: handleCompletion: function UP_handleCompletion() { michael@0: deferred.resolve(); michael@0: } michael@0: } michael@0: ); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Checks that the favicon for the given page matches the provided data. michael@0: * michael@0: * @param aPageURI michael@0: * nsIURI object for the page to check. michael@0: * @param aExpectedMimeType michael@0: * Expected MIME type of the icon, for example "image/png". michael@0: * @param aExpectedData michael@0: * Expected icon data, expressed as an array of byte values. michael@0: * @param aCallback michael@0: * This function is called after the check finished. michael@0: */ michael@0: function checkFaviconDataForPage(aPageURI, aExpectedMimeType, aExpectedData, michael@0: aWindow, aCallback) { michael@0: aWindow.PlacesUtils.favicons.getFaviconDataForPage(aPageURI, michael@0: function (aURI, aDataLen, aData, aMimeType) { michael@0: is(aExpectedMimeType, aMimeType, "Check expected MimeType"); michael@0: is(aExpectedData.length, aData.length, michael@0: "Check favicon data for the given page matches the provided data"); michael@0: checkGuidForURI(aPageURI); michael@0: aCallback(); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Tests that a guid was set in moz_places for a given uri. michael@0: * michael@0: * @param aURI michael@0: * The uri to check. michael@0: * @param [optional] aGUID michael@0: * The expected guid in the database. michael@0: */ michael@0: function checkGuidForURI(aURI, aGUID) { michael@0: let guid = doGetGuidForURI(aURI); michael@0: if (aGUID) { michael@0: doCheckValidPlacesGuid(aGUID); michael@0: is(guid, aGUID, "Check equal guid for URIs"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Retrieves the guid for a given uri. michael@0: * michael@0: * @param aURI michael@0: * The uri to check. michael@0: * @return the associated the guid. michael@0: */ michael@0: function doGetGuidForURI(aURI) { michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT guid " michael@0: + "FROM moz_places " michael@0: + "WHERE url = :url " michael@0: ); michael@0: stmt.params.url = aURI.spec; michael@0: ok(stmt.executeStep(), "Check get guid for uri from moz_places"); michael@0: let guid = stmt.row.guid; michael@0: stmt.finalize(); michael@0: doCheckValidPlacesGuid(guid); michael@0: return guid; michael@0: } michael@0: michael@0: /** michael@0: * Tests if a given guid is valid for use in Places or not. michael@0: * michael@0: * @param aGuid michael@0: * The guid to test. michael@0: */ michael@0: function doCheckValidPlacesGuid(aGuid) { michael@0: ok(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), "Check guid for valid places"); michael@0: } michael@0: michael@0: /** michael@0: * Gets the database connection. If the Places connection is invalid it will michael@0: * try to create a new connection. michael@0: * michael@0: * @param [optional] aForceNewConnection michael@0: * Forces creation of a new connection to the database. When a michael@0: * connection is asyncClosed it cannot anymore schedule async statements, michael@0: * though connectionReady will keep returning true (Bug 726990). michael@0: * michael@0: * @return The database connection or null if unable to get one. michael@0: */ michael@0: function DBConn(aForceNewConnection) { michael@0: let gDBConn; michael@0: if (!aForceNewConnection) { michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: if (db.connectionReady) michael@0: return db; michael@0: } michael@0: michael@0: // If the Places database connection has been closed, create a new connection. michael@0: if (!gDBConn || aForceNewConnection) { michael@0: let file = Services.dirsvc.get('ProfD', Ci.nsIFile); michael@0: file.append("places.sqlite"); michael@0: let dbConn = gDBConn = Services.storage.openDatabase(file); michael@0: michael@0: // Be sure to cleanly close this connection. michael@0: Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) { michael@0: Services.obs.removeObserver(DBCloseCallback, aTopic); michael@0: dbConn.asyncClose(); michael@0: }, "profile-before-change", false); michael@0: } michael@0: michael@0: return gDBConn.connectionReady ? gDBConn : null; michael@0: } michael@0: michael@0: function whenDelayedStartupFinished(aWindow, aCallback) { michael@0: Services.obs.addObserver(function observer(aSubject, aTopic) { michael@0: if (aWindow == aSubject) { michael@0: Services.obs.removeObserver(observer, aTopic); michael@0: executeSoon(function() { aCallback(aWindow); }); michael@0: } michael@0: }, "browser-delayed-startup-finished", false); michael@0: } michael@0: michael@0: function whenNewWindowLoaded(aOptions, aCallback) { michael@0: let win = OpenBrowserWindow(aOptions); michael@0: whenDelayedStartupFinished(win, aCallback); michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously check a url is visited. michael@0: * michael@0: * @param aURI The URI. michael@0: * @param aExpectedValue The expected value. michael@0: * @return {Promise} michael@0: * @resolves When the check has been added successfully. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: function promiseIsURIVisited(aURI, aExpectedValue) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { michael@0: deferred.resolve(aIsVisited); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function waitForCondition(condition, nextTest, errorMsg) { michael@0: let tries = 0; michael@0: let interval = setInterval(function() { michael@0: if (tries >= 30) { michael@0: ok(false, errorMsg); michael@0: moveOn(); michael@0: } michael@0: let conditionPassed; michael@0: try { michael@0: conditionPassed = condition(); michael@0: } catch (e) { michael@0: ok(false, e + "\n" + e.stack); michael@0: conditionPassed = false; michael@0: } michael@0: if (conditionPassed) { michael@0: moveOn(); michael@0: } michael@0: tries++; michael@0: }, 200); michael@0: function moveOn() { michael@0: clearInterval(interval); michael@0: nextTest(); michael@0: }; michael@0: }