Bug 606966 - Need an async history visit API exposed to JS

Part 20 - API implementation with tests.
r=mak
r=lw
a=blocking
This commit is contained in:
Shawn Wilsher 2011-01-21 20:09:10 -08:00
Родитель 5e94cb1462
Коммит 92b5b0cad4
4 изменённых файлов: 920 добавлений и 21 удалений

Просмотреть файл

@ -57,6 +57,7 @@
#include "mozilla/Services.h"
#include "nsThreadUtils.h"
#include "nsNetUtil.h"
#include "nsContentUtils.h"
// Initial size for the cache holding visited status observers.
#define VISIT_OBSERVERS_INITIAL_CACHE_SIZE 128
@ -168,6 +169,141 @@ struct VisitData {
namespace {
/**
* Obtains an nsIURI from the "uri" property of a JSObject.
*
* @param aCtx
* The JSContext for aObject.
* @param aObject
* The JSObject to get the URI from.
* @param aProperty
* The name of the property to get the URI from.
* @return the URI if it exists.
*/
already_AddRefed<nsIURI>
GetURIFromJSObject(JSContext* aCtx,
JSObject* aObject,
const char* aProperty)
{
jsval uriVal;
JSBool rc = JS_GetProperty(aCtx, aObject, aProperty, &uriVal);
NS_ENSURE_TRUE(rc, nsnull);
if (!JSVAL_IS_PRIMITIVE(uriVal)) {
nsCOMPtr<nsIXPConnectWrappedNative> wrappedObj;
nsresult rv = nsContentUtils::XPConnect()->GetWrappedNativeOfJSObject(
aCtx,
JSVAL_TO_OBJECT(uriVal),
getter_AddRefs(wrappedObj)
);
NS_ENSURE_SUCCESS(rv, nsnull);
nsCOMPtr<nsIURI> uri = do_QueryWrappedNative(wrappedObj);
return uri.forget();
}
return nsnull;
}
/**
* Obtains the specified property of a JSObject.
*
* @param aCtx
* The JSContext for aObject.
* @param aObject
* The JSObject to get the string from.
* @param aProperty
* The property to get the value from.
* @param _string
* The string to populate with the value, or set it to void.
*/
void
GetStringFromJSObject(JSContext* aCtx,
JSObject* aObject,
const char* aProperty,
nsString& _string)
{
jsval val;
JSBool rc = JS_GetProperty(aCtx, aObject, aProperty, &val);
if (!rc || JSVAL_IS_VOID(val) || !JSVAL_IS_STRING(val)) {
_string.SetIsVoid(PR_TRUE);
return;
}
size_t length;
const jschar* chars =
JS_GetStringCharsZAndLength(aCtx, JSVAL_TO_STRING(val), &length);
if (!chars) {
_string.SetIsVoid(PR_TRUE);
return;
}
_string.Assign(static_cast<const PRUnichar*>(chars), length);
}
/**
* Obtains the specified property of a JSObject.
*
* @param aCtx
* The JSContext for aObject.
* @param aObject
* The JSObject to get the int from.
* @param aProperty
* The property to get the value from.
* @param _int
* The integer to populate with the value on success.
*/
template <typename IntType>
nsresult
GetIntFromJSObject(JSContext* aCtx,
JSObject* aObject,
const char* aProperty,
IntType* _int)
{
jsval value;
JSBool rc = JS_GetProperty(aCtx, aObject, aProperty, &value);
NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
if (JSVAL_IS_VOID(value)) {
return NS_ERROR_INVALID_ARG;
}
NS_ENSURE_ARG(JSVAL_IS_PRIMITIVE(value));
NS_ENSURE_ARG(JSVAL_IS_NUMBER(value));
jsdouble num;
rc = JS_ValueToNumber(aCtx, value, &num);
NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
NS_ENSURE_ARG(IntType(num) == num);
*_int = IntType(num);
return NS_OK;
}
/**
* Obtains the specified property of a JSObject.
*
* @pre aArray must be an Array object.
*
* @param aCtx
* The JSContext for aArray.
* @param aArray
* The JSObject to get the object from.
* @param aIndex
* The index to get the object from.
* @param _object
* The JSObject pointer on success.
*/
nsresult
GetJSObjectFromArray(JSContext* aCtx,
JSObject* aArray,
jsuint aIndex,
JSObject** _rooter)
{
NS_PRECONDITION(JS_IsArrayObject(aCtx, aArray),
"Must provide an object that is an array!");
jsval value;
JSBool rc = JS_GetElement(aCtx, aArray, aIndex, &value);
NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
NS_ENSURE_ARG(!JSVAL_IS_PRIMITIVE(value));
*_rooter = JSVAL_TO_OBJECT(value);
return NS_OK;
}
class VisitedQuery : public AsyncStatementCallback
{
public:
@ -1604,8 +1740,141 @@ History::UpdatePlaces(const jsval& aPlaceInfos,
JSContext* aCtx)
{
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
NS_ENSURE_TRUE(!JSVAL_IS_PRIMITIVE(aPlaceInfos), NS_ERROR_INVALID_ARG);
return NS_ERROR_NOT_IMPLEMENTED;
jsuint infosLength = 1;
JSObject* infos;
if (JS_IsArrayObject(aCtx, JSVAL_TO_OBJECT(aPlaceInfos))) {
infos = JSVAL_TO_OBJECT(aPlaceInfos);
(void)JS_GetArrayLength(aCtx, infos, &infosLength);
NS_ENSURE_ARG(infosLength > 0);
}
else {
// Build a temporary array to store this one item so the code below can
// just loop.
infos = JS_NewArrayObject(aCtx, 0, NULL);
NS_ENSURE_TRUE(infos, NS_ERROR_OUT_OF_MEMORY);
JSBool rc = JS_DefineElement(aCtx, infos, 0, aPlaceInfos, NULL, NULL, 0);
NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
}
nsTArray<VisitData> visitData;
for (jsuint i = 0; i < infosLength; i++) {
JSObject* info;
nsresult rv = GetJSObjectFromArray(aCtx, infos, i, &info);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> uri = GetURIFromJSObject(aCtx, info, "uri");
PRInt64 placeId;
rv = GetIntFromJSObject(aCtx, info, "placeId", &placeId);
if (rv == NS_ERROR_INVALID_ARG) {
placeId = 0;
}
else {
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_ARG(placeId > 0);
}
nsCString guid;
{
nsString fatGUID;
GetStringFromJSObject(aCtx, info, "guid", fatGUID);
if (fatGUID.IsVoid()) {
guid.SetIsVoid(PR_TRUE);
}
else {
guid = NS_ConvertUTF16toUTF8(fatGUID);
}
}
// We must have at least one of uri, valid id, or guid.
NS_ENSURE_ARG(uri || placeId > 0 || !guid.IsVoid());
// If we were given a guid, make sure it is valid.
bool isValidGUID = IsValidGUID(guid);
NS_ENSURE_ARG(guid.IsVoid() || isValidGUID);
nsString title;
GetStringFromJSObject(aCtx, info, "title", title);
JSObject* visits = NULL;
{
jsval visitsVal;
JSBool rc = JS_GetProperty(aCtx, info, "visits", &visitsVal);
NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
if (!JSVAL_IS_PRIMITIVE(visitsVal)) {
visits = JSVAL_TO_OBJECT(visitsVal);
NS_ENSURE_ARG(JS_IsArrayObject(aCtx, visits));
}
}
jsuint visitsLength = 0;
if (visits) {
(void)JS_GetArrayLength(aCtx, visits, &visitsLength);
}
// If we have no id or guid, we must have visits.
if (!(placeId > 0 || isValidGUID)) {
NS_ENSURE_ARG(visits);
NS_ENSURE_ARG(visitsLength > 0);
}
// Check each visit, and build our array of VisitData objects.
visitData.SetCapacity(visitData.Length() + visitsLength);
for (jsuint j = 0; j < visitsLength; j++) {
JSObject* visit;
rv = GetJSObjectFromArray(aCtx, visits, j, &visit);
NS_ENSURE_SUCCESS(rv, rv);
VisitData& data = *visitData.AppendElement(VisitData(uri));
data.placeId = placeId;
data.title = title;
data.guid = guid;
// We must have a date and a transaction type!
rv = GetIntFromJSObject(aCtx, visit, "visitDate", &data.visitTime);
NS_ENSURE_SUCCESS(rv, rv);
PRUint32 transitionType;
rv = GetIntFromJSObject(aCtx, visit, "transitionType", &transitionType);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_ARG_RANGE(transitionType,
nsINavHistoryService::TRANSITION_LINK,
nsINavHistoryService::TRANSITION_FRAMED_LINK);
data.SetTransitionType(transitionType);
// If the visit is an embed visit, we do not actually add it to the
// database.
if (transitionType == nsINavHistoryService::TRANSITION_EMBED) {
StoreAndNotifyEmbedVisit(data, aCallback);
visitData.RemoveElementAt(visitData.Length() - 1);
continue;
}
// The session id is optional.
rv = GetIntFromJSObject(aCtx, visit, "sessionId", &data.sessionId);
if (rv == NS_ERROR_INVALID_ARG) {
data.sessionId = 0;
}
else {
NS_ENSURE_SUCCESS(rv, rv);
}
// The referrer is optional.
nsCOMPtr<nsIURI> referrer = GetURIFromJSObject(aCtx, visit,
"referrerURI");
if (referrer) {
(void)referrer->GetSpec(data.referrerSpec);
}
}
}
mozIStorageConnection* dbConn = GetDBConn();
NS_ENSURE_STATE(dbConn);
nsresult rv = InsertVisitedURIs::Start(dbConn, visitData, aCallback);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
////////////////////////////////////////////////////////////////////////////////

Просмотреть файл

@ -597,10 +597,16 @@ function waitForAsyncUpdates(aCallback, aScope, aArguments)
*
* @param aGuid
* The guid to test.
* @param [optional] aStack
* The stack frame used to report the error.
*/
function do_check_valid_places_guid(aGuid)
function do_check_valid_places_guid(aGuid,
aStack)
{
do_check_true(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), Components.stack.caller);
if (!aStack) {
aStack = Components.stack.caller;
}
do_check_true(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), aStack);
}
/**
@ -608,17 +614,25 @@ function do_check_valid_places_guid(aGuid)
*
* @param aURI
* The uri to check.
* @param [optional] aGUID
* The expected guid in the database.
*/
function do_check_guid_for_uri(aURI)
function do_check_guid_for_uri(aURI,
aGUID)
{
let caller = Components.stack.caller;
let stmt = DBConn().createStatement(
"SELECT guid "
+ "FROM moz_places "
+ "WHERE url = :url "
);
stmt.params.url = aURI.spec;
do_check_true(stmt.executeStep());
do_check_valid_places_guid(stmt.row.guid);
do_check_true(stmt.executeStep(), caller);
do_check_valid_places_guid(stmt.row.guid, caller);
if (aGUID) {
do_check_valid_places_guid(aGUID, caller);
do_check_eq(stmt.row.guid, aGUID, caller);
}
stmt.finalize();
}
@ -641,11 +655,6 @@ let gRunningTest = null;
let gTestIndex = 0; // The index of the currently running test.
function run_next_test()
{
if (gRunningTest !== null) {
// Close the previous test do_test_pending call.
do_test_finished();
}
function _run_next_test()
{
if (gTestIndex < gTests.length) {
@ -665,4 +674,9 @@ function run_next_test()
// For sane stacks during failures, we execute this code soon, but not now.
do_execute_soon(_run_next_test);
if (gRunningTest !== null) {
// Close the previous test do_test_pending call.
do_test_finished();
}
}

Просмотреть файл

@ -590,12 +590,6 @@ var gTests = [
cleanDatabase(run_next_test);
},
function lastTestJustCallsTestFinished() {
// close out our over-arching test
do_test_finished();
// and let run_next_test close out its pending test too.
run_next_test();
},
];
// The tag keys in query URIs, i.e., "place:tag=foo&!tags=1"
@ -832,9 +826,5 @@ function setsAreEqual(aArr1, aArr2, aIsOrdered) {
///////////////////////////////////////////////////////////////////////////////
function run_test() {
// Create an overarching test for our entire test run. this is balanced
// by a dummy test as the last test in our test list that just calls
// do_test_finished().
do_test_pending();
run_next_test();
}

Просмотреть файл

@ -5,6 +5,62 @@
* This file tests the async history API exposed by mozIAsyncHistory.
*/
////////////////////////////////////////////////////////////////////////////////
//// Globals
XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
"@mozilla.org/browser/history;1",
"mozIAsyncHistory");
XPCOMUtils.defineLazyServiceGetter(this, "gGlobalHistory",
"@mozilla.org/browser/nav-history-service;1",
"nsIGlobalHistory2");
const TEST_DOMAIN = "http://mozilla.org/"
////////////////////////////////////////////////////////////////////////////////
//// 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;
}
/**
* 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 stack = Components.stack.caller;
let stmt = DBConn().createStatement(
"SELECT title " +
"FROM moz_places " +
"WHERE url = :url "
);
stmt.params.url = aURI.spec;
do_check_true(stmt.executeStep(), stack);
do_check_eq(stmt.row.title, aTitle, stack);
stmt.finalize();
}
////////////////////////////////////////////////////////////////////////////////
//// Test Functions
@ -15,11 +71,581 @@ function test_interface_exists()
run_next_test();
}
function test_invalid_uri_throws()
{
// First, test passing in nothing.
let place = {
visits: [
new VisitInfo(),
],
};
try {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(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 {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
}
run_next_test();
}
function test_invalid_places_throws()
{
// First, test passing in nothing.
try {
gHistory.updatePlaces();
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(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 {
gHistory.updatePlaces(value);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
}
run_next_test();
}
function test_invalid_id_throws()
{
// First check invalid id "0".
let place = {
placeId: 0,
uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_id_throws"),
visits: [
new VisitInfo(),
],
};
try {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
// Now check negative id.
place.placeId = -5;
try {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
run_next_test();
}
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 {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
// Now check invalid character guid.
place.guid = "__BADGUID+__";
do_check_eq(place.guid.length, 12);
try {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
run_next_test();
}
function test_no_id_or_guid_no_visits_throws()
{
// First, test with no visit attribute.
let place = {
uri: NetUtil.newURI(TEST_DOMAIN + "test_no_id_or_guid_no_visits_throws"),
};
try {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
// Now test with an empty array.
place.visits = [];
try {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
run_next_test();
}
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 {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
run_next_test();
}
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 {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
run_next_test();
}
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 {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(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_FRAMED_LINK + 1);
try {
gHistory.updatePlaces(place);
do_throw("Should have thrown!");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
}
run_next_test();
}
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 transitionType = TRANSITION_LINK;
transitionType <= TRANSITION_FRAMED_LINK;
transitionType++) {
place.visits.push(new VisitInfo(transitionType, VISIT_TIME));
}
do_check_false(gGlobalHistory.isVisited(place.uri));
let callbackCount = 0;
gHistory.updatePlaces(place, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
do_check_true(gGlobalHistory.isVisited(place.uri));
// Check mozIPlaceInfo properties.
do_check_true(place.uri.equals(aPlaceInfo.uri));
do_check_eq(aPlaceInfo.frecency, -1); // We don't pass frecency here!
do_check_eq(aPlaceInfo.title, place.title);
// Check mozIVisitInfo properties.
let visits = aPlaceInfo.visits;
do_check_eq(visits.length, 1);
let visit = visits[0];
do_check_eq(visit.visitDate, VISIT_TIME);
do_check_true(visit.transitionType >= TRANSITION_LINK &&
visit.transitionType <= TRANSITION_FRAMED_LINK);
do_check_true(visit.referrerURI === null);
// For TRANSITION_EMBED visits, many properties will always be zero or
// undefined.
if (visit.transitionType == TRANSITION_EMBED) {
// Check mozIPlaceInfo properties.
do_check_eq(aPlaceInfo.placeId, 0);
do_check_eq(aPlaceInfo.guid, null);
// Check mozIVisitInfo properties.
do_check_eq(visit.visitId, 0);
do_check_eq(visit.sessionId, 0);
}
// But they should be valid for non-embed visits.
else {
// Check mozIPlaceInfo properties.
do_check_true(aPlaceInfo.placeId > 0);
do_check_valid_places_guid(aPlaceInfo.guid);
// Check mozIVisitInfo properties.
do_check_true(visit.visitId > 0);
do_check_true(visit.sessionId > 0);
}
// If we have had all of our callbacks, continue running tests.
if (++callbackCount == place.visits.length) {
run_next_test();
}
});
}
function test_properties_saved()
{
// Check each transition type to make sure it is saved properly.
let places = [];
for (let transitionType = TRANSITION_LINK;
transitionType <= TRANSITION_FRAMED_LINK;
transitionType++) {
let place = {
uri: NetUtil.newURI(TEST_DOMAIN + "test_properties_saved/" +
transitionType),
title: "test_properties_saved test",
visits: [
new VisitInfo(transitionType),
],
};
do_check_false(gGlobalHistory.isVisited(place.uri));
places.push(place);
}
let callbackCount = 0;
gHistory.updatePlaces(places, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
let uri = aPlaceInfo.uri;
do_check_true(gGlobalHistory.isVisited(uri));
let visit = aPlaceInfo.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 = :page_url " +
"AND v.visit_date = :visit_date "
);
stmt.params.page_url = uri.spec;
stmt.params.visit_date = visit.visitDate;
do_check_true(stmt.executeStep());
do_check_eq(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 = :page_url " +
"AND v.visit_type = :transition_type "
);
stmt.params.page_url = uri.spec;
stmt.params.transition_type = visit.transitionType;
do_check_true(stmt.executeStep());
do_check_eq(stmt.row.count, EXPECTED_COUNT);
stmt.finalize();
// mozIVisitInfo::sessionId
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 = :page_url " +
"AND v.session = :session_id "
);
stmt.params.page_url = uri.spec;
stmt.params.session_id = visit.sessionId;
do_check_true(stmt.executeStep());
do_check_eq(stmt.row.count, EXPECTED_COUNT);
stmt.finalize();
// mozIPlaceInfo::title
stmt = DBConn().createStatement(
"SELECT COUNT(1) AS count " +
"FROM moz_places h " +
"WHERE h.url = :page_url " +
"AND h.title = :title "
);
stmt.params.page_url = uri.spec;
stmt.params.title = aPlaceInfo.title;
do_check_true(stmt.executeStep());
do_check_eq(stmt.row.count, EXPECTED_COUNT);
stmt.finalize();
// If we have had all of our callbacks, continue running tests.
if (++callbackCount == places.length) {
run_next_test();
}
});
}
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);
do_check_false(gGlobalHistory.isVisited(place.uri));
gHistory.updatePlaces(place, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
let uri = aPlaceInfo.uri;
do_check_true(gGlobalHistory.isVisited(uri));
do_check_eq(aPlaceInfo.guid, place.guid);
do_check_guid_for_uri(uri, place.guid);
run_next_test();
});
}
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;
do_check_false(gGlobalHistory.isVisited(places[0].uri));
do_check_false(gGlobalHistory.isVisited(places[1].uri));
let callbackCount = 0;
let referrerSessionId;
gHistory.updatePlaces(places, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
let uri = aPlaceInfo.uri;
do_check_true(gGlobalHistory.isVisited(uri));
let visit = aPlaceInfo.visits[0];
// We need to insert all of our visits before we can test conditions.
if (++callbackCount != places.length) {
referrerSessionId = visit.sessionId;
return;
}
do_check_true(places[0].uri.equals(visit.referrerURI));
do_check_eq(visit.sessionId, referrerSessionId);
let stmt = DBConn().createStatement(
"SELECT COUNT(1) AS count " +
"FROM moz_historyvisits " +
"WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) " +
"AND from_visit = ( " +
"SELECT id " +
"FROM moz_historyvisits " +
"WHERE place_id = (SELECT id FROM moz_places WHERE url = :referrer) " +
") "
);
stmt.params.page_url = uri.spec;
stmt.params.referrer = visit.referrerURI.spec;
do_check_true(stmt.executeStep());
do_check_eq(stmt.row.count, 1);
stmt.finalize();
run_next_test();
});
}
function test_sessionId_saved()
{
let place = {
uri: NetUtil.newURI(TEST_DOMAIN + "test_sessionId_saved"),
visits: [
new VisitInfo(),
],
};
place.visits[0].sessionId = 3;
do_check_false(gGlobalHistory.isVisited(place.uri));
gHistory.updatePlaces(place, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
let uri = aPlaceInfo.uri;
do_check_true(gGlobalHistory.isVisited(uri));
let visit = aPlaceInfo.visits[0];
do_check_eq(visit.sessionId, place.visits[0].sessionId);
let stmt = DBConn().createStatement(
"SELECT COUNT(1) AS count " +
"FROM moz_historyvisits " +
"WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) " +
"AND session = :session_id "
);
stmt.params.page_url = uri.spec;
stmt.params.session_id = visit.sessionId;
do_check_true(stmt.executeStep());
do_check_eq(stmt.row.count, 1);
stmt.finalize();
run_next_test();
});
}
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(),
],
};
do_check_false(gGlobalHistory.isVisited(place.uri));
gHistory.updatePlaces(place, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
// Then, change the guid with visits.
place.guid = "_GUIDCHANGE_";
place.visits = [new VisitInfo()];
gHistory.updatePlaces(place, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
do_check_guid_for_uri(place.uri, place.guid);
run_next_test();
});
});
}
function test_title_change_saved()
{
// First, add a visit for it.
let place = {
uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"),
visits: [
new VisitInfo(),
],
};
do_check_false(gGlobalHistory.isVisited(place.uri));
gHistory.updatePlaces(place, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
// Then, change the title with visits.
place.title = "title change";
place.visits = [new VisitInfo()];
gHistory.updatePlaces(place, function(aResultCode, aPlaceInfo) {
do_check_true(Components.isSuccessCode(aResultCode));
do_check_title_for_uri(place.uri, place.title);
run_next_test();
});
});
}
////////////////////////////////////////////////////////////////////////////////
//// Test Runner
let gTests = [
test_interface_exists,
test_invalid_uri_throws,
test_invalid_places_throws,
test_invalid_id_throws,
test_invalid_guid_throws,
test_no_id_or_guid_no_visits_throws,
test_add_visit_no_date_throws,
test_add_visit_no_transitionType_throws,
test_add_visit_invalid_transitionType_throws,
test_add_visit,
test_properties_saved,
test_guid_saved,
test_referrer_saved,
test_sessionId_saved,
test_guid_change_saved,
test_title_change_saved,
];
function run_test()