michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: /** michael@0: * This file tests the async history API exposed by mozIAsyncHistory. michael@0: */ michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: const TEST_DOMAIN = "http://mozilla.org/"; michael@0: const URI_VISIT_SAVED = "uri-visit-saved"; michael@0: const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Helpers michael@0: /** michael@0: * Object that represents a mozIVisitInfo object. michael@0: * michael@0: * @param [optional] aTransitionType michael@0: * The transition type of the visit. Defaults to TRANSITION_LINK if not michael@0: * provided. michael@0: * @param [optional] aVisitTime michael@0: * The time of the visit. Defaults to now if not provided. michael@0: */ michael@0: function VisitInfo(aTransitionType, michael@0: aVisitTime) michael@0: { michael@0: this.transitionType = michael@0: aTransitionType === undefined ? TRANSITION_LINK : aTransitionType; michael@0: this.visitDate = aVisitTime || Date.now() * 1000; michael@0: } michael@0: michael@0: function promiseUpdatePlaces(aPlaces) { michael@0: let deferred = Promise.defer(); michael@0: PlacesUtils.asyncHistory.updatePlaces(aPlaces, { michael@0: _errors: [], michael@0: _results: [], michael@0: handleError: function handleError(aResultCode, aPlace) { michael@0: this._errors.push({ resultCode: aResultCode, info: aPlace}); michael@0: }, michael@0: handleResult: function handleResult(aPlace) { michael@0: this._results.push(aPlace); michael@0: }, michael@0: handleCompletion: function handleCompletion() { michael@0: deferred.resolve({ errors: this._errors, results: this._results }); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Listens for a title change notification, and calls aCallback when it gets it. michael@0: * michael@0: * @param aURI michael@0: * The URI of the page we expect a notification for. michael@0: * @param aExpectedTitle michael@0: * The expected title of the URI we expect a notification for. michael@0: * @param aCallback michael@0: * The method to call when we have gotten the proper notification about michael@0: * the title changing. michael@0: */ michael@0: function TitleChangedObserver(aURI, michael@0: aExpectedTitle, michael@0: aCallback) michael@0: { michael@0: this.uri = aURI; michael@0: this.expectedTitle = aExpectedTitle; michael@0: this.callback = aCallback; michael@0: } michael@0: TitleChangedObserver.prototype = { michael@0: __proto__: NavHistoryObserver.prototype, michael@0: onTitleChanged: function(aURI, michael@0: aTitle, michael@0: aGUID) michael@0: { michael@0: do_log_info("onTitleChanged(" + aURI.spec + ", " + aTitle + ", " + aGUID + ")"); michael@0: if (!this.uri.equals(aURI)) { michael@0: return; michael@0: } michael@0: do_check_eq(aTitle, this.expectedTitle); michael@0: do_check_guid_for_uri(aURI, aGUID); michael@0: this.callback(); michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Listens for a visit notification, and calls aCallback when it gets it. michael@0: * michael@0: * @param aURI michael@0: * The URI of the page we expect a notification for. michael@0: * @param aCallback michael@0: * The method to call when we have gotten the proper notification about michael@0: * being visited. michael@0: */ michael@0: function VisitObserver(aURI, michael@0: aGUID, michael@0: aCallback) michael@0: { michael@0: this.uri = aURI; michael@0: this.guid = aGUID; michael@0: this.callback = aCallback; michael@0: } michael@0: VisitObserver.prototype = { michael@0: __proto__: NavHistoryObserver.prototype, michael@0: onVisit: function(aURI, michael@0: aVisitId, michael@0: aTime, michael@0: aSessionId, michael@0: aReferringId, michael@0: aTransitionType, michael@0: aGUID) michael@0: { michael@0: do_log_info("onVisit(" + aURI.spec + ", " + aVisitId + ", " + aTime + michael@0: ", " + aSessionId + ", " + aReferringId + ", " + michael@0: aTransitionType + ", " + aGUID + ")"); michael@0: if (!this.uri.equals(aURI) || this.guid != aGUID) { michael@0: return; michael@0: } michael@0: this.callback(aTime, aTransitionType); michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Tests that a title was set properly in the database. michael@0: * michael@0: * @param aURI michael@0: * The uri to check. michael@0: * @param aTitle michael@0: * The expected title in the database. michael@0: */ michael@0: function do_check_title_for_uri(aURI, michael@0: aTitle) michael@0: { michael@0: let stack = Components.stack.caller; michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT title " + michael@0: "FROM moz_places " + michael@0: "WHERE url = :url " michael@0: ); michael@0: stmt.params.url = aURI.spec; michael@0: do_check_true(stmt.executeStep(), stack); michael@0: do_check_eq(stmt.row.title, aTitle, stack); michael@0: stmt.finalize(); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Test Functions michael@0: michael@0: function test_interface_exists() michael@0: { michael@0: let history = Cc["@mozilla.org/browser/history;1"].getService(Ci.nsISupports); michael@0: do_check_true(history instanceof Ci.mozIAsyncHistory); michael@0: } michael@0: michael@0: function test_invalid_uri_throws() michael@0: { michael@0: // First, test passing in nothing. michael@0: let place = { michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: // Now, test other bogus things. michael@0: const TEST_VALUES = [ michael@0: null, michael@0: undefined, michael@0: {}, michael@0: [], michael@0: TEST_DOMAIN + "test_invalid_id_throws", michael@0: ]; michael@0: for (let i = 0; i < TEST_VALUES.length; i++) { michael@0: place.uri = TEST_VALUES[i]; michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function test_invalid_places_throws() michael@0: { michael@0: // First, test passing in nothing. michael@0: try { michael@0: PlacesUtils.asyncHistory.updatePlaces(); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS); michael@0: } michael@0: michael@0: // Now, test other bogus things. michael@0: const TEST_VALUES = [ michael@0: null, michael@0: undefined, michael@0: {}, michael@0: [], michael@0: "", michael@0: ]; michael@0: for (let i = 0; i < TEST_VALUES.length; i++) { michael@0: let value = TEST_VALUES[i]; michael@0: try { michael@0: yield promiseUpdatePlaces(value); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function test_invalid_guid_throws() michael@0: { michael@0: // First check invalid length guid. michael@0: let place = { michael@0: guid: "BAD_GUID", michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_guid_throws"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: // Now check invalid character guid. michael@0: place.guid = "__BADGUID+__"; michael@0: do_check_eq(place.guid.length, 12); michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: } michael@0: michael@0: function test_no_visits_throws() michael@0: { michael@0: const TEST_URI = michael@0: NetUtil.newURI(TEST_DOMAIN + "test_no_id_or_guid_no_visits_throws"); michael@0: const TEST_GUID = "_RANDOMGUID_"; michael@0: const TEST_PLACEID = 2; michael@0: michael@0: let log_test_conditions = function(aPlace) { michael@0: let str = "Testing place with " + michael@0: (aPlace.uri ? "uri" : "no uri") + ", " + michael@0: (aPlace.guid ? "guid" : "no guid") + ", " + michael@0: (aPlace.visits ? "visits array" : "no visits array"); michael@0: do_log_info(str); michael@0: }; michael@0: michael@0: // Loop through every possible case. Note that we don't actually care about michael@0: // the case where we have no uri, place id, or guid (covered by another test), michael@0: // but it is easier to just make sure it too throws than to exclude it. michael@0: let place = { }; michael@0: for (let uri = 1; uri >= 0; uri--) { michael@0: place.uri = uri ? TEST_URI : undefined; michael@0: michael@0: for (let guid = 1; guid >= 0; guid--) { michael@0: place.guid = guid ? TEST_GUID : undefined; michael@0: michael@0: for (let visits = 1; visits >= 0; visits--) { michael@0: place.visits = visits ? [] : undefined; michael@0: michael@0: log_test_conditions(place); michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: function test_add_visit_no_date_throws() michael@0: { michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_date_throws"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: delete place.visits[0].visitDate; michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: } michael@0: michael@0: function test_add_visit_no_transitionType_throws() michael@0: { michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_transitionType_throws"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: delete place.visits[0].transitionType; michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: } michael@0: michael@0: function test_add_visit_invalid_transitionType_throws() michael@0: { michael@0: // First, test something that has a transition type lower than the first one. michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + michael@0: "test_add_visit_invalid_transitionType_throws"), michael@0: visits: [ michael@0: new VisitInfo(TRANSITION_LINK - 1), michael@0: ], michael@0: }; michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: // Now, test something that has a transition type greater than the last one. michael@0: place.visits[0] = new VisitInfo(TRANSITION_FRAMED_LINK + 1); michael@0: try { michael@0: yield promiseUpdatePlaces(place); michael@0: do_throw("Should have thrown!"); michael@0: } michael@0: catch (e) { michael@0: do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: } michael@0: michael@0: function test_non_addable_uri_errors() michael@0: { michael@0: // Array of protocols that nsINavHistoryService::canAddURI returns false for. michael@0: const URLS = [ michael@0: "about:config", michael@0: "imap://cyrus.andrew.cmu.edu/archive.imap", michael@0: "news://new.mozilla.org/mozilla.dev.apps.firefox", michael@0: "mailbox:Inbox", michael@0: "moz-anno:favicon:http://mozilla.org/made-up-favicon", michael@0: "view-source:http://mozilla.org", michael@0: "chrome://browser/content/browser.xul", michael@0: "resource://gre-resources/hiddenWindow.html", michael@0: "data:,Hello%2C%20World!", michael@0: "wyciwyg:/0/http://mozilla.org", michael@0: "javascript:alert('hello wolrd!');", michael@0: "blob:foo", michael@0: ]; michael@0: let places = []; michael@0: URLS.forEach(function(url) { michael@0: try { michael@0: let place = { michael@0: uri: NetUtil.newURI(url), michael@0: title: "test for " + url, michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: places.push(place); michael@0: } michael@0: catch (e if e.result === Cr.NS_ERROR_FAILURE) { michael@0: // NetUtil.newURI() can throw if e.g. our app knows about imap:// michael@0: // but the account is not set up and so the URL is invalid for us. michael@0: // Note this in the log but ignore as it's not the subject of this test. michael@0: do_log_info("Could not construct URI for '" + url + "'; ignoring"); michael@0: } michael@0: }); michael@0: michael@0: let placesResult = yield promiseUpdatePlaces(places); michael@0: if (placesResult.results.length > 0) { michael@0: do_throw("Unexpected success."); michael@0: } michael@0: for (let place of placesResult.errors) { michael@0: do_log_info("Checking '" + place.info.uri.spec + "'"); michael@0: do_check_eq(place.resultCode, Cr.NS_ERROR_INVALID_ARG); michael@0: do_check_false(yield promiseIsURIVisited(place.info.uri)); michael@0: } michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_duplicate_guid_errors() michael@0: { michael@0: // This test ensures that trying to add a visit, with a guid already found in michael@0: // another visit, fails. michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: let placeInfo = placesResult.results[0]; michael@0: do_check_true(yield promiseIsURIVisited(placeInfo.uri)); michael@0: michael@0: let badPlace = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: guid: placeInfo.guid, michael@0: }; michael@0: michael@0: do_check_false(yield promiseIsURIVisited(badPlace.uri)); michael@0: placesResult = yield promiseUpdatePlaces(badPlace); michael@0: if (placesResult.results.length > 0) { michael@0: do_throw("Unexpected success."); michael@0: } michael@0: let badPlaceInfo = placesResult.errors[0]; michael@0: do_check_eq(badPlaceInfo.resultCode, Cr.NS_ERROR_STORAGE_CONSTRAINT); michael@0: do_check_false(yield promiseIsURIVisited(badPlaceInfo.info.uri)); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_invalid_referrerURI_ignored() michael@0: { michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + michael@0: "test_invalid_referrerURI_ignored"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: place.visits[0].referrerURI = NetUtil.newURI(place.uri.spec + "_unvisistedURI"); michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI)); michael@0: michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: let placeInfo = placesResult.results[0]; michael@0: do_check_true(yield promiseIsURIVisited(placeInfo.uri)); michael@0: michael@0: // Check to make sure we do not visit the invalid referrer. michael@0: do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI)); michael@0: michael@0: // Check to make sure from_visit is zero in database. michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT from_visit " + michael@0: "FROM moz_historyvisits " + michael@0: "WHERE id = :visit_id" michael@0: ); michael@0: stmt.params.visit_id = placeInfo.visits[0].visitId; michael@0: do_check_true(stmt.executeStep()); michael@0: do_check_eq(stmt.row.from_visit, 0); michael@0: stmt.finalize(); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_nonnsIURI_referrerURI_ignored() michael@0: { michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + michael@0: "test_nonnsIURI_referrerURI_ignored"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: place.visits[0].referrerURI = place.uri.spec + "_nonnsIURI"; michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: let placeInfo = placesResult.results[0]; michael@0: do_check_true(yield promiseIsURIVisited(placeInfo.uri)); michael@0: michael@0: // Check to make sure from_visit is zero in database. michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT from_visit " + michael@0: "FROM moz_historyvisits " + michael@0: "WHERE id = :visit_id" michael@0: ); michael@0: stmt.params.visit_id = placeInfo.visits[0].visitId; michael@0: do_check_true(stmt.executeStep()); michael@0: do_check_eq(stmt.row.from_visit, 0); michael@0: stmt.finalize(); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_old_referrer_ignored() michael@0: { michael@0: // This tests that a referrer for a visit which is not recent (specifically, michael@0: // older than 15 minutes as per RECENT_EVENT_THRESHOLD) is not saved by michael@0: // updatePlaces. michael@0: let oldTime = (Date.now() * 1000) - (RECENT_EVENT_THRESHOLD + 1); michael@0: let referrerPlace = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_referrer"), michael@0: visits: [ michael@0: new VisitInfo(TRANSITION_LINK, oldTime), michael@0: ], michael@0: }; michael@0: michael@0: // First we must add our referrer to the history so that it is not ignored michael@0: // as being invalid. michael@0: do_check_false(yield promiseIsURIVisited(referrerPlace.uri)); michael@0: let placesResult = yield promiseUpdatePlaces(referrerPlace); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: michael@0: // Now that the referrer is added, we can add a page with a valid michael@0: // referrer to determine if the recency of the referrer is taken into michael@0: // account. michael@0: do_check_true(yield promiseIsURIVisited(referrerPlace.uri)); michael@0: michael@0: let visitInfo = new VisitInfo(); michael@0: visitInfo.referrerURI = referrerPlace.uri; michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_page"), michael@0: visits: [ michael@0: visitInfo, michael@0: ], michael@0: }; michael@0: michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: let placeInfo = placesResult.results[0]; michael@0: do_check_true(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: // Though the visit will not contain the referrer, we must examine the michael@0: // database to be sure. michael@0: do_check_eq(placeInfo.visits[0].referrerURI, null); michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT COUNT(1) AS count " + michael@0: "FROM moz_historyvisits " + michael@0: "WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) " + michael@0: "AND from_visit = 0 " michael@0: ); michael@0: stmt.params.page_url = place.uri.spec; michael@0: do_check_true(stmt.executeStep()); michael@0: do_check_eq(stmt.row.count, 1); michael@0: stmt.finalize(); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_place_id_ignored() michael@0: { michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_first"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: let placeInfo = placesResult.results[0]; michael@0: do_check_true(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: let placeId = placeInfo.placeId; michael@0: do_check_neq(placeId, 0); michael@0: michael@0: let badPlace = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: placeId: placeId, michael@0: }; michael@0: michael@0: do_check_false(yield promiseIsURIVisited(badPlace.uri)); michael@0: placesResult = yield promiseUpdatePlaces(badPlace); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: placeInfo = placesResult.results[0]; michael@0: michael@0: do_check_neq(placeInfo.placeId, placeId); michael@0: do_check_true(yield promiseIsURIVisited(badPlace.uri)); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_handleCompletion_called_when_complete() michael@0: { michael@0: // We test a normal visit, and embeded visit, and a uri that would fail michael@0: // the canAddURI test to make sure that the notification happens after *all* michael@0: // of them have had a callback. michael@0: let places = [ michael@0: { uri: NetUtil.newURI(TEST_DOMAIN + michael@0: "test_handleCompletion_called_when_complete"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: new VisitInfo(TRANSITION_EMBED), michael@0: ], michael@0: }, michael@0: { uri: NetUtil.newURI("data:,Hello%2C%20World!"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }, michael@0: ]; michael@0: do_check_false(yield promiseIsURIVisited(places[0].uri)); michael@0: do_check_false(yield promiseIsURIVisited(places[1].uri)); michael@0: michael@0: const EXPECTED_COUNT_SUCCESS = 2; michael@0: const EXPECTED_COUNT_FAILURE = 1; michael@0: let callbackCountSuccess = 0; michael@0: let callbackCountFailure = 0; michael@0: michael@0: let placesResult = yield promiseUpdatePlaces(places); michael@0: for (let place of placesResult.results) { michael@0: let checker = PlacesUtils.history.canAddURI(place.uri) ? michael@0: do_check_true : do_check_false; michael@0: callbackCountSuccess++; michael@0: } michael@0: for (let error of placesResult.errors) { michael@0: callbackCountFailure++; michael@0: } michael@0: michael@0: do_check_eq(callbackCountSuccess, EXPECTED_COUNT_SUCCESS); michael@0: do_check_eq(callbackCountFailure, EXPECTED_COUNT_FAILURE); michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_add_visit() michael@0: { michael@0: const VISIT_TIME = Date.now() * 1000; michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"), michael@0: title: "test_add_visit title", michael@0: visits: [], michael@0: }; michael@0: for (let transitionType = TRANSITION_LINK; michael@0: transitionType <= TRANSITION_FRAMED_LINK; michael@0: transitionType++) { michael@0: place.visits.push(new VisitInfo(transitionType, VISIT_TIME)); michael@0: } michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: let callbackCount = 0; michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: for (let placeInfo of placesResult.results) { michael@0: do_check_true(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: // Check mozIPlaceInfo properties. michael@0: do_check_true(place.uri.equals(placeInfo.uri)); michael@0: do_check_eq(placeInfo.frecency, -1); // We don't pass frecency here! michael@0: do_check_eq(placeInfo.title, place.title); michael@0: michael@0: // Check mozIVisitInfo properties. michael@0: let visits = placeInfo.visits; michael@0: do_check_eq(visits.length, 1); michael@0: let visit = visits[0]; michael@0: do_check_eq(visit.visitDate, VISIT_TIME); michael@0: do_check_true(visit.transitionType >= TRANSITION_LINK && michael@0: visit.transitionType <= TRANSITION_FRAMED_LINK); michael@0: do_check_true(visit.referrerURI === null); michael@0: michael@0: // For TRANSITION_EMBED visits, many properties will always be zero or michael@0: // undefined. michael@0: if (visit.transitionType == TRANSITION_EMBED) { michael@0: // Check mozIPlaceInfo properties. michael@0: do_check_eq(placeInfo.placeId, 0, '//'); michael@0: do_check_eq(placeInfo.guid, null); michael@0: michael@0: // Check mozIVisitInfo properties. michael@0: do_check_eq(visit.visitId, 0); michael@0: } michael@0: // But they should be valid for non-embed visits. michael@0: else { michael@0: // Check mozIPlaceInfo properties. michael@0: do_check_true(placeInfo.placeId > 0); michael@0: do_check_valid_places_guid(placeInfo.guid); michael@0: michael@0: // Check mozIVisitInfo properties. michael@0: do_check_true(visit.visitId > 0); michael@0: } michael@0: michael@0: // If we have had all of our callbacks, continue running tests. michael@0: if (++callbackCount == place.visits.length) { michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function test_properties_saved() michael@0: { michael@0: // Check each transition type to make sure it is saved properly. michael@0: let places = []; michael@0: for (let transitionType = TRANSITION_LINK; michael@0: transitionType <= TRANSITION_FRAMED_LINK; michael@0: transitionType++) { michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_properties_saved/" + michael@0: transitionType), michael@0: title: "test_properties_saved test", michael@0: visits: [ michael@0: new VisitInfo(transitionType), michael@0: ], michael@0: }; michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: places.push(place); michael@0: } michael@0: michael@0: let callbackCount = 0; michael@0: let placesResult = yield promiseUpdatePlaces(places); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: for (let placeInfo of placesResult.results) { michael@0: let uri = placeInfo.uri; michael@0: do_check_true(yield promiseIsURIVisited(uri)); michael@0: let visit = placeInfo.visits[0]; michael@0: print("TEST-INFO | test_properties_saved | updatePlaces callback for " + michael@0: "transition type " + visit.transitionType); michael@0: michael@0: // Note that TRANSITION_EMBED should not be in the database. michael@0: const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1; michael@0: michael@0: // mozIVisitInfo::date michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT COUNT(1) AS count " + michael@0: "FROM moz_places h " + michael@0: "JOIN moz_historyvisits v " + michael@0: "ON h.id = v.place_id " + michael@0: "WHERE h.url = :page_url " + michael@0: "AND v.visit_date = :visit_date " michael@0: ); michael@0: stmt.params.page_url = uri.spec; michael@0: stmt.params.visit_date = visit.visitDate; michael@0: do_check_true(stmt.executeStep()); michael@0: do_check_eq(stmt.row.count, EXPECTED_COUNT); michael@0: stmt.finalize(); michael@0: michael@0: // mozIVisitInfo::transitionType michael@0: stmt = DBConn().createStatement( michael@0: "SELECT COUNT(1) AS count " + michael@0: "FROM moz_places h " + michael@0: "JOIN moz_historyvisits v " + michael@0: "ON h.id = v.place_id " + michael@0: "WHERE h.url = :page_url " + michael@0: "AND v.visit_type = :transition_type " michael@0: ); michael@0: stmt.params.page_url = uri.spec; michael@0: stmt.params.transition_type = visit.transitionType; michael@0: do_check_true(stmt.executeStep()); michael@0: do_check_eq(stmt.row.count, EXPECTED_COUNT); michael@0: stmt.finalize(); michael@0: michael@0: // mozIPlaceInfo::title michael@0: stmt = DBConn().createStatement( michael@0: "SELECT COUNT(1) AS count " + michael@0: "FROM moz_places h " + michael@0: "WHERE h.url = :page_url " + michael@0: "AND h.title = :title " michael@0: ); michael@0: stmt.params.page_url = uri.spec; michael@0: stmt.params.title = placeInfo.title; michael@0: do_check_true(stmt.executeStep()); michael@0: do_check_eq(stmt.row.count, EXPECTED_COUNT); michael@0: stmt.finalize(); michael@0: michael@0: // If we have had all of our callbacks, continue running tests. michael@0: if (++callbackCount == places.length) { michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function test_guid_saved() michael@0: { michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_saved"), michael@0: guid: "__TESTGUID__", michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: do_check_valid_places_guid(place.guid); michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: let placeInfo = placesResult.results[0]; michael@0: let uri = placeInfo.uri; michael@0: do_check_true(yield promiseIsURIVisited(uri)); michael@0: do_check_eq(placeInfo.guid, place.guid); michael@0: do_check_guid_for_uri(uri, place.guid); michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_referrer_saved() michael@0: { michael@0: let places = [ michael@0: { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/referrer"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }, michael@0: { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/test"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }, michael@0: ]; michael@0: places[1].visits[0].referrerURI = places[0].uri; michael@0: do_check_false(yield promiseIsURIVisited(places[0].uri)); michael@0: do_check_false(yield promiseIsURIVisited(places[1].uri)); michael@0: michael@0: let resultCount = 0; michael@0: let placesResult = yield promiseUpdatePlaces(places); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: for (let placeInfo of placesResult.results) { michael@0: let uri = placeInfo.uri; michael@0: do_check_true(yield promiseIsURIVisited(uri)); michael@0: let visit = placeInfo.visits[0]; michael@0: michael@0: // We need to insert all of our visits before we can test conditions. michael@0: if (++resultCount == places.length) { michael@0: do_check_true(places[0].uri.equals(visit.referrerURI)); michael@0: michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT COUNT(1) AS count " + michael@0: "FROM moz_historyvisits " + michael@0: "WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) " + michael@0: "AND from_visit = ( " + michael@0: "SELECT id " + michael@0: "FROM moz_historyvisits " + michael@0: "WHERE place_id = (SELECT id FROM moz_places WHERE url = :referrer) " + michael@0: ") " michael@0: ); michael@0: stmt.params.page_url = uri.spec; michael@0: stmt.params.referrer = visit.referrerURI.spec; michael@0: do_check_true(stmt.executeStep()); michael@0: do_check_eq(stmt.row.count, 1); michael@0: stmt.finalize(); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function test_guid_change_saved() michael@0: { michael@0: // First, add a visit for it. michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_change_saved"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: // Then, change the guid with visits. michael@0: place.guid = "_GUIDCHANGE_"; michael@0: place.visits = [new VisitInfo()]; michael@0: placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: do_check_guid_for_uri(place.uri, place.guid); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_title_change_saved() michael@0: { michael@0: // First, add a visit for it. michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"), michael@0: title: "original title", michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: michael@0: // Now, make sure the empty string clears the title. michael@0: place.title = ""; michael@0: place.visits = [new VisitInfo()]; michael@0: placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: do_check_title_for_uri(place.uri, null); michael@0: michael@0: // Then, change the title with visits. michael@0: place.title = "title change"; michael@0: place.visits = [new VisitInfo()]; michael@0: placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: do_check_title_for_uri(place.uri, place.title); michael@0: michael@0: // Lastly, check that the title is cleared if we set it to null. michael@0: place.title = null; michael@0: place.visits = [new VisitInfo()]; michael@0: placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: do_check_title_for_uri(place.uri, place.title); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_no_title_does_not_clear_title() michael@0: { michael@0: const TITLE = "test title"; michael@0: // First, add a visit for it. michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_no_title_does_not_clear_title"), michael@0: title: TITLE, michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: // Now, make sure that not specifying a title does not clear it. michael@0: delete place.title; michael@0: place.visits = [new VisitInfo()]; michael@0: placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: do_check_title_for_uri(place.uri, TITLE); michael@0: michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_title_change_notifies() michael@0: { michael@0: // There are three cases to test. The first case is to make sure we do not michael@0: // get notified if we do not specify a title. michael@0: let place = { michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_notifies"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: let silentObserver = michael@0: new TitleChangedObserver(place.uri, "DO NOT WANT", function() { michael@0: do_throw("unexpected callback!"); michael@0: }); michael@0: michael@0: PlacesUtils.history.addObserver(silentObserver, false); michael@0: let placesResult = yield promiseUpdatePlaces(place); michael@0: if (placesResult.errors.length > 0) { michael@0: do_throw("Unexpected error."); michael@0: } michael@0: michael@0: // The second case to test is that we get the notification when we add michael@0: // it for the first time. The first case will fail before our callback if it michael@0: // is busted, so we can do this now. michael@0: place.uri = NetUtil.newURI(place.uri.spec + "/new-visit-with-title"); michael@0: place.title = "title 1"; michael@0: function promiseTitleChangedObserver(aPlace) { michael@0: let deferred = Promise.defer(); michael@0: let callbackCount = 0; michael@0: let observer = new TitleChangedObserver(aPlace.uri, aPlace.title, function() { michael@0: switch (++callbackCount) { michael@0: case 1: michael@0: // The third case to test is to make sure we get a notification when michael@0: // we change an existing place. michael@0: observer.expectedTitle = place.title = "title 2"; michael@0: place.visits = [new VisitInfo()]; michael@0: PlacesUtils.asyncHistory.updatePlaces(place); michael@0: break; michael@0: case 2: michael@0: PlacesUtils.history.removeObserver(silentObserver); michael@0: PlacesUtils.history.removeObserver(observer); michael@0: deferred.resolve(); michael@0: break; michael@0: }; michael@0: }); michael@0: michael@0: PlacesUtils.history.addObserver(observer, false); michael@0: PlacesUtils.asyncHistory.updatePlaces(aPlace); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: yield promiseTitleChangedObserver(place); michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: function test_visit_notifies() michael@0: { michael@0: // There are two observers we need to see for each visit. One is an michael@0: // nsINavHistoryObserver and the other is the uri-visit-saved observer topic. michael@0: let place = { michael@0: guid: "abcdefghijkl", michael@0: uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_notifies"), michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: do_check_false(yield promiseIsURIVisited(place.uri)); michael@0: michael@0: function promiseVisitObserver(aPlace) { michael@0: let deferred = Promise.defer(); michael@0: let callbackCount = 0; michael@0: let finisher = function() { michael@0: if (++callbackCount == 2) { michael@0: deferred.resolve(); michael@0: } michael@0: } michael@0: let visitObserver = new VisitObserver(place.uri, place.guid, michael@0: function(aVisitDate, michael@0: aTransitionType) { michael@0: let visit = place.visits[0]; michael@0: do_check_eq(visit.visitDate, aVisitDate); michael@0: do_check_eq(visit.transitionType, aTransitionType); michael@0: michael@0: PlacesUtils.history.removeObserver(visitObserver); michael@0: finisher(); michael@0: }); michael@0: PlacesUtils.history.addObserver(visitObserver, false); michael@0: let observer = function(aSubject, aTopic, aData) { michael@0: do_log_info("observe(" + aSubject + ", " + aTopic + ", " + aData + ")"); michael@0: do_check_true(aSubject instanceof Ci.nsIURI); michael@0: do_check_true(aSubject.equals(place.uri)); michael@0: michael@0: Services.obs.removeObserver(observer, URI_VISIT_SAVED); michael@0: finisher(); michael@0: }; michael@0: Services.obs.addObserver(observer, URI_VISIT_SAVED, false); michael@0: PlacesUtils.asyncHistory.updatePlaces(place); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: yield promiseVisitObserver(place); michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: // test with empty mozIVisitInfoCallback object michael@0: function test_callbacks_not_supplied() michael@0: { michael@0: const URLS = [ michael@0: "imap://cyrus.andrew.cmu.edu/archive.imap", // bad URI michael@0: "http://mozilla.org/" // valid URI michael@0: ]; michael@0: let places = []; michael@0: URLS.forEach(function(url) { michael@0: try { michael@0: let place = { michael@0: uri: NetUtil.newURI(url), michael@0: title: "test for " + url, michael@0: visits: [ michael@0: new VisitInfo(), michael@0: ], michael@0: }; michael@0: places.push(place); michael@0: } michael@0: catch (e if e.result === Cr.NS_ERROR_FAILURE) { michael@0: // NetUtil.newURI() can throw if e.g. our app knows about imap:// michael@0: // but the account is not set up and so the URL is invalid for us. michael@0: // Note this in the log but ignore as it's not the subject of this test. michael@0: do_log_info("Could not construct URI for '" + url + "'; ignoring"); michael@0: } michael@0: }); michael@0: michael@0: PlacesUtils.asyncHistory.updatePlaces(places, {}); michael@0: yield promiseAsyncUpdates(); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Test Runner michael@0: michael@0: [ michael@0: test_interface_exists, michael@0: test_invalid_uri_throws, michael@0: test_invalid_places_throws, michael@0: test_invalid_guid_throws, michael@0: test_no_visits_throws, michael@0: test_add_visit_no_date_throws, michael@0: test_add_visit_no_transitionType_throws, michael@0: test_add_visit_invalid_transitionType_throws, michael@0: // Note: all asynchronous tests (every test below this point) should wait for michael@0: // async updates before calling run_next_test. michael@0: test_non_addable_uri_errors, michael@0: test_duplicate_guid_errors, michael@0: test_invalid_referrerURI_ignored, michael@0: test_nonnsIURI_referrerURI_ignored, michael@0: test_old_referrer_ignored, michael@0: test_place_id_ignored, michael@0: test_handleCompletion_called_when_complete, michael@0: test_add_visit, michael@0: test_properties_saved, michael@0: test_guid_saved, michael@0: test_referrer_saved, michael@0: test_guid_change_saved, michael@0: test_title_change_saved, michael@0: test_no_title_does_not_clear_title, michael@0: test_title_change_notifies, michael@0: test_visit_notifies, michael@0: test_callbacks_not_supplied, michael@0: ].forEach(add_task); michael@0: michael@0: function run_test() michael@0: { michael@0: run_next_test(); michael@0: }