michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cc = Components.classes; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: // Import common head. michael@0: let (commonFile = do_get_file("../head_common.js", false)) { michael@0: let uri = Services.io.newFileURI(commonFile); michael@0: Services.scriptloader.loadSubScript(uri.spec, this); michael@0: } michael@0: michael@0: // Put any other stuff relative to this test folder below. michael@0: michael@0: michael@0: // Some Useful Date constants - PRTime uses microseconds, so convert michael@0: const DAY_MICROSEC = 86400000000; michael@0: const today = Date.now() * 1000; michael@0: const yesterday = today - DAY_MICROSEC; michael@0: const lastweek = today - (DAY_MICROSEC * 7); michael@0: const daybefore = today - (DAY_MICROSEC * 2); michael@0: const tomorrow = today + DAY_MICROSEC; michael@0: const old = today - (DAY_MICROSEC * 3); michael@0: const futureday = today + (DAY_MICROSEC * 3); michael@0: const olderthansixmonths = today - (DAY_MICROSEC * 31 * 7); michael@0: michael@0: michael@0: /** michael@0: * Generalized function to pull in an array of objects of data and push it into michael@0: * the database. It does NOT do any checking to see that the input is michael@0: * appropriate. This function is an asynchronous task, it can be called using michael@0: * "Task.spawn" or using the "yield" function inside another task. michael@0: */ michael@0: function task_populateDB(aArray) michael@0: { michael@0: // Iterate over aArray and execute all the instructions that can be done with michael@0: // asynchronous APIs, excluding those that will be done in batch mode later. michael@0: for ([, data] in Iterator(aArray)) { michael@0: try { michael@0: // make the data object into a query data object in order to create proper michael@0: // default values for anything left unspecified michael@0: var qdata = new queryData(data); michael@0: if (qdata.isVisit) { michael@0: // Then we should add a visit for this node michael@0: yield promiseAddVisits({ michael@0: uri: uri(qdata.uri), michael@0: transition: qdata.transType, michael@0: visitDate: qdata.lastVisit, michael@0: referrer: qdata.referrer ? uri(qdata.referrer) : null, michael@0: title: qdata.title michael@0: }); michael@0: if (qdata.visitCount && !qdata.isDetails) { michael@0: // Set a fake visit_count, this is not a real count but can be used michael@0: // to test sorting by visit_count. michael@0: let stmt = DBConn().createAsyncStatement( michael@0: "UPDATE moz_places SET visit_count = :vc WHERE url = :url"); michael@0: stmt.params.vc = qdata.visitCount; michael@0: stmt.params.url = qdata.uri; michael@0: try { michael@0: stmt.executeAsync(); michael@0: } michael@0: catch (ex) { michael@0: print("Error while setting visit_count."); michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (qdata.isRedirect) { michael@0: // This must be async to properly enqueue after the updateFrecency call michael@0: // done by the visit addition. michael@0: let stmt = DBConn().createAsyncStatement( michael@0: "UPDATE moz_places SET hidden = 1 WHERE url = :url"); michael@0: stmt.params.url = qdata.uri; michael@0: try { michael@0: stmt.executeAsync(); michael@0: } michael@0: catch (ex) { michael@0: print("Error while setting hidden."); michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: } michael@0: michael@0: if (qdata.isDetails) { michael@0: // Then we add extraneous page details for testing michael@0: yield promiseAddVisits({ michael@0: uri: uri(qdata.uri), michael@0: visitDate: qdata.lastVisit, michael@0: title: qdata.title michael@0: }); michael@0: } michael@0: } catch (ex) { michael@0: // use the data object here in case instantiation of qdata failed michael@0: LOG("Problem with this URI: " + data.uri); michael@0: do_throw("Error creating database: " + ex + "\n"); michael@0: } michael@0: } michael@0: michael@0: // Now execute the part of the instructions made with synchronous APIs. michael@0: PlacesUtils.history.runInBatchMode({ michael@0: runBatched: function (aUserData) michael@0: { michael@0: aArray.forEach(function (data) michael@0: { michael@0: try { michael@0: // make the data object into a query data object in order to create proper michael@0: // default values for anything left unspecified michael@0: var qdata = new queryData(data); michael@0: michael@0: if (qdata.markPageAsTyped) { michael@0: PlacesUtils.history.markPageAsTyped(uri(qdata.uri)); michael@0: } michael@0: michael@0: if (qdata.isPageAnnotation) { michael@0: if (qdata.removeAnnotation) michael@0: PlacesUtils.annotations.removePageAnnotation(uri(qdata.uri), michael@0: qdata.annoName); michael@0: else { michael@0: PlacesUtils.annotations.setPageAnnotation(uri(qdata.uri), michael@0: qdata.annoName, michael@0: qdata.annoVal, michael@0: qdata.annoFlags, michael@0: qdata.annoExpiration); michael@0: } michael@0: } michael@0: michael@0: if (qdata.isItemAnnotation) { michael@0: if (qdata.removeAnnotation) michael@0: PlacesUtils.annotations.removeItemAnnotation(qdata.itemId, michael@0: qdata.annoName); michael@0: else { michael@0: PlacesUtils.annotations.setItemAnnotation(qdata.itemId, michael@0: qdata.annoName, michael@0: qdata.annoVal, michael@0: qdata.annoFlags, michael@0: qdata.annoExpiration); michael@0: } michael@0: } michael@0: michael@0: if (qdata.isFolder) { michael@0: let folderId = PlacesUtils.bookmarks.createFolder(qdata.parentFolder, michael@0: qdata.title, michael@0: qdata.index); michael@0: if (qdata.readOnly) michael@0: PlacesUtils.bookmarks.setFolderReadonly(folderId, true); michael@0: } michael@0: michael@0: if (qdata.isLivemark) { michael@0: PlacesUtils.livemarks.addLivemark({ title: qdata.title michael@0: , parentId: qdata.parentFolder michael@0: , index: qdata.index michael@0: , feedURI: uri(qdata.feedURI) michael@0: , siteURI: uri(qdata.uri) michael@0: }).then(null, do_throw); michael@0: } michael@0: michael@0: if (qdata.isBookmark) { michael@0: let itemId = PlacesUtils.bookmarks.insertBookmark(qdata.parentFolder, michael@0: uri(qdata.uri), michael@0: qdata.index, michael@0: qdata.title); michael@0: if (qdata.keyword) michael@0: PlacesUtils.bookmarks.setKeywordForBookmark(itemId, qdata.keyword); michael@0: if (qdata.dateAdded) michael@0: PlacesUtils.bookmarks.setItemDateAdded(itemId, qdata.dateAdded); michael@0: if (qdata.lastModified) michael@0: PlacesUtils.bookmarks.setItemLastModified(itemId, qdata.lastModified); michael@0: } michael@0: michael@0: if (qdata.isTag) { michael@0: PlacesUtils.tagging.tagURI(uri(qdata.uri), qdata.tagArray); michael@0: } michael@0: michael@0: if (qdata.isDynContainer) { michael@0: PlacesUtils.bookmarks.createDynamicContainer(qdata.parentFolder, michael@0: qdata.title, michael@0: qdata.contractId, michael@0: qdata.index); michael@0: } michael@0: michael@0: if (qdata.isSeparator) michael@0: PlacesUtils.bookmarks.insertSeparator(qdata.parentFolder, qdata.index); michael@0: } catch (ex) { michael@0: // use the data object here in case instantiation of qdata failed michael@0: LOG("Problem with this URI: " + data.uri); michael@0: do_throw("Error creating database: " + ex + "\n"); michael@0: } michael@0: }); // End of function and array michael@0: } michael@0: }, null); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * The Query Data Object - this object encapsulates data for our queries and is michael@0: * used to parameterize our calls to the Places APIs to put data into the michael@0: * database. It also has some interesting meta functions to determine which APIs michael@0: * should be called, and to determine if this object should show up in the michael@0: * resulting query. michael@0: * Its parameter is an object specifying which attributes you want to set. michael@0: * For ex: michael@0: * var myobj = new queryData({isVisit: true, uri:"http://mozilla.com", title="foo"}); michael@0: * Note that it doesn't do any input checking on that object. michael@0: */ michael@0: function queryData(obj) { michael@0: this.isVisit = obj.isVisit ? obj.isVisit : false; michael@0: this.isBookmark = obj.isBookmark ? obj.isBookmark: false; michael@0: this.uri = obj.uri ? obj.uri : ""; michael@0: this.lastVisit = obj.lastVisit ? obj.lastVisit : today; michael@0: this.referrer = obj.referrer ? obj.referrer : null; michael@0: this.transType = obj.transType ? obj.transType : Ci.nsINavHistoryService.TRANSITION_TYPED; michael@0: this.isRedirect = obj.isRedirect ? obj.isRedirect : false; michael@0: this.isDetails = obj.isDetails ? obj.isDetails : false; michael@0: this.title = obj.title ? obj.title : ""; michael@0: this.markPageAsTyped = obj.markPageAsTyped ? obj.markPageAsTyped : false; michael@0: this.isPageAnnotation = obj.isPageAnnotation ? obj.isPageAnnotation : false; michael@0: this.removeAnnotation= obj.removeAnnotation ? true : false; michael@0: this.annoName = obj.annoName ? obj.annoName : ""; michael@0: this.annoVal = obj.annoVal ? obj.annoVal : ""; michael@0: this.annoFlags = obj.annoFlags ? obj.annoFlags : 0; michael@0: this.annoExpiration = obj.annoExpiration ? obj.annoExpiration : 0; michael@0: this.isItemAnnotation = obj.isItemAnnotation ? obj.isItemAnnotation : false; michael@0: this.itemId = obj.itemId ? obj.itemId : 0; michael@0: this.annoMimeType = obj.annoMimeType ? obj.annoMimeType : ""; michael@0: this.isTag = obj.isTag ? obj.isTag : false; michael@0: this.tagArray = obj.tagArray ? obj.tagArray : null; michael@0: this.isLivemark = obj.isLivemark ? obj.isLivemark : false; michael@0: this.parentFolder = obj.parentFolder ? obj.parentFolder michael@0: : PlacesUtils.placesRootId; michael@0: this.feedURI = obj.feedURI ? obj.feedURI : ""; michael@0: this.index = obj.index ? obj.index : PlacesUtils.bookmarks.DEFAULT_INDEX; michael@0: this.isFolder = obj.isFolder ? obj.isFolder : false; michael@0: this.contractId = obj.contractId ? obj.contractId : ""; michael@0: this.lastModified = obj.lastModified ? obj.lastModified : today; michael@0: this.dateAdded = obj.dateAdded ? obj.dateAdded : today; michael@0: this.keyword = obj.keyword ? obj.keyword : ""; michael@0: this.visitCount = obj.visitCount ? obj.visitCount : 0; michael@0: this.readOnly = obj.readOnly ? obj.readOnly : false; michael@0: this.isSeparator = obj.hasOwnProperty("isSeparator") && obj.isSeparator; michael@0: michael@0: // And now, the attribute for whether or not this object should appear in the michael@0: // resulting query michael@0: this.isInQuery = obj.isInQuery ? obj.isInQuery : false; michael@0: } michael@0: michael@0: // All attributes are set in the constructor above michael@0: queryData.prototype = { } michael@0: michael@0: michael@0: /** michael@0: * Helper function to compare an array of query objects with a result set. michael@0: * It assumes the array of query objects contains the SAME SORT as the result michael@0: * set. It checks the the uri, title, time, and bookmarkIndex properties of michael@0: * the results, where appropriate. michael@0: */ michael@0: function compareArrayToResult(aArray, aRoot) { michael@0: LOG("Comparing Array to Results"); michael@0: michael@0: var wasOpen = aRoot.containerOpen; michael@0: if (!wasOpen) michael@0: aRoot.containerOpen = true; michael@0: michael@0: // check expected number of results against actual michael@0: var expectedResultCount = aArray.filter(function(aEl) { return aEl.isInQuery; }).length; michael@0: if (expectedResultCount != aRoot.childCount) { michael@0: // Debugging code for failures. michael@0: dump_table("moz_places"); michael@0: dump_table("moz_historyvisits"); michael@0: LOG("Found children:"); michael@0: for (let i = 0; i < aRoot.childCount; i++) { michael@0: LOG(aRoot.getChild(i).uri); michael@0: } michael@0: LOG("Expected:"); michael@0: for (let i = 0; i < aArray.length; i++) { michael@0: if (aArray[i].isInQuery) michael@0: LOG(aArray[i].uri); michael@0: } michael@0: } michael@0: do_check_eq(expectedResultCount, aRoot.childCount); michael@0: michael@0: var inQueryIndex = 0; michael@0: for (var i = 0; i < aArray.length; i++) { michael@0: if (aArray[i].isInQuery) { michael@0: var child = aRoot.getChild(inQueryIndex); michael@0: //LOG("testing testData[" + i + "] vs result[" + inQueryIndex + "]"); michael@0: if (!aArray[i].isFolder && !aArray[i].isSeparator) { michael@0: LOG("testing testData[" + aArray[i].uri + "] vs result[" + child.uri + "]"); michael@0: if (aArray[i].uri != child.uri) { michael@0: dump_table("moz_places"); michael@0: do_throw("Expected " + aArray[i].uri + " found " + child.uri); michael@0: } michael@0: } michael@0: if (!aArray[i].isSeparator && aArray[i].title != child.title) michael@0: do_throw("Expected " + aArray[i].title + " found " + child.title); michael@0: if (aArray[i].hasOwnProperty("lastVisit") && michael@0: aArray[i].lastVisit != child.time) michael@0: do_throw("Expected " + aArray[i].lastVisit + " found " + child.time); michael@0: if (aArray[i].hasOwnProperty("index") && michael@0: aArray[i].index != PlacesUtils.bookmarks.DEFAULT_INDEX && michael@0: aArray[i].index != child.bookmarkIndex) michael@0: do_throw("Expected " + aArray[i].index + " found " + child.bookmarkIndex); michael@0: michael@0: inQueryIndex++; michael@0: } michael@0: } michael@0: michael@0: if (!wasOpen) michael@0: aRoot.containerOpen = false; michael@0: LOG("Comparing Array to Results passes"); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Helper function to check to see if one object either is or is not in the michael@0: * result set. It can accept either a queryData object or an array of queryData michael@0: * objects. If it gets an array, it only compares the first object in the array michael@0: * to see if it is in the result set. michael@0: * Returns: True if item is in query set, and false if item is not in query set michael@0: * If input is an array, returns True if FIRST object in array is in michael@0: * query set. To compare entire array, use the function above. michael@0: */ michael@0: function isInResult(aQueryData, aRoot) { michael@0: var rv = false; michael@0: var uri; michael@0: var wasOpen = aRoot.containerOpen; michael@0: if (!wasOpen) michael@0: aRoot.containerOpen = true; michael@0: michael@0: // If we have an array, pluck out the first item. If an object, pluc out the michael@0: // URI, we just compare URI's here. michael@0: if ("uri" in aQueryData) { michael@0: uri = aQueryData.uri; michael@0: } else { michael@0: uri = aQueryData[0].uri; michael@0: } michael@0: michael@0: for (var i=0; i < aRoot.childCount; i++) { michael@0: if (uri == aRoot.getChild(i).uri) { michael@0: rv = true; michael@0: break; michael@0: } michael@0: } michael@0: if (!wasOpen) michael@0: aRoot.containerOpen = false; michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * A nice helper function for debugging things. It LOGs the contents of a michael@0: * result set. michael@0: */ michael@0: function displayResultSet(aRoot) { michael@0: michael@0: var wasOpen = aRoot.containerOpen; michael@0: if (!wasOpen) michael@0: aRoot.containerOpen = true; michael@0: michael@0: if (!aRoot.hasChildren) { michael@0: // Something wrong? Empty result set? michael@0: LOG("Result Set Empty"); michael@0: return; michael@0: } michael@0: michael@0: for (var i=0; i < aRoot.childCount; ++i) { michael@0: LOG("Result Set URI: " + aRoot.getChild(i).uri + " Title: " + michael@0: aRoot.getChild(i).title + " Visit Time: " + aRoot.getChild(i).time); michael@0: } michael@0: if (!wasOpen) michael@0: aRoot.containerOpen = false; michael@0: }