michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 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 CURRENT_SCHEMA_VERSION = 23; michael@0: michael@0: const NS_APP_USER_PROFILE_50_DIR = "ProfD"; michael@0: const NS_APP_PROFILE_DIR_STARTUP = "ProfDS"; michael@0: michael@0: // Shortcuts to transitions type. michael@0: const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK; michael@0: const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED; michael@0: const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK; michael@0: const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED; michael@0: const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK; michael@0: const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT; michael@0: const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY; michael@0: const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD; michael@0: michael@0: const TITLE_LENGTH_MAX = 4096; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Services", michael@0: "resource://gre/modules/Services.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", michael@0: "resource://gre/modules/BookmarkJSONUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils", michael@0: "resource://gre/modules/BookmarkHTMLUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", michael@0: "resource://gre/modules/PlacesBackups.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions", michael@0: "resource://gre/modules/PlacesTransactions.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: michael@0: // This imports various other objects in addition to PlacesUtils. michael@0: Cu.import("resource://gre/modules/PlacesUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() { michael@0: return NetUtil.newURI( michael@0: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" + michael@0: "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="); michael@0: }); michael@0: michael@0: function LOG(aMsg) { michael@0: aMsg = ("*** PLACES TESTS: " + aMsg); michael@0: Services.console.logStringMessage(aMsg); michael@0: print(aMsg); michael@0: } michael@0: michael@0: let gTestDir = do_get_cwd(); michael@0: michael@0: // Initialize profile. michael@0: let gProfD = do_get_profile(); michael@0: michael@0: // Remove any old database. michael@0: clearDB(); michael@0: michael@0: /** michael@0: * Shortcut to create a nsIURI. michael@0: * michael@0: * @param aSpec michael@0: * URLString of the uri. michael@0: */ michael@0: function uri(aSpec) NetUtil.newURI(aSpec); michael@0: michael@0: michael@0: /** michael@0: * Gets the database connection. If the Places connection is invalid it will michael@0: * try to create a new connection. michael@0: * michael@0: * @param [optional] aForceNewConnection michael@0: * Forces creation of a new connection to the database. When a michael@0: * connection is asyncClosed it cannot anymore schedule async statements, michael@0: * though connectionReady will keep returning true (Bug 726990). michael@0: * michael@0: * @return The database connection or null if unable to get one. michael@0: */ michael@0: let gDBConn; michael@0: function DBConn(aForceNewConnection) { michael@0: if (!aForceNewConnection) { michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: if (db.connectionReady) michael@0: return db; michael@0: } michael@0: michael@0: // If the Places database connection has been closed, create a new connection. michael@0: if (!gDBConn || aForceNewConnection) { michael@0: let file = Services.dirsvc.get('ProfD', Ci.nsIFile); michael@0: file.append("places.sqlite"); michael@0: let dbConn = gDBConn = Services.storage.openDatabase(file); michael@0: michael@0: // Be sure to cleanly close this connection. michael@0: Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) { michael@0: Services.obs.removeObserver(DBCloseCallback, aTopic); michael@0: dbConn.asyncClose(); michael@0: }, "profile-before-change", false); michael@0: } michael@0: michael@0: return gDBConn.connectionReady ? gDBConn : null; michael@0: }; michael@0: michael@0: /** michael@0: * Reads data from the provided inputstream. michael@0: * michael@0: * @return an array of bytes. michael@0: */ michael@0: function readInputStreamData(aStream) { michael@0: let bistream = Cc["@mozilla.org/binaryinputstream;1"]. michael@0: createInstance(Ci.nsIBinaryInputStream); michael@0: try { michael@0: bistream.setInputStream(aStream); michael@0: let expectedData = []; michael@0: let avail; michael@0: while ((avail = bistream.available())) { michael@0: expectedData = expectedData.concat(bistream.readByteArray(avail)); michael@0: } michael@0: return expectedData; michael@0: } finally { michael@0: bistream.close(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Reads the data from the specified nsIFile. michael@0: * michael@0: * @param aFile michael@0: * The nsIFile to read from. michael@0: * @return an array of bytes. michael@0: */ michael@0: function readFileData(aFile) { michael@0: let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: // init the stream as RD_ONLY, -1 == default permissions. michael@0: inputStream.init(aFile, 0x01, -1, null); michael@0: michael@0: // Check the returned size versus the expected size. michael@0: let size = inputStream.available(); michael@0: let bytes = readInputStreamData(inputStream); michael@0: if (size != bytes.length) { michael@0: throw "Didn't read expected number of bytes"; michael@0: } michael@0: return bytes; michael@0: } michael@0: michael@0: /** michael@0: * Reads the data from the named file, verifying the expected file length. michael@0: * michael@0: * @param aFileName michael@0: * This file should be located in the same folder as the test. michael@0: * @param aExpectedLength michael@0: * Expected length of the file. michael@0: * michael@0: * @return The array of bytes read from the file. michael@0: */ michael@0: function readFileOfLength(aFileName, aExpectedLength) { michael@0: let data = readFileData(do_get_file(aFileName)); michael@0: do_check_eq(data.length, aExpectedLength); michael@0: return data; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Returns the base64-encoded version of the given string. This function is michael@0: * similar to window.btoa, but is available to xpcshell tests also. michael@0: * michael@0: * @param aString michael@0: * Each character in this string corresponds to a byte, and must be a michael@0: * code point in the range 0-255. michael@0: * michael@0: * @return The base64-encoded string. michael@0: */ michael@0: function base64EncodeString(aString) { michael@0: var stream = Cc["@mozilla.org/io/string-input-stream;1"] michael@0: .createInstance(Ci.nsIStringInputStream); michael@0: stream.setData(aString, aString.length); michael@0: var encoder = Cc["@mozilla.org/scriptablebase64encoder;1"] michael@0: .createInstance(Ci.nsIScriptableBase64Encoder); michael@0: return encoder.encodeToString(stream, aString.length); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Compares two arrays, and returns true if they are equal. michael@0: * michael@0: * @param aArray1 michael@0: * First array to compare. michael@0: * @param aArray2 michael@0: * Second array to compare. michael@0: */ michael@0: function compareArrays(aArray1, aArray2) { michael@0: if (aArray1.length != aArray2.length) { michael@0: print("compareArrays: array lengths differ\n"); michael@0: return false; michael@0: } michael@0: michael@0: for (let i = 0; i < aArray1.length; i++) { michael@0: if (aArray1[i] != aArray2[i]) { michael@0: print("compareArrays: arrays differ at index " + i + ": " + michael@0: "(" + aArray1[i] + ") != (" + aArray2[i] +")\n"); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Deletes a previously created sqlite file from the profile folder. michael@0: */ michael@0: function clearDB() { michael@0: try { michael@0: let file = Services.dirsvc.get('ProfD', Ci.nsIFile); michael@0: file.append("places.sqlite"); michael@0: if (file.exists()) michael@0: file.remove(false); michael@0: } catch(ex) { dump("Exception: " + ex); } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Dumps the rows of a table out to the console. michael@0: * michael@0: * @param aName michael@0: * The name of the table or view to output. michael@0: */ michael@0: function dump_table(aName) michael@0: { michael@0: let stmt = DBConn().createStatement("SELECT * FROM " + aName); michael@0: michael@0: print("\n*** Printing data from " + aName); michael@0: let count = 0; michael@0: while (stmt.executeStep()) { michael@0: let columns = stmt.numEntries; michael@0: michael@0: if (count == 0) { michael@0: // Print the column names. michael@0: for (let i = 0; i < columns; i++) michael@0: dump(stmt.getColumnName(i) + "\t"); michael@0: dump("\n"); michael@0: } michael@0: michael@0: // Print the rows. michael@0: for (let i = 0; i < columns; i++) { michael@0: switch (stmt.getTypeOfIndex(i)) { michael@0: case Ci.mozIStorageValueArray.VALUE_TYPE_NULL: michael@0: dump("NULL\t"); michael@0: break; michael@0: case Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER: michael@0: dump(stmt.getInt64(i) + "\t"); michael@0: break; michael@0: case Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT: michael@0: dump(stmt.getDouble(i) + "\t"); michael@0: break; michael@0: case Ci.mozIStorageValueArray.VALUE_TYPE_TEXT: michael@0: dump(stmt.getString(i) + "\t"); michael@0: break; michael@0: } michael@0: } michael@0: dump("\n"); michael@0: michael@0: count++; michael@0: } michael@0: print("*** There were a total of " + count + " rows of data.\n"); michael@0: michael@0: stmt.finalize(); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Checks if an address is found in the database. michael@0: * @param aURI michael@0: * nsIURI or address to look for. michael@0: * @return place id of the page or 0 if not found michael@0: */ michael@0: function page_in_database(aURI) michael@0: { michael@0: let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT id FROM moz_places WHERE url = :url" michael@0: ); michael@0: stmt.params.url = url; michael@0: try { michael@0: if (!stmt.executeStep()) michael@0: return 0; michael@0: return stmt.getInt64(0); michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Checks how many visits exist for a specified page. michael@0: * @param aURI michael@0: * nsIURI or address to look for. michael@0: * @return number of visits found. michael@0: */ michael@0: function visits_in_database(aURI) michael@0: { michael@0: let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT count(*) FROM moz_historyvisits v " michael@0: + "JOIN moz_places h ON h.id = v.place_id " michael@0: + "WHERE url = :url" michael@0: ); michael@0: stmt.params.url = url; michael@0: try { michael@0: if (!stmt.executeStep()) michael@0: return 0; michael@0: return stmt.getInt64(0); michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Removes all bookmarks and checks for correct cleanup michael@0: */ michael@0: function remove_all_bookmarks() { michael@0: let PU = PlacesUtils; michael@0: // Clear all bookmarks michael@0: PU.bookmarks.removeFolderChildren(PU.bookmarks.bookmarksMenuFolder); michael@0: PU.bookmarks.removeFolderChildren(PU.bookmarks.toolbarFolder); michael@0: PU.bookmarks.removeFolderChildren(PU.bookmarks.unfiledBookmarksFolder); michael@0: // Check for correct cleanup michael@0: check_no_bookmarks(); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Checks that we don't have any bookmark michael@0: */ michael@0: function check_no_bookmarks() { michael@0: let query = PlacesUtils.history.getNewQuery(); michael@0: let folders = [ michael@0: PlacesUtils.bookmarks.toolbarFolder, michael@0: PlacesUtils.bookmarks.bookmarksMenuFolder, michael@0: PlacesUtils.bookmarks.unfiledBookmarksFolder, michael@0: ]; michael@0: query.setFolders(folders, 3); michael@0: let options = PlacesUtils.history.getNewQueryOptions(); michael@0: options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; michael@0: let root = PlacesUtils.history.executeQuery(query, options).root; michael@0: root.containerOpen = true; michael@0: if (root.childCount != 0) michael@0: do_throw("Unable to remove all bookmarks"); michael@0: root.containerOpen = false; michael@0: } michael@0: michael@0: /** michael@0: * Allows waiting for an observer notification once. michael@0: * michael@0: * @param aTopic michael@0: * Notification topic to observe. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The array [aSubject, aData] from the observed notification. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseTopicObserved(aTopic) michael@0: { michael@0: let deferred = Promise.defer(); michael@0: michael@0: Services.obs.addObserver( michael@0: function PTO_observe(aSubject, aTopic, aData) { michael@0: Services.obs.removeObserver(PTO_observe, aTopic); michael@0: deferred.resolve([aSubject, aData]); michael@0: }, aTopic, false); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Clears history asynchronously. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When history has been cleared. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseClearHistory() { michael@0: let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); michael@0: do_execute_soon(function() PlacesUtils.bhistory.removeAllPages()); michael@0: return promise; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Simulates a Places shutdown. michael@0: */ michael@0: function shutdownPlaces(aKeepAliveConnection) michael@0: { michael@0: let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver); michael@0: hs.observe(null, "profile-change-teardown", null); michael@0: hs.observe(null, "profile-before-change", null); michael@0: } michael@0: michael@0: const FILENAME_BOOKMARKS_HTML = "bookmarks.html"; michael@0: let (backup_date = new Date().toLocaleFormat("%Y-%m-%d")) { michael@0: const FILENAME_BOOKMARKS_JSON = "bookmarks-" + backup_date + ".json"; michael@0: } michael@0: michael@0: /** michael@0: * Creates a bookmarks.html file in the profile folder from a given source file. michael@0: * michael@0: * @param aFilename michael@0: * Name of the file to copy to the profile folder. This file must michael@0: * exist in the directory that contains the test files. michael@0: * michael@0: * @return nsIFile object for the file. michael@0: */ michael@0: function create_bookmarks_html(aFilename) { michael@0: if (!aFilename) michael@0: do_throw("you must pass a filename to create_bookmarks_html function"); michael@0: remove_bookmarks_html(); michael@0: let bookmarksHTMLFile = gTestDir.clone(); michael@0: bookmarksHTMLFile.append(aFilename); michael@0: do_check_true(bookmarksHTMLFile.exists()); michael@0: bookmarksHTMLFile.copyTo(gProfD, FILENAME_BOOKMARKS_HTML); michael@0: let profileBookmarksHTMLFile = gProfD.clone(); michael@0: profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); michael@0: do_check_true(profileBookmarksHTMLFile.exists()); michael@0: return profileBookmarksHTMLFile; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Remove bookmarks.html file from the profile folder. michael@0: */ michael@0: function remove_bookmarks_html() { michael@0: let profileBookmarksHTMLFile = gProfD.clone(); michael@0: profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); michael@0: if (profileBookmarksHTMLFile.exists()) { michael@0: profileBookmarksHTMLFile.remove(false); michael@0: do_check_false(profileBookmarksHTMLFile.exists()); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Check bookmarks.html file exists in the profile folder. michael@0: * michael@0: * @return nsIFile object for the file. michael@0: */ michael@0: function check_bookmarks_html() { michael@0: let profileBookmarksHTMLFile = gProfD.clone(); michael@0: profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); michael@0: do_check_true(profileBookmarksHTMLFile.exists()); michael@0: return profileBookmarksHTMLFile; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Creates a JSON backup in the profile folder folder from a given source file. michael@0: * michael@0: * @param aFilename michael@0: * Name of the file to copy to the profile folder. This file must michael@0: * exist in the directory that contains the test files. michael@0: * michael@0: * @return nsIFile object for the file. michael@0: */ michael@0: function create_JSON_backup(aFilename) { michael@0: if (!aFilename) michael@0: do_throw("you must pass a filename to create_JSON_backup function"); michael@0: let bookmarksBackupDir = gProfD.clone(); michael@0: bookmarksBackupDir.append("bookmarkbackups"); michael@0: if (!bookmarksBackupDir.exists()) { michael@0: bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); michael@0: do_check_true(bookmarksBackupDir.exists()); michael@0: } michael@0: let profileBookmarksJSONFile = bookmarksBackupDir.clone(); michael@0: profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); michael@0: if (profileBookmarksJSONFile.exists()) { michael@0: profileBookmarksJSONFile.remove(); michael@0: } michael@0: let bookmarksJSONFile = gTestDir.clone(); michael@0: bookmarksJSONFile.append(aFilename); michael@0: do_check_true(bookmarksJSONFile.exists()); michael@0: bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON); michael@0: profileBookmarksJSONFile = bookmarksBackupDir.clone(); michael@0: profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); michael@0: do_check_true(profileBookmarksJSONFile.exists()); michael@0: return profileBookmarksJSONFile; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Remove bookmarksbackup dir and all backups from the profile folder. michael@0: */ michael@0: function remove_all_JSON_backups() { michael@0: let bookmarksBackupDir = gProfD.clone(); michael@0: bookmarksBackupDir.append("bookmarkbackups"); michael@0: if (bookmarksBackupDir.exists()) { michael@0: bookmarksBackupDir.remove(true); michael@0: do_check_false(bookmarksBackupDir.exists()); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Check a JSON backup file for today exists in the profile folder. michael@0: * michael@0: * @param aIsAutomaticBackup The boolean indicates whether it's an automatic michael@0: * backup. michael@0: * @return nsIFile object for the file. michael@0: */ michael@0: function check_JSON_backup(aIsAutomaticBackup) { michael@0: let profileBookmarksJSONFile; michael@0: if (aIsAutomaticBackup) { michael@0: let bookmarksBackupDir = gProfD.clone(); michael@0: bookmarksBackupDir.append("bookmarkbackups"); michael@0: let files = bookmarksBackupDir.directoryEntries; michael@0: let backup_date = new Date().toLocaleFormat("%Y-%m-%d"); michael@0: while (files.hasMoreElements()) { michael@0: let entry = files.getNext().QueryInterface(Ci.nsIFile); michael@0: if (PlacesBackups.filenamesRegex.test(entry.leafName)) { michael@0: profileBookmarksJSONFile = entry; michael@0: break; michael@0: } michael@0: } michael@0: } else { michael@0: profileBookmarksJSONFile = gProfD.clone(); michael@0: profileBookmarksJSONFile.append("bookmarkbackups"); michael@0: profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); michael@0: } michael@0: do_check_true(profileBookmarksJSONFile.exists()); michael@0: return profileBookmarksJSONFile; michael@0: } michael@0: michael@0: /** michael@0: * Returns the frecency of a url. michael@0: * michael@0: * @param aURI michael@0: * The URI or spec to get frecency for. michael@0: * @return the frecency value. michael@0: */ michael@0: function frecencyForUrl(aURI) michael@0: { michael@0: let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT frecency FROM moz_places WHERE url = ?1" michael@0: ); michael@0: stmt.bindByIndex(0, url); michael@0: try { michael@0: if (!stmt.executeStep()) { michael@0: throw new Error("No result for frecency."); michael@0: } michael@0: return stmt.getInt32(0); michael@0: } finally { michael@0: stmt.finalize(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Returns the hidden status of a url. michael@0: * michael@0: * @param aURI michael@0: * The URI or spec to get hidden for. michael@0: * @return @return true if the url is hidden, false otherwise. michael@0: */ michael@0: function isUrlHidden(aURI) michael@0: { michael@0: let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT hidden FROM moz_places WHERE url = ?1" michael@0: ); michael@0: stmt.bindByIndex(0, url); michael@0: if (!stmt.executeStep()) michael@0: throw new Error("No result for hidden."); michael@0: let hidden = stmt.getInt32(0); michael@0: stmt.finalize(); michael@0: michael@0: return !!hidden; michael@0: } michael@0: michael@0: /** michael@0: * Compares two times in usecs, considering eventual platform timers skews. michael@0: * michael@0: * @param aTimeBefore michael@0: * The older time in usecs. michael@0: * @param aTimeAfter michael@0: * The newer time in usecs. michael@0: * @return true if times are ordered, false otherwise. michael@0: */ michael@0: function is_time_ordered(before, after) { michael@0: // Windows has an estimated 16ms timers precision, since Date.now() and michael@0: // PR_Now() use different code atm, the results can be unordered by this michael@0: // amount of time. See bug 558745 and bug 557406. michael@0: let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc); michael@0: // Just to be safe we consider 20ms. michael@0: let skew = isWindows ? 20000000 : 0; michael@0: return after - before > -skew; michael@0: } michael@0: michael@0: /** michael@0: * Waits for all pending async statements on the default connection. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When all pending async statements finished. michael@0: * @rejects Never. michael@0: * michael@0: * @note The result is achieved by asynchronously executing a query requiring michael@0: * a write lock. Since all statements on the same connection are michael@0: * serialized, the end of this write operation means that all writes are michael@0: * complete. Note that WAL makes so that writers don't block readers, but michael@0: * this is a problem only across different connections. michael@0: */ michael@0: function promiseAsyncUpdates() michael@0: { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let db = DBConn(); michael@0: let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); michael@0: begin.executeAsync(); michael@0: begin.finalize(); michael@0: michael@0: let commit = db.createAsyncStatement("COMMIT"); michael@0: commit.executeAsync({ michael@0: handleResult: function () {}, michael@0: handleError: function () {}, michael@0: handleCompletion: function(aReason) michael@0: { michael@0: deferred.resolve(); michael@0: } michael@0: }); michael@0: commit.finalize(); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Shutdowns Places, invoking the callback when the connection has been closed. michael@0: * michael@0: * @param aCallback michael@0: * Function to be called when done. michael@0: */ michael@0: function waitForConnectionClosed(aCallback) michael@0: { michael@0: Services.obs.addObserver(function WFCCCallback() { michael@0: Services.obs.removeObserver(WFCCCallback, "places-connection-closed"); michael@0: aCallback(); michael@0: }, "places-connection-closed", false); michael@0: shutdownPlaces(); michael@0: } michael@0: michael@0: /** michael@0: * Tests if a given guid is valid for use in Places or not. michael@0: * michael@0: * @param aGuid michael@0: * The guid to test. michael@0: * @param [optional] aStack michael@0: * The stack frame used to report the error. michael@0: */ michael@0: function do_check_valid_places_guid(aGuid, michael@0: aStack) michael@0: { michael@0: if (!aStack) { michael@0: aStack = Components.stack.caller; michael@0: } michael@0: do_check_true(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), aStack); michael@0: } michael@0: michael@0: /** michael@0: * Retrieves the guid for a given uri. michael@0: * michael@0: * @param aURI michael@0: * The uri to check. michael@0: * @param [optional] aStack michael@0: * The stack frame used to report the error. michael@0: * @return the associated the guid. michael@0: */ michael@0: function do_get_guid_for_uri(aURI, michael@0: aStack) michael@0: { michael@0: if (!aStack) { michael@0: aStack = Components.stack.caller; michael@0: } michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT guid " 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(), aStack); michael@0: let guid = stmt.row.guid; michael@0: stmt.finalize(); michael@0: do_check_valid_places_guid(guid, aStack); michael@0: return guid; michael@0: } michael@0: michael@0: /** michael@0: * Tests that a guid was set in moz_places for a given uri. michael@0: * michael@0: * @param aURI michael@0: * The uri to check. michael@0: * @param [optional] aGUID michael@0: * The expected guid in the database. michael@0: */ michael@0: function do_check_guid_for_uri(aURI, michael@0: aGUID) michael@0: { michael@0: let caller = Components.stack.caller; michael@0: let guid = do_get_guid_for_uri(aURI, caller); michael@0: if (aGUID) { michael@0: do_check_valid_places_guid(aGUID, caller); michael@0: do_check_eq(guid, aGUID, caller); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Retrieves the guid for a given bookmark. michael@0: * michael@0: * @param aId michael@0: * The bookmark id to check. michael@0: * @param [optional] aStack michael@0: * The stack frame used to report the error. michael@0: * @return the associated the guid. michael@0: */ michael@0: function do_get_guid_for_bookmark(aId, michael@0: aStack) michael@0: { michael@0: if (!aStack) { michael@0: aStack = Components.stack.caller; michael@0: } michael@0: let stmt = DBConn().createStatement( michael@0: "SELECT guid " michael@0: + "FROM moz_bookmarks " michael@0: + "WHERE id = :item_id " michael@0: ); michael@0: stmt.params.item_id = aId; michael@0: do_check_true(stmt.executeStep(), aStack); michael@0: let guid = stmt.row.guid; michael@0: stmt.finalize(); michael@0: do_check_valid_places_guid(guid, aStack); michael@0: return guid; michael@0: } michael@0: michael@0: /** michael@0: * Tests that a guid was set in moz_places for a given bookmark. michael@0: * michael@0: * @param aId michael@0: * The bookmark id to check. michael@0: * @param [optional] aGUID michael@0: * The expected guid in the database. michael@0: */ michael@0: function do_check_guid_for_bookmark(aId, michael@0: aGUID) michael@0: { michael@0: let caller = Components.stack.caller; michael@0: let guid = do_get_guid_for_bookmark(aId, caller); michael@0: if (aGUID) { michael@0: do_check_valid_places_guid(aGUID, caller); michael@0: do_check_eq(guid, aGUID, caller); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Logs info to the console in the standard way (includes the filename). michael@0: * michael@0: * @param aMessage michael@0: * The message to log to the console. michael@0: */ michael@0: function do_log_info(aMessage) michael@0: { michael@0: print("TEST-INFO | " + _TEST_FILE + " | " + aMessage); michael@0: } michael@0: michael@0: /** michael@0: * Compares 2 arrays returning whether they contains the same elements. michael@0: * michael@0: * @param a1 michael@0: * First array to compare. michael@0: * @param a2 michael@0: * Second array to compare. michael@0: * @param [optional] sorted michael@0: * Whether the comparison should take in count position of the elements. michael@0: * @return true if the arrays contain the same elements, false otherwise. michael@0: */ michael@0: function do_compare_arrays(a1, a2, sorted) michael@0: { michael@0: if (a1.length != a2.length) michael@0: return false; michael@0: michael@0: if (sorted) { michael@0: return a1.every(function (e, i) e == a2[i]); michael@0: } michael@0: else { michael@0: return a1.filter(function (e) a2.indexOf(e) == -1).length == 0 && michael@0: a2.filter(function (e) a1.indexOf(e) == -1).length == 0; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Generic nsINavBookmarkObserver that doesn't implement anything, but provides michael@0: * dummy methods to prevent errors about an object not having a certain method. michael@0: */ michael@0: function NavBookmarkObserver() {} michael@0: michael@0: NavBookmarkObserver.prototype = { michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: onItemAdded: function () {}, michael@0: onItemRemoved: function () {}, michael@0: onItemChanged: function () {}, michael@0: onItemVisited: function () {}, michael@0: onItemMoved: function () {}, michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsINavBookmarkObserver, michael@0: ]) michael@0: }; michael@0: michael@0: /** michael@0: * Generic nsINavHistoryObserver that doesn't implement anything, but provides michael@0: * dummy methods to prevent errors about an object not having a certain method. michael@0: */ michael@0: function NavHistoryObserver() {} michael@0: michael@0: NavHistoryObserver.prototype = { michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: onVisit: function () {}, michael@0: onTitleChanged: function () {}, michael@0: onDeleteURI: function () {}, michael@0: onClearHistory: function () {}, michael@0: onPageChanged: function () {}, michael@0: onDeleteVisits: function () {}, michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsINavHistoryObserver, michael@0: ]) michael@0: }; michael@0: michael@0: /** michael@0: * Generic nsINavHistoryResultObserver that doesn't implement anything, but michael@0: * provides dummy methods to prevent errors about an object not having a certain michael@0: * method. michael@0: */ michael@0: function NavHistoryResultObserver() {} michael@0: michael@0: NavHistoryResultObserver.prototype = { michael@0: batching: function () {}, michael@0: containerStateChanged: function () {}, michael@0: invalidateContainer: function () {}, michael@0: nodeAnnotationChanged: function () {}, michael@0: nodeDateAddedChanged: function () {}, michael@0: nodeHistoryDetailsChanged: function () {}, michael@0: nodeIconChanged: function () {}, michael@0: nodeInserted: function () {}, michael@0: nodeKeywordChanged: function () {}, michael@0: nodeLastModifiedChanged: function () {}, michael@0: nodeMoved: function () {}, michael@0: nodeRemoved: function () {}, michael@0: nodeTagsChanged: function () {}, michael@0: nodeTitleChanged: function () {}, michael@0: nodeURIChanged: function () {}, michael@0: sortingChanged: function () {}, michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsINavHistoryResultObserver, michael@0: ]) michael@0: }; michael@0: michael@0: /** michael@0: * Asynchronously adds visits to a page. michael@0: * michael@0: * @param aPlaceInfo michael@0: * Can be an nsIURI, in such a case a single LINK visit will be added. michael@0: * Otherwise can be an object describing the visit to add, or an array michael@0: * of these objects: michael@0: * { uri: nsIURI of the page, michael@0: * transition: one of the TRANSITION_* from nsINavHistoryService, michael@0: * [optional] title: title of the page, michael@0: * [optional] visitDate: visit date in microseconds from the epoch michael@0: * [optional] referrer: nsIURI of the referrer for this visit michael@0: * } michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When all visits have been added successfully. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: function promiseAddVisits(aPlaceInfo) michael@0: { michael@0: let deferred = Promise.defer(); michael@0: let places = []; michael@0: if (aPlaceInfo instanceof Ci.nsIURI) { michael@0: places.push({ uri: aPlaceInfo }); michael@0: } michael@0: else if (Array.isArray(aPlaceInfo)) { michael@0: places = places.concat(aPlaceInfo); michael@0: } else { michael@0: places.push(aPlaceInfo) michael@0: } michael@0: michael@0: // Create mozIVisitInfo for each entry. michael@0: let now = Date.now(); michael@0: for (let i = 0; i < places.length; i++) { michael@0: if (!places[i].title) { michael@0: places[i].title = "test visit for " + places[i].uri.spec; michael@0: } michael@0: places[i].visits = [{ michael@0: transitionType: places[i].transition === undefined ? TRANSITION_LINK michael@0: : places[i].transition, michael@0: visitDate: places[i].visitDate || (now++) * 1000, michael@0: referrerURI: places[i].referrer michael@0: }]; michael@0: } michael@0: michael@0: PlacesUtils.asyncHistory.updatePlaces( michael@0: places, michael@0: { michael@0: handleError: function AAV_handleError(aResultCode, aPlaceInfo) { michael@0: let ex = new Components.Exception("Unexpected error in adding visits.", michael@0: aResultCode); michael@0: deferred.reject(ex); michael@0: }, michael@0: handleResult: function () {}, michael@0: handleCompletion: function UP_handleCompletion() { michael@0: deferred.resolve(); michael@0: } michael@0: } michael@0: ); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously check a url is visited. michael@0: * michael@0: * @param aURI The URI. michael@0: * @return {Promise} michael@0: * @resolves When the check has been added successfully. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: function promiseIsURIVisited(aURI) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { michael@0: deferred.resolve(aIsVisited); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: