1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/tests/browser/head.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,468 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 +const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK; 1.7 +const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED; 1.8 +const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK; 1.9 +const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT; 1.10 +const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY; 1.11 +const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED; 1.12 +const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK; 1.13 +const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD; 1.14 + 1.15 +Components.utils.import("resource://gre/modules/NetUtil.jsm"); 1.16 + 1.17 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.18 + "resource://gre/modules/Promise.jsm"); 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.20 + "resource://gre/modules/Task.jsm"); 1.21 + 1.22 +/** 1.23 + * Allows waiting for an observer notification once. 1.24 + * 1.25 + * @param aTopic 1.26 + * Notification topic to observe. 1.27 + * 1.28 + * @return {Promise} 1.29 + * @resolves The array [aSubject, aData] from the observed notification. 1.30 + * @rejects Never. 1.31 + */ 1.32 +function promiseTopicObserved(aTopic) 1.33 +{ 1.34 + let deferred = Promise.defer(); 1.35 + 1.36 + Services.obs.addObserver( 1.37 + function PTO_observe(aSubject, aTopic, aData) { 1.38 + Services.obs.removeObserver(PTO_observe, aTopic); 1.39 + deferred.resolve([aSubject, aData]); 1.40 + }, aTopic, false); 1.41 + 1.42 + return deferred.promise; 1.43 +} 1.44 + 1.45 +/** 1.46 + * Clears history asynchronously. 1.47 + * 1.48 + * @return {Promise} 1.49 + * @resolves When history has been cleared. 1.50 + * @rejects Never. 1.51 + */ 1.52 +function promiseClearHistory() { 1.53 + let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); 1.54 + PlacesUtils.bhistory.removeAllPages(); 1.55 + return promise; 1.56 +} 1.57 + 1.58 +/** 1.59 + * Waits for all pending async statements on the default connection. 1.60 + * 1.61 + * @return {Promise} 1.62 + * @resolves When all pending async statements finished. 1.63 + * @rejects Never. 1.64 + * 1.65 + * @note The result is achieved by asynchronously executing a query requiring 1.66 + * a write lock. Since all statements on the same connection are 1.67 + * serialized, the end of this write operation means that all writes are 1.68 + * complete. Note that WAL makes so that writers don't block readers, but 1.69 + * this is a problem only across different connections. 1.70 + */ 1.71 +function promiseAsyncUpdates() 1.72 +{ 1.73 + let deferred = Promise.defer(); 1.74 + 1.75 + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) 1.76 + .DBConnection; 1.77 + let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); 1.78 + begin.executeAsync(); 1.79 + begin.finalize(); 1.80 + 1.81 + let commit = db.createAsyncStatement("COMMIT"); 1.82 + commit.executeAsync({ 1.83 + handleResult: function() {}, 1.84 + handleError: function() {}, 1.85 + handleCompletion: function(aReason) 1.86 + { 1.87 + deferred.resolve(); 1.88 + } 1.89 + }); 1.90 + commit.finalize(); 1.91 + 1.92 + return deferred.promise; 1.93 +} 1.94 + 1.95 +/** 1.96 + * Returns a moz_places field value for a url. 1.97 + * 1.98 + * @param aURI 1.99 + * The URI or spec to get field for. 1.100 + * param aCallback 1.101 + * Callback function that will get the property value. 1.102 + */ 1.103 +function fieldForUrl(aURI, aFieldName, aCallback) 1.104 +{ 1.105 + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; 1.106 + let stmt = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) 1.107 + .DBConnection.createAsyncStatement( 1.108 + "SELECT " + aFieldName + " FROM moz_places WHERE url = :page_url" 1.109 + ); 1.110 + stmt.params.page_url = url; 1.111 + stmt.executeAsync({ 1.112 + _value: -1, 1.113 + handleResult: function(aResultSet) { 1.114 + let row = aResultSet.getNextRow(); 1.115 + if (!row) 1.116 + ok(false, "The page should exist in the database"); 1.117 + this._value = row.getResultByName(aFieldName); 1.118 + }, 1.119 + handleError: function() {}, 1.120 + handleCompletion: function(aReason) { 1.121 + if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) 1.122 + ok(false, "The statement should properly succeed"); 1.123 + aCallback(this._value); 1.124 + } 1.125 + }); 1.126 + stmt.finalize(); 1.127 +} 1.128 + 1.129 +/** 1.130 + * Generic nsINavHistoryObserver that doesn't implement anything, but provides 1.131 + * dummy methods to prevent errors about an object not having a certain method. 1.132 + */ 1.133 +function NavHistoryObserver() {} 1.134 + 1.135 +NavHistoryObserver.prototype = { 1.136 + onBeginUpdateBatch: function () {}, 1.137 + onEndUpdateBatch: function () {}, 1.138 + onVisit: function () {}, 1.139 + onTitleChanged: function () {}, 1.140 + onDeleteURI: function () {}, 1.141 + onClearHistory: function () {}, 1.142 + onPageChanged: function () {}, 1.143 + onDeleteVisits: function () {}, 1.144 + QueryInterface: XPCOMUtils.generateQI([ 1.145 + Ci.nsINavHistoryObserver, 1.146 + ]) 1.147 +}; 1.148 + 1.149 +/** 1.150 + * Waits for the first OnPageChanged notification for ATTRIBUTE_FAVICON, and 1.151 + * verifies that it matches the expected page URI and associated favicon URI. 1.152 + * 1.153 + * This function also double-checks the GUID parameter of the notification. 1.154 + * 1.155 + * @param aExpectedPageURI 1.156 + * nsIURI object of the page whose favicon should change. 1.157 + * @param aExpectedFaviconURI 1.158 + * nsIURI object of the newly associated favicon. 1.159 + * @param aCallback 1.160 + * This function is called after the check finished. 1.161 + */ 1.162 +function waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, aWindow, 1.163 + aCallback) { 1.164 + let historyObserver = { 1.165 + __proto__: NavHistoryObserver.prototype, 1.166 + onPageChanged: function WFFC_onPageChanged(aURI, aWhat, aValue, aGUID) { 1.167 + if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { 1.168 + return; 1.169 + } 1.170 + aWindow.PlacesUtils.history.removeObserver(this); 1.171 + 1.172 + ok(aURI.equals(aExpectedPageURI), 1.173 + "Check URIs are equal for the page which favicon changed"); 1.174 + is(aValue, aExpectedFaviconURI.spec, 1.175 + "Check changed favicon URI is the expected"); 1.176 + checkGuidForURI(aURI, aGUID); 1.177 + 1.178 + if (aCallback) { 1.179 + aCallback(); 1.180 + } 1.181 + } 1.182 + }; 1.183 + aWindow.PlacesUtils.history.addObserver(historyObserver, false); 1.184 +} 1.185 + 1.186 +/** 1.187 + * Asynchronously adds visits to a page, invoking a callback function when done. 1.188 + * 1.189 + * @param aPlaceInfo 1.190 + * Either an nsIURI, in such a case a single LINK visit will be added. 1.191 + * Or can be an object describing the visit to add, or an array 1.192 + * of these objects: 1.193 + * { uri: nsIURI of the page, 1.194 + * transition: one of the TRANSITION_* from nsINavHistoryService, 1.195 + * [optional] title: title of the page, 1.196 + * [optional] visitDate: visit date in microseconds from the epoch 1.197 + * [optional] referrer: nsIURI of the referrer for this visit 1.198 + * } 1.199 + * @param [optional] aCallback 1.200 + * Function to be invoked on completion. 1.201 + * @param [optional] aStack 1.202 + * The stack frame used to report errors. 1.203 + */ 1.204 +function addVisits(aPlaceInfo, aWindow, aCallback, aStack) { 1.205 + let stack = aStack || Components.stack.caller; 1.206 + let places = []; 1.207 + if (aPlaceInfo instanceof Ci.nsIURI) { 1.208 + places.push({ uri: aPlaceInfo }); 1.209 + } 1.210 + else if (Array.isArray(aPlaceInfo)) { 1.211 + places = places.concat(aPlaceInfo); 1.212 + } else { 1.213 + places.push(aPlaceInfo) 1.214 + } 1.215 + 1.216 + // Create mozIVisitInfo for each entry. 1.217 + let now = Date.now(); 1.218 + for (let place of places) { 1.219 + if (!place.title) { 1.220 + place.title = "test visit for " + place.uri.spec; 1.221 + } 1.222 + place.visits = [{ 1.223 + transitionType: place.transition === undefined ? TRANSITION_LINK 1.224 + : place.transition, 1.225 + visitDate: place.visitDate || (now++) * 1000, 1.226 + referrerURI: place.referrer 1.227 + }]; 1.228 + } 1.229 + 1.230 + aWindow.PlacesUtils.asyncHistory.updatePlaces( 1.231 + places, 1.232 + { 1.233 + handleError: function AAV_handleError() { 1.234 + throw("Unexpected error in adding visit."); 1.235 + }, 1.236 + handleResult: function () {}, 1.237 + handleCompletion: function UP_handleCompletion() { 1.238 + if (aCallback) 1.239 + aCallback(); 1.240 + } 1.241 + } 1.242 + ); 1.243 +} 1.244 + 1.245 +/** 1.246 + * Asynchronously adds visits to a page. 1.247 + * 1.248 + * @param aPlaceInfo 1.249 + * Can be an nsIURI, in such a case a single LINK visit will be added. 1.250 + * Otherwise can be an object describing the visit to add, or an array 1.251 + * of these objects: 1.252 + * { uri: nsIURI of the page, 1.253 + * transition: one of the TRANSITION_* from nsINavHistoryService, 1.254 + * [optional] title: title of the page, 1.255 + * [optional] visitDate: visit date in microseconds from the epoch 1.256 + * [optional] referrer: nsIURI of the referrer for this visit 1.257 + * } 1.258 + * 1.259 + * @return {Promise} 1.260 + * @resolves When all visits have been added successfully. 1.261 + * @rejects JavaScript exception. 1.262 + */ 1.263 +function promiseAddVisits(aPlaceInfo) 1.264 +{ 1.265 + let deferred = Promise.defer(); 1.266 + let places = []; 1.267 + if (aPlaceInfo instanceof Ci.nsIURI) { 1.268 + places.push({ uri: aPlaceInfo }); 1.269 + } 1.270 + else if (Array.isArray(aPlaceInfo)) { 1.271 + places = places.concat(aPlaceInfo); 1.272 + } else { 1.273 + places.push(aPlaceInfo) 1.274 + } 1.275 + 1.276 + // Create mozIVisitInfo for each entry. 1.277 + let now = Date.now(); 1.278 + for (let i = 0; i < places.length; i++) { 1.279 + if (!places[i].title) { 1.280 + places[i].title = "test visit for " + places[i].uri.spec; 1.281 + } 1.282 + places[i].visits = [{ 1.283 + transitionType: places[i].transition === undefined ? TRANSITION_LINK 1.284 + : places[i].transition, 1.285 + visitDate: places[i].visitDate || (now++) * 1000, 1.286 + referrerURI: places[i].referrer 1.287 + }]; 1.288 + } 1.289 + 1.290 + PlacesUtils.asyncHistory.updatePlaces( 1.291 + places, 1.292 + { 1.293 + handleError: function AAV_handleError(aResultCode, aPlaceInfo) { 1.294 + let ex = new Components.Exception("Unexpected error in adding visits.", 1.295 + aResultCode); 1.296 + deferred.reject(ex); 1.297 + }, 1.298 + handleResult: function () {}, 1.299 + handleCompletion: function UP_handleCompletion() { 1.300 + deferred.resolve(); 1.301 + } 1.302 + } 1.303 + ); 1.304 + 1.305 + return deferred.promise; 1.306 +} 1.307 + 1.308 +/** 1.309 + * Checks that the favicon for the given page matches the provided data. 1.310 + * 1.311 + * @param aPageURI 1.312 + * nsIURI object for the page to check. 1.313 + * @param aExpectedMimeType 1.314 + * Expected MIME type of the icon, for example "image/png". 1.315 + * @param aExpectedData 1.316 + * Expected icon data, expressed as an array of byte values. 1.317 + * @param aCallback 1.318 + * This function is called after the check finished. 1.319 + */ 1.320 +function checkFaviconDataForPage(aPageURI, aExpectedMimeType, aExpectedData, 1.321 + aWindow, aCallback) { 1.322 + aWindow.PlacesUtils.favicons.getFaviconDataForPage(aPageURI, 1.323 + function (aURI, aDataLen, aData, aMimeType) { 1.324 + is(aExpectedMimeType, aMimeType, "Check expected MimeType"); 1.325 + is(aExpectedData.length, aData.length, 1.326 + "Check favicon data for the given page matches the provided data"); 1.327 + checkGuidForURI(aPageURI); 1.328 + aCallback(); 1.329 + }); 1.330 +} 1.331 + 1.332 +/** 1.333 + * Tests that a guid was set in moz_places for a given uri. 1.334 + * 1.335 + * @param aURI 1.336 + * The uri to check. 1.337 + * @param [optional] aGUID 1.338 + * The expected guid in the database. 1.339 + */ 1.340 +function checkGuidForURI(aURI, aGUID) { 1.341 + let guid = doGetGuidForURI(aURI); 1.342 + if (aGUID) { 1.343 + doCheckValidPlacesGuid(aGUID); 1.344 + is(guid, aGUID, "Check equal guid for URIs"); 1.345 + } 1.346 +} 1.347 + 1.348 +/** 1.349 + * Retrieves the guid for a given uri. 1.350 + * 1.351 + * @param aURI 1.352 + * The uri to check. 1.353 + * @return the associated the guid. 1.354 + */ 1.355 +function doGetGuidForURI(aURI) { 1.356 + let stmt = DBConn().createStatement( 1.357 + "SELECT guid " 1.358 + + "FROM moz_places " 1.359 + + "WHERE url = :url " 1.360 + ); 1.361 + stmt.params.url = aURI.spec; 1.362 + ok(stmt.executeStep(), "Check get guid for uri from moz_places"); 1.363 + let guid = stmt.row.guid; 1.364 + stmt.finalize(); 1.365 + doCheckValidPlacesGuid(guid); 1.366 + return guid; 1.367 +} 1.368 + 1.369 +/** 1.370 + * Tests if a given guid is valid for use in Places or not. 1.371 + * 1.372 + * @param aGuid 1.373 + * The guid to test. 1.374 + */ 1.375 +function doCheckValidPlacesGuid(aGuid) { 1.376 + ok(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), "Check guid for valid places"); 1.377 +} 1.378 + 1.379 +/** 1.380 + * Gets the database connection. If the Places connection is invalid it will 1.381 + * try to create a new connection. 1.382 + * 1.383 + * @param [optional] aForceNewConnection 1.384 + * Forces creation of a new connection to the database. When a 1.385 + * connection is asyncClosed it cannot anymore schedule async statements, 1.386 + * though connectionReady will keep returning true (Bug 726990). 1.387 + * 1.388 + * @return The database connection or null if unable to get one. 1.389 + */ 1.390 +function DBConn(aForceNewConnection) { 1.391 + let gDBConn; 1.392 + if (!aForceNewConnection) { 1.393 + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) 1.394 + .DBConnection; 1.395 + if (db.connectionReady) 1.396 + return db; 1.397 + } 1.398 + 1.399 + // If the Places database connection has been closed, create a new connection. 1.400 + if (!gDBConn || aForceNewConnection) { 1.401 + let file = Services.dirsvc.get('ProfD', Ci.nsIFile); 1.402 + file.append("places.sqlite"); 1.403 + let dbConn = gDBConn = Services.storage.openDatabase(file); 1.404 + 1.405 + // Be sure to cleanly close this connection. 1.406 + Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) { 1.407 + Services.obs.removeObserver(DBCloseCallback, aTopic); 1.408 + dbConn.asyncClose(); 1.409 + }, "profile-before-change", false); 1.410 + } 1.411 + 1.412 + return gDBConn.connectionReady ? gDBConn : null; 1.413 +} 1.414 + 1.415 +function whenDelayedStartupFinished(aWindow, aCallback) { 1.416 + Services.obs.addObserver(function observer(aSubject, aTopic) { 1.417 + if (aWindow == aSubject) { 1.418 + Services.obs.removeObserver(observer, aTopic); 1.419 + executeSoon(function() { aCallback(aWindow); }); 1.420 + } 1.421 + }, "browser-delayed-startup-finished", false); 1.422 +} 1.423 + 1.424 +function whenNewWindowLoaded(aOptions, aCallback) { 1.425 + let win = OpenBrowserWindow(aOptions); 1.426 + whenDelayedStartupFinished(win, aCallback); 1.427 +} 1.428 + 1.429 +/** 1.430 + * Asynchronously check a url is visited. 1.431 + * 1.432 + * @param aURI The URI. 1.433 + * @param aExpectedValue The expected value. 1.434 + * @return {Promise} 1.435 + * @resolves When the check has been added successfully. 1.436 + * @rejects JavaScript exception. 1.437 + */ 1.438 +function promiseIsURIVisited(aURI, aExpectedValue) { 1.439 + let deferred = Promise.defer(); 1.440 + 1.441 + PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { 1.442 + deferred.resolve(aIsVisited); 1.443 + }); 1.444 + 1.445 + return deferred.promise; 1.446 +} 1.447 + 1.448 +function waitForCondition(condition, nextTest, errorMsg) { 1.449 + let tries = 0; 1.450 + let interval = setInterval(function() { 1.451 + if (tries >= 30) { 1.452 + ok(false, errorMsg); 1.453 + moveOn(); 1.454 + } 1.455 + let conditionPassed; 1.456 + try { 1.457 + conditionPassed = condition(); 1.458 + } catch (e) { 1.459 + ok(false, e + "\n" + e.stack); 1.460 + conditionPassed = false; 1.461 + } 1.462 + if (conditionPassed) { 1.463 + moveOn(); 1.464 + } 1.465 + tries++; 1.466 + }, 200); 1.467 + function moveOn() { 1.468 + clearInterval(interval); 1.469 + nextTest(); 1.470 + }; 1.471 +}