/** * This file tests the async history API exposed by mozIAsyncHistory. */ // Globals XPCOMUtils.defineLazyServiceGetter( this, "asyncHistory", "@mozilla.org/browser/history;1", "mozIAsyncHistory" ); const TEST_DOMAIN = "http://mozilla.org/"; const URI_VISIT_SAVED = "uri-visit-saved"; const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000; // Helpers /** * Object that represents a mozIVisitInfo object. * * @param [optional] aTransitionType * The transition type of the visit. Defaults to TRANSITION_LINK if not * provided. * @param [optional] aVisitTime * The time of the visit. Defaults to now if not provided. */ function VisitInfo(aTransitionType, aVisitTime) { this.transitionType = aTransitionType === undefined ? TRANSITION_LINK : aTransitionType; this.visitDate = aVisitTime || Date.now() * 1000; } function promiseUpdatePlaces(aPlaces, aOptions = {}) { return new Promise((resolve, reject) => { asyncHistory.updatePlaces( aPlaces, Object.assign( { _errors: [], _results: [], handleError(aResultCode, aPlace) { this._errors.push({ resultCode: aResultCode, info: aPlace }); }, handleResult(aPlace) { this._results.push(aPlace); }, handleCompletion(resultCount) { resolve({ errors: this._errors, results: this._results, resultCount, }); }, }, aOptions ) ); }); } /** * Listens for a title change notification, and calls aCallback when it gets it. */ class TitleChangedObserver { /** * Constructor. * * @param aURI * The URI of the page we expect a notification for. * @param aExpectedTitle * The expected title of the URI we expect a notification for. * @param aCallback * The method to call when we have gotten the proper notification about * the title changing. */ constructor(aURI, aExpectedTitle, aCallback) { this.uri = aURI; this.expectedTitle = aExpectedTitle; this.callback = aCallback; this.handlePlacesEvent = this.handlePlacesEvent.bind(this); PlacesObservers.addListener(["page-title-changed"], this.handlePlacesEvent); } async handlePlacesEvent(aEvents) { info("'page-title-changed'!!!"); Assert.equal(aEvents.length, 1, "Right number of title changed notified"); Assert.equal(aEvents[0].type, "page-title-changed"); if (this.uri.spec !== aEvents[0].url) { return; } Assert.equal(aEvents[0].title, this.expectedTitle); await check_guid_for_uri(this.uri, aEvents[0].pageGuid); this.callback(); PlacesObservers.removeListener( ["page-title-changed"], this.handlePlacesEvent ); } } /** * Listens for a visit notification, and calls aCallback when it gets it. */ class VisitObserver { constructor(aURI, aGUID, aCallback) { this.uri = aURI; this.guid = aGUID; this.callback = aCallback; this.handlePlacesEvent = this.handlePlacesEvent.bind(this); PlacesObservers.addListener(["page-visited"], this.handlePlacesEvent); } handlePlacesEvent(aEvents) { info("'page-visited'!!!"); Assert.equal(aEvents.length, 1, "Right number of visits notified"); Assert.equal(aEvents[0].type, "page-visited"); let { url, visitId, visitTime, referringVisitId, transitionType, pageGuid, hidden, visitCount, typedCount, lastKnownTitle, } = aEvents[0]; let args = [ visitId, visitTime, referringVisitId, transitionType, pageGuid, hidden, visitCount, typedCount, lastKnownTitle, ]; info("'page-visited' (" + url + args.join(", ") + ")"); if (this.uri.spec != url || this.guid != pageGuid) { return; } this.callback(visitTime * 1000, transitionType, lastKnownTitle); PlacesObservers.removeListener(["page-visited"], this.handlePlacesEvent); } } /** * Tests that a title was set properly in the database. * * @param aURI * The uri to check. * @param aTitle * The expected title in the database. */ function do_check_title_for_uri(aURI, aTitle) { let stmt = DBConn().createStatement( `SELECT title FROM moz_places WHERE url_hash = hash(:url) AND url = :url` ); stmt.params.url = aURI.spec; Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.title, aTitle); stmt.finalize(); } // Test Functions add_task(async function test_interface_exists() { let history = Cc["@mozilla.org/browser/history;1"].getService(Ci.nsISupports); Assert.ok(history instanceof Ci.mozIAsyncHistory); }); add_task(async function test_invalid_uri_throws() { // First, test passing in nothing. let place = { visits: [new VisitInfo()], }; try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } // Now, test other bogus things. const TEST_VALUES = [ null, undefined, {}, [], TEST_DOMAIN + "test_invalid_id_throws", ]; for (let i = 0; i < TEST_VALUES.length; i++) { place.uri = TEST_VALUES[i]; try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } } }); add_task(async function test_invalid_places_throws() { // First, test passing in nothing. try { asyncHistory.updatePlaces(); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS); } // Now, test other bogus things. const TEST_VALUES = [null, undefined, {}, [], ""]; for (let i = 0; i < TEST_VALUES.length; i++) { let value = TEST_VALUES[i]; try { await promiseUpdatePlaces(value); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } } }); add_task(async function test_invalid_guid_throws() { // First check invalid length guid. let place = { guid: "BAD_GUID", uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_guid_throws"), visits: [new VisitInfo()], }; try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } // Now check invalid character guid. place.guid = "__BADGUID+__"; Assert.equal(place.guid.length, 12); try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } }); add_task(async function test_no_visits_throws() { const TEST_URI = NetUtil.newURI( TEST_DOMAIN + "test_no_id_or_guid_no_visits_throws" ); const TEST_GUID = "_RANDOMGUID_"; let log_test_conditions = function (aPlace) { let str = "Testing place with " + (aPlace.uri ? "uri" : "no uri") + ", " + (aPlace.guid ? "guid" : "no guid") + ", " + (aPlace.visits ? "visits array" : "no visits array"); info(str); }; // Loop through every possible case. Note that we don't actually care about // the case where we have no uri, place id, or guid (covered by another test), // but it is easier to just make sure it too throws than to exclude it. let place = {}; for (let uri = 1; uri >= 0; uri--) { place.uri = uri ? TEST_URI : undefined; for (let guid = 1; guid >= 0; guid--) { place.guid = guid ? TEST_GUID : undefined; for (let visits = 1; visits >= 0; visits--) { place.visits = visits ? [] : undefined; log_test_conditions(place); try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } } } } }); add_task(async function test_add_visit_no_date_throws() { let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_date_throws"), visits: [new VisitInfo()], }; delete place.visits[0].visitDate; try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } }); add_task(async function test_add_visit_no_transitionType_throws() { let place = { uri: NetUtil.newURI( TEST_DOMAIN + "test_add_visit_no_transitionType_throws" ), visits: [new VisitInfo()], }; delete place.visits[0].transitionType; try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } }); add_task(async function test_add_visit_invalid_transitionType_throws() { // First, test something that has a transition type lower than the first one. let place = { uri: NetUtil.newURI( TEST_DOMAIN + "test_add_visit_invalid_transitionType_throws" ), visits: [new VisitInfo(TRANSITION_LINK - 1)], }; try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } // Now, test something that has a transition type greater than the last one. place.visits[0] = new VisitInfo(TRANSITION_RELOAD + 1); try { await promiseUpdatePlaces(place); do_throw("Should have thrown!"); } catch (e) { Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); } }); add_task(async function test_non_addable_uri_errors() { // Array of protocols that nsINavHistoryService::canAddURI returns false for. const URLS = [ "about:config", "imap://cyrus.andrew.cmu.edu/archive.imap", "news://new.mozilla.org/mozilla.dev.apps.firefox", "mailbox:Inbox", "cached-favicon:http://mozilla.org/made-up-favicon", "view-source:http://mozilla.org", "chrome://browser/content/browser.xhtml", "resource://gre-resources/hiddenWindow.html", "data:,Hello%2C%20World!", "javascript:alert('hello wolrd!');", "blob:foo", "moz-extension://f49fb5b3-a1e7-cd41-85e1-d61a3950f5e4/index.html", ]; let places = []; URLS.forEach(function (url) { try { let place = { uri: NetUtil.newURI(url), title: "test for " + url, visits: [new VisitInfo()], }; places.push(place); } catch (e) { if (e.result != Cr.NS_ERROR_FAILURE) { throw e; } // NetUtil.newURI() can throw if e.g. our app knows about imap:// // but the account is not set up and so the URL is invalid for us. // Note this in the log but ignore as it's not the subject of this test. info("Could not construct URI for '" + url + "'; ignoring"); } }); let placesResult = await promiseUpdatePlaces(places); if (placesResult.results.length) { do_throw("Unexpected success."); } for (let place of placesResult.errors) { info("Checking '" + place.info.uri.spec + "'"); Assert.equal(place.resultCode, Cr.NS_ERROR_INVALID_ARG); Assert.equal(false, await PlacesUtils.history.hasVisits(place.info.uri)); } await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_duplicate_guid_errors() { // This test ensures that trying to add a visit, with a guid already found in // another visit, fails. let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"), visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } let placeInfo = placesResult.results[0]; Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri)); let badPlace = { uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"), visits: [new VisitInfo()], guid: placeInfo.guid, }; Assert.equal(false, await PlacesUtils.history.hasVisits(badPlace.uri)); placesResult = await promiseUpdatePlaces(badPlace); if (placesResult.results.length) { do_throw("Unexpected success."); } let badPlaceInfo = placesResult.errors[0]; Assert.equal(badPlaceInfo.resultCode, Cr.NS_ERROR_STORAGE_CONSTRAINT); Assert.equal( false, await PlacesUtils.history.hasVisits(badPlaceInfo.info.uri) ); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_invalid_referrerURI_ignored() { let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_referrerURI_ignored"), visits: [new VisitInfo()], }; place.visits[0].referrerURI = NetUtil.newURI( place.uri.spec + "_unvisistedURI" ); Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); Assert.equal( false, await PlacesUtils.history.hasVisits(place.visits[0].referrerURI) ); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } let placeInfo = placesResult.results[0]; Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri)); // Check to make sure we do not visit the invalid referrer. Assert.equal( false, await PlacesUtils.history.hasVisits(place.visits[0].referrerURI) ); // Check to make sure from_visit is zero in database. let stmt = DBConn().createStatement( `SELECT from_visit FROM moz_historyvisits WHERE id = :visit_id` ); stmt.params.visit_id = placeInfo.visits[0].visitId; Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.from_visit, 0); stmt.finalize(); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_nonnsIURI_referrerURI_ignored() { let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_nonnsIURI_referrerURI_ignored"), visits: [new VisitInfo()], }; place.visits[0].referrerURI = place.uri.spec + "_nonnsIURI"; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } let placeInfo = placesResult.results[0]; Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri)); // Check to make sure from_visit is zero in database. let stmt = DBConn().createStatement( `SELECT from_visit FROM moz_historyvisits WHERE id = :visit_id` ); stmt.params.visit_id = placeInfo.visits[0].visitId; Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.from_visit, 0); stmt.finalize(); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_old_referrer_ignored() { // This tests that a referrer for a visit which is not recent (specifically, // older than 15 minutes as per RECENT_EVENT_THRESHOLD) is not saved by // updatePlaces. let oldTime = Date.now() * 1000 - (RECENT_EVENT_THRESHOLD + 1); let referrerPlace = { uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_referrer"), visits: [new VisitInfo(TRANSITION_LINK, oldTime)], }; // First we must add our referrer to the history so that it is not ignored // as being invalid. Assert.equal(false, await PlacesUtils.history.hasVisits(referrerPlace.uri)); let placesResult = await promiseUpdatePlaces(referrerPlace); if (placesResult.errors.length) { do_throw("Unexpected error."); } // Now that the referrer is added, we can add a page with a valid // referrer to determine if the recency of the referrer is taken into // account. Assert.ok(await PlacesUtils.history.hasVisits(referrerPlace.uri)); let visitInfo = new VisitInfo(); visitInfo.referrerURI = referrerPlace.uri; let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_page"), visits: [visitInfo], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } let placeInfo = placesResult.results[0]; Assert.ok(await PlacesUtils.history.hasVisits(place.uri)); // Though the visit will not contain the referrer, we must examine the // database to be sure. Assert.equal(placeInfo.visits[0].referrerURI, null); let stmt = DBConn().createStatement( `SELECT COUNT(1) AS count FROM moz_historyvisits JOIN moz_places h ON h.id = place_id WHERE url_hash = hash(:page_url) AND url = :page_url AND from_visit = 0` ); stmt.params.page_url = place.uri.spec; Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.count, 1); stmt.finalize(); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_place_id_ignored() { let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_first"), visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } let placeInfo = placesResult.results[0]; Assert.ok(await PlacesUtils.history.hasVisits(place.uri)); let placeId = placeInfo.placeId; Assert.notEqual(placeId, 0); let badPlace = { uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"), visits: [new VisitInfo()], placeId, }; Assert.equal(false, await PlacesUtils.history.hasVisits(badPlace.uri)); placesResult = await promiseUpdatePlaces(badPlace); if (placesResult.errors.length) { do_throw("Unexpected error."); } placeInfo = placesResult.results[0]; Assert.notEqual(placeInfo.placeId, placeId); Assert.ok(await PlacesUtils.history.hasVisits(badPlace.uri)); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_handleCompletion_called_when_complete() { // We test a normal visit, and embeded visit, and a uri that would fail // the canAddURI test to make sure that the notification happens after *all* // of them have had a callback. let places = [ { uri: NetUtil.newURI( TEST_DOMAIN + "test_handleCompletion_called_when_complete" ), visits: [new VisitInfo(), new VisitInfo(TRANSITION_EMBED)], }, { uri: NetUtil.newURI("data:,Hello%2C%20World!"), visits: [new VisitInfo()], }, ]; Assert.equal(false, await PlacesUtils.history.hasVisits(places[0].uri)); Assert.equal(false, await PlacesUtils.history.hasVisits(places[1].uri)); const EXPECTED_COUNT_SUCCESS = 2; const EXPECTED_COUNT_FAILURE = 1; let { results, errors } = await promiseUpdatePlaces(places); Assert.equal(results.length, EXPECTED_COUNT_SUCCESS); Assert.equal(errors.length, EXPECTED_COUNT_FAILURE); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_add_visit() { const VISIT_TIME = Date.now() * 1000; let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"), title: "test_add_visit title", visits: [], }; for (let t in PlacesUtils.history.TRANSITIONS) { if (t == "EMBED") { continue; } let transitionType = PlacesUtils.history.TRANSITIONS[t]; place.visits.push(new VisitInfo(transitionType, VISIT_TIME)); } Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let callbackCount = 0; let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } for (let placeInfo of placesResult.results) { Assert.ok(await PlacesUtils.history.hasVisits(place.uri)); // Check mozIPlaceInfo properties. Assert.ok(place.uri.equals(placeInfo.uri)); Assert.equal(placeInfo.frecency, -1); // We don't pass frecency here! Assert.equal(placeInfo.title, place.title); // Check mozIVisitInfo properties. let visits = placeInfo.visits; Assert.equal(visits.length, 1); let visit = visits[0]; Assert.equal(visit.visitDate, VISIT_TIME); Assert.ok( Object.values(PlacesUtils.history.TRANSITIONS).includes( visit.transitionType ) ); Assert.ok(visit.referrerURI === null); // For TRANSITION_EMBED visits, many properties will always be zero or // undefined. if (visit.transitionType == TRANSITION_EMBED) { // Check mozIPlaceInfo properties. Assert.equal(placeInfo.placeId, 0, "//"); Assert.equal(placeInfo.guid, null); // Check mozIVisitInfo properties. Assert.equal(visit.visitId, 0); } else { // But they should be valid for non-embed visits. // Check mozIPlaceInfo properties. Assert.ok(placeInfo.placeId > 0); do_check_valid_places_guid(placeInfo.guid); // Check mozIVisitInfo properties. Assert.ok(visit.visitId > 0); } // If we have had all of our callbacks, continue running tests. if (++callbackCount == place.visits.length) { await PlacesTestUtils.promiseAsyncUpdates(); } } }); add_task(async function test_properties_saved() { // Check each transition type to make sure it is saved properly. let places = []; for (let t in PlacesUtils.history.TRANSITIONS) { if (t == "EMBED") { continue; } let transitionType = PlacesUtils.history.TRANSITIONS[t]; let place = { uri: NetUtil.newURI( TEST_DOMAIN + "test_properties_saved/" + transitionType ), title: "test_properties_saved test", visits: [new VisitInfo(transitionType)], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); places.push(place); } let callbackCount = 0; let placesResult = await promiseUpdatePlaces(places); if (placesResult.errors.length) { do_throw("Unexpected error."); } for (let placeInfo of placesResult.results) { let uri = placeInfo.uri; Assert.ok(await PlacesUtils.history.hasVisits(uri)); let visit = placeInfo.visits[0]; print( "TEST-INFO | test_properties_saved | updatePlaces callback for " + "transition type " + visit.transitionType ); // Note that TRANSITION_EMBED should not be in the database. const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1; // mozIVisitInfo::date let stmt = DBConn().createStatement( `SELECT COUNT(1) AS count FROM moz_places h JOIN moz_historyvisits v ON h.id = v.place_id WHERE h.url_hash = hash(:page_url) AND h.url = :page_url AND v.visit_date = :visit_date` ); stmt.params.page_url = uri.spec; stmt.params.visit_date = visit.visitDate; Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.count, EXPECTED_COUNT); stmt.finalize(); // mozIVisitInfo::transitionType stmt = DBConn().createStatement( `SELECT COUNT(1) AS count FROM moz_places h JOIN moz_historyvisits v ON h.id = v.place_id WHERE h.url_hash = hash(:page_url) AND h.url = :page_url AND v.visit_type = :transition_type` ); stmt.params.page_url = uri.spec; stmt.params.transition_type = visit.transitionType; Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.count, EXPECTED_COUNT); stmt.finalize(); // mozIPlaceInfo::title stmt = DBConn().createStatement( `SELECT COUNT(1) AS count FROM moz_places h WHERE h.url_hash = hash(:page_url) AND h.url = :page_url AND h.title = :title` ); stmt.params.page_url = uri.spec; stmt.params.title = placeInfo.title; Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.count, EXPECTED_COUNT); stmt.finalize(); // If we have had all of our callbacks, continue running tests. if (++callbackCount == places.length) { await PlacesTestUtils.promiseAsyncUpdates(); } } }); add_task(async function test_guid_saved() { let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_saved"), guid: "__TESTGUID__", visits: [new VisitInfo()], }; do_check_valid_places_guid(place.guid); Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } let placeInfo = placesResult.results[0]; let uri = placeInfo.uri; Assert.ok(await PlacesUtils.history.hasVisits(uri)); Assert.equal(placeInfo.guid, place.guid); await check_guid_for_uri(uri, place.guid); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_referrer_saved() { let places = [ { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/referrer"), visits: [new VisitInfo()], }, { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/test"), visits: [new VisitInfo()], }, ]; places[1].visits[0].referrerURI = places[0].uri; Assert.equal(false, await PlacesUtils.history.hasVisits(places[0].uri)); Assert.equal(false, await PlacesUtils.history.hasVisits(places[1].uri)); let resultCount = 0; let placesResult = await promiseUpdatePlaces(places); if (placesResult.errors.length) { do_throw("Unexpected error."); } for (let placeInfo of placesResult.results) { let uri = placeInfo.uri; Assert.ok(await PlacesUtils.history.hasVisits(uri)); let visit = placeInfo.visits[0]; // We need to insert all of our visits before we can test conditions. if (++resultCount == places.length) { Assert.ok(places[0].uri.equals(visit.referrerURI)); let stmt = DBConn().createStatement( `SELECT COUNT(1) AS count FROM moz_historyvisits JOIN moz_places h ON h.id = place_id WHERE url_hash = hash(:page_url) AND url = :page_url AND from_visit = ( SELECT v.id FROM moz_historyvisits v JOIN moz_places h ON h.id = place_id WHERE url_hash = hash(:referrer) AND url = :referrer )` ); stmt.params.page_url = uri.spec; stmt.params.referrer = visit.referrerURI.spec; Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.count, 1); stmt.finalize(); await PlacesTestUtils.promiseAsyncUpdates(); } } }); add_task(async function test_guid_change_saved() { // First, add a visit for it. let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_change_saved"), visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } // Then, change the guid with visits. place.guid = "_GUIDCHANGE_"; place.visits = [new VisitInfo()]; placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } await check_guid_for_uri(place.uri, place.guid); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_title_change_saved() { // First, add a visit for it. let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"), title: "original title", visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } // Now, make sure the empty string clears the title. place.title = ""; place.visits = [new VisitInfo()]; placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } do_check_title_for_uri(place.uri, null); // Then, change the title with visits. place.title = "title change"; place.visits = [new VisitInfo()]; placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } do_check_title_for_uri(place.uri, place.title); // Lastly, check that the title is cleared if we set it to null. place.title = null; place.visits = [new VisitInfo()]; placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } do_check_title_for_uri(place.uri, place.title); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_no_title_does_not_clear_title() { const TITLE = "test title"; // First, add a visit for it. let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_no_title_does_not_clear_title"), title: TITLE, visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } // Now, make sure that not specifying a title does not clear it. delete place.title; place.visits = [new VisitInfo()]; placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } do_check_title_for_uri(place.uri, TITLE); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_title_change_notifies() { // There are three cases to test. The first case is to make sure we do not // get notified if we do not specify a title. let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_notifies"), visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); new TitleChangedObserver(place.uri, "DO NOT WANT", function () { do_throw("unexpected callback!"); }); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } // The second case to test is that we don't get the notification when we add // it for the first time. The first case will fail before our callback if it // is busted, so we can do this now. place.uri = NetUtil.newURI(place.uri.spec + "/new-visit-with-title"); place.title = "title 1"; let expectedNotification = false; let titleChangeObserver; let titleChangePromise = new Promise((resolve, reject) => { titleChangeObserver = new TitleChangedObserver( place.uri, place.title, function () { Assert.ok( expectedNotification, "Should not get notified for " + place.uri.spec + " with title " + place.title ); if (expectedNotification) { resolve(); } } ); }); let visitPromise = new Promise(resolve => { function onVisits(events) { Assert.equal(events.length, 1, "Should only get notified for one visit."); Assert.equal(events[0].type, "page-visited"); let { url } = events[0]; Assert.equal( url, place.uri.spec, "Should get notified for visiting the new URI." ); PlacesObservers.removeListener(["page-visited"], onVisits); resolve(); } PlacesObservers.addListener(["page-visited"], onVisits); }); asyncHistory.updatePlaces(place); await visitPromise; // The third case to test is to make sure we get a notification when // we change an existing place. expectedNotification = true; titleChangeObserver.expectedTitle = place.title = "title 2"; place.visits = [new VisitInfo()]; asyncHistory.updatePlaces(place); await titleChangePromise; await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_visit_notifies() { // There are two observers we need to see for each visit. One is an // PlacesObservers and the other is the uri-visit-saved observer topic. let place = { guid: "abcdefghijkl", uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_notifies"), visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); function promiseVisitObserver(aPlace) { return new Promise((resolve, reject) => { let callbackCount = 0; let finisher = function () { if (++callbackCount == 2) { resolve(); } }; new VisitObserver(place.uri, place.guid, function ( aVisitDate, aTransitionType ) { let visit = place.visits[0]; Assert.equal(visit.visitDate, aVisitDate); Assert.equal(visit.transitionType, aTransitionType); finisher(); }); let observer = function (aSubject, aTopic, aData) { info("observe(" + aSubject + ", " + aTopic + ", " + aData + ")"); Assert.ok(aSubject instanceof Ci.nsIURI); Assert.ok(aSubject.equals(place.uri)); Services.obs.removeObserver(observer, URI_VISIT_SAVED); finisher(); }; Services.obs.addObserver(observer, URI_VISIT_SAVED); asyncHistory.updatePlaces(place); }); } await promiseVisitObserver(place); await PlacesTestUtils.promiseAsyncUpdates(); }); // test with empty mozIVisitInfoCallback object add_task(async function test_callbacks_not_supplied() { const URLS = [ "imap://cyrus.andrew.cmu.edu/archive.imap", // bad URI "http://mozilla.org/", // valid URI ]; let places = []; URLS.forEach(function (url) { try { let place = { uri: NetUtil.newURI(url), title: "test for " + url, visits: [new VisitInfo()], }; places.push(place); } catch (e) { if (e.result != Cr.NS_ERROR_FAILURE) { throw e; } // NetUtil.newURI() can throw if e.g. our app knows about imap:// // but the account is not set up and so the URL is invalid for us. // Note this in the log but ignore as it's not the subject of this test. info("Could not construct URI for '" + url + "'; ignoring"); } }); asyncHistory.updatePlaces(places, {}); await PlacesTestUtils.promiseAsyncUpdates(); }); // Test that we don't wrongly overwrite typed and hidden when adding new visits. add_task(async function test_typed_hidden_not_overwritten() { await PlacesUtils.history.clear(); let places = [ { uri: NetUtil.newURI("http://mozilla.org/"), title: "test", visits: [new VisitInfo(TRANSITION_TYPED), new VisitInfo(TRANSITION_LINK)], }, { uri: NetUtil.newURI("http://mozilla.org/"), title: "test", visits: [new VisitInfo(TRANSITION_FRAMED_LINK)], }, ]; await promiseUpdatePlaces(places); let db = await PlacesUtils.promiseDBConnection(); let rows = await db.execute( "SELECT hidden, typed FROM moz_places WHERE url_hash = hash(:url) AND url = :url", { url: "http://mozilla.org/" } ); Assert.equal( rows[0].getResultByName("typed"), 1, "The page should be marked as typed" ); Assert.equal( rows[0].getResultByName("hidden"), 0, "The page should be marked as not hidden" ); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_omit_frecency_notifications() { // When multiple entries are inserted, frecency is calculated delayed, so // we won't get a ranking changed notification until recalculation happens. await PlacesUtils.history.clear(); let notified = false; let listener = events => { notified = true; PlacesUtils.observers.removeListener(["pages-rank-changed"], listener); }; PlacesUtils.observers.addListener(["pages-rank-changed"], listener); let places = [ { uri: NetUtil.newURI("http://mozilla.org/"), title: "test", visits: [new VisitInfo(TRANSITION_TYPED)], }, { uri: NetUtil.newURI("http://example.org/"), title: "test", visits: [new VisitInfo(TRANSITION_TYPED)], }, ]; await promiseUpdatePlaces(places); Assert.ok(!notified); await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); Assert.ok(notified); }); add_task(async function test_ignore_errors() { await PlacesUtils.history.clear(); // This test ensures that trying to add a visit, with a guid already found in // another visit, fails - but doesn't report if we told it not to. let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"), visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } let placeInfo = placesResult.results[0]; Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri)); let badPlace = { uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"), visits: [new VisitInfo()], guid: placeInfo.guid, }; Assert.equal(false, await PlacesUtils.history.hasVisits(badPlace.uri)); placesResult = await promiseUpdatePlaces(badPlace, { ignoreErrors: true }); if (placesResult.results.length) { do_throw("Unexpected success."); } Assert.equal( placesResult.errors.length, 0, "Should have seen 0 errors because we disabled reporting." ); Assert.equal( placesResult.results.length, 0, "Should have seen 0 results because there were none." ); Assert.equal( placesResult.resultCount, 0, "Should know that we updated 0 items from the completion callback." ); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_ignore_results() { await PlacesUtils.history.clear(); let place = { uri: NetUtil.newURI("http://mozilla.org/"), title: "test", visits: [new VisitInfo()], }; let placesResult = await promiseUpdatePlaces(place, { ignoreResults: true }); Assert.equal( placesResult.results.length, 0, "Should have seen 0 results because we disabled reporting." ); Assert.equal( placesResult.errors.length, 0, "Should have seen 0 errors because there were none." ); Assert.equal( placesResult.resultCount, 1, "Should know that we updated 1 item from the completion callback." ); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_ignore_results_and_errors() { await PlacesUtils.history.clear(); // This test ensures that trying to add a visit, with a guid already found in // another visit, fails - but doesn't report if we told it not to. let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"), visits: [new VisitInfo()], }; Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri)); let placesResult = await promiseUpdatePlaces(place); if (placesResult.errors.length) { do_throw("Unexpected error."); } let placeInfo = placesResult.results[0]; Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri)); let badPlace = { uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"), visits: [new VisitInfo()], guid: placeInfo.guid, }; let allPlaces = [ { uri: NetUtil.newURI(TEST_DOMAIN + "test_other_successful_item"), visits: [new VisitInfo()], }, badPlace, ]; Assert.equal(false, await PlacesUtils.history.hasVisits(badPlace.uri)); placesResult = await promiseUpdatePlaces(allPlaces, { ignoreErrors: true, ignoreResults: true, }); Assert.equal( placesResult.errors.length, 0, "Should have seen 0 errors because we disabled reporting." ); Assert.equal( placesResult.results.length, 0, "Should have seen 0 results because we disabled reporting." ); Assert.equal( placesResult.resultCount, 1, "Should know that we updated 1 item from the completion callback." ); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function test_title_on_initial_visit() { let place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_title"), title: "My title", visits: [new VisitInfo()], guid: "mnopqrstuvwx", }; let visitPromise = new Promise(resolve => { new VisitObserver(place.uri, place.guid, function ( aVisitDate, aTransitionType, aLastKnownTitle ) { Assert.equal(place.title, aLastKnownTitle); resolve(); }); }); await promiseUpdatePlaces(place); await visitPromise; // Now check an empty title doesn't get reported as null place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_title"), title: "", visits: [new VisitInfo()], guid: "fghijklmnopq", }; visitPromise = new Promise(resolve => { new VisitObserver(place.uri, place.guid, function ( aVisitDate, aTransitionType, aLastKnownTitle ) { Assert.equal(place.title, aLastKnownTitle); resolve(); }); }); await promiseUpdatePlaces(place); await visitPromise; // and that a missing title correctly gets reported as null. place = { uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_title"), visits: [new VisitInfo()], guid: "fghijklmnopq", }; visitPromise = new Promise(resolve => { new VisitObserver(place.uri, place.guid, function ( aVisitDate, aTransitionType, aLastKnownTitle ) { Assert.equal(null, aLastKnownTitle); resolve(); }); }); await promiseUpdatePlaces(place); await visitPromise; });