summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/head_common.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/head_common.js')
-rw-r--r--toolkit/components/places/tests/head_common.js1002
1 files changed, 1002 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js
new file mode 100644
index 0000000000..1123f9e2b3
--- /dev/null
+++ b/toolkit/components/places/tests/head_common.js
@@ -0,0 +1,1002 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+
+// Shortcuts to transitions type.
+const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
+const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
+const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK;
+const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED;
+const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK;
+const TRANSITION_REDIRECT_PERMANENT =
+ Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT;
+const TRANSITION_REDIRECT_TEMPORARY =
+ Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY;
+const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD;
+const TRANSITION_RELOAD = Ci.nsINavHistoryService.TRANSITION_RELOAD;
+
+const TITLE_LENGTH_MAX = 4096;
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { PlacesSyncUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesSyncUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
+ BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ PlacesBackups: "resource://gre/modules/PlacesBackups.sys.mjs",
+ PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() {
+ return NetUtil.newURI(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
+ "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="
+ );
+});
+const SMALLPNG_DATA_LEN = 67;
+
+XPCOMUtils.defineLazyGetter(this, "SMALLSVG_DATA_URI", function() {
+ return NetUtil.newURI(
+ "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy5" +
+ "3My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBmaWxs" +
+ "PSIjNDI0ZTVhIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iN" +
+ "DQiIHN0cm9rZT0iIzQyNGU1YSIgc3Ryb2tlLXdpZHRoPSIxMSIgZmlsbD" +
+ "0ibm9uZSIvPg0KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjI0LjYiIHI9IjY" +
+ "uNCIvPg0KICA8cmVjdCB4PSI0NSIgeT0iMzkuOSIgd2lkdGg9IjEwLjEi" +
+ "IGhlaWdodD0iNDEuOCIvPg0KPC9zdmc%2BDQo%3D"
+ );
+});
+
+var gTestDir = do_get_cwd();
+
+// Initialize profile.
+var gProfD = do_get_profile(true);
+
+Services.prefs.setBoolPref("browser.urlbar.usepreloadedtopurls.enabled", false);
+registerCleanupFunction(() =>
+ Services.prefs.clearUserPref("browser.urlbar.usepreloadedtopurls.enabled")
+);
+
+// Remove any old database.
+clearDB();
+
+/**
+ * Shortcut to create a nsIURI.
+ *
+ * @param aSpec
+ * URLString of the uri.
+ */
+function uri(aSpec) {
+ return NetUtil.newURI(aSpec);
+}
+
+/**
+ * Gets the database connection. If the Places connection is invalid it will
+ * try to create a new connection.
+ *
+ * @param [optional] aForceNewConnection
+ * Forces creation of a new connection to the database. When a
+ * connection is asyncClosed it cannot anymore schedule async statements,
+ * though connectionReady will keep returning true (Bug 726990).
+ *
+ * @return The database connection or null if unable to get one.
+ */
+var gDBConn;
+function DBConn(aForceNewConnection) {
+ if (!aForceNewConnection) {
+ let db = PlacesUtils.history.DBConnection;
+ if (db.connectionReady) {
+ return db;
+ }
+ }
+
+ // If the Places database connection has been closed, create a new connection.
+ if (!gDBConn || aForceNewConnection) {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("places.sqlite");
+ let dbConn = (gDBConn = Services.storage.openDatabase(file));
+
+ // Be sure to cleanly close this connection.
+ promiseTopicObserved("profile-before-change").then(() =>
+ dbConn.asyncClose()
+ );
+ }
+
+ return gDBConn.connectionReady ? gDBConn : null;
+}
+
+/**
+ * Reads data from the provided inputstream.
+ *
+ * @return an array of bytes.
+ */
+function readInputStreamData(aStream) {
+ let bistream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ try {
+ bistream.setInputStream(aStream);
+ let expectedData = [];
+ let avail;
+ while ((avail = bistream.available())) {
+ expectedData = expectedData.concat(bistream.readByteArray(avail));
+ }
+ return expectedData;
+ } finally {
+ bistream.close();
+ }
+}
+
+/**
+ * Reads the data from the specified nsIFile.
+ *
+ * @param aFile
+ * The nsIFile to read from.
+ * @return an array of bytes.
+ */
+function readFileData(aFile) {
+ let inputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ // init the stream as RD_ONLY, -1 == default permissions.
+ inputStream.init(aFile, 0x01, -1, null);
+
+ // Check the returned size versus the expected size.
+ let size = inputStream.available();
+ let bytes = readInputStreamData(inputStream);
+ if (size != bytes.length) {
+ throw new Error("Didn't read expected number of bytes");
+ }
+ return bytes;
+}
+
+/**
+ * Reads the data from the named file, verifying the expected file length.
+ *
+ * @param aFileName
+ * This file should be located in the same folder as the test.
+ * @param aExpectedLength
+ * Expected length of the file.
+ *
+ * @return The array of bytes read from the file.
+ */
+function readFileOfLength(aFileName, aExpectedLength) {
+ let data = readFileData(do_get_file(aFileName));
+ Assert.equal(data.length, aExpectedLength);
+ return data;
+}
+
+/**
+ * Returns the base64-encoded version of the given string. This function is
+ * similar to window.btoa, but is available to xpcshell tests also.
+ *
+ * @param aString
+ * Each character in this string corresponds to a byte, and must be a
+ * code point in the range 0-255.
+ *
+ * @return The base64-encoded string.
+ */
+function base64EncodeString(aString) {
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.setData(aString, aString.length);
+ var encoder = Cc["@mozilla.org/scriptablebase64encoder;1"].createInstance(
+ Ci.nsIScriptableBase64Encoder
+ );
+ return encoder.encodeToString(stream, aString.length);
+}
+
+/**
+ * Compares two arrays, and returns true if they are equal.
+ *
+ * @param aArray1
+ * First array to compare.
+ * @param aArray2
+ * Second array to compare.
+ */
+function compareArrays(aArray1, aArray2) {
+ if (aArray1.length != aArray2.length) {
+ print("compareArrays: array lengths differ\n");
+ return false;
+ }
+
+ for (let i = 0; i < aArray1.length; i++) {
+ if (aArray1[i] != aArray2[i]) {
+ print(
+ "compareArrays: arrays differ at index " +
+ i +
+ ": " +
+ "(" +
+ aArray1[i] +
+ ") != (" +
+ aArray2[i] +
+ ")\n"
+ );
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Deletes a previously created sqlite file from the profile folder.
+ */
+function clearDB() {
+ try {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("places.sqlite");
+ if (file.exists()) {
+ file.remove(false);
+ }
+ } catch (ex) {
+ dump("Exception: " + ex);
+ }
+}
+
+/**
+ * Dumps the rows of a table out to the console.
+ *
+ * @param aName
+ * The name of the table or view to output.
+ */
+function dump_table(aName, dbConn) {
+ if (!dbConn) {
+ dbConn = DBConn();
+ }
+ let stmt = dbConn.createStatement("SELECT * FROM " + aName);
+
+ print("\n*** Printing data from " + aName);
+ let count = 0;
+ while (stmt.executeStep()) {
+ let columns = stmt.numEntries;
+
+ if (count == 0) {
+ // Print the column names.
+ for (let i = 0; i < columns; i++) {
+ dump(stmt.getColumnName(i) + "\t");
+ }
+ dump("\n");
+ }
+
+ // Print the rows.
+ for (let i = 0; i < columns; i++) {
+ switch (stmt.getTypeOfIndex(i)) {
+ case Ci.mozIStorageValueArray.VALUE_TYPE_NULL:
+ dump("NULL\t");
+ break;
+ case Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER:
+ dump(stmt.getInt64(i) + "\t");
+ break;
+ case Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT:
+ dump(stmt.getDouble(i) + "\t");
+ break;
+ case Ci.mozIStorageValueArray.VALUE_TYPE_TEXT:
+ dump(stmt.getString(i) + "\t");
+ break;
+ }
+ }
+ dump("\n");
+
+ count++;
+ }
+ print("*** There were a total of " + count + " rows of data.\n");
+
+ stmt.finalize();
+}
+
+/**
+ * Checks if an address is found in the database.
+ * @param aURI
+ * nsIURI or address to look for.
+ * @return place id of the page or 0 if not found
+ */
+function page_in_database(aURI) {
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let stmt = DBConn().createStatement(
+ "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url"
+ );
+ stmt.params.url = url;
+ try {
+ if (!stmt.executeStep()) {
+ return 0;
+ }
+ return stmt.getInt64(0);
+ } finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Checks how many visits exist for a specified page.
+ * @param aURI
+ * nsIURI or address to look for.
+ * @return number of visits found.
+ */
+function visits_in_database(aURI) {
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let stmt = DBConn().createStatement(
+ `SELECT count(*) FROM moz_historyvisits v
+ JOIN moz_places h ON h.id = v.place_id
+ WHERE url_hash = hash(:url) AND url = :url`
+ );
+ stmt.params.url = url;
+ try {
+ if (!stmt.executeStep()) {
+ return 0;
+ }
+ return stmt.getInt64(0);
+ } finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Allows waiting for an observer notification once.
+ *
+ * @param aTopic
+ * Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves The array [aSubject, aData] from the observed notification.
+ * @rejects Never.
+ */
+function promiseTopicObserved(aTopic) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observe(
+ aObsSubject,
+ aObsTopic,
+ aObsData
+ ) {
+ Services.obs.removeObserver(observe, aObsTopic);
+ resolve([aObsSubject, aObsData]);
+ },
+ aTopic);
+ });
+}
+
+/**
+ * Simulates a Places shutdown.
+ */
+var shutdownPlaces = function() {
+ info("shutdownPlaces: starting");
+ let promise = new Promise(resolve => {
+ Services.obs.addObserver(resolve, "places-connection-closed");
+ });
+ let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver);
+ hs.observe(null, "profile-change-teardown", null);
+ info("shutdownPlaces: sent profile-change-teardown");
+ hs.observe(null, "test-simulate-places-shutdown", null);
+ info("shutdownPlaces: sent test-simulate-places-shutdown");
+ return promise.then(() => {
+ info("shutdownPlaces: complete");
+ });
+};
+
+const FILENAME_BOOKMARKS_HTML = "bookmarks.html";
+const FILENAME_BOOKMARKS_JSON =
+ "bookmarks-" + PlacesBackups.toISODateString(new Date()) + ".json";
+
+/**
+ * Creates a bookmarks.html file in the profile folder from a given source file.
+ *
+ * @param aFilename
+ * Name of the file to copy to the profile folder. This file must
+ * exist in the directory that contains the test files.
+ *
+ * @return nsIFile object for the file.
+ */
+function create_bookmarks_html(aFilename) {
+ if (!aFilename) {
+ do_throw("you must pass a filename to create_bookmarks_html function");
+ }
+ remove_bookmarks_html();
+ let bookmarksHTMLFile = gTestDir.clone();
+ bookmarksHTMLFile.append(aFilename);
+ Assert.ok(bookmarksHTMLFile.exists());
+ bookmarksHTMLFile.copyTo(gProfD, FILENAME_BOOKMARKS_HTML);
+ let profileBookmarksHTMLFile = gProfD.clone();
+ profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
+ Assert.ok(profileBookmarksHTMLFile.exists());
+ return profileBookmarksHTMLFile;
+}
+
+/**
+ * Remove bookmarks.html file from the profile folder.
+ */
+function remove_bookmarks_html() {
+ let profileBookmarksHTMLFile = gProfD.clone();
+ profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
+ if (profileBookmarksHTMLFile.exists()) {
+ profileBookmarksHTMLFile.remove(false);
+ Assert.ok(!profileBookmarksHTMLFile.exists());
+ }
+}
+
+/**
+ * Check bookmarks.html file exists in the profile folder.
+ *
+ * @return nsIFile object for the file.
+ */
+function check_bookmarks_html() {
+ let profileBookmarksHTMLFile = gProfD.clone();
+ profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
+ Assert.ok(profileBookmarksHTMLFile.exists());
+ return profileBookmarksHTMLFile;
+}
+
+/**
+ * Creates a JSON backup in the profile folder folder from a given source file.
+ *
+ * @param aFilename
+ * Name of the file to copy to the profile folder. This file must
+ * exist in the directory that contains the test files.
+ *
+ * @return nsIFile object for the file.
+ */
+function create_JSON_backup(aFilename) {
+ if (!aFilename) {
+ do_throw("you must pass a filename to create_JSON_backup function");
+ }
+ let bookmarksBackupDir = gProfD.clone();
+ bookmarksBackupDir.append("bookmarkbackups");
+ if (!bookmarksBackupDir.exists()) {
+ bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ Assert.ok(bookmarksBackupDir.exists());
+ }
+ let profileBookmarksJSONFile = bookmarksBackupDir.clone();
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ if (profileBookmarksJSONFile.exists()) {
+ profileBookmarksJSONFile.remove();
+ }
+ let bookmarksJSONFile = gTestDir.clone();
+ bookmarksJSONFile.append(aFilename);
+ Assert.ok(bookmarksJSONFile.exists());
+ bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON);
+ profileBookmarksJSONFile = bookmarksBackupDir.clone();
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ Assert.ok(profileBookmarksJSONFile.exists());
+ return profileBookmarksJSONFile;
+}
+
+/**
+ * Remove bookmarksbackup dir and all backups from the profile folder.
+ */
+function remove_all_JSON_backups() {
+ let bookmarksBackupDir = gProfD.clone();
+ bookmarksBackupDir.append("bookmarkbackups");
+ if (bookmarksBackupDir.exists()) {
+ bookmarksBackupDir.remove(true);
+ Assert.ok(!bookmarksBackupDir.exists());
+ }
+}
+
+/**
+ * Check a JSON backup file for today exists in the profile folder.
+ *
+ * @param aIsAutomaticBackup The boolean indicates whether it's an automatic
+ * backup.
+ * @return nsIFile object for the file.
+ */
+function check_JSON_backup(aIsAutomaticBackup) {
+ let profileBookmarksJSONFile;
+ if (aIsAutomaticBackup) {
+ let bookmarksBackupDir = gProfD.clone();
+ bookmarksBackupDir.append("bookmarkbackups");
+ let files = bookmarksBackupDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let entry = files.nextFile;
+ if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
+ profileBookmarksJSONFile = entry;
+ break;
+ }
+ }
+ } else {
+ profileBookmarksJSONFile = gProfD.clone();
+ profileBookmarksJSONFile.append("bookmarkbackups");
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ }
+ Assert.ok(profileBookmarksJSONFile.exists());
+ return profileBookmarksJSONFile;
+}
+
+/**
+ * Returns the frecency of a url.
+ *
+ * @param aURI
+ * The URI or spec to get frecency for.
+ * @return the frecency value.
+ */
+function frecencyForUrl(aURI) {
+ let url = aURI;
+ if (aURI instanceof Ci.nsIURI) {
+ url = aURI.spec;
+ } else if (URL.isInstance(aURI)) {
+ url = aURI.href;
+ }
+ let stmt = DBConn().createStatement(
+ "SELECT frecency FROM moz_places WHERE url_hash = hash(?1) AND url = ?1"
+ );
+ stmt.bindByIndex(0, url);
+ try {
+ if (!stmt.executeStep()) {
+ throw new Error("No result for frecency.");
+ }
+ return stmt.getInt32(0);
+ } finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Returns the hidden status of a url.
+ *
+ * @param aURI
+ * The URI or spec to get hidden for.
+ * @return @return true if the url is hidden, false otherwise.
+ */
+function isUrlHidden(aURI) {
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let stmt = DBConn().createStatement(
+ "SELECT hidden FROM moz_places WHERE url_hash = hash(?1) AND url = ?1"
+ );
+ stmt.bindByIndex(0, url);
+ if (!stmt.executeStep()) {
+ throw new Error("No result for hidden.");
+ }
+ let hidden = stmt.getInt32(0);
+ stmt.finalize();
+
+ return !!hidden;
+}
+
+/**
+ * Compares two times in usecs, considering eventual platform timers skews.
+ *
+ * @param aTimeBefore
+ * The older time in usecs.
+ * @param aTimeAfter
+ * The newer time in usecs.
+ * @return true if times are ordered, false otherwise.
+ */
+function is_time_ordered(before, after) {
+ // Windows has an estimated 16ms timers precision, since Date.now() and
+ // PR_Now() use different code atm, the results can be unordered by this
+ // amount of time. See bug 558745 and bug 557406.
+ let isWindows = "@mozilla.org/windows-registry-key;1" in Cc;
+ // Just to be safe we consider 20ms.
+ let skew = isWindows ? 20000000 : 0;
+ return after - before > -skew;
+}
+
+/**
+ * Shutdowns Places, invoking the callback when the connection has been closed.
+ *
+ * @param aCallback
+ * Function to be called when done.
+ */
+function waitForConnectionClosed(aCallback) {
+ promiseTopicObserved("places-connection-closed").then(aCallback);
+ shutdownPlaces();
+}
+
+/**
+ * Tests if a given guid is valid for use in Places or not.
+ *
+ * @param aGuid
+ * The guid to test.
+ * @param [optional] aStack
+ * The stack frame used to report the error.
+ */
+function do_check_valid_places_guid(aGuid) {
+ Assert.ok(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), "Should be a valid GUID");
+}
+
+/**
+ * Retrieves the guid for a given uri.
+ *
+ * @param aURI
+ * The uri to check.
+ * @param [optional] aStack
+ * The stack frame used to report the error.
+ * @return the associated the guid.
+ */
+function do_get_guid_for_uri(aURI) {
+ let stmt = DBConn().createStatement(
+ `SELECT guid
+ FROM moz_places
+ WHERE url_hash = hash(:url) AND url = :url`
+ );
+ stmt.params.url = aURI.spec;
+ Assert.ok(stmt.executeStep(), "GUID for URI statement should succeed");
+ let guid = stmt.row.guid;
+ stmt.finalize();
+ do_check_valid_places_guid(guid);
+ return guid;
+}
+
+/**
+ * Tests that a guid was set in moz_places for a given uri.
+ *
+ * @param aURI
+ * The uri to check.
+ * @param [optional] aGUID
+ * The expected guid in the database.
+ */
+function do_check_guid_for_uri(aURI, aGUID) {
+ let guid = do_get_guid_for_uri(aURI);
+ if (aGUID) {
+ do_check_valid_places_guid(aGUID);
+ Assert.equal(guid, aGUID, "Should have a guid in moz_places for the URI");
+ }
+}
+
+/**
+ * Retrieves the guid for a given bookmark.
+ *
+ * @param aId
+ * The bookmark id to check.
+ * @param [optional] aStack
+ * The stack frame used to report the error.
+ * @return the associated the guid.
+ */
+function do_get_guid_for_bookmark(aId) {
+ let stmt = DBConn().createStatement(
+ `SELECT guid
+ FROM moz_bookmarks
+ WHERE id = :item_id`
+ );
+ stmt.params.item_id = aId;
+ Assert.ok(stmt.executeStep(), "Should succeed executing the SQL statement");
+ let guid = stmt.row.guid;
+ stmt.finalize();
+ do_check_valid_places_guid(guid);
+ return guid;
+}
+
+/**
+ * Tests that a guid was set in moz_places for a given bookmark.
+ *
+ * @param aId
+ * The bookmark id to check.
+ * @param [optional] aGUID
+ * The expected guid in the database.
+ */
+function do_check_guid_for_bookmark(aId, aGUID) {
+ let guid = do_get_guid_for_bookmark(aId);
+ if (aGUID) {
+ do_check_valid_places_guid(aGUID);
+ Assert.equal(guid, aGUID, "Should have the correct GUID for the bookmark");
+ }
+}
+
+/**
+ * Compares 2 arrays returning whether they contains the same elements.
+ *
+ * @param a1
+ * First array to compare.
+ * @param a2
+ * Second array to compare.
+ * @param [optional] sorted
+ * Whether the comparison should take in count position of the elements.
+ * @return true if the arrays contain the same elements, false otherwise.
+ */
+function do_compare_arrays(a1, a2, sorted) {
+ if (a1.length != a2.length) {
+ return false;
+ }
+
+ if (sorted) {
+ return a1.every((e, i) => e == a2[i]);
+ }
+ return (
+ !a1.filter(e => !a2.includes(e)).length &&
+ !a2.filter(e => !a1.includes(e)).length
+ );
+}
+
+/**
+ * Generic nsINavBookmarkObserver that doesn't implement anything, but provides
+ * dummy methods to prevent errors about an object not having a certain method.
+ */
+function NavBookmarkObserver() {}
+
+NavBookmarkObserver.prototype = {
+ onItemRemoved() {},
+ onItemChanged() {},
+ QueryInterface: ChromeUtils.generateQI(["nsINavBookmarkObserver"]),
+};
+
+/**
+ * Generic nsINavHistoryResultObserver that doesn't implement anything, but
+ * provides dummy methods to prevent errors about an object not having a certain
+ * method.
+ */
+function NavHistoryResultObserver() {}
+
+NavHistoryResultObserver.prototype = {
+ batching() {},
+ containerStateChanged() {},
+ invalidateContainer() {},
+ nodeDateAddedChanged() {},
+ nodeHistoryDetailsChanged() {},
+ nodeIconChanged() {},
+ nodeInserted() {},
+ nodeKeywordChanged() {},
+ nodeLastModifiedChanged() {},
+ nodeMoved() {},
+ nodeRemoved() {},
+ nodeTagsChanged() {},
+ nodeTitleChanged() {},
+ nodeURIChanged() {},
+ sortingChanged() {},
+ QueryInterface: ChromeUtils.generateQI(["nsINavHistoryResultObserver"]),
+};
+
+function checkBookmarkObject(info) {
+ do_check_valid_places_guid(info.guid);
+ do_check_valid_places_guid(info.parentGuid);
+ Assert.ok(typeof info.index == "number", "index should be a number");
+ Assert.ok(
+ info.dateAdded.constructor.name == "Date",
+ "dateAdded should be a Date"
+ );
+ Assert.ok(
+ info.lastModified.constructor.name == "Date",
+ "lastModified should be a Date"
+ );
+ Assert.ok(
+ info.lastModified >= info.dateAdded,
+ "lastModified should never be smaller than dateAdded"
+ );
+ Assert.ok(typeof info.type == "number", "type should be a number");
+}
+
+/**
+ * Reads foreign_count value for a given url.
+ */
+async function foreign_count(url) {
+ if (url instanceof Ci.nsIURI) {
+ url = url.spec;
+ }
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(
+ `SELECT foreign_count FROM moz_places
+ WHERE url_hash = hash(:url) AND url = :url
+ `,
+ { url }
+ );
+ return !rows.length ? 0 : rows[0].getResultByName("foreign_count");
+}
+
+function compareAscending(a, b) {
+ if (a > b) {
+ return 1;
+ }
+ if (a < b) {
+ return -1;
+ }
+ return 0;
+}
+
+function sortBy(array, prop) {
+ return array.sort((a, b) => compareAscending(a[prop], b[prop]));
+}
+
+/**
+ * Asynchronously set the favicon associated with a page.
+ * @param page
+ * The page's URL
+ * @param icon
+ * The URL of the favicon to be set.
+ * @param [optional] forceReload
+ * Whether to enforce reloading the icon.
+ */
+function setFaviconForPage(page, icon, forceReload = true) {
+ let pageURI =
+ page instanceof Ci.nsIURI ? page : NetUtil.newURI(new URL(page).href);
+ let iconURI =
+ icon instanceof Ci.nsIURI ? icon : NetUtil.newURI(new URL(icon).href);
+ return new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ pageURI,
+ iconURI,
+ forceReload,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ });
+}
+
+function getFaviconUrlForPage(page, width = 0) {
+ let pageURI =
+ page instanceof Ci.nsIURI ? page : NetUtil.newURI(new URL(page).href);
+ return new Promise((resolve, reject) => {
+ PlacesUtils.favicons.getFaviconURLForPage(
+ pageURI,
+ iconURI => {
+ if (iconURI) {
+ resolve(iconURI.spec);
+ } else {
+ reject("Unable to find an icon for " + pageURI.spec);
+ }
+ },
+ width
+ );
+ });
+}
+
+function getFaviconDataForPage(page, width = 0) {
+ let pageURI =
+ page instanceof Ci.nsIURI ? page : NetUtil.newURI(new URL(page).href);
+ return new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconDataForPage(
+ pageURI,
+ (iconUri, len, data, mimeType) => {
+ resolve({ data, mimeType });
+ },
+ width
+ );
+ });
+}
+
+/**
+ * Asynchronously compares contents from 2 favicon urls.
+ */
+async function compareFavicons(icon1, icon2, msg) {
+ icon1 = new URL(icon1 instanceof Ci.nsIURI ? icon1.spec : icon1);
+ icon2 = new URL(icon2 instanceof Ci.nsIURI ? icon2.spec : icon2);
+
+ function getIconData(icon) {
+ return new Promise((resolve, reject) => {
+ NetUtil.asyncFetch(
+ {
+ uri: icon.href,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON,
+ },
+ function(inputStream, status) {
+ if (!Components.isSuccessCode(status)) {
+ reject();
+ }
+ let size = inputStream.available();
+ resolve(NetUtil.readInputStreamToString(inputStream, size));
+ }
+ );
+ });
+ }
+
+ let data1 = await getIconData(icon1);
+ Assert.ok(!!data1.length, "Should fetch icon data");
+ let data2 = await getIconData(icon2);
+ Assert.ok(!!data2.length, "Should fetch icon data");
+ Assert.deepEqual(data1, data2, msg);
+}
+
+/**
+ * Get the internal "root" folder name for an item, specified by its itemGuid.
+ * If the itemGuid does not point to a root folder, null is returned.
+ *
+ * @param itemGuid
+ * the item guid.
+ * @return the internal-root name for the root folder, if itemGuid points
+ * to such folder, null otherwise.
+ */
+function mapItemGuidToInternalRootName(itemGuid) {
+ switch (itemGuid) {
+ case PlacesUtils.bookmarks.rootGuid:
+ return "placesRoot";
+ case PlacesUtils.bookmarks.menuGuid:
+ return "bookmarksMenuFolder";
+ case PlacesUtils.bookmarks.toolbarGuid:
+ return "toolbarFolder";
+ case PlacesUtils.bookmarks.unfiledGuid:
+ return "unfiledBookmarksFolder";
+ case PlacesUtils.bookmarks.mobileGuid:
+ return "mobileFolder";
+ }
+ return null;
+}
+
+const DB_FILENAME = "places.sqlite";
+
+/**
+ * Sets the database to use for the given test. This should be the very first
+ * thing in the test, otherwise this database will not be used!
+ *
+ * @param {string|string[]} path
+ * A filename or path to a database. The database must exist.
+ * If this is a string, then this is assumed to be a filename in the
+ * directory where the test calling this is located.
+ * If this is an array, this is assumed to be a path relative to the
+ * directory that this file, head_common.js, is located.
+ * @param {string} destFileName
+ * The destination filename to copy the database to.
+ * @return {Promise} the final path to the database
+ */
+async function setupPlacesDatabase(path, destFileName = DB_FILENAME) {
+ let currentDir = do_get_cwd().path;
+
+ if (typeof path == "string") {
+ path = [path];
+ } else {
+ currentDir = PathUtils.parent(currentDir);
+ }
+ let src = PathUtils.join(currentDir, ...path);
+ Assert.ok(await IOUtils.exists(src), "Database file found");
+
+ // Ensure that our database doesn't already exist.
+ let dest = PathUtils.join(PathUtils.profileDir, destFileName);
+ Assert.ok(
+ !(await IOUtils.exists(dest)),
+ "Database file should not exist yet"
+ );
+
+ await IOUtils.copy(src, dest);
+ return dest;
+}
+
+/**
+ * Gets the URLs of pages that have a particular annotation.
+ *
+ * @param {String} name The name of the annotation to search for.
+ * @return An array of URLs found.
+ */
+function getPagesWithAnnotation(name) {
+ return PlacesUtils.promiseDBConnection().then(async db => {
+ let rows = await db.execute(
+ `
+ SELECT h.url FROM moz_anno_attributes n
+ JOIN moz_annos a ON n.id = a.anno_attribute_id
+ JOIN moz_places h ON h.id = a.place_id
+ WHERE n.name = :name
+ `,
+ { name }
+ );
+
+ return rows.map(row => row.getResultByName("url"));
+ });
+}
+
+/**
+ * Checks there are no orphan page annotations in the database, and no
+ * orphan anno attribute names.
+ */
+async function assertNoOrphanPageAnnotations() {
+ let db = await PlacesUtils.promiseDBConnection();
+
+ let rows = await db.execute(`
+ SELECT place_id FROM moz_annos
+ WHERE place_id NOT IN (SELECT id FROM moz_places)
+ `);
+
+ Assert.equal(rows.length, 0, "Should not have any orphan page annotations");
+
+ rows = await db.execute(`
+ SELECT id FROM moz_anno_attributes
+ WHERE id NOT IN (SELECT anno_attribute_id FROM moz_annos) AND
+ id NOT IN (SELECT anno_attribute_id FROM moz_items_annos)`);
+}