1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/tests/unit/test_async_history_api.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1162 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +/** 1.8 + * This file tests the async history API exposed by mozIAsyncHistory. 1.9 + */ 1.10 + 1.11 +//////////////////////////////////////////////////////////////////////////////// 1.12 +//// Globals 1.13 + 1.14 +const TEST_DOMAIN = "http://mozilla.org/"; 1.15 +const URI_VISIT_SAVED = "uri-visit-saved"; 1.16 +const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000; 1.17 + 1.18 +//////////////////////////////////////////////////////////////////////////////// 1.19 +//// Helpers 1.20 +/** 1.21 + * Object that represents a mozIVisitInfo object. 1.22 + * 1.23 + * @param [optional] aTransitionType 1.24 + * The transition type of the visit. Defaults to TRANSITION_LINK if not 1.25 + * provided. 1.26 + * @param [optional] aVisitTime 1.27 + * The time of the visit. Defaults to now if not provided. 1.28 + */ 1.29 +function VisitInfo(aTransitionType, 1.30 + aVisitTime) 1.31 +{ 1.32 + this.transitionType = 1.33 + aTransitionType === undefined ? TRANSITION_LINK : aTransitionType; 1.34 + this.visitDate = aVisitTime || Date.now() * 1000; 1.35 +} 1.36 + 1.37 +function promiseUpdatePlaces(aPlaces) { 1.38 + let deferred = Promise.defer(); 1.39 + PlacesUtils.asyncHistory.updatePlaces(aPlaces, { 1.40 + _errors: [], 1.41 + _results: [], 1.42 + handleError: function handleError(aResultCode, aPlace) { 1.43 + this._errors.push({ resultCode: aResultCode, info: aPlace}); 1.44 + }, 1.45 + handleResult: function handleResult(aPlace) { 1.46 + this._results.push(aPlace); 1.47 + }, 1.48 + handleCompletion: function handleCompletion() { 1.49 + deferred.resolve({ errors: this._errors, results: this._results }); 1.50 + } 1.51 + }); 1.52 + 1.53 + return deferred.promise; 1.54 +} 1.55 + 1.56 +/** 1.57 + * Listens for a title change notification, and calls aCallback when it gets it. 1.58 + * 1.59 + * @param aURI 1.60 + * The URI of the page we expect a notification for. 1.61 + * @param aExpectedTitle 1.62 + * The expected title of the URI we expect a notification for. 1.63 + * @param aCallback 1.64 + * The method to call when we have gotten the proper notification about 1.65 + * the title changing. 1.66 + */ 1.67 +function TitleChangedObserver(aURI, 1.68 + aExpectedTitle, 1.69 + aCallback) 1.70 +{ 1.71 + this.uri = aURI; 1.72 + this.expectedTitle = aExpectedTitle; 1.73 + this.callback = aCallback; 1.74 +} 1.75 +TitleChangedObserver.prototype = { 1.76 + __proto__: NavHistoryObserver.prototype, 1.77 + onTitleChanged: function(aURI, 1.78 + aTitle, 1.79 + aGUID) 1.80 + { 1.81 + do_log_info("onTitleChanged(" + aURI.spec + ", " + aTitle + ", " + aGUID + ")"); 1.82 + if (!this.uri.equals(aURI)) { 1.83 + return; 1.84 + } 1.85 + do_check_eq(aTitle, this.expectedTitle); 1.86 + do_check_guid_for_uri(aURI, aGUID); 1.87 + this.callback(); 1.88 + }, 1.89 +}; 1.90 + 1.91 +/** 1.92 + * Listens for a visit notification, and calls aCallback when it gets it. 1.93 + * 1.94 + * @param aURI 1.95 + * The URI of the page we expect a notification for. 1.96 + * @param aCallback 1.97 + * The method to call when we have gotten the proper notification about 1.98 + * being visited. 1.99 + */ 1.100 +function VisitObserver(aURI, 1.101 + aGUID, 1.102 + aCallback) 1.103 +{ 1.104 + this.uri = aURI; 1.105 + this.guid = aGUID; 1.106 + this.callback = aCallback; 1.107 +} 1.108 +VisitObserver.prototype = { 1.109 + __proto__: NavHistoryObserver.prototype, 1.110 + onVisit: function(aURI, 1.111 + aVisitId, 1.112 + aTime, 1.113 + aSessionId, 1.114 + aReferringId, 1.115 + aTransitionType, 1.116 + aGUID) 1.117 + { 1.118 + do_log_info("onVisit(" + aURI.spec + ", " + aVisitId + ", " + aTime + 1.119 + ", " + aSessionId + ", " + aReferringId + ", " + 1.120 + aTransitionType + ", " + aGUID + ")"); 1.121 + if (!this.uri.equals(aURI) || this.guid != aGUID) { 1.122 + return; 1.123 + } 1.124 + this.callback(aTime, aTransitionType); 1.125 + }, 1.126 +}; 1.127 + 1.128 +/** 1.129 + * Tests that a title was set properly in the database. 1.130 + * 1.131 + * @param aURI 1.132 + * The uri to check. 1.133 + * @param aTitle 1.134 + * The expected title in the database. 1.135 + */ 1.136 +function do_check_title_for_uri(aURI, 1.137 + aTitle) 1.138 +{ 1.139 + let stack = Components.stack.caller; 1.140 + let stmt = DBConn().createStatement( 1.141 + "SELECT title " + 1.142 + "FROM moz_places " + 1.143 + "WHERE url = :url " 1.144 + ); 1.145 + stmt.params.url = aURI.spec; 1.146 + do_check_true(stmt.executeStep(), stack); 1.147 + do_check_eq(stmt.row.title, aTitle, stack); 1.148 + stmt.finalize(); 1.149 +} 1.150 + 1.151 +//////////////////////////////////////////////////////////////////////////////// 1.152 +//// Test Functions 1.153 + 1.154 +function test_interface_exists() 1.155 +{ 1.156 + let history = Cc["@mozilla.org/browser/history;1"].getService(Ci.nsISupports); 1.157 + do_check_true(history instanceof Ci.mozIAsyncHistory); 1.158 +} 1.159 + 1.160 +function test_invalid_uri_throws() 1.161 +{ 1.162 + // First, test passing in nothing. 1.163 + let place = { 1.164 + visits: [ 1.165 + new VisitInfo(), 1.166 + ], 1.167 + }; 1.168 + try { 1.169 + yield promiseUpdatePlaces(place); 1.170 + do_throw("Should have thrown!"); 1.171 + } 1.172 + catch (e) { 1.173 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.174 + } 1.175 + 1.176 + // Now, test other bogus things. 1.177 + const TEST_VALUES = [ 1.178 + null, 1.179 + undefined, 1.180 + {}, 1.181 + [], 1.182 + TEST_DOMAIN + "test_invalid_id_throws", 1.183 + ]; 1.184 + for (let i = 0; i < TEST_VALUES.length; i++) { 1.185 + place.uri = TEST_VALUES[i]; 1.186 + try { 1.187 + yield promiseUpdatePlaces(place); 1.188 + do_throw("Should have thrown!"); 1.189 + } 1.190 + catch (e) { 1.191 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.192 + } 1.193 + } 1.194 +} 1.195 + 1.196 +function test_invalid_places_throws() 1.197 +{ 1.198 + // First, test passing in nothing. 1.199 + try { 1.200 + PlacesUtils.asyncHistory.updatePlaces(); 1.201 + do_throw("Should have thrown!"); 1.202 + } 1.203 + catch (e) { 1.204 + do_check_eq(e.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS); 1.205 + } 1.206 + 1.207 + // Now, test other bogus things. 1.208 + const TEST_VALUES = [ 1.209 + null, 1.210 + undefined, 1.211 + {}, 1.212 + [], 1.213 + "", 1.214 + ]; 1.215 + for (let i = 0; i < TEST_VALUES.length; i++) { 1.216 + let value = TEST_VALUES[i]; 1.217 + try { 1.218 + yield promiseUpdatePlaces(value); 1.219 + do_throw("Should have thrown!"); 1.220 + } 1.221 + catch (e) { 1.222 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.223 + } 1.224 + } 1.225 +} 1.226 + 1.227 +function test_invalid_guid_throws() 1.228 +{ 1.229 + // First check invalid length guid. 1.230 + let place = { 1.231 + guid: "BAD_GUID", 1.232 + uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_guid_throws"), 1.233 + visits: [ 1.234 + new VisitInfo(), 1.235 + ], 1.236 + }; 1.237 + try { 1.238 + yield promiseUpdatePlaces(place); 1.239 + do_throw("Should have thrown!"); 1.240 + } 1.241 + catch (e) { 1.242 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.243 + } 1.244 + 1.245 + // Now check invalid character guid. 1.246 + place.guid = "__BADGUID+__"; 1.247 + do_check_eq(place.guid.length, 12); 1.248 + try { 1.249 + yield promiseUpdatePlaces(place); 1.250 + do_throw("Should have thrown!"); 1.251 + } 1.252 + catch (e) { 1.253 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.254 + } 1.255 +} 1.256 + 1.257 +function test_no_visits_throws() 1.258 +{ 1.259 + const TEST_URI = 1.260 + NetUtil.newURI(TEST_DOMAIN + "test_no_id_or_guid_no_visits_throws"); 1.261 + const TEST_GUID = "_RANDOMGUID_"; 1.262 + const TEST_PLACEID = 2; 1.263 + 1.264 + let log_test_conditions = function(aPlace) { 1.265 + let str = "Testing place with " + 1.266 + (aPlace.uri ? "uri" : "no uri") + ", " + 1.267 + (aPlace.guid ? "guid" : "no guid") + ", " + 1.268 + (aPlace.visits ? "visits array" : "no visits array"); 1.269 + do_log_info(str); 1.270 + }; 1.271 + 1.272 + // Loop through every possible case. Note that we don't actually care about 1.273 + // the case where we have no uri, place id, or guid (covered by another test), 1.274 + // but it is easier to just make sure it too throws than to exclude it. 1.275 + let place = { }; 1.276 + for (let uri = 1; uri >= 0; uri--) { 1.277 + place.uri = uri ? TEST_URI : undefined; 1.278 + 1.279 + for (let guid = 1; guid >= 0; guid--) { 1.280 + place.guid = guid ? TEST_GUID : undefined; 1.281 + 1.282 + for (let visits = 1; visits >= 0; visits--) { 1.283 + place.visits = visits ? [] : undefined; 1.284 + 1.285 + log_test_conditions(place); 1.286 + try { 1.287 + yield promiseUpdatePlaces(place); 1.288 + do_throw("Should have thrown!"); 1.289 + } 1.290 + catch (e) { 1.291 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.292 + } 1.293 + } 1.294 + } 1.295 + } 1.296 +} 1.297 + 1.298 +function test_add_visit_no_date_throws() 1.299 +{ 1.300 + let place = { 1.301 + uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_date_throws"), 1.302 + visits: [ 1.303 + new VisitInfo(), 1.304 + ], 1.305 + }; 1.306 + delete place.visits[0].visitDate; 1.307 + try { 1.308 + yield promiseUpdatePlaces(place); 1.309 + do_throw("Should have thrown!"); 1.310 + } 1.311 + catch (e) { 1.312 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.313 + } 1.314 +} 1.315 + 1.316 +function test_add_visit_no_transitionType_throws() 1.317 +{ 1.318 + let place = { 1.319 + uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_transitionType_throws"), 1.320 + visits: [ 1.321 + new VisitInfo(), 1.322 + ], 1.323 + }; 1.324 + delete place.visits[0].transitionType; 1.325 + try { 1.326 + yield promiseUpdatePlaces(place); 1.327 + do_throw("Should have thrown!"); 1.328 + } 1.329 + catch (e) { 1.330 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.331 + } 1.332 +} 1.333 + 1.334 +function test_add_visit_invalid_transitionType_throws() 1.335 +{ 1.336 + // First, test something that has a transition type lower than the first one. 1.337 + let place = { 1.338 + uri: NetUtil.newURI(TEST_DOMAIN + 1.339 + "test_add_visit_invalid_transitionType_throws"), 1.340 + visits: [ 1.341 + new VisitInfo(TRANSITION_LINK - 1), 1.342 + ], 1.343 + }; 1.344 + try { 1.345 + yield promiseUpdatePlaces(place); 1.346 + do_throw("Should have thrown!"); 1.347 + } 1.348 + catch (e) { 1.349 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.350 + } 1.351 + 1.352 + // Now, test something that has a transition type greater than the last one. 1.353 + place.visits[0] = new VisitInfo(TRANSITION_FRAMED_LINK + 1); 1.354 + try { 1.355 + yield promiseUpdatePlaces(place); 1.356 + do_throw("Should have thrown!"); 1.357 + } 1.358 + catch (e) { 1.359 + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); 1.360 + } 1.361 +} 1.362 + 1.363 +function test_non_addable_uri_errors() 1.364 +{ 1.365 + // Array of protocols that nsINavHistoryService::canAddURI returns false for. 1.366 + const URLS = [ 1.367 + "about:config", 1.368 + "imap://cyrus.andrew.cmu.edu/archive.imap", 1.369 + "news://new.mozilla.org/mozilla.dev.apps.firefox", 1.370 + "mailbox:Inbox", 1.371 + "moz-anno:favicon:http://mozilla.org/made-up-favicon", 1.372 + "view-source:http://mozilla.org", 1.373 + "chrome://browser/content/browser.xul", 1.374 + "resource://gre-resources/hiddenWindow.html", 1.375 + "data:,Hello%2C%20World!", 1.376 + "wyciwyg:/0/http://mozilla.org", 1.377 + "javascript:alert('hello wolrd!');", 1.378 + "blob:foo", 1.379 + ]; 1.380 + let places = []; 1.381 + URLS.forEach(function(url) { 1.382 + try { 1.383 + let place = { 1.384 + uri: NetUtil.newURI(url), 1.385 + title: "test for " + url, 1.386 + visits: [ 1.387 + new VisitInfo(), 1.388 + ], 1.389 + }; 1.390 + places.push(place); 1.391 + } 1.392 + catch (e if e.result === Cr.NS_ERROR_FAILURE) { 1.393 + // NetUtil.newURI() can throw if e.g. our app knows about imap:// 1.394 + // but the account is not set up and so the URL is invalid for us. 1.395 + // Note this in the log but ignore as it's not the subject of this test. 1.396 + do_log_info("Could not construct URI for '" + url + "'; ignoring"); 1.397 + } 1.398 + }); 1.399 + 1.400 + let placesResult = yield promiseUpdatePlaces(places); 1.401 + if (placesResult.results.length > 0) { 1.402 + do_throw("Unexpected success."); 1.403 + } 1.404 + for (let place of placesResult.errors) { 1.405 + do_log_info("Checking '" + place.info.uri.spec + "'"); 1.406 + do_check_eq(place.resultCode, Cr.NS_ERROR_INVALID_ARG); 1.407 + do_check_false(yield promiseIsURIVisited(place.info.uri)); 1.408 + } 1.409 + yield promiseAsyncUpdates(); 1.410 +} 1.411 + 1.412 +function test_duplicate_guid_errors() 1.413 +{ 1.414 + // This test ensures that trying to add a visit, with a guid already found in 1.415 + // another visit, fails. 1.416 + let place = { 1.417 + uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"), 1.418 + visits: [ 1.419 + new VisitInfo(), 1.420 + ], 1.421 + }; 1.422 + 1.423 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.424 + let placesResult = yield promiseUpdatePlaces(place); 1.425 + if (placesResult.errors.length > 0) { 1.426 + do_throw("Unexpected error."); 1.427 + } 1.428 + let placeInfo = placesResult.results[0]; 1.429 + do_check_true(yield promiseIsURIVisited(placeInfo.uri)); 1.430 + 1.431 + let badPlace = { 1.432 + uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"), 1.433 + visits: [ 1.434 + new VisitInfo(), 1.435 + ], 1.436 + guid: placeInfo.guid, 1.437 + }; 1.438 + 1.439 + do_check_false(yield promiseIsURIVisited(badPlace.uri)); 1.440 + placesResult = yield promiseUpdatePlaces(badPlace); 1.441 + if (placesResult.results.length > 0) { 1.442 + do_throw("Unexpected success."); 1.443 + } 1.444 + let badPlaceInfo = placesResult.errors[0]; 1.445 + do_check_eq(badPlaceInfo.resultCode, Cr.NS_ERROR_STORAGE_CONSTRAINT); 1.446 + do_check_false(yield promiseIsURIVisited(badPlaceInfo.info.uri)); 1.447 + 1.448 + yield promiseAsyncUpdates(); 1.449 +} 1.450 + 1.451 +function test_invalid_referrerURI_ignored() 1.452 +{ 1.453 + let place = { 1.454 + uri: NetUtil.newURI(TEST_DOMAIN + 1.455 + "test_invalid_referrerURI_ignored"), 1.456 + visits: [ 1.457 + new VisitInfo(), 1.458 + ], 1.459 + }; 1.460 + place.visits[0].referrerURI = NetUtil.newURI(place.uri.spec + "_unvisistedURI"); 1.461 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.462 + do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI)); 1.463 + 1.464 + let placesResult = yield promiseUpdatePlaces(place); 1.465 + if (placesResult.errors.length > 0) { 1.466 + do_throw("Unexpected error."); 1.467 + } 1.468 + let placeInfo = placesResult.results[0]; 1.469 + do_check_true(yield promiseIsURIVisited(placeInfo.uri)); 1.470 + 1.471 + // Check to make sure we do not visit the invalid referrer. 1.472 + do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI)); 1.473 + 1.474 + // Check to make sure from_visit is zero in database. 1.475 + let stmt = DBConn().createStatement( 1.476 + "SELECT from_visit " + 1.477 + "FROM moz_historyvisits " + 1.478 + "WHERE id = :visit_id" 1.479 + ); 1.480 + stmt.params.visit_id = placeInfo.visits[0].visitId; 1.481 + do_check_true(stmt.executeStep()); 1.482 + do_check_eq(stmt.row.from_visit, 0); 1.483 + stmt.finalize(); 1.484 + 1.485 + yield promiseAsyncUpdates(); 1.486 +} 1.487 + 1.488 +function test_nonnsIURI_referrerURI_ignored() 1.489 +{ 1.490 + let place = { 1.491 + uri: NetUtil.newURI(TEST_DOMAIN + 1.492 + "test_nonnsIURI_referrerURI_ignored"), 1.493 + visits: [ 1.494 + new VisitInfo(), 1.495 + ], 1.496 + }; 1.497 + place.visits[0].referrerURI = place.uri.spec + "_nonnsIURI"; 1.498 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.499 + 1.500 + let placesResult = yield promiseUpdatePlaces(place); 1.501 + if (placesResult.errors.length > 0) { 1.502 + do_throw("Unexpected error."); 1.503 + } 1.504 + let placeInfo = placesResult.results[0]; 1.505 + do_check_true(yield promiseIsURIVisited(placeInfo.uri)); 1.506 + 1.507 + // Check to make sure from_visit is zero in database. 1.508 + let stmt = DBConn().createStatement( 1.509 + "SELECT from_visit " + 1.510 + "FROM moz_historyvisits " + 1.511 + "WHERE id = :visit_id" 1.512 + ); 1.513 + stmt.params.visit_id = placeInfo.visits[0].visitId; 1.514 + do_check_true(stmt.executeStep()); 1.515 + do_check_eq(stmt.row.from_visit, 0); 1.516 + stmt.finalize(); 1.517 + 1.518 + yield promiseAsyncUpdates(); 1.519 +} 1.520 + 1.521 +function test_old_referrer_ignored() 1.522 +{ 1.523 + // This tests that a referrer for a visit which is not recent (specifically, 1.524 + // older than 15 minutes as per RECENT_EVENT_THRESHOLD) is not saved by 1.525 + // updatePlaces. 1.526 + let oldTime = (Date.now() * 1000) - (RECENT_EVENT_THRESHOLD + 1); 1.527 + let referrerPlace = { 1.528 + uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_referrer"), 1.529 + visits: [ 1.530 + new VisitInfo(TRANSITION_LINK, oldTime), 1.531 + ], 1.532 + }; 1.533 + 1.534 + // First we must add our referrer to the history so that it is not ignored 1.535 + // as being invalid. 1.536 + do_check_false(yield promiseIsURIVisited(referrerPlace.uri)); 1.537 + let placesResult = yield promiseUpdatePlaces(referrerPlace); 1.538 + if (placesResult.errors.length > 0) { 1.539 + do_throw("Unexpected error."); 1.540 + } 1.541 + 1.542 + // Now that the referrer is added, we can add a page with a valid 1.543 + // referrer to determine if the recency of the referrer is taken into 1.544 + // account. 1.545 + do_check_true(yield promiseIsURIVisited(referrerPlace.uri)); 1.546 + 1.547 + let visitInfo = new VisitInfo(); 1.548 + visitInfo.referrerURI = referrerPlace.uri; 1.549 + let place = { 1.550 + uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_page"), 1.551 + visits: [ 1.552 + visitInfo, 1.553 + ], 1.554 + }; 1.555 + 1.556 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.557 + placesResult = yield promiseUpdatePlaces(place); 1.558 + if (placesResult.errors.length > 0) { 1.559 + do_throw("Unexpected error."); 1.560 + } 1.561 + let placeInfo = placesResult.results[0]; 1.562 + do_check_true(yield promiseIsURIVisited(place.uri)); 1.563 + 1.564 + // Though the visit will not contain the referrer, we must examine the 1.565 + // database to be sure. 1.566 + do_check_eq(placeInfo.visits[0].referrerURI, null); 1.567 + let stmt = DBConn().createStatement( 1.568 + "SELECT COUNT(1) AS count " + 1.569 + "FROM moz_historyvisits " + 1.570 + "WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) " + 1.571 + "AND from_visit = 0 " 1.572 + ); 1.573 + stmt.params.page_url = place.uri.spec; 1.574 + do_check_true(stmt.executeStep()); 1.575 + do_check_eq(stmt.row.count, 1); 1.576 + stmt.finalize(); 1.577 + 1.578 + yield promiseAsyncUpdates(); 1.579 +} 1.580 + 1.581 +function test_place_id_ignored() 1.582 +{ 1.583 + let place = { 1.584 + uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_first"), 1.585 + visits: [ 1.586 + new VisitInfo(), 1.587 + ], 1.588 + }; 1.589 + 1.590 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.591 + let placesResult = yield promiseUpdatePlaces(place); 1.592 + if (placesResult.errors.length > 0) { 1.593 + do_throw("Unexpected error."); 1.594 + } 1.595 + let placeInfo = placesResult.results[0]; 1.596 + do_check_true(yield promiseIsURIVisited(place.uri)); 1.597 + 1.598 + let placeId = placeInfo.placeId; 1.599 + do_check_neq(placeId, 0); 1.600 + 1.601 + let badPlace = { 1.602 + uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"), 1.603 + visits: [ 1.604 + new VisitInfo(), 1.605 + ], 1.606 + placeId: placeId, 1.607 + }; 1.608 + 1.609 + do_check_false(yield promiseIsURIVisited(badPlace.uri)); 1.610 + placesResult = yield promiseUpdatePlaces(badPlace); 1.611 + if (placesResult.errors.length > 0) { 1.612 + do_throw("Unexpected error."); 1.613 + } 1.614 + placeInfo = placesResult.results[0]; 1.615 + 1.616 + do_check_neq(placeInfo.placeId, placeId); 1.617 + do_check_true(yield promiseIsURIVisited(badPlace.uri)); 1.618 + 1.619 + yield promiseAsyncUpdates(); 1.620 +} 1.621 + 1.622 +function test_handleCompletion_called_when_complete() 1.623 +{ 1.624 + // We test a normal visit, and embeded visit, and a uri that would fail 1.625 + // the canAddURI test to make sure that the notification happens after *all* 1.626 + // of them have had a callback. 1.627 + let places = [ 1.628 + { uri: NetUtil.newURI(TEST_DOMAIN + 1.629 + "test_handleCompletion_called_when_complete"), 1.630 + visits: [ 1.631 + new VisitInfo(), 1.632 + new VisitInfo(TRANSITION_EMBED), 1.633 + ], 1.634 + }, 1.635 + { uri: NetUtil.newURI("data:,Hello%2C%20World!"), 1.636 + visits: [ 1.637 + new VisitInfo(), 1.638 + ], 1.639 + }, 1.640 + ]; 1.641 + do_check_false(yield promiseIsURIVisited(places[0].uri)); 1.642 + do_check_false(yield promiseIsURIVisited(places[1].uri)); 1.643 + 1.644 + const EXPECTED_COUNT_SUCCESS = 2; 1.645 + const EXPECTED_COUNT_FAILURE = 1; 1.646 + let callbackCountSuccess = 0; 1.647 + let callbackCountFailure = 0; 1.648 + 1.649 + let placesResult = yield promiseUpdatePlaces(places); 1.650 + for (let place of placesResult.results) { 1.651 + let checker = PlacesUtils.history.canAddURI(place.uri) ? 1.652 + do_check_true : do_check_false; 1.653 + callbackCountSuccess++; 1.654 + } 1.655 + for (let error of placesResult.errors) { 1.656 + callbackCountFailure++; 1.657 + } 1.658 + 1.659 + do_check_eq(callbackCountSuccess, EXPECTED_COUNT_SUCCESS); 1.660 + do_check_eq(callbackCountFailure, EXPECTED_COUNT_FAILURE); 1.661 + yield promiseAsyncUpdates(); 1.662 +} 1.663 + 1.664 +function test_add_visit() 1.665 +{ 1.666 + const VISIT_TIME = Date.now() * 1000; 1.667 + let place = { 1.668 + uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"), 1.669 + title: "test_add_visit title", 1.670 + visits: [], 1.671 + }; 1.672 + for (let transitionType = TRANSITION_LINK; 1.673 + transitionType <= TRANSITION_FRAMED_LINK; 1.674 + transitionType++) { 1.675 + place.visits.push(new VisitInfo(transitionType, VISIT_TIME)); 1.676 + } 1.677 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.678 + 1.679 + let callbackCount = 0; 1.680 + let placesResult = yield promiseUpdatePlaces(place); 1.681 + if (placesResult.errors.length > 0) { 1.682 + do_throw("Unexpected error."); 1.683 + } 1.684 + for (let placeInfo of placesResult.results) { 1.685 + do_check_true(yield promiseIsURIVisited(place.uri)); 1.686 + 1.687 + // Check mozIPlaceInfo properties. 1.688 + do_check_true(place.uri.equals(placeInfo.uri)); 1.689 + do_check_eq(placeInfo.frecency, -1); // We don't pass frecency here! 1.690 + do_check_eq(placeInfo.title, place.title); 1.691 + 1.692 + // Check mozIVisitInfo properties. 1.693 + let visits = placeInfo.visits; 1.694 + do_check_eq(visits.length, 1); 1.695 + let visit = visits[0]; 1.696 + do_check_eq(visit.visitDate, VISIT_TIME); 1.697 + do_check_true(visit.transitionType >= TRANSITION_LINK && 1.698 + visit.transitionType <= TRANSITION_FRAMED_LINK); 1.699 + do_check_true(visit.referrerURI === null); 1.700 + 1.701 + // For TRANSITION_EMBED visits, many properties will always be zero or 1.702 + // undefined. 1.703 + if (visit.transitionType == TRANSITION_EMBED) { 1.704 + // Check mozIPlaceInfo properties. 1.705 + do_check_eq(placeInfo.placeId, 0, '//'); 1.706 + do_check_eq(placeInfo.guid, null); 1.707 + 1.708 + // Check mozIVisitInfo properties. 1.709 + do_check_eq(visit.visitId, 0); 1.710 + } 1.711 + // But they should be valid for non-embed visits. 1.712 + else { 1.713 + // Check mozIPlaceInfo properties. 1.714 + do_check_true(placeInfo.placeId > 0); 1.715 + do_check_valid_places_guid(placeInfo.guid); 1.716 + 1.717 + // Check mozIVisitInfo properties. 1.718 + do_check_true(visit.visitId > 0); 1.719 + } 1.720 + 1.721 + // If we have had all of our callbacks, continue running tests. 1.722 + if (++callbackCount == place.visits.length) { 1.723 + yield promiseAsyncUpdates(); 1.724 + } 1.725 + } 1.726 +} 1.727 + 1.728 +function test_properties_saved() 1.729 +{ 1.730 + // Check each transition type to make sure it is saved properly. 1.731 + let places = []; 1.732 + for (let transitionType = TRANSITION_LINK; 1.733 + transitionType <= TRANSITION_FRAMED_LINK; 1.734 + transitionType++) { 1.735 + let place = { 1.736 + uri: NetUtil.newURI(TEST_DOMAIN + "test_properties_saved/" + 1.737 + transitionType), 1.738 + title: "test_properties_saved test", 1.739 + visits: [ 1.740 + new VisitInfo(transitionType), 1.741 + ], 1.742 + }; 1.743 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.744 + places.push(place); 1.745 + } 1.746 + 1.747 + let callbackCount = 0; 1.748 + let placesResult = yield promiseUpdatePlaces(places); 1.749 + if (placesResult.errors.length > 0) { 1.750 + do_throw("Unexpected error."); 1.751 + } 1.752 + for (let placeInfo of placesResult.results) { 1.753 + let uri = placeInfo.uri; 1.754 + do_check_true(yield promiseIsURIVisited(uri)); 1.755 + let visit = placeInfo.visits[0]; 1.756 + print("TEST-INFO | test_properties_saved | updatePlaces callback for " + 1.757 + "transition type " + visit.transitionType); 1.758 + 1.759 + // Note that TRANSITION_EMBED should not be in the database. 1.760 + const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1; 1.761 + 1.762 + // mozIVisitInfo::date 1.763 + let stmt = DBConn().createStatement( 1.764 + "SELECT COUNT(1) AS count " + 1.765 + "FROM moz_places h " + 1.766 + "JOIN moz_historyvisits v " + 1.767 + "ON h.id = v.place_id " + 1.768 + "WHERE h.url = :page_url " + 1.769 + "AND v.visit_date = :visit_date " 1.770 + ); 1.771 + stmt.params.page_url = uri.spec; 1.772 + stmt.params.visit_date = visit.visitDate; 1.773 + do_check_true(stmt.executeStep()); 1.774 + do_check_eq(stmt.row.count, EXPECTED_COUNT); 1.775 + stmt.finalize(); 1.776 + 1.777 + // mozIVisitInfo::transitionType 1.778 + stmt = DBConn().createStatement( 1.779 + "SELECT COUNT(1) AS count " + 1.780 + "FROM moz_places h " + 1.781 + "JOIN moz_historyvisits v " + 1.782 + "ON h.id = v.place_id " + 1.783 + "WHERE h.url = :page_url " + 1.784 + "AND v.visit_type = :transition_type " 1.785 + ); 1.786 + stmt.params.page_url = uri.spec; 1.787 + stmt.params.transition_type = visit.transitionType; 1.788 + do_check_true(stmt.executeStep()); 1.789 + do_check_eq(stmt.row.count, EXPECTED_COUNT); 1.790 + stmt.finalize(); 1.791 + 1.792 + // mozIPlaceInfo::title 1.793 + stmt = DBConn().createStatement( 1.794 + "SELECT COUNT(1) AS count " + 1.795 + "FROM moz_places h " + 1.796 + "WHERE h.url = :page_url " + 1.797 + "AND h.title = :title " 1.798 + ); 1.799 + stmt.params.page_url = uri.spec; 1.800 + stmt.params.title = placeInfo.title; 1.801 + do_check_true(stmt.executeStep()); 1.802 + do_check_eq(stmt.row.count, EXPECTED_COUNT); 1.803 + stmt.finalize(); 1.804 + 1.805 + // If we have had all of our callbacks, continue running tests. 1.806 + if (++callbackCount == places.length) { 1.807 + yield promiseAsyncUpdates(); 1.808 + } 1.809 + } 1.810 +} 1.811 + 1.812 +function test_guid_saved() 1.813 +{ 1.814 + let place = { 1.815 + uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_saved"), 1.816 + guid: "__TESTGUID__", 1.817 + visits: [ 1.818 + new VisitInfo(), 1.819 + ], 1.820 + }; 1.821 + do_check_valid_places_guid(place.guid); 1.822 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.823 + 1.824 + let placesResult = yield promiseUpdatePlaces(place); 1.825 + if (placesResult.errors.length > 0) { 1.826 + do_throw("Unexpected error."); 1.827 + } 1.828 + let placeInfo = placesResult.results[0]; 1.829 + let uri = placeInfo.uri; 1.830 + do_check_true(yield promiseIsURIVisited(uri)); 1.831 + do_check_eq(placeInfo.guid, place.guid); 1.832 + do_check_guid_for_uri(uri, place.guid); 1.833 + yield promiseAsyncUpdates(); 1.834 +} 1.835 + 1.836 +function test_referrer_saved() 1.837 +{ 1.838 + let places = [ 1.839 + { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/referrer"), 1.840 + visits: [ 1.841 + new VisitInfo(), 1.842 + ], 1.843 + }, 1.844 + { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/test"), 1.845 + visits: [ 1.846 + new VisitInfo(), 1.847 + ], 1.848 + }, 1.849 + ]; 1.850 + places[1].visits[0].referrerURI = places[0].uri; 1.851 + do_check_false(yield promiseIsURIVisited(places[0].uri)); 1.852 + do_check_false(yield promiseIsURIVisited(places[1].uri)); 1.853 + 1.854 + let resultCount = 0; 1.855 + let placesResult = yield promiseUpdatePlaces(places); 1.856 + if (placesResult.errors.length > 0) { 1.857 + do_throw("Unexpected error."); 1.858 + } 1.859 + for (let placeInfo of placesResult.results) { 1.860 + let uri = placeInfo.uri; 1.861 + do_check_true(yield promiseIsURIVisited(uri)); 1.862 + let visit = placeInfo.visits[0]; 1.863 + 1.864 + // We need to insert all of our visits before we can test conditions. 1.865 + if (++resultCount == places.length) { 1.866 + do_check_true(places[0].uri.equals(visit.referrerURI)); 1.867 + 1.868 + let stmt = DBConn().createStatement( 1.869 + "SELECT COUNT(1) AS count " + 1.870 + "FROM moz_historyvisits " + 1.871 + "WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) " + 1.872 + "AND from_visit = ( " + 1.873 + "SELECT id " + 1.874 + "FROM moz_historyvisits " + 1.875 + "WHERE place_id = (SELECT id FROM moz_places WHERE url = :referrer) " + 1.876 + ") " 1.877 + ); 1.878 + stmt.params.page_url = uri.spec; 1.879 + stmt.params.referrer = visit.referrerURI.spec; 1.880 + do_check_true(stmt.executeStep()); 1.881 + do_check_eq(stmt.row.count, 1); 1.882 + stmt.finalize(); 1.883 + 1.884 + yield promiseAsyncUpdates(); 1.885 + } 1.886 + } 1.887 +} 1.888 + 1.889 +function test_guid_change_saved() 1.890 +{ 1.891 + // First, add a visit for it. 1.892 + let place = { 1.893 + uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_change_saved"), 1.894 + visits: [ 1.895 + new VisitInfo(), 1.896 + ], 1.897 + }; 1.898 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.899 + 1.900 + let placesResult = yield promiseUpdatePlaces(place); 1.901 + if (placesResult.errors.length > 0) { 1.902 + do_throw("Unexpected error."); 1.903 + } 1.904 + // Then, change the guid with visits. 1.905 + place.guid = "_GUIDCHANGE_"; 1.906 + place.visits = [new VisitInfo()]; 1.907 + placesResult = yield promiseUpdatePlaces(place); 1.908 + if (placesResult.errors.length > 0) { 1.909 + do_throw("Unexpected error."); 1.910 + } 1.911 + do_check_guid_for_uri(place.uri, place.guid); 1.912 + 1.913 + yield promiseAsyncUpdates(); 1.914 +} 1.915 + 1.916 +function test_title_change_saved() 1.917 +{ 1.918 + // First, add a visit for it. 1.919 + let place = { 1.920 + uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"), 1.921 + title: "original title", 1.922 + visits: [ 1.923 + new VisitInfo(), 1.924 + ], 1.925 + }; 1.926 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.927 + 1.928 + let placesResult = yield promiseUpdatePlaces(place); 1.929 + if (placesResult.errors.length > 0) { 1.930 + do_throw("Unexpected error."); 1.931 + } 1.932 + 1.933 + // Now, make sure the empty string clears the title. 1.934 + place.title = ""; 1.935 + place.visits = [new VisitInfo()]; 1.936 + placesResult = yield promiseUpdatePlaces(place); 1.937 + if (placesResult.errors.length > 0) { 1.938 + do_throw("Unexpected error."); 1.939 + } 1.940 + do_check_title_for_uri(place.uri, null); 1.941 + 1.942 + // Then, change the title with visits. 1.943 + place.title = "title change"; 1.944 + place.visits = [new VisitInfo()]; 1.945 + placesResult = yield promiseUpdatePlaces(place); 1.946 + if (placesResult.errors.length > 0) { 1.947 + do_throw("Unexpected error."); 1.948 + } 1.949 + do_check_title_for_uri(place.uri, place.title); 1.950 + 1.951 + // Lastly, check that the title is cleared if we set it to null. 1.952 + place.title = null; 1.953 + place.visits = [new VisitInfo()]; 1.954 + placesResult = yield promiseUpdatePlaces(place); 1.955 + if (placesResult.errors.length > 0) { 1.956 + do_throw("Unexpected error."); 1.957 + } 1.958 + do_check_title_for_uri(place.uri, place.title); 1.959 + 1.960 + yield promiseAsyncUpdates(); 1.961 +} 1.962 + 1.963 +function test_no_title_does_not_clear_title() 1.964 +{ 1.965 + const TITLE = "test title"; 1.966 + // First, add a visit for it. 1.967 + let place = { 1.968 + uri: NetUtil.newURI(TEST_DOMAIN + "test_no_title_does_not_clear_title"), 1.969 + title: TITLE, 1.970 + visits: [ 1.971 + new VisitInfo(), 1.972 + ], 1.973 + }; 1.974 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.975 + 1.976 + let placesResult = yield promiseUpdatePlaces(place); 1.977 + if (placesResult.errors.length > 0) { 1.978 + do_throw("Unexpected error."); 1.979 + } 1.980 + // Now, make sure that not specifying a title does not clear it. 1.981 + delete place.title; 1.982 + place.visits = [new VisitInfo()]; 1.983 + placesResult = yield promiseUpdatePlaces(place); 1.984 + if (placesResult.errors.length > 0) { 1.985 + do_throw("Unexpected error."); 1.986 + } 1.987 + do_check_title_for_uri(place.uri, TITLE); 1.988 + 1.989 + yield promiseAsyncUpdates(); 1.990 +} 1.991 + 1.992 +function test_title_change_notifies() 1.993 +{ 1.994 + // There are three cases to test. The first case is to make sure we do not 1.995 + // get notified if we do not specify a title. 1.996 + let place = { 1.997 + uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_notifies"), 1.998 + visits: [ 1.999 + new VisitInfo(), 1.1000 + ], 1.1001 + }; 1.1002 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.1003 + 1.1004 + let silentObserver = 1.1005 + new TitleChangedObserver(place.uri, "DO NOT WANT", function() { 1.1006 + do_throw("unexpected callback!"); 1.1007 + }); 1.1008 + 1.1009 + PlacesUtils.history.addObserver(silentObserver, false); 1.1010 + let placesResult = yield promiseUpdatePlaces(place); 1.1011 + if (placesResult.errors.length > 0) { 1.1012 + do_throw("Unexpected error."); 1.1013 + } 1.1014 + 1.1015 + // The second case to test is that we get the notification when we add 1.1016 + // it for the first time. The first case will fail before our callback if it 1.1017 + // is busted, so we can do this now. 1.1018 + place.uri = NetUtil.newURI(place.uri.spec + "/new-visit-with-title"); 1.1019 + place.title = "title 1"; 1.1020 + function promiseTitleChangedObserver(aPlace) { 1.1021 + let deferred = Promise.defer(); 1.1022 + let callbackCount = 0; 1.1023 + let observer = new TitleChangedObserver(aPlace.uri, aPlace.title, function() { 1.1024 + switch (++callbackCount) { 1.1025 + case 1: 1.1026 + // The third case to test is to make sure we get a notification when 1.1027 + // we change an existing place. 1.1028 + observer.expectedTitle = place.title = "title 2"; 1.1029 + place.visits = [new VisitInfo()]; 1.1030 + PlacesUtils.asyncHistory.updatePlaces(place); 1.1031 + break; 1.1032 + case 2: 1.1033 + PlacesUtils.history.removeObserver(silentObserver); 1.1034 + PlacesUtils.history.removeObserver(observer); 1.1035 + deferred.resolve(); 1.1036 + break; 1.1037 + }; 1.1038 + }); 1.1039 + 1.1040 + PlacesUtils.history.addObserver(observer, false); 1.1041 + PlacesUtils.asyncHistory.updatePlaces(aPlace); 1.1042 + return deferred.promise; 1.1043 + } 1.1044 + 1.1045 + yield promiseTitleChangedObserver(place); 1.1046 + yield promiseAsyncUpdates(); 1.1047 +} 1.1048 + 1.1049 +function test_visit_notifies() 1.1050 +{ 1.1051 + // There are two observers we need to see for each visit. One is an 1.1052 + // nsINavHistoryObserver and the other is the uri-visit-saved observer topic. 1.1053 + let place = { 1.1054 + guid: "abcdefghijkl", 1.1055 + uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_notifies"), 1.1056 + visits: [ 1.1057 + new VisitInfo(), 1.1058 + ], 1.1059 + }; 1.1060 + do_check_false(yield promiseIsURIVisited(place.uri)); 1.1061 + 1.1062 + function promiseVisitObserver(aPlace) { 1.1063 + let deferred = Promise.defer(); 1.1064 + let callbackCount = 0; 1.1065 + let finisher = function() { 1.1066 + if (++callbackCount == 2) { 1.1067 + deferred.resolve(); 1.1068 + } 1.1069 + } 1.1070 + let visitObserver = new VisitObserver(place.uri, place.guid, 1.1071 + function(aVisitDate, 1.1072 + aTransitionType) { 1.1073 + let visit = place.visits[0]; 1.1074 + do_check_eq(visit.visitDate, aVisitDate); 1.1075 + do_check_eq(visit.transitionType, aTransitionType); 1.1076 + 1.1077 + PlacesUtils.history.removeObserver(visitObserver); 1.1078 + finisher(); 1.1079 + }); 1.1080 + PlacesUtils.history.addObserver(visitObserver, false); 1.1081 + let observer = function(aSubject, aTopic, aData) { 1.1082 + do_log_info("observe(" + aSubject + ", " + aTopic + ", " + aData + ")"); 1.1083 + do_check_true(aSubject instanceof Ci.nsIURI); 1.1084 + do_check_true(aSubject.equals(place.uri)); 1.1085 + 1.1086 + Services.obs.removeObserver(observer, URI_VISIT_SAVED); 1.1087 + finisher(); 1.1088 + }; 1.1089 + Services.obs.addObserver(observer, URI_VISIT_SAVED, false); 1.1090 + PlacesUtils.asyncHistory.updatePlaces(place); 1.1091 + return deferred.promise; 1.1092 + } 1.1093 + 1.1094 + yield promiseVisitObserver(place); 1.1095 + yield promiseAsyncUpdates(); 1.1096 +} 1.1097 + 1.1098 +// test with empty mozIVisitInfoCallback object 1.1099 +function test_callbacks_not_supplied() 1.1100 +{ 1.1101 + const URLS = [ 1.1102 + "imap://cyrus.andrew.cmu.edu/archive.imap", // bad URI 1.1103 + "http://mozilla.org/" // valid URI 1.1104 + ]; 1.1105 + let places = []; 1.1106 + URLS.forEach(function(url) { 1.1107 + try { 1.1108 + let place = { 1.1109 + uri: NetUtil.newURI(url), 1.1110 + title: "test for " + url, 1.1111 + visits: [ 1.1112 + new VisitInfo(), 1.1113 + ], 1.1114 + }; 1.1115 + places.push(place); 1.1116 + } 1.1117 + catch (e if e.result === Cr.NS_ERROR_FAILURE) { 1.1118 + // NetUtil.newURI() can throw if e.g. our app knows about imap:// 1.1119 + // but the account is not set up and so the URL is invalid for us. 1.1120 + // Note this in the log but ignore as it's not the subject of this test. 1.1121 + do_log_info("Could not construct URI for '" + url + "'; ignoring"); 1.1122 + } 1.1123 + }); 1.1124 + 1.1125 + PlacesUtils.asyncHistory.updatePlaces(places, {}); 1.1126 + yield promiseAsyncUpdates(); 1.1127 +} 1.1128 + 1.1129 +//////////////////////////////////////////////////////////////////////////////// 1.1130 +//// Test Runner 1.1131 + 1.1132 +[ 1.1133 + test_interface_exists, 1.1134 + test_invalid_uri_throws, 1.1135 + test_invalid_places_throws, 1.1136 + test_invalid_guid_throws, 1.1137 + test_no_visits_throws, 1.1138 + test_add_visit_no_date_throws, 1.1139 + test_add_visit_no_transitionType_throws, 1.1140 + test_add_visit_invalid_transitionType_throws, 1.1141 + // Note: all asynchronous tests (every test below this point) should wait for 1.1142 + // async updates before calling run_next_test. 1.1143 + test_non_addable_uri_errors, 1.1144 + test_duplicate_guid_errors, 1.1145 + test_invalid_referrerURI_ignored, 1.1146 + test_nonnsIURI_referrerURI_ignored, 1.1147 + test_old_referrer_ignored, 1.1148 + test_place_id_ignored, 1.1149 + test_handleCompletion_called_when_complete, 1.1150 + test_add_visit, 1.1151 + test_properties_saved, 1.1152 + test_guid_saved, 1.1153 + test_referrer_saved, 1.1154 + test_guid_change_saved, 1.1155 + test_title_change_saved, 1.1156 + test_no_title_does_not_clear_title, 1.1157 + test_title_change_notifies, 1.1158 + test_visit_notifies, 1.1159 + test_callbacks_not_supplied, 1.1160 +].forEach(add_task); 1.1161 + 1.1162 +function run_test() 1.1163 +{ 1.1164 + run_next_test(); 1.1165 +}