summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/queries
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/queries')
-rw-r--r--toolkit/components/places/tests/queries/head_queries.js342
-rw-r--r--toolkit/components/places/tests/queries/readme.txt16
-rw-r--r--toolkit/components/places/tests/queries/test_async.js379
-rw-r--r--toolkit/components/places/tests/queries/test_bookmarks.js105
-rw-r--r--toolkit/components/places/tests/queries/test_containersQueries_sorting.js492
-rw-r--r--toolkit/components/places/tests/queries/test_downloadHistory_liveUpdate.js121
-rw-r--r--toolkit/components/places/tests/queries/test_excludeQueries.js118
-rw-r--r--toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js131
-rw-r--r--toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js217
-rw-r--r--toolkit/components/places/tests/queries/test_options_inherit.js118
-rw-r--r--toolkit/components/places/tests/queries/test_queryMultipleFolder.js106
-rw-r--r--toolkit/components/places/tests/queries/test_querySerialization.js718
-rw-r--r--toolkit/components/places/tests/queries/test_query_uri_liveupdate.js45
-rw-r--r--toolkit/components/places/tests/queries/test_redirects.js351
-rw-r--r--toolkit/components/places/tests/queries/test_result_observeHistoryDetails.js155
-rw-r--r--toolkit/components/places/tests/queries/test_results-as-left-pane.js83
-rw-r--r--toolkit/components/places/tests/queries/test_results-as-roots.js114
-rw-r--r--toolkit/components/places/tests/queries/test_results-as-tag-query.js63
-rw-r--r--toolkit/components/places/tests/queries/test_results-as-visit.js158
-rw-r--r--toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js74
-rw-r--r--toolkit/components/places/tests/queries/test_searchTerms_time.js109
-rw-r--r--toolkit/components/places/tests/queries/test_search_tags.js73
-rw-r--r--toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js63
-rw-r--r--toolkit/components/places/tests/queries/test_searchterms-domain.js197
-rw-r--r--toolkit/components/places/tests/queries/test_searchterms-uri.js125
-rw-r--r--toolkit/components/places/tests/queries/test_sort-date-site-grouping.js223
-rw-r--r--toolkit/components/places/tests/queries/test_sorting.js961
-rw-r--r--toolkit/components/places/tests/queries/test_tags.js626
-rw-r--r--toolkit/components/places/tests/queries/test_transitions.js175
-rw-r--r--toolkit/components/places/tests/queries/xpcshell.toml57
30 files changed, 6515 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/queries/head_queries.js b/toolkit/components/places/tests/queries/head_queries.js
new file mode 100644
index 0000000000..ebb6eb4455
--- /dev/null
+++ b/toolkit/components/places/tests/queries/head_queries.js
@@ -0,0 +1,342 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// Import common head.
+{
+ /* import-globals-from ../head_common.js */
+ let commonFile = do_get_file("../head_common.js", false);
+ let uri = Services.io.newFileURI(commonFile);
+ Services.scriptloader.loadSubScript(uri.spec, this);
+}
+
+// Put any other stuff relative to this test folder below.
+
+// Some Useful Date constants - PRTime uses microseconds, so convert
+const DAY_MICROSEC = 86400000000;
+const today = PlacesUtils.toPRTime(Date.now());
+const yesterday = today - DAY_MICROSEC;
+const lastweek = today - DAY_MICROSEC * 7;
+const daybefore = today - DAY_MICROSEC * 2;
+const old = today - DAY_MICROSEC * 3;
+const futureday = today + DAY_MICROSEC * 3;
+const olderthansixmonths = today - DAY_MICROSEC * 31 * 7;
+
+/**
+ * Generalized function to pull in an array of objects of data and push it into
+ * the database. It does NOT do any checking to see that the input is
+ * appropriate. This function is an asynchronous task, it can be called using
+ * "Task.spawn" or using the "yield" function inside another task.
+ */
+async function task_populateDB(aArray) {
+ // Iterate over aArray and execute all instructions.
+ for (let arrayItem of aArray) {
+ try {
+ // make the data object into a query data object in order to create proper
+ // default values for anything left unspecified
+ var qdata = new queryData(arrayItem);
+ if (qdata.isVisit) {
+ // Then we should add a visit for this node
+ await PlacesTestUtils.addVisits({
+ uri: uri(qdata.uri),
+ transition: qdata.transType,
+ visitDate: qdata.lastVisit,
+ referrer: qdata.referrer ? uri(qdata.referrer) : null,
+ title: qdata.title,
+ });
+ if (qdata.visitCount && !qdata.isDetails) {
+ // Set a fake visit_count, this is not a real count but can be used
+ // to test sorting by visit_count.
+ await PlacesTestUtils.updateDatabaseValues(
+ "moz_places",
+ { visit_count: qdata.visitCount },
+ { url: qdata.uri }
+ );
+ }
+ }
+
+ if (qdata.isRedirect) {
+ // This must be async to properly enqueue after the updateFrecency call
+ // done by the visit addition.
+ await PlacesTestUtils.updateDatabaseValues(
+ "moz_places",
+ { hidden: 1 },
+ { url: qdata.uri }
+ );
+ }
+
+ if (qdata.isDetails) {
+ // Then we add extraneous page details for testing
+ await PlacesTestUtils.addVisits({
+ uri: uri(qdata.uri),
+ visitDate: qdata.lastVisit,
+ title: qdata.title,
+ });
+ }
+
+ if (qdata.markPageAsTyped) {
+ PlacesUtils.history.markPageAsTyped(uri(qdata.uri));
+ }
+
+ if (qdata.isPageAnnotation) {
+ await PlacesUtils.history.update({
+ url: qdata.uri,
+ annotations: new Map([
+ [qdata.annoName, qdata.removeAnnotation ? null : qdata.annoVal],
+ ]),
+ });
+ }
+
+ if (qdata.isFolder) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: qdata.parentGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: qdata.title,
+ index: qdata.index,
+ });
+ }
+
+ if (qdata.isBookmark) {
+ let data = {
+ parentGuid: qdata.parentGuid,
+ index: qdata.index,
+ title: qdata.title,
+ url: qdata.uri,
+ };
+
+ if (qdata.dateAdded) {
+ data.dateAdded = new Date(qdata.dateAdded / 1000);
+ }
+
+ if (qdata.lastModified) {
+ data.lastModified = new Date(qdata.lastModified / 1000);
+ }
+
+ await PlacesUtils.bookmarks.insert(data);
+
+ if (qdata.keyword) {
+ await PlacesUtils.keywords.insert({
+ url: qdata.uri,
+ keyword: qdata.keyword,
+ });
+ }
+ }
+
+ if (qdata.isTag) {
+ PlacesUtils.tagging.tagURI(uri(qdata.uri), qdata.tagArray);
+ }
+
+ if (qdata.isSeparator) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: qdata.parentGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: qdata.index,
+ });
+ }
+ } catch (ex) {
+ // use the arrayItem object here in case instantiation of qdata failed
+ info("Problem with this URI: " + arrayItem.uri);
+ do_throw("Error creating database: " + ex + "\n");
+ }
+ }
+}
+
+/**
+ * The Query Data Object - this object encapsulates data for our queries and is
+ * used to parameterize our calls to the Places APIs to put data into the
+ * database. It also has some interesting meta functions to determine which APIs
+ * should be called, and to determine if this object should show up in the
+ * resulting query.
+ * Its parameter is an object specifying which attributes you want to set.
+ * For ex:
+ * var myobj = new queryData({isVisit: true, uri:"http://mozilla.com", title="foo"});
+ * Note that it doesn't do any input checking on that object.
+ */
+function queryData(obj) {
+ this.isVisit = obj.isVisit ? obj.isVisit : false;
+ this.isBookmark = obj.isBookmark ? obj.isBookmark : false;
+ this.uri = obj.uri ? obj.uri : "";
+ this.lastVisit = obj.lastVisit ? obj.lastVisit : today;
+ this.referrer = obj.referrer ? obj.referrer : null;
+ this.transType = obj.transType
+ ? obj.transType
+ : Ci.nsINavHistoryService.TRANSITION_TYPED;
+ this.isRedirect = obj.isRedirect ? obj.isRedirect : false;
+ this.isDetails = obj.isDetails ? obj.isDetails : false;
+ this.title = obj.title ? obj.title : "";
+ this.markPageAsTyped = obj.markPageAsTyped ? obj.markPageAsTyped : false;
+ this.isPageAnnotation = obj.isPageAnnotation ? obj.isPageAnnotation : false;
+ this.removeAnnotation = !!obj.removeAnnotation;
+ this.annoName = obj.annoName ? obj.annoName : "";
+ this.annoVal = obj.annoVal ? obj.annoVal : "";
+ this.itemId = obj.itemId ? obj.itemId : 0;
+ this.annoMimeType = obj.annoMimeType ? obj.annoMimeType : "";
+ this.isTag = obj.isTag ? obj.isTag : false;
+ this.tagArray = obj.tagArray ? obj.tagArray : null;
+ this.parentGuid = obj.parentGuid || PlacesUtils.bookmarks.unfiledGuid;
+ this.feedURI = obj.feedURI ? obj.feedURI : "";
+ this.index = obj.index ? obj.index : PlacesUtils.bookmarks.DEFAULT_INDEX;
+ this.isFolder = obj.isFolder ? obj.isFolder : false;
+ this.contractId = obj.contractId ? obj.contractId : "";
+ this.lastModified = obj.lastModified ? obj.lastModified : null;
+ this.dateAdded = obj.dateAdded ? obj.dateAdded : null;
+ this.keyword = obj.keyword ? obj.keyword : "";
+ this.visitCount = obj.visitCount ? obj.visitCount : 0;
+ this.isSeparator = obj.hasOwnProperty("isSeparator") && obj.isSeparator;
+
+ // And now, the attribute for whether or not this object should appear in the
+ // resulting query
+ this.isInQuery = obj.isInQuery ? obj.isInQuery : false;
+}
+
+// All attributes are set in the constructor above
+queryData.prototype = {};
+
+/**
+ * Helper function to compare an array of query objects with a result set.
+ * It assumes the array of query objects contains the SAME SORT as the result
+ * set. It checks the the uri, title, time, and bookmarkIndex properties of
+ * the results, where appropriate.
+ */
+function compareArrayToResult(aArray, aRoot) {
+ info("Comparing Array to Results");
+
+ var wasOpen = aRoot.containerOpen;
+ if (!wasOpen) {
+ aRoot.containerOpen = true;
+ }
+
+ // check expected number of results against actual
+ var expectedResultCount = aArray.filter(function (aEl) {
+ return aEl.isInQuery;
+ }).length;
+ if (expectedResultCount != aRoot.childCount) {
+ // Debugging code for failures.
+ dump_table("moz_places");
+ dump_table("moz_historyvisits");
+ info("Found children:");
+ for (let i = 0; i < aRoot.childCount; i++) {
+ info(aRoot.getChild(i).uri);
+ }
+ info("Expected:");
+ for (let i = 0; i < aArray.length; i++) {
+ if (aArray[i].isInQuery) {
+ info(aArray[i].uri);
+ }
+ }
+ }
+ Assert.equal(expectedResultCount, aRoot.childCount);
+
+ var inQueryIndex = 0;
+ for (var i = 0; i < aArray.length; i++) {
+ if (aArray[i].isInQuery) {
+ var child = aRoot.getChild(inQueryIndex);
+ // do_print("testing testData[" + i + "] vs result[" + inQueryIndex + "]");
+ if (!aArray[i].isFolder && !aArray[i].isSeparator) {
+ info(
+ "testing testData[" + aArray[i].uri + "] vs result[" + child.uri + "]"
+ );
+ if (aArray[i].uri != child.uri) {
+ dump_table("moz_places");
+ do_throw("Expected " + aArray[i].uri + " found " + child.uri);
+ }
+ }
+ if (!aArray[i].isSeparator && aArray[i].title != child.title) {
+ do_throw("Expected " + aArray[i].title + " found " + child.title);
+ }
+ if (
+ aArray[i].hasOwnProperty("lastVisit") &&
+ aArray[i].lastVisit != child.time
+ ) {
+ do_throw("Expected " + aArray[i].lastVisit + " found " + child.time);
+ }
+ if (
+ aArray[i].hasOwnProperty("index") &&
+ aArray[i].index != PlacesUtils.bookmarks.DEFAULT_INDEX &&
+ aArray[i].index != child.bookmarkIndex
+ ) {
+ do_throw(
+ "Expected " + aArray[i].index + " found " + child.bookmarkIndex
+ );
+ }
+
+ inQueryIndex++;
+ }
+ }
+
+ if (!wasOpen) {
+ aRoot.containerOpen = false;
+ }
+ info("Comparing Array to Results passes");
+}
+
+/**
+ * Helper function to check to see if one object either is or is not in the
+ * result set. It can accept either a queryData object or an array of queryData
+ * objects. If it gets an array, it only compares the first object in the array
+ * to see if it is in the result set.
+ * @returns {nsINavHistoryResultNode}: Either the node, if found, or null.
+ * If input is an array, returns a result only for the first node.
+ * To compare entire array, use the function above.
+ */
+function nodeInResult(aQueryData, aRoot) {
+ var rv = null;
+ var uri;
+ var wasOpen = aRoot.containerOpen;
+ if (!wasOpen) {
+ aRoot.containerOpen = true;
+ }
+
+ // If we have an array, pluck out the first item. If an object, pluc out the
+ // URI, we just compare URI's here.
+ if ("uri" in aQueryData) {
+ uri = aQueryData.uri;
+ } else {
+ uri = aQueryData[0].uri;
+ }
+
+ for (var i = 0; i < aRoot.childCount; i++) {
+ let node = aRoot.getChild(i);
+ if (uri == node.uri) {
+ rv = node;
+ break;
+ }
+ }
+ if (!wasOpen) {
+ aRoot.containerOpen = false;
+ }
+ return rv;
+}
+
+/**
+ * A nice helper function for debugging things. It prints the contents of a
+ * result set.
+ */
+function displayResultSet(aRoot) {
+ var wasOpen = aRoot.containerOpen;
+ if (!wasOpen) {
+ aRoot.containerOpen = true;
+ }
+
+ if (!aRoot.hasChildren) {
+ // Something wrong? Empty result set?
+ info("Result Set Empty");
+ return;
+ }
+
+ for (var i = 0; i < aRoot.childCount; ++i) {
+ info(
+ "Result Set URI: " +
+ aRoot.getChild(i).uri +
+ " Title: " +
+ aRoot.getChild(i).title +
+ " Visit Time: " +
+ aRoot.getChild(i).time
+ );
+ }
+ if (!wasOpen) {
+ aRoot.containerOpen = false;
+ }
+}
diff --git a/toolkit/components/places/tests/queries/readme.txt b/toolkit/components/places/tests/queries/readme.txt
new file mode 100644
index 0000000000..19414f96ed
--- /dev/null
+++ b/toolkit/components/places/tests/queries/readme.txt
@@ -0,0 +1,16 @@
+These are tests specific to the Places Query API.
+
+We are tracking the coverage of these tests here:
+http://wiki.mozilla.org/QA/TDAI/Projects/Places_Tests
+
+When creating one of these tests, you need to update those tables so that we
+know how well our test coverage is of this area. Furthermore, when adding tests
+ensure to cover live update (changing the query set) by performing the following
+operations on the query set you get after running the query:
+* Adding a new item to the query set
+* Updating an existing item so that it matches the query set
+* Change an existing item so that it does not match the query set
+* Do multiple of the above inside an Update Batch transaction.
+* Try these transactions in different orders.
+
+Use the stub test to help you create a test with the proper structure.
diff --git a/toolkit/components/places/tests/queries/test_async.js b/toolkit/components/places/tests/queries/test_async.js
new file mode 100644
index 0000000000..8e895748ab
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_async.js
@@ -0,0 +1,379 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+var tests = [
+ {
+ desc:
+ "nsNavHistoryFolderResultNode: Basic test, asynchronously open and " +
+ "close container with a single child",
+
+ loading(node, newState, oldState) {
+ this.checkStateChanged("loading", 1);
+ this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
+ },
+
+ opened(node, newState, oldState) {
+ this.checkStateChanged("opened", 1);
+ this.checkState("loading", 1);
+ this.checkArgs("opened", node, oldState, node.STATE_LOADING);
+
+ print("Checking node children");
+ compareArrayToResult(this.data, node);
+
+ print("Closing container");
+ node.containerOpen = false;
+ },
+
+ closed(node, newState, oldState) {
+ this.checkStateChanged("closed", 1);
+ this.checkState("opened", 1);
+ this.checkArgs("closed", node, oldState, node.STATE_OPENED);
+ this.success();
+ },
+ },
+
+ {
+ desc:
+ "nsNavHistoryFolderResultNode: After async open and no changes, " +
+ "second open should be synchronous",
+
+ loading(node, newState, oldState) {
+ this.checkStateChanged("loading", 1);
+ this.checkState("closed", 0);
+ this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
+ },
+
+ opened(node, newState, oldState) {
+ let cnt = this.checkStateChanged("opened", 1, 2);
+ let expectOldState = cnt === 1 ? node.STATE_LOADING : node.STATE_CLOSED;
+ this.checkArgs("opened", node, oldState, expectOldState);
+
+ print("Checking node children");
+ compareArrayToResult(this.data, node);
+
+ print("Closing container");
+ node.containerOpen = false;
+ },
+
+ closed(node, newState, oldState) {
+ let cnt = this.checkStateChanged("closed", 1, 2);
+ this.checkArgs("closed", node, oldState, node.STATE_OPENED);
+
+ switch (cnt) {
+ case 1:
+ node.containerOpen = true;
+ break;
+ case 2:
+ this.success();
+ break;
+ }
+ },
+ },
+
+ {
+ desc:
+ "nsNavHistoryFolderResultNode: After closing container in " +
+ "loading(), opened() should not be called",
+
+ loading(node, newState, oldState) {
+ this.checkStateChanged("loading", 1);
+ this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
+ print("Closing container");
+ node.containerOpen = false;
+ },
+
+ opened(node, newState, oldState) {
+ do_throw("opened should not be called");
+ },
+
+ closed(node, newState, oldState) {
+ this.checkStateChanged("closed", 1);
+ this.checkState("loading", 1);
+ this.checkArgs("closed", node, oldState, node.STATE_LOADING);
+ this.success();
+ },
+ },
+];
+
+/**
+ * Instances of this class become the prototypes of the test objects above.
+ * Each test can therefore use the methods of this class, or they can override
+ * them if they want. To run a test, call setup() and then run().
+ */
+function Test() {
+ // This maps a state name to the number of times it's been observed.
+ this.stateCounts = {};
+ // Promise object resolved when the next test can be run.
+ this.deferNextTest = Promise.withResolvers();
+}
+
+Test.prototype = {
+ /**
+ * Call this when an observer observes a container state change to sanity
+ * check the arguments.
+ *
+ * @param aNewState
+ * The name of the new state. Used only for printing out helpful info.
+ * @param aNode
+ * The node argument passed to containerStateChanged.
+ * @param aOldState
+ * The old state argument passed to containerStateChanged.
+ * @param aExpectOldState
+ * The expected old state.
+ */
+ checkArgs(aNewState, aNode, aOldState, aExpectOldState) {
+ print("Node passed on " + aNewState + " should be result.root");
+ Assert.equal(this.result.root, aNode);
+ print("Old state passed on " + aNewState + " should be " + aExpectOldState);
+
+ // aOldState comes from xpconnect and will therefore be defined. It may be
+ // zero, though, so use strict equality just to make sure aExpectOldState is
+ // also defined.
+ Assert.ok(aOldState === aExpectOldState);
+ },
+
+ /**
+ * Call this when an observer observes a container state change. It registers
+ * the state change and ensures that it has been observed the given number
+ * of times. See checkState for parameter explanations.
+ *
+ * @return The number of times aState has been observed, including the new
+ * observation.
+ */
+ checkStateChanged(aState, aExpectedMin, aExpectedMax) {
+ print(aState + " state change observed");
+ if (!this.stateCounts.hasOwnProperty(aState)) {
+ this.stateCounts[aState] = 0;
+ }
+ this.stateCounts[aState]++;
+ return this.checkState(aState, aExpectedMin, aExpectedMax);
+ },
+
+ /**
+ * Ensures that the state has been observed the given number of times.
+ *
+ * @param aState
+ * The name of the state.
+ * @param aExpectedMin
+ * The state must have been observed at least this number of times.
+ * @param aExpectedMax
+ * The state must have been observed at most this number of times.
+ * This parameter is optional. If undefined, it's set to
+ * aExpectedMin.
+ * @return The number of times aState has been observed, including the new
+ * observation.
+ */
+ checkState(aState, aExpectedMin, aExpectedMax) {
+ let cnt = this.stateCounts[aState] || 0;
+ if (aExpectedMax === undefined) {
+ aExpectedMax = aExpectedMin;
+ }
+ if (aExpectedMin === aExpectedMax) {
+ print(
+ aState +
+ " should be observed only " +
+ aExpectedMin +
+ " times (actual = " +
+ cnt +
+ ")"
+ );
+ } else {
+ print(
+ aState +
+ " should be observed at least " +
+ aExpectedMin +
+ " times and at most " +
+ aExpectedMax +
+ " times (actual = " +
+ cnt +
+ ")"
+ );
+ }
+ Assert.ok(cnt >= aExpectedMin && cnt <= aExpectedMax);
+ return cnt;
+ },
+
+ /**
+ * Asynchronously opens the root of the test's result.
+ */
+ openContainer() {
+ // Set up the result observer. It delegates to this object's callbacks and
+ // wraps them in a try-catch so that errors don't get eaten.
+ let self = this;
+ this.observer = {
+ containerStateChanged(container, oldState, newState) {
+ print(
+ "New state passed to containerStateChanged() should equal the " +
+ "container's current state"
+ );
+ Assert.equal(newState, container.state);
+
+ try {
+ switch (newState) {
+ case Ci.nsINavHistoryContainerResultNode.STATE_LOADING:
+ self.loading(container, newState, oldState);
+ break;
+ case Ci.nsINavHistoryContainerResultNode.STATE_OPENED:
+ self.opened(container, newState, oldState);
+ break;
+ case Ci.nsINavHistoryContainerResultNode.STATE_CLOSED:
+ self.closed(container, newState, oldState);
+ break;
+ default:
+ do_throw("Unexpected new state! " + newState);
+ }
+ } catch (err) {
+ do_throw(err);
+ }
+ },
+ };
+ this.result.addObserver(this.observer);
+
+ print("Opening container");
+ this.result.root.containerOpen = true;
+ },
+
+ /**
+ * Starts the test and returns a promise resolved when the test completes.
+ */
+ run() {
+ this.openContainer();
+ return this.deferNextTest.promise;
+ },
+
+ /**
+ * This must be called before run(). It adds a bookmark and sets up the
+ * test's result. Override if need be.
+ */
+ async setup() {
+ // Populate the database with different types of bookmark items.
+ this.data = DataHelper.makeDataArray([
+ { type: "bookmark" },
+ { type: "separator" },
+ { type: "folder" },
+ { type: "bookmark", uri: "place:terms=foo" },
+ ]);
+ await task_populateDB(this.data);
+
+ // Make a query.
+ this.query = PlacesUtils.history.getNewQuery();
+ this.query.setParents([DataHelper.defaults.bookmark.parentGuid]);
+ this.opts = PlacesUtils.history.getNewQueryOptions();
+ this.opts.asyncEnabled = true;
+ this.result = PlacesUtils.history.executeQuery(this.query, this.opts);
+ },
+
+ /**
+ * Call this when the test has succeeded. It cleans up resources and starts
+ * the next test.
+ */
+ success() {
+ this.result.removeObserver(this.observer);
+
+ // Resolve the promise object that indicates that the next test can be run.
+ this.deferNextTest.resolve();
+ },
+};
+
+/**
+ * This makes it a little bit easier to use the functions of head_queries.js.
+ */
+var DataHelper = {
+ defaults: {
+ bookmark: {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ uri: "http://example.com/",
+ title: "test bookmark",
+ },
+
+ folder: {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test folder",
+ },
+
+ separator: {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ },
+ },
+
+ /**
+ * Converts an array of simple bookmark item descriptions to the more verbose
+ * format required by task_populateDB() in head_queries.js.
+ *
+ * @param aData
+ * An array of objects, each of which describes a bookmark item.
+ * @return An array of objects suitable for passing to populateDB().
+ */
+ makeDataArray: function DH_makeDataArray(aData) {
+ let self = this;
+ return aData.map(function (dat) {
+ let type = dat.type;
+ dat = self._makeDataWithDefaults(dat, self.defaults[type]);
+ switch (type) {
+ case "bookmark":
+ return {
+ isBookmark: true,
+ uri: dat.uri,
+ parentGuid: dat.parentGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: dat.title,
+ isInQuery: true,
+ };
+ case "separator":
+ return {
+ isSeparator: true,
+ parentGuid: dat.parentGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ isInQuery: true,
+ };
+ case "folder":
+ return {
+ isFolder: true,
+ parentGuid: dat.parentGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: dat.title,
+ isInQuery: true,
+ };
+ default:
+ do_throw("Unknown data type when populating DB: " + type);
+ return undefined;
+ }
+ });
+ },
+
+ /**
+ * Returns a copy of aData, except that any properties that are undefined but
+ * defined in aDefaults are set to the corresponding values in aDefaults.
+ *
+ * @param aData
+ * An object describing a bookmark item.
+ * @param aDefaults
+ * An object describing the default bookmark item.
+ * @return A copy of aData with defaults values set.
+ */
+ _makeDataWithDefaults: function DH__makeDataWithDefaults(aData, aDefaults) {
+ let dat = {};
+ for (let [prop, val] of Object.entries(aDefaults)) {
+ dat[prop] = aData.hasOwnProperty(prop) ? aData[prop] : val;
+ }
+ return dat;
+ },
+};
+
+add_task(async function test_async() {
+ for (let test of tests) {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ Object.setPrototypeOf(test, new Test());
+ await test.setup();
+
+ print("------ Running test: " + test.desc);
+ await test.run();
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ print("All tests done, exiting");
+});
diff --git a/toolkit/components/places/tests/queries/test_bookmarks.js b/toolkit/components/places/tests/queries/test_bookmarks.js
new file mode 100644
index 0000000000..b5f2ef754f
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_bookmarks.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_eraseEverything() {
+ info("Test folder with eraseEverything");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "remove-folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ { url: "http://mozilla.org/", title: "title 1" },
+ { url: "http://mozilla.org/", title: "title 2" },
+ { title: "sub-folder", type: PlacesUtils.bookmarks.TYPE_FOLDER },
+ { type: PlacesUtils.bookmarks.TYPE_SEPARATOR },
+ ],
+ },
+ ],
+ });
+
+ let unfiled = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid
+ ).root;
+ Assert.equal(unfiled.childCount, 1, "There should be 1 folder");
+ let folder = unfiled.getChild(0);
+ // Test dateAdded and lastModified properties.
+ Assert.equal(typeof folder.dateAdded, "number");
+ Assert.ok(folder.dateAdded > 0);
+ Assert.equal(typeof folder.lastModified, "number");
+ Assert.ok(folder.lastModified > 0);
+
+ let root = PlacesUtils.getFolderContents(folder.bookmarkGuid).root;
+ Assert.equal(root.childCount, 4, "The folder should have 4 children");
+ for (let i = 0; i < root.childCount; ++i) {
+ let node = root.getChild(i);
+ Assert.greater(node.itemId, 0, "The node should have an itemId");
+ }
+ Assert.equal(root.getChild(0).title, "title 1");
+ Assert.equal(root.getChild(1).title, "title 2");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Refetch the guid to refresh the data.
+ unfiled = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid
+ ).root;
+ Assert.equal(unfiled.childCount, 0);
+ unfiled.containerOpen = false;
+});
+
+add_task(async function test_search_title() {
+ let title = "ZZZXXXYYY";
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://mozilla.org/",
+ title,
+ });
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "ZZZXXXYYY";
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 1);
+ let node = root.getChild(0);
+ Assert.equal(node.title, title);
+
+ // Test dateAdded and lastModified properties.
+ Assert.equal(typeof node.dateAdded, "number");
+ Assert.ok(node.dateAdded > 0);
+ Assert.equal(typeof node.lastModified, "number");
+ Assert.ok(node.lastModified > 0);
+ Assert.equal(node.bookmarkGuid, bm.guid);
+
+ await PlacesUtils.bookmarks.remove(bm);
+ Assert.equal(root.childCount, 0);
+ root.containerOpen = false;
+});
+
+add_task(async function test_long_title() {
+ let title = Array(TITLE_LENGTH_MAX + 5).join("A");
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://mozilla.org/",
+ title,
+ });
+ let root = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid
+ ).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 1);
+ let node = root.getChild(0);
+ Assert.equal(node.title, title.substr(0, TITLE_LENGTH_MAX));
+
+ // Update with another long title.
+ let newTitle = Array(TITLE_LENGTH_MAX + 5).join("B");
+ bm.title = newTitle;
+ await PlacesUtils.bookmarks.update(bm);
+ Assert.equal(node.title, newTitle.substr(0, TITLE_LENGTH_MAX));
+
+ await PlacesUtils.bookmarks.remove(bm);
+ Assert.equal(root.childCount, 0);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_containersQueries_sorting.js b/toolkit/components/places/tests/queries/test_containersQueries_sorting.js
new file mode 100644
index 0000000000..9cdc0f2a52
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_containersQueries_sorting.js
@@ -0,0 +1,492 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Testing behavior of bug 473157
+ * "Want to sort history in container view without sorting the containers"
+ * and regression bug 488783
+ * Tags list no longer sorted (alphabetized).
+ * This test is for global testing sorting containers queries.
+ */
+
+// Globals and Constants
+
+var resultTypes = [
+ {
+ value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY,
+ name: "RESULTS_AS_DATE_QUERY",
+ },
+ {
+ value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY,
+ name: "RESULTS_AS_SITE_QUERY",
+ },
+ {
+ value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY,
+ name: "RESULTS_AS_DATE_SITE_QUERY",
+ },
+ {
+ value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT,
+ name: "RESULTS_AS_TAGS_ROOT",
+ },
+];
+
+var sortingModes = [
+ {
+ value: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING,
+ name: "SORT_BY_TITLE_ASCENDING",
+ },
+ {
+ value: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING,
+ name: "SORT_BY_TITLE_DESCENDING",
+ },
+ {
+ value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING,
+ name: "SORT_BY_DATE_ASCENDING",
+ },
+ {
+ value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING,
+ name: "SORT_BY_DATE_DESCENDING",
+ },
+ {
+ value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING,
+ name: "SORT_BY_DATEADDED_ASCENDING",
+ },
+ {
+ value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING,
+ name: "SORT_BY_DATEADDED_DESCENDING",
+ },
+];
+
+// These pages will be added from newest to oldest and from less visited to most
+// visited.
+var pages = [
+ "http://www.mozilla.org/c/",
+ "http://www.mozilla.org/a/",
+ "http://www.mozilla.org/b/",
+ "http://www.mozilla.com/c/",
+ "http://www.mozilla.com/a/",
+ "http://www.mozilla.com/b/",
+];
+
+var tags = ["mozilla", "Development", "test"];
+
+// Test Runner
+
+/**
+ * Enumerates all the sequences of the cartesian product of the arrays contained
+ * in aSequences. Examples:
+ *
+ * cartProd([[1, 2, 3], ["a", "b"]], callback);
+ * // callback is called 3 * 2 = 6 times with the following arrays:
+ * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
+ *
+ * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
+ * // callback is called 1 * 3 * 2 = 6 times with the following arrays:
+ * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
+ * // ["a", 3, "X"], ["a", 3, "Y"]
+ *
+ * cartProd([[1], [2], [3], [4]], callback);
+ * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
+ * // [1, 2, 3, 4]
+ *
+ * cartProd([], callback);
+ * // callback is 0 times
+ *
+ * cartProd([[1, 2, 3, 4]], callback);
+ * // callback is called 4 times with the following arrays:
+ * // [1], [2], [3], [4]
+ *
+ * @param aSequences
+ * an array that contains an arbitrary number of arrays
+ * @param aCallback
+ * a function that is passed each sequence of the product as it's
+ * computed
+ * @return the total number of sequences in the product
+ */
+function cartProd(aSequences, aCallback) {
+ if (aSequences.length === 0) {
+ return 0;
+ }
+
+ // For each sequence in aSequences, we maintain a pointer (an array index,
+ // really) to the element we're currently enumerating in that sequence
+ var seqEltPtrs = aSequences.map(i => 0);
+
+ var numProds = 0;
+ var done = false;
+ while (!done) {
+ numProds++;
+
+ // prod = sequence in product we're currently enumerating
+ var prod = [];
+ for (var i = 0; i < aSequences.length; i++) {
+ prod.push(aSequences[i][seqEltPtrs[i]]);
+ }
+ aCallback(prod);
+
+ // The next sequence in the product differs from the current one by just a
+ // single element. Determine which element that is. We advance the
+ // "rightmost" element pointer to the "right" by one. If we move past the
+ // end of that pointer's sequence, reset the pointer to the first element
+ // in its sequence and then try the sequence to the "left", and so on.
+
+ // seqPtr = index of rightmost input sequence whose element pointer is not
+ // past the end of the sequence
+ var seqPtr = aSequences.length - 1;
+ while (!done) {
+ // Advance the rightmost element pointer.
+ seqEltPtrs[seqPtr]++;
+
+ // The rightmost element pointer is past the end of its sequence.
+ if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
+ seqEltPtrs[seqPtr] = 0;
+ seqPtr--;
+
+ // All element pointers are past the ends of their sequences.
+ if (seqPtr < 0) {
+ done = true;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ return numProds;
+}
+
+/**
+ * Test a query based on passed-in options.
+ *
+ * @param aSequence
+ * array of options we will use to query.
+ */
+function test_query_callback(aSequence) {
+ Assert.equal(aSequence.length, 2);
+ var resultType = aSequence[0];
+ var sortingMode = aSequence[1];
+ print(
+ "\n\n*** Testing default sorting for resultType (" +
+ resultType.name +
+ ") and sortingMode (" +
+ sortingMode.name +
+ ")"
+ );
+
+ // Skip invalid combinations sorting queries by none.
+ if (
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT &&
+ (sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING ||
+ sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING)
+ ) {
+ // This is a bookmark query, we can't sort by visit date.
+ sortingMode.value = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ }
+ if (
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ ) {
+ // This is an history query, we can't sort by date added.
+ if (
+ sortingMode.value ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING ||
+ sortingMode.value ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING
+ ) {
+ sortingMode.value = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ }
+ }
+
+ // Create a new query with required options.
+ var query = PlacesUtils.history.getNewQuery();
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = resultType.value;
+ options.sortingMode = sortingMode.value;
+
+ // Compare resultset with expectedData.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ if (
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ ) {
+ // Date containers are always sorted by date descending.
+ check_children_sorting(
+ root,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING
+ );
+ } else {
+ check_children_sorting(root, sortingMode.value);
+ }
+
+ // Now Check sorting of the first child container.
+ var container = root
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ container.containerOpen = true;
+
+ if (
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ ) {
+ // Has more than one level of containers, first we check the sorting of
+ // the first level (site containers), those won't inherit sorting...
+ check_children_sorting(
+ container,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING
+ );
+ // ...then we check sorting of the contained urls, we can't inherit sorting
+ // since the above level does not inherit it, so they will be sorted by
+ // title ascending.
+ let innerContainer = container
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ innerContainer.containerOpen = true;
+ check_children_sorting(
+ innerContainer,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING
+ );
+ innerContainer.containerOpen = false;
+ } else if (
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT
+ ) {
+ // Sorting mode for tag contents is hardcoded for now, to allow for faster
+ // duplicates filtering.
+ check_children_sorting(
+ container,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
+ );
+ } else {
+ check_children_sorting(container, sortingMode.value);
+ }
+
+ container.containerOpen = false;
+ root.containerOpen = false;
+
+ test_result_sortingMode_change(result, resultType, sortingMode);
+}
+
+/**
+ * Sets sortingMode on aResult and checks for correct sorting of children.
+ * Containers should not change their sorting, while contained uri nodes should.
+ *
+ * @param aResult
+ * nsINavHistoryResult generated by our query.
+ * @param aResultType
+ * required result type.
+ * @param aOriginalSortingMode
+ * the sorting mode from query's options.
+ */
+function test_result_sortingMode_change(
+ aResult,
+ aResultType,
+ aOriginalSortingMode
+) {
+ var root = aResult.root;
+ // Now we set sortingMode on the result and check that containers are not
+ // sorted while children are.
+ sortingModes.forEach(function sortingModeChecker(aForcedSortingMode) {
+ print(
+ "\n* Test setting sortingMode (" +
+ aForcedSortingMode.name +
+ ") " +
+ "on result with resultType (" +
+ aResultType.name +
+ ") " +
+ "currently sorted as (" +
+ aOriginalSortingMode.name +
+ ")"
+ );
+
+ aResult.sortingMode = aForcedSortingMode.value;
+ root.containerOpen = true;
+
+ if (
+ aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ aResultType.value ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ ) {
+ // Date containers are always sorted by date descending.
+ check_children_sorting(
+ root,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING
+ );
+ } else if (
+ aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY &&
+ (aOriginalSortingMode.value ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING ||
+ aOriginalSortingMode.value ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING)
+ ) {
+ // Site containers don't have a good time property to sort by.
+ check_children_sorting(root, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE);
+ } else {
+ check_children_sorting(root, aOriginalSortingMode.value);
+ }
+
+ // Now Check sorting of the first child container.
+ var container = root
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ container.containerOpen = true;
+
+ if (
+ aResultType.value ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ ) {
+ // Has more than one level of containers, first we check the sorting of
+ // the first level (site containers), those won't be sorted...
+ check_children_sorting(
+ container,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING
+ );
+ // ...then we check sorting of the second level of containers, result
+ // will sort them through recursiveSort.
+ let innerContainer = container
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ innerContainer.containerOpen = true;
+ check_children_sorting(innerContainer, aForcedSortingMode.value);
+ innerContainer.containerOpen = false;
+ } else {
+ if (
+ aResultType.value ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ aResultType.value ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
+ aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY
+ ) {
+ // Date containers are always sorted by date descending.
+ check_children_sorting(root, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE);
+ } else {
+ check_children_sorting(root, aOriginalSortingMode.value);
+ }
+
+ // Children should always be sorted.
+ check_children_sorting(container, aForcedSortingMode.value);
+ }
+
+ container.containerOpen = false;
+ root.containerOpen = false;
+ });
+}
+
+/**
+ * Test if children of aRootNode are correctly sorted.
+ * @param aRootNode
+ * already opened root node from our query's result.
+ * @param aExpectedSortingMode
+ * The sortingMode we expect results to be.
+ */
+function check_children_sorting(aRootNode, aExpectedSortingMode) {
+ var results = [];
+ print("Found children:");
+ for (let i = 0; i < aRootNode.childCount; i++) {
+ results[i] = aRootNode.getChild(i);
+ print(i + " " + results[i].title);
+ }
+
+ // Helper for case insensitive string comparison.
+ function caseInsensitiveStringComparator(a, b) {
+ var aLC = a.toLowerCase();
+ var bLC = b.toLowerCase();
+ if (aLC < bLC) {
+ return -1;
+ }
+ if (aLC > bLC) {
+ return 1;
+ }
+ return 0;
+ }
+
+ // Get a comparator based on expected sortingMode.
+ var comparator;
+ switch (aExpectedSortingMode) {
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_NONE:
+ comparator = function (a, b) {
+ return 0;
+ };
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING:
+ comparator = function (a, b) {
+ return caseInsensitiveStringComparator(a.title, b.title);
+ };
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING:
+ comparator = function (a, b) {
+ return -caseInsensitiveStringComparator(a.title, b.title);
+ };
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING:
+ comparator = function (a, b) {
+ return a.time - b.time;
+ };
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING:
+ comparator = function (a, b) {
+ return b.time - a.time;
+ };
+ // fall through - we shouldn't do this, see bug 1572437.
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
+ comparator = function (a, b) {
+ return a.dateAdded - b.dateAdded;
+ };
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING:
+ comparator = function (a, b) {
+ return b.dateAdded - a.dateAdded;
+ };
+ break;
+ default:
+ do_throw("Unknown sorting type: " + aExpectedSortingMode);
+ }
+
+ // Make an independent copy of the results array and sort it.
+ var sortedResults = results.slice();
+ sortedResults.sort(comparator);
+ // Actually compare returned children with our sorted array.
+ for (let i = 0; i < sortedResults.length; i++) {
+ if (sortedResults[i].title != results[i].title) {
+ print(
+ i +
+ " index wrong, expected " +
+ sortedResults[i].title +
+ " found " +
+ results[i].title
+ );
+ }
+ Assert.equal(sortedResults[i].title, results[i].title);
+ }
+}
+
+// Main
+
+add_task(async function test_containersQueries_sorting() {
+ // Add visits, bookmarks and tags to our database.
+ var timeInMilliseconds = Date.now();
+ var visitCount = 0;
+ var dayOffset = 0;
+ var visits = [];
+ pages.forEach(aPageUrl =>
+ visits.push({
+ isVisit: true,
+ isBookmark: true,
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ uri: aPageUrl,
+ title: aPageUrl,
+ // subtract 5 hours per iteration, to expose more than one day container.
+ lastVisit: (timeInMilliseconds - 18000 * 1000 * dayOffset++) * 1000,
+ visitCount: visitCount++,
+ isTag: true,
+ tagArray: tags,
+ isInQuery: true,
+ })
+ );
+ await task_populateDB(visits);
+
+ cartProd([resultTypes, sortingModes], test_query_callback);
+});
diff --git a/toolkit/components/places/tests/queries/test_downloadHistory_liveUpdate.js b/toolkit/components/places/tests/queries/test_downloadHistory_liveUpdate.js
new file mode 100644
index 0000000000..ba0f528b62
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_downloadHistory_liveUpdate.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that download history (filtered by transition) queries
+// don't invalidate (and requery) too often.
+
+function accumulateNotifications(result) {
+ let notifications = [];
+ let resultObserver = new Proxy(NavHistoryResultObserver, {
+ get(target, name) {
+ if (name == "check") {
+ result.removeObserver(resultObserver, false);
+ return expectedNotifications =>
+ Assert.deepEqual(notifications, expectedNotifications);
+ }
+ // ignore a few uninteresting notifications.
+ if (["QueryInterface", "containerStateChanged"].includes(name)) {
+ return () => {};
+ }
+ return () => {
+ notifications.push(name);
+ };
+ },
+ });
+ result.addObserver(resultObserver, false);
+ return resultObserver;
+}
+
+add_task(async function test_downloadhistory_query_notifications() {
+ const MAX_RESULTS = 5;
+ let query = PlacesUtils.history.getNewQuery();
+ query.setTransitions([PlacesUtils.history.TRANSITIONS.DOWNLOAD]);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+ options.maxResults = MAX_RESULTS;
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let notifications = accumulateNotifications(result);
+ let root = PlacesUtils.asContainer(result.root);
+ root.containerOpen = true;
+ // Add more maxResults downloads in order.
+ let transitions = Object.values(PlacesUtils.history.TRANSITIONS);
+ for (let transition of transitions) {
+ let uri = "http://fx-search.com/" + transition;
+ await PlacesTestUtils.addVisits({
+ uri,
+ transition,
+ title: "test " + transition,
+ });
+ // For each visit also set apart:
+ // - a bookmark
+ // - an annotation
+ // - an icon
+ await PlacesUtils.bookmarks.insert({
+ url: uri,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesUtils.history.update({
+ url: uri,
+ annotations: new Map([["test/anno", "testValue"]]),
+ });
+ await PlacesTestUtils.addFavicons(new Map([[uri, SMALLPNG_DATA_URI.spec]]));
+ }
+ // Remove all the visits one by one.
+ for (let transition of transitions) {
+ let uri = Services.io.newURI("http://fx-search.com/" + transition);
+ await PlacesUtils.history.remove(uri);
+ }
+ root.containerOpen = false;
+ // We pretty much don't want to see invalidateContainer here, because that
+ // means we requeried.
+ // We also don't want to see changes caused by filtered-out transition types.
+ notifications.check([
+ "nodeHistoryDetailsChanged",
+ "nodeInserted",
+ "nodeTitleChanged",
+ "nodeIconChanged",
+ "nodeRemoved",
+ ]);
+});
+
+add_task(async function test_downloadhistory_query_filtering() {
+ const MAX_RESULTS = 3;
+ let query = PlacesUtils.history.getNewQuery();
+ query.setTransitions([PlacesUtils.history.TRANSITIONS.DOWNLOAD]);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+ options.maxResults = MAX_RESULTS;
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let root = PlacesUtils.asContainer(result.root);
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 0, "No visits found");
+ // Add more than maxResults downloads.
+ let uris = [];
+ // Define a monotonic visit date to ensure results order stability.
+ let visitDate = Date.now() * 1000;
+ for (let i = 0; i < MAX_RESULTS + 1; ++i, visitDate += 1000) {
+ let uri = `http://fx-search.com/download/${i}`;
+ await PlacesTestUtils.addVisits({
+ uri,
+ transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
+ visitDate,
+ });
+ uris.push(uri);
+ }
+ // Add an older download visit out of the maxResults timeframe.
+ await PlacesTestUtils.addVisits({
+ uri: `http://fx-search.com/download/unordered`,
+ transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
+ visitDate: new Date(Date.now() - 7200000),
+ });
+
+ Assert.equal(root.childCount, MAX_RESULTS, "Result should be limited");
+ // Invert the uris array because we are sorted by date descending.
+ uris.reverse();
+ for (let i = 0; i < root.childCount; ++i) {
+ let node = root.getChild(i);
+ Assert.equal(node.uri, uris[i], "Found the expected uri");
+ }
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_excludeQueries.js b/toolkit/components/places/tests/queries/test_excludeQueries.js
new file mode 100644
index 0000000000..c48f84c7f4
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_excludeQueries.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var bm;
+var fakeQuery;
+var folderShortcut;
+
+add_task(async function setup() {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ title: "a bookmark",
+ });
+ fakeQuery = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "place:terms=foo",
+ title: "a bookmark",
+ });
+ folderShortcut = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`,
+ title: "a bookmark",
+ });
+
+ checkBookmarkObject(bm);
+ checkBookmarkObject(fakeQuery);
+ checkBookmarkObject(folderShortcut);
+});
+
+add_task(async function test_bookmarks_url_query_implicit_exclusions() {
+ // When we run bookmarks url queries, we implicity filter out queries and
+ // folder shortcuts regardless of excludeQueries. They don't make sense to
+ // include in the results.
+ let expectedGuids = [bm.guid];
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.excludeQueries = true;
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ Assert.equal(
+ root.childCount,
+ expectedGuids.length,
+ "Checking root child count"
+ );
+ for (let i = 0; i < expectedGuids.length; i++) {
+ Assert.equal(
+ root.getChild(i).bookmarkGuid,
+ expectedGuids[i],
+ "should have got the expected item"
+ );
+ }
+
+ root.containerOpen = false;
+});
+
+add_task(async function test_bookmarks_excludeQueries() {
+ // When excluding queries, we exclude actual queries, but not folder shortcuts.
+ let expectedGuids = [bm.guid, folderShortcut.guid];
+ let query = {},
+ options = {};
+ let queryString = `place:parent=${PlacesUtils.bookmarks.unfiledGuid}&excludeQueries=1`;
+ PlacesUtils.history.queryStringToQuery(queryString, query, options);
+
+ let root = PlacesUtils.history.executeQuery(query.value, options.value).root;
+ root.containerOpen = true;
+
+ Assert.equal(
+ root.childCount,
+ expectedGuids.length,
+ "Checking root child count"
+ );
+ for (let i = 0; i < expectedGuids.length; i++) {
+ Assert.equal(
+ root.getChild(i).bookmarkGuid,
+ expectedGuids[i],
+ "should have got the expected item"
+ );
+ }
+
+ root.containerOpen = false;
+});
+
+add_task(async function test_search_excludesQueries() {
+ // Searching implicity removes queries and folder shortcuts even if excludeQueries
+ // is not specified.
+ let expectedGuids = [bm.guid];
+
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "bookmark";
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ Assert.equal(
+ root.childCount,
+ expectedGuids.length,
+ "Checking root child count"
+ );
+ for (let i = 0; i < expectedGuids.length; i++) {
+ Assert.equal(
+ root.getChild(i).bookmarkGuid,
+ expectedGuids[i],
+ "should have got the expected item"
+ );
+ }
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js b/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js
new file mode 100644
index 0000000000..aef45ff8f1
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that tags changes are correctly live-updated in a history
+// query.
+
+let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+}
+
+var gTestData = [
+ {
+ isVisit: true,
+ uri: "http://example.com/1/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "example1",
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/2/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "example2",
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/3/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "example3",
+ },
+];
+
+function newQueryWithOptions() {
+ return [
+ PlacesUtils.history.getNewQuery(),
+ PlacesUtils.history.getNewQueryOptions(),
+ ];
+}
+
+function testQueryContents(aQuery, aOptions, aCallback) {
+ let root = PlacesUtils.history.executeQuery(aQuery, aOptions).root;
+ root.containerOpen = true;
+ aCallback(root);
+ root.containerOpen = false;
+}
+
+add_task(async function test_initialize() {
+ await task_populateDB(gTestData);
+});
+
+add_task(function pages_query() {
+ let [query, options] = newQueryWithOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ Assert.equal(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ Assert.equal(node.tags, "test-tag");
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ Assert.equal(node.tags, null);
+ }
+ });
+});
+
+add_task(function visits_query() {
+ let [query, options] = newQueryWithOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ Assert.equal(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ Assert.equal(node.tags, "test-tag");
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ Assert.equal(node.tags, null);
+ }
+ });
+});
+
+add_task(function bookmark_parent_query() {
+ let [query, options] = newQueryWithOptions();
+ query.setParents([PlacesUtils.bookmarks.unfiledGuid]);
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ Assert.equal(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ Assert.equal(node.tags, "test-tag");
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ Assert.equal(node.tags, null);
+ }
+ });
+});
+
+add_task(function history_query() {
+ let [query, options] = newQueryWithOptions();
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ Assert.equal(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ Assert.equal(node.tags, null);
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ Assert.equal(node.tags, null);
+ }
+ });
+});
diff --git a/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js b/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js
new file mode 100644
index 0000000000..ac3931892f
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that tags changes are correctly live-updated in a history
+// query.
+
+let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+}
+
+var gTestData = [
+ {
+ isVisit: true,
+ uri: "http://example.com/1/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ title: "title1",
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/2/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ title: "title2",
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/3/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ title: "title3",
+ },
+];
+
+function searchNodeHavingUrl(aRoot, aUrl) {
+ for (let i = 0; i < aRoot.childCount; i++) {
+ if (aRoot.getChild(i).uri == aUrl) {
+ return aRoot.getChild(i);
+ }
+ }
+ return undefined;
+}
+
+function newQueryWithOptions() {
+ return [
+ PlacesUtils.history.getNewQuery(),
+ PlacesUtils.history.getNewQueryOptions(),
+ ];
+}
+
+add_task(async function pages_query() {
+ await task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ Assert.equal(node.title, gTestData[i].title);
+ let uri = NetUtil.newURI(node.uri);
+ await PlacesTestUtils.addVisits({ uri, title: "changedTitle" });
+ Assert.equal(node.title, "changedTitle");
+ await PlacesTestUtils.addVisits({ uri, title: gTestData[i].title });
+ Assert.equal(node.title, gTestData[i].title);
+ }
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function visits_query() {
+ await task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+
+ for (let testData of gTestData) {
+ let uri = NetUtil.newURI(testData.uri);
+ let node = searchNodeHavingUrl(root, testData.uri);
+ Assert.equal(node.title, testData.title);
+ await PlacesTestUtils.addVisits({ uri, title: "changedTitle" });
+ node = searchNodeHavingUrl(root, testData.uri);
+ Assert.equal(node.title, "changedTitle");
+ await PlacesTestUtils.addVisits({ uri, title: testData.title });
+ node = searchNodeHavingUrl(root, testData.uri);
+ Assert.equal(node.title, testData.title);
+ }
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function pages_searchterm_query() {
+ await task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "example";
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ Assert.equal(node.title, gTestData[i].title);
+ await PlacesTestUtils.addVisits({ uri, title: "changedTitle" });
+ Assert.equal(node.title, "changedTitle");
+ await PlacesTestUtils.addVisits({ uri, title: gTestData[i].title });
+ Assert.equal(node.title, gTestData[i].title);
+ }
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function visits_searchterm_query() {
+ await task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "example";
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let testData of gTestData) {
+ let uri = NetUtil.newURI(testData.uri);
+ let node = searchNodeHavingUrl(root, testData.uri);
+ Assert.equal(node.title, testData.title);
+ await PlacesTestUtils.addVisits({ uri, title: "changedTitle" });
+ node = searchNodeHavingUrl(root, testData.uri);
+ Assert.equal(node.title, "changedTitle");
+ await PlacesTestUtils.addVisits({ uri, title: testData.title });
+ node = searchNodeHavingUrl(root, testData.uri);
+ Assert.equal(node.title, testData.title);
+ }
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function pages_searchterm_is_title_query() {
+ await task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "match";
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult([], root);
+ for (let data of gTestData) {
+ let uri = NetUtil.newURI(data.uri);
+ let origTitle = data.title;
+ data.title = "match";
+ await PlacesTestUtils.addVisits({
+ uri,
+ title: data.title,
+ visitDate: data.lastVisit,
+ });
+ compareArrayToResult([data], root);
+ data.title = origTitle;
+ await PlacesTestUtils.addVisits({
+ uri,
+ title: data.title,
+ visitDate: data.lastVisit,
+ });
+ compareArrayToResult([], root);
+ }
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function visits_searchterm_is_title_query() {
+ await task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "match";
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult([], root);
+ for (let data of gTestData) {
+ let uri = NetUtil.newURI(data.uri);
+ let origTitle = data.title;
+ data.title = "match";
+
+ info("Adding " + uri.spec);
+ await PlacesTestUtils.addVisits({
+ uri,
+ title: data.title,
+ visitDate: data.lastVisit,
+ });
+
+ compareArrayToResult([data], root);
+ data.title = origTitle;
+ info("Clobbering " + uri.spec);
+ await PlacesTestUtils.addVisits({
+ uri,
+ title: data.title,
+ visitDate: data.lastVisit,
+ });
+
+ compareArrayToResult([], root);
+ }
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+});
diff --git a/toolkit/components/places/tests/queries/test_options_inherit.js b/toolkit/components/places/tests/queries/test_options_inherit.js
new file mode 100644
index 0000000000..ae43350eda
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_options_inherit.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests inheritance of certain query options like:
+ * excludeItems, excludeQueries, expandQueries.
+ */
+
+"use strict";
+
+add_task(async function () {
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "query",
+ url:
+ "place:queryType=" +
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS,
+ },
+ { title: "bm", url: "http://example.com" },
+ {
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ },
+ ],
+ },
+ { title: "bm", url: "http://example.com" },
+ {
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ },
+ ],
+ });
+
+ await test_query({}, 3, 3, 2);
+ await test_query({ expandQueries: false }, 3, 3, 0);
+ await test_query({ excludeItems: true }, 1, 1, 0);
+ await test_query({ excludeItems: true, expandQueries: false }, 1, 1, 0);
+ await test_query({ excludeItems: true, excludeQueries: true }, 1, 0, 0);
+});
+
+async function test_query(
+ opts,
+ expectedRootCc,
+ expectedFolderCc,
+ expectedQueryCc
+) {
+ let query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.unfiledGuid]);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ for (const [o, v] of Object.entries(opts)) {
+ info(`Setting ${o} to ${v}`);
+ options[o] = v;
+ }
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ Assert.equal(root.childCount, expectedRootCc, "Checking root child count");
+ if (root.childCount > 0) {
+ let folder = root.getChild(0);
+ Assert.equal(folder.title, "folder", "Found the expected folder");
+
+ // Check the folder uri doesn't reflect the root options, since those
+ // options are inherited and not part of this node declaration.
+ checkURIOptions(folder.uri);
+
+ PlacesUtils.asContainer(folder).containerOpen = true;
+ Assert.equal(
+ folder.childCount,
+ expectedFolderCc,
+ "Checking folder child count"
+ );
+ if (folder.childCount) {
+ let placeQuery = folder.getChild(0);
+ PlacesUtils.asQuery(placeQuery).containerOpen = true;
+ Assert.equal(
+ placeQuery.childCount,
+ expectedQueryCc,
+ "Checking query child count"
+ );
+ placeQuery.containerOpen = false;
+ }
+ folder.containerOpen = false;
+ }
+ let f = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ checkURIOptions(root.getChild(root.childCount - 1).uri);
+ await PlacesUtils.bookmarks.remove(f);
+
+ root.containerOpen = false;
+}
+
+function checkURIOptions(uri) {
+ info("Checking options for uri " + uri);
+ let folderOptions = {};
+ PlacesUtils.history.queryStringToQuery(uri, {}, folderOptions);
+ folderOptions = folderOptions.value;
+ Assert.equal(
+ folderOptions.excludeItems,
+ false,
+ "ExcludeItems should not be changed"
+ );
+ Assert.equal(
+ folderOptions.excludeQueries,
+ false,
+ "ExcludeQueries should not be changed"
+ );
+ Assert.equal(
+ folderOptions.expandQueries,
+ true,
+ "ExpandQueries should not be changed"
+ );
+}
diff --git a/toolkit/components/places/tests/queries/test_queryMultipleFolder.js b/toolkit/components/places/tests/queries/test_queryMultipleFolder.js
new file mode 100644
index 0000000000..7c24bef74e
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_queryMultipleFolder.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var folderGuids = [];
+var bookmarkGuids = [];
+
+add_task(async function setup() {
+ // adding bookmarks in the folders
+ for (let i = 0; i < 3; ++i) {
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: `Folder${i}`,
+ });
+ folderGuids.push(folder.guid);
+
+ for (let j = 0; j < 7; ++j) {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuids[i],
+ url: `http://Bookmark${i}_${j}.com`,
+ title: "",
+ });
+ bookmarkGuids.push(bm.guid);
+ }
+ }
+});
+
+add_task(async function test_queryMultipleFolders_ids() {
+ // using queryStringToQuery
+ let query = {},
+ options = {};
+ let maxResults = 20;
+ let queryString = `place:${folderGuids
+ .map(guid => "parent=" + guid)
+ .join("&")}&sort=5&maxResults=${maxResults}`;
+ PlacesUtils.history.queryStringToQuery(queryString, query, options);
+ let rootNode = PlacesUtils.history.executeQuery(
+ query.value,
+ options.value
+ ).root;
+ rootNode.containerOpen = true;
+ let resultLength = rootNode.childCount;
+ Assert.equal(resultLength, maxResults);
+ for (let i = 0; i < resultLength; ++i) {
+ let node = rootNode.getChild(i);
+ Assert.equal(bookmarkGuids[i], node.bookmarkGuid, node.uri);
+ }
+ rootNode.containerOpen = false;
+
+ // using getNewQuery and getNewQueryOptions
+ query = PlacesUtils.history.getNewQuery();
+ options = PlacesUtils.history.getNewQueryOptions();
+ query.setParents(folderGuids);
+ options.sortingMode = options.SORT_BY_URI_ASCENDING;
+ options.maxResults = maxResults;
+ rootNode = PlacesUtils.history.executeQuery(query, options).root;
+ rootNode.containerOpen = true;
+ resultLength = rootNode.childCount;
+ Assert.equal(resultLength, maxResults);
+ for (let i = 0; i < resultLength; ++i) {
+ let node = rootNode.getChild(i);
+ Assert.equal(bookmarkGuids[i], node.bookmarkGuid, node.uri);
+ }
+ rootNode.containerOpen = false;
+});
+
+add_task(async function test_queryMultipleFolders_guids() {
+ // using queryStringToQuery
+ let query = {},
+ options = {};
+ let maxResults = 20;
+ let queryString = `place:${folderGuids
+ .map(guid => "parent=" + guid)
+ .join("&")}&sort=5&maxResults=${maxResults}`;
+ PlacesUtils.history.queryStringToQuery(queryString, query, options);
+ let rootNode = PlacesUtils.history.executeQuery(
+ query.value,
+ options.value
+ ).root;
+ rootNode.containerOpen = true;
+ let resultLength = rootNode.childCount;
+ Assert.equal(resultLength, maxResults);
+ for (let i = 0; i < resultLength; ++i) {
+ let node = rootNode.getChild(i);
+ Assert.equal(bookmarkGuids[i], node.bookmarkGuid, node.uri);
+ }
+ rootNode.containerOpen = false;
+
+ // using getNewQuery and getNewQueryOptions
+ query = PlacesUtils.history.getNewQuery();
+ options = PlacesUtils.history.getNewQueryOptions();
+ query.setParents(folderGuids);
+ options.sortingMode = options.SORT_BY_URI_ASCENDING;
+ options.maxResults = maxResults;
+ rootNode = PlacesUtils.history.executeQuery(query, options).root;
+ rootNode.containerOpen = true;
+ resultLength = rootNode.childCount;
+ Assert.equal(resultLength, maxResults);
+ for (let i = 0; i < resultLength; ++i) {
+ let node = rootNode.getChild(i);
+ Assert.equal(bookmarkGuids[i], node.bookmarkGuid, node.uri);
+ }
+ rootNode.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_querySerialization.js b/toolkit/components/places/tests/queries/test_querySerialization.js
new file mode 100644
index 0000000000..4c33854718
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_querySerialization.js
@@ -0,0 +1,718 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests Places query serialization. Associated bug is
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=370197
+ *
+ * The simple idea behind this test is to try out different combinations of
+ * query switches and ensure that queries are the same before serialization
+ * as they are after de-serialization.
+ *
+ * In the code below, "switch" refers to a query option -- "option" in a broad
+ * sense, not nsINavHistoryQueryOptions specifically (which is why we refer to
+ * them as switches, not options). Both nsINavHistoryQuery and
+ * nsINavHistoryQueryOptions allow you to specify switches that affect query
+ * strings. nsINavHistoryQuery instances have attributes hasBeginTime,
+ * hasEndTime, hasSearchTerms, and so on. nsINavHistoryQueryOptions instances
+ * have attributes sortingMode, resultType, excludeItems, etc.
+ *
+ * Ideally we would like to test all 2^N subsets of switches, where N is the
+ * total number of switches; switches might interact in erroneous or other ways
+ * we do not expect. However, since N is large (21 at this time), that's
+ * impractical for a single test in a suite.
+ *
+ * Instead we choose all possible subsets of a certain, smaller size. In fact
+ * we begin by choosing CHOOSE_HOW_MANY_SWITCHES_LO and ramp up to
+ * CHOOSE_HOW_MANY_SWITCHES_HI.
+ *
+ * There are two more wrinkles. First, for some switches we'd like to be able to
+ * test multiple values. For example, it seems like a good idea to test both an
+ * empty string and a non-empty string for switch nsINavHistoryQuery.searchTerms.
+ * When switches have more than one value for a test run, we use the Cartesian
+ * product of their values to generate all possible combinations of values.
+ *
+ * To summarize, here's how this test works:
+ *
+ * - For n = CHOOSE_HOW_MANY_SWITCHES_LO to CHOOSE_HOW_MANY_SWITCHES_HI:
+ * - From the total set of switches choose all possible subsets of size n.
+ * For each of those subsets s:
+ * - Collect the test runs of each switch in subset s and take their
+ * Cartesian product. For each sequence in the product:
+ * - Create nsINavHistoryQuery and nsINavHistoryQueryOptions objects
+ * with the chosen switches and test run values.
+ * - Serialize the query.
+ * - De-serialize and ensure that the de-serialized query objects equal
+ * the originals.
+ */
+
+const CHOOSE_HOW_MANY_SWITCHES_LO = 1;
+const CHOOSE_HOW_MANY_SWITCHES_HI = 2;
+
+// The switches are represented by objects below, in arrays querySwitches and
+// queryOptionSwitches. Use them to set up test runs.
+//
+// Some switches have special properties (where noted), but all switches must
+// have the following properties:
+//
+// matches: A function that takes two nsINavHistoryQuery objects (in the case
+// of nsINavHistoryQuery switches) or two nsINavHistoryQueryOptions
+// objects (for nsINavHistoryQueryOptions switches) and returns true
+// if the values of the switch in the two objects are equal. This is
+// the foundation of how we determine if two queries are equal.
+// runs: An array of functions. Each function takes an nsINavHistoryQuery
+// object and an nsINavHistoryQueryOptions object. The functions
+// should set the attributes of one of the two objects as appropriate
+// to their switches. This is how switch values are set for each test
+// run.
+//
+// The following properties are optional:
+//
+// desc: An informational string to print out during runs when the switch
+// is chosen. Hopefully helpful if the test fails.
+
+// nsINavHistoryQuery switches
+const querySwitches = [
+ // hasBeginTime
+ {
+ // flag and subswitches are used by the flagSwitchMatches function. Several
+ // of the nsINavHistoryQuery switches (like this one) are really guard flags
+ // that indicate if other "subswitches" are enabled.
+ flag: "hasBeginTime",
+ subswitches: ["beginTime", "beginTimeReference", "absoluteBeginTime"],
+ desc: "nsINavHistoryQuery.hasBeginTime",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.beginTime = Date.now() * 1000;
+ aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.beginTime = Date.now() * 1000;
+ aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY;
+ },
+ ],
+ },
+ // hasEndTime
+ {
+ flag: "hasEndTime",
+ subswitches: ["endTime", "endTimeReference", "absoluteEndTime"],
+ desc: "nsINavHistoryQuery.hasEndTime",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.endTime = Date.now() * 1000;
+ aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.endTime = Date.now() * 1000;
+ aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY;
+ },
+ ],
+ },
+ // hasSearchTerms
+ {
+ flag: "hasSearchTerms",
+ subswitches: ["searchTerms"],
+ desc: "nsINavHistoryQuery.hasSearchTerms",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.searchTerms = "shrimp and white wine";
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.searchTerms = "";
+ },
+ ],
+ },
+ // hasDomain
+ {
+ flag: "hasDomain",
+ subswitches: ["domain", "domainIsHost"],
+ desc: "nsINavHistoryQuery.hasDomain",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "mozilla.com";
+ aQuery.domainIsHost = false;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "www.mozilla.com";
+ aQuery.domainIsHost = true;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "";
+ },
+ ],
+ },
+ // hasUri
+ {
+ flag: "hasUri",
+ subswitches: ["uri"],
+ desc: "nsINavHistoryQuery.hasUri",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.uri = uri("http://mozilla.com");
+ },
+ ],
+ },
+ // minVisits
+ {
+ // property is used by function simplePropertyMatches.
+ property: "minVisits",
+ desc: "nsINavHistoryQuery.minVisits",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.minVisits = 0x7fffffff; // 2^31 - 1
+ },
+ ],
+ },
+ // maxVisits
+ {
+ property: "maxVisits",
+ desc: "nsINavHistoryQuery.maxVisits",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.maxVisits = 0x7fffffff; // 2^31 - 1
+ },
+ ],
+ },
+ // getFolders
+ {
+ desc: "nsINavHistoryQuery.getParents",
+ matches(aQuery1, aQuery2) {
+ var q1Parents = aQuery1.getParents();
+ var q2Parents = aQuery2.getParents();
+ if (q1Parents.length !== q2Parents.length) {
+ return false;
+ }
+ for (let i = 0; i < q1Parents.length; i++) {
+ if (!q2Parents.includes(q1Parents[i])) {
+ return false;
+ }
+ }
+ for (let i = 0; i < q2Parents.length; i++) {
+ if (!q1Parents.includes(q2Parents[i])) {
+ return false;
+ }
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.setParents([]);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setParents([PlacesUtils.bookmarks.rootGuid]);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setParents([
+ PlacesUtils.bookmarks.rootGuid,
+ PlacesUtils.bookmarks.tagsGuid,
+ ]);
+ },
+ ],
+ },
+ // tags
+ {
+ desc: "nsINavHistoryQuery.getTags",
+ matches(aQuery1, aQuery2) {
+ if (aQuery1.tagsAreNot !== aQuery2.tagsAreNot) {
+ return false;
+ }
+ var q1Tags = aQuery1.tags;
+ var q2Tags = aQuery2.tags;
+ if (q1Tags.length !== q2Tags.length) {
+ return false;
+ }
+ for (let i = 0; i < q1Tags.length; i++) {
+ if (!q2Tags.includes(q1Tags[i])) {
+ return false;
+ }
+ }
+ for (let i = 0; i < q2Tags.length; i++) {
+ if (!q1Tags.includes(q2Tags[i])) {
+ return false;
+ }
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [""];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ];
+ aQuery.tagsAreNot = true;
+ },
+ ],
+ },
+ // transitions
+ {
+ desc: "tests nsINavHistoryQuery.getTransitions",
+ matches(aQuery1, aQuery2) {
+ var q1Trans = aQuery1.getTransitions();
+ var q2Trans = aQuery2.getTransitions();
+ if (q1Trans.length !== q2Trans.length) {
+ return false;
+ }
+ for (let i = 0; i < q1Trans.length; i++) {
+ if (!q2Trans.includes(q1Trans[i])) {
+ return false;
+ }
+ }
+ for (let i = 0; i < q2Trans.length; i++) {
+ if (!q1Trans.includes(q2Trans[i])) {
+ return false;
+ }
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([]);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD]);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([
+ Ci.nsINavHistoryService.TRANSITION_TYPED,
+ Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ ]);
+ },
+ ],
+ },
+];
+
+// nsINavHistoryQueryOptions switches
+const queryOptionSwitches = [
+ // sortingMode
+ {
+ desc: "nsINavHistoryQueryOptions.sortingMode",
+ matches(aOptions1, aOptions2) {
+ if (aOptions1.sortingMode === aOptions2.sortingMode) {
+ return true;
+ }
+ return false;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.sortingMode = aQueryOptions.SORT_BY_DATE_ASCENDING;
+ },
+ ],
+ },
+ // resultType
+ {
+ // property is used by function simplePropertyMatches.
+ property: "resultType",
+ desc: "nsINavHistoryQueryOptions.resultType",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.resultType = aQueryOptions.RESULTS_AS_URI;
+ },
+ ],
+ },
+ // excludeItems
+ {
+ property: "excludeItems",
+ desc: "nsINavHistoryQueryOptions.excludeItems",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.excludeItems = true;
+ },
+ ],
+ },
+ // excludeQueries
+ {
+ property: "excludeQueries",
+ desc: "nsINavHistoryQueryOptions.excludeQueries",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.excludeQueries = true;
+ },
+ ],
+ },
+ // expandQueries
+ {
+ property: "expandQueries",
+ desc: "nsINavHistoryQueryOptions.expandQueries",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.expandQueries = true;
+ },
+ ],
+ },
+ // includeHidden
+ {
+ property: "includeHidden",
+ desc: "nsINavHistoryQueryOptions.includeHidden",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.includeHidden = true;
+ },
+ ],
+ },
+ // maxResults
+ {
+ property: "maxResults",
+ desc: "nsINavHistoryQueryOptions.maxResults",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.maxResults = 0xffffffff; // 2^32 - 1
+ },
+ ],
+ },
+ // queryType
+ {
+ property: "queryType",
+ desc: "nsINavHistoryQueryOptions.queryType",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_HISTORY;
+ },
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_BOOKMARKS;
+ },
+ ],
+ },
+];
+
+/**
+ * Enumerates all the sequences of the cartesian product of the arrays contained
+ * in aSequences. Examples:
+ *
+ * cartProd([[1, 2, 3], ["a", "b"]], callback);
+ * // callback is called 3 * 2 = 6 times with the following arrays:
+ * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
+ *
+ * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
+ * // callback is called 1 * 3 * 2 = 6 times with the following arrays:
+ * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
+ * // ["a", 3, "X"], ["a", 3, "Y"]
+ *
+ * cartProd([[1], [2], [3], [4]], callback);
+ * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
+ * // [1, 2, 3, 4]
+ *
+ * cartProd([], callback);
+ * // callback is 0 times
+ *
+ * cartProd([[1, 2, 3, 4]], callback);
+ * // callback is called 4 times with the following arrays:
+ * // [1], [2], [3], [4]
+ *
+ * @param aSequences
+ * an array that contains an arbitrary number of arrays
+ * @param aCallback
+ * a function that is passed each sequence of the product as it's
+ * computed
+ * @return the total number of sequences in the product
+ */
+function cartProd(aSequences, aCallback) {
+ if (aSequences.length === 0) {
+ return 0;
+ }
+
+ // For each sequence in aSequences, we maintain a pointer (an array index,
+ // really) to the element we're currently enumerating in that sequence
+ var seqEltPtrs = aSequences.map(i => 0);
+
+ var numProds = 0;
+ var done = false;
+ while (!done) {
+ numProds++;
+
+ // prod = sequence in product we're currently enumerating
+ let prod = [];
+ for (let i = 0; i < aSequences.length; i++) {
+ prod.push(aSequences[i][seqEltPtrs[i]]);
+ }
+ aCallback(prod);
+
+ // The next sequence in the product differs from the current one by just a
+ // single element. Determine which element that is. We advance the
+ // "rightmost" element pointer to the "right" by one. If we move past the
+ // end of that pointer's sequence, reset the pointer to the first element
+ // in its sequence and then try the sequence to the "left", and so on.
+
+ // seqPtr = index of rightmost input sequence whose element pointer is not
+ // past the end of the sequence
+ let seqPtr = aSequences.length - 1;
+ while (!done) {
+ // Advance the rightmost element pointer.
+ seqEltPtrs[seqPtr]++;
+
+ // The rightmost element pointer is past the end of its sequence.
+ if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
+ seqEltPtrs[seqPtr] = 0;
+ seqPtr--;
+
+ // All element pointers are past the ends of their sequences.
+ if (seqPtr < 0) {
+ done = true;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ return numProds;
+}
+
+/**
+ * Enumerates all the subsets in aSet of size aHowMany. There are
+ * C(aSet.length, aHowMany) such subsets. aCallback will be passed each subset
+ * as it is generated. Note that aSet and the subsets enumerated are -- even
+ * though they're arrays -- not sequences; the ordering of their elements is not
+ * important. Example:
+ *
+ * choose([1, 2, 3, 4], 2, callback);
+ * // callback is called C(4, 2) = 6 times with the following sets (arrays):
+ * // [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]
+ *
+ * @param aSet
+ * an array from which to choose elements, aSet.length > 0
+ * @param aHowMany
+ * the number of elements to choose, > 0 and <= aSet.length
+ * @return the total number of sets chosen
+ */
+function choose(aSet, aHowMany, aCallback) {
+ // ptrs = indices of the elements in aSet we're currently choosing
+ var ptrs = [];
+ for (let i = 0; i < aHowMany; i++) {
+ ptrs.push(i);
+ }
+
+ var numFound = 0;
+ var done = false;
+ while (!done) {
+ numFound++;
+ aCallback(ptrs.map(p => aSet[p]));
+
+ // The next subset to be chosen differs from the current one by just a
+ // single element. Determine which element that is. Advance the "rightmost"
+ // pointer to the "right" by one. If we move past the end of set, move the
+ // next non-adjacent rightmost pointer to the right by one, and reset all
+ // succeeding pointers so that they're adjacent to it. When all pointers
+ // are clustered all the way to the right, we're done.
+
+ // Advance the rightmost pointer.
+ ptrs[ptrs.length - 1]++;
+
+ // The rightmost pointer has gone past the end of set.
+ if (ptrs[ptrs.length - 1] >= aSet.length) {
+ // Find the next rightmost pointer that is not adjacent to the current one.
+ let si = aSet.length - 2; // aSet index
+ let pi = ptrs.length - 2; // ptrs index
+ while (pi >= 0 && ptrs[pi] === si) {
+ pi--;
+ si--;
+ }
+
+ // All pointers are adjacent and clustered all the way to the right.
+ if (pi < 0) {
+ done = true;
+ } else {
+ // pi = index of rightmost pointer with a gap between it and its
+ // succeeding pointer. Move it right and reset all succeeding pointers
+ // so that they're adjacent to it.
+ ptrs[pi]++;
+ for (let i = 0; i < ptrs.length - pi - 1; i++) {
+ ptrs[i + pi + 1] = ptrs[pi] + i + 1;
+ }
+ }
+ }
+ }
+ return numFound;
+}
+
+/**
+ * Convenience function for nsINavHistoryQuery switches that act as flags. This
+ * is attached to switch objects. See querySwitches array above.
+ *
+ * @param aQuery1
+ * an nsINavHistoryQuery object
+ * @param aQuery2
+ * another nsINavHistoryQuery object
+ * @return true if this switch is the same in both aQuery1 and aQuery2
+ */
+function flagSwitchMatches(aQuery1, aQuery2) {
+ if (aQuery1[this.flag] && aQuery2[this.flag]) {
+ for (let p in this.subswitches) {
+ if (p in aQuery1 && p in aQuery2) {
+ if (aQuery1[p] instanceof Ci.nsIURI) {
+ if (!aQuery1[p].equals(aQuery2[p])) {
+ return false;
+ }
+ } else if (aQuery1[p] !== aQuery2[p]) {
+ return false;
+ }
+ }
+ }
+ } else if (aQuery1[this.flag] || aQuery2[this.flag]) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Tests if aObj1 and aObj2 are equal. This function is general and may be used
+ * for either nsINavHistoryQuery or nsINavHistoryQueryOptions objects. aSwitches
+ * determines which set of switches is used for comparison. Pass in either
+ * querySwitches or queryOptionSwitches.
+ *
+ * @param aSwitches
+ * determines which set of switches applies to aObj1 and aObj2, either
+ * querySwitches or queryOptionSwitches
+ * @param aObj1
+ * an nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @param aObj2
+ * another nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @return true if aObj1 and aObj2 are equal
+ */
+function queryObjsEqual(aSwitches, aObj1, aObj2) {
+ for (let i = 0; i < aSwitches.length; i++) {
+ if (!aSwitches[i].matches(aObj1, aObj2)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * This drives the test runs. See the comment at the top of this file.
+ *
+ * @param aHowManyLo
+ * the size of the switch subsets to start with
+ * @param aHowManyHi
+ * the size of the switch subsets to end with (inclusive)
+ */
+function runQuerySequences(aHowManyLo, aHowManyHi) {
+ var allSwitches = querySwitches.concat(queryOptionSwitches);
+
+ // Choose aHowManyLo switches up to aHowManyHi switches.
+ for (let howMany = aHowManyLo; howMany <= aHowManyHi; howMany++) {
+ let numIters = 0;
+ print("CHOOSING " + howMany + " SWITCHES");
+
+ // Choose all subsets of size howMany from allSwitches.
+ choose(allSwitches, howMany, function (chosenSwitches) {
+ print(numIters);
+ numIters++;
+
+ // Collect the runs.
+ // runs = [ [runs from switch 1], ..., [runs from switch howMany] ]
+ var runs = chosenSwitches.map(function (s) {
+ if (s.desc) {
+ print(" " + s.desc);
+ }
+ return s.runs;
+ });
+
+ // cartProd(runs) => [
+ // [switch 1 run 1, switch 2 run 1, ..., switch howMany run 1 ],
+ // ...,
+ // [switch 1 run 1, switch 2 run 1, ..., switch howMany run N ],
+ // ..., ...,
+ // [switch 1 run N, switch 2 run N, ..., switch howMany run 1 ],
+ // ...,
+ // [switch 1 run N, switch 2 run N, ..., switch howMany run N ],
+ // ]
+ cartProd(runs, function (runSet) {
+ // Create a new query, apply the switches in runSet, and test it.
+ var query = PlacesUtils.history.getNewQuery();
+ var opts = PlacesUtils.history.getNewQueryOptions();
+ for (let i = 0; i < runSet.length; i++) {
+ runSet[i](query, opts);
+ }
+ serializeDeserialize(query, opts);
+ });
+ });
+ }
+ print("\n");
+}
+
+/**
+ * Serializes the nsINavHistoryQuery objects in aQuery and the
+ * nsINavHistoryQueryOptions object aQueryOptions, de-serializes the
+ * serialization, and ensures (using do_check_* functions) that the
+ * de-serialized objects equal the originals.
+ *
+ * @param aQuery
+ * an nsINavHistoryQuery object
+ * @param aQueryOptions
+ * an nsINavHistoryQueryOptions object
+ */
+function serializeDeserialize(aQuery, aQueryOptions) {
+ let queryStr = PlacesUtils.history.queryToQueryString(aQuery, aQueryOptions);
+ print(" " + queryStr);
+ let query2 = {},
+ opts2 = {};
+ PlacesUtils.history.queryStringToQuery(queryStr, query2, opts2);
+ query2 = query2.value;
+ opts2 = opts2.value;
+
+ Assert.ok(queryObjsEqual(querySwitches, aQuery, query2));
+
+ // Finally check the query options objects.
+ Assert.ok(queryObjsEqual(queryOptionSwitches, aQueryOptions, opts2));
+}
+
+/**
+ * Convenience function for switches that have simple values. This is attached
+ * to switch objects. See querySwitches and queryOptionSwitches arrays above.
+ *
+ * @param aObj1
+ * an nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @param aObj2
+ * another nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @return true if this switch is the same in both aObj1 and aObj2
+ */
+function simplePropertyMatches(aObj1, aObj2) {
+ return aObj1[this.property] === aObj2[this.property];
+}
+
+function run_test() {
+ runQuerySequences(CHOOSE_HOW_MANY_SWITCHES_LO, CHOOSE_HOW_MANY_SWITCHES_HI);
+}
diff --git a/toolkit/components/places/tests/queries/test_query_uri_liveupdate.js b/toolkit/components/places/tests/queries/test_query_uri_liveupdate.js
new file mode 100644
index 0000000000..5ada4a84d4
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_query_uri_liveupdate.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_results_as_tag_query() {
+ let bms = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ { url: "http://tag1.moz.com/", tags: ["tag1"] },
+ { url: "http://tag2.moz.com/", tags: ["tag2"] },
+ { url: "place:tag=tag1" },
+ ],
+ });
+
+ let root = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid,
+ false,
+ true
+ ).root;
+ Assert.equal(root.childCount, 3, "We should get 3 results");
+ let queryRoot = root.getChild(2);
+ PlacesUtils.asContainer(queryRoot).containerOpen = true;
+
+ Assert.equal(queryRoot.uri, "place:tag=tag1", "Found the query");
+ Assert.equal(queryRoot.childCount, 1, "We should get 1 result");
+ Assert.equal(
+ queryRoot.getChild(0).uri,
+ "http://tag1.moz.com/",
+ "Found the tagged bookmark"
+ );
+
+ await PlacesUtils.bookmarks.update({
+ guid: bms[2].guid,
+ url: "place:tag=tag2",
+ });
+ Assert.equal(queryRoot.uri, "place:tag=tag2", "Found the query");
+ Assert.equal(queryRoot.childCount, 1, "We should get 1 result");
+ Assert.equal(
+ queryRoot.getChild(0).uri,
+ "http://tag2.moz.com/",
+ "Found the tagged bookmark"
+ );
+
+ queryRoot.containerOpen = false;
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_redirects.js b/toolkit/components/places/tests/queries/test_redirects.js
new file mode 100644
index 0000000000..b0e7c9b421
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_redirects.js
@@ -0,0 +1,351 @@
+/* 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/. */
+
+// Array of visits we will add to the database, will be populated later
+// in the test.
+var visits = [];
+
+/**
+ * Takes a sequence of query options, and compare query results obtained through
+ * them with a custom filtered array of visits, based on the values we are
+ * expecting from the query.
+ *
+ * @param aSequence
+ * an array that contains query options in the form:
+ * [includeHidden, maxResults, sortingMode]
+ */
+function check_results_callback(aSequence) {
+ // Sanity check: we should receive 3 parameters.
+ Assert.equal(aSequence.length, 3);
+ let includeHidden = aSequence[0];
+ let maxResults = aSequence[1];
+ let sortingMode = aSequence[2];
+ info(" - - - ");
+ info(
+ "TESTING: includeHidden(" +
+ includeHidden +
+ ")," +
+ " maxResults(" +
+ maxResults +
+ ")," +
+ " sortingMode(" +
+ sortingMode +
+ ")."
+ );
+
+ function isHidden(aVisit) {
+ return (
+ aVisit.transType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK ||
+ aVisit.isRedirect
+ );
+ }
+
+ // Build expectedData array.
+ let expectedData = visits.filter(function (aVisit, aIndex, aArray) {
+ // Embed visits never appear in results.
+ if (aVisit.transType == Ci.nsINavHistoryService.TRANSITION_EMBED) {
+ return false;
+ }
+
+ if (!includeHidden && isHidden(aVisit)) {
+ // If the page has any non-hidden visit, then it's visible.
+ if (
+ !visits.filter(function (refVisit) {
+ return refVisit.uri == aVisit.uri && !isHidden(refVisit);
+ }).length
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ // Remove duplicates, since queries are RESULTS_AS_URI (unique pages).
+ let seen = [];
+ expectedData = expectedData.filter(function (aData) {
+ if (seen.includes(aData.uri)) {
+ return false;
+ }
+ seen.push(aData.uri);
+ return true;
+ });
+
+ // Sort expectedData.
+ function getFirstIndexFor(aEntry) {
+ for (let i = 0; i < visits.length; i++) {
+ if (visits[i].uri == aEntry.uri) {
+ return i;
+ }
+ }
+ return undefined;
+ }
+ function comparator(a, b) {
+ if (sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING) {
+ return b.lastVisit - a.lastVisit;
+ }
+ if (
+ sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING
+ ) {
+ return b.visitCount - a.visitCount;
+ }
+ return getFirstIndexFor(a) - getFirstIndexFor(b);
+ }
+ expectedData.sort(comparator);
+
+ // Crop results to maxResults if it's defined.
+ if (maxResults) {
+ expectedData = expectedData.slice(0, maxResults);
+ }
+
+ // Create a new query with required options.
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.includeHidden = includeHidden;
+ options.sortingMode = sortingMode;
+ if (maxResults) {
+ options.maxResults = maxResults;
+ }
+
+ // Compare resultset with expectedData.
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(expectedData, root);
+ root.containerOpen = false;
+}
+
+/**
+ * Enumerates all the sequences of the cartesian product of the arrays contained
+ * in aSequences. Examples:
+ *
+ * cartProd([[1, 2, 3], ["a", "b"]], callback);
+ * // callback is called 3 * 2 = 6 times with the following arrays:
+ * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
+ *
+ * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
+ * // callback is called 1 * 3 * 2 = 6 times with the following arrays:
+ * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
+ * // ["a", 3, "X"], ["a", 3, "Y"]
+ *
+ * cartProd([[1], [2], [3], [4]], callback);
+ * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
+ * // [1, 2, 3, 4]
+ *
+ * cartProd([], callback);
+ * // callback is 0 times
+ *
+ * cartProd([[1, 2, 3, 4]], callback);
+ * // callback is called 4 times with the following arrays:
+ * // [1], [2], [3], [4]
+ *
+ * @param aSequences
+ * an array that contains an arbitrary number of arrays
+ * @param aCallback
+ * a function that is passed each sequence of the product as it's
+ * computed
+ * @return the total number of sequences in the product
+ */
+function cartProd(aSequences, aCallback) {
+ if (aSequences.length === 0) {
+ return 0;
+ }
+
+ // For each sequence in aSequences, we maintain a pointer (an array index,
+ // really) to the element we're currently enumerating in that sequence
+ let seqEltPtrs = aSequences.map(i => 0);
+
+ let numProds = 0;
+ let done = false;
+ while (!done) {
+ numProds++;
+
+ // prod = sequence in product we're currently enumerating
+ let prod = [];
+ for (let i = 0; i < aSequences.length; i++) {
+ prod.push(aSequences[i][seqEltPtrs[i]]);
+ }
+ aCallback(prod);
+
+ // The next sequence in the product differs from the current one by just a
+ // single element. Determine which element that is. We advance the
+ // "rightmost" element pointer to the "right" by one. If we move past the
+ // end of that pointer's sequence, reset the pointer to the first element
+ // in its sequence and then try the sequence to the "left", and so on.
+
+ // seqPtr = index of rightmost input sequence whose element pointer is not
+ // past the end of the sequence
+ let seqPtr = aSequences.length - 1;
+ while (!done) {
+ // Advance the rightmost element pointer.
+ seqEltPtrs[seqPtr]++;
+
+ // The rightmost element pointer is past the end of its sequence.
+ if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
+ seqEltPtrs[seqPtr] = 0;
+ seqPtr--;
+
+ // All element pointers are past the ends of their sequences.
+ if (seqPtr < 0) {
+ done = true;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ return numProds;
+}
+
+/**
+ * Populate the visits array and add visits to the database.
+ * We will generate visit-chains like:
+ * visit -> redirect_temp -> redirect_perm
+ */
+add_task(async function test_add_visits_to_database() {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // We don't really bother on this, but we need a time to add visits.
+ let timeInMicroseconds = Date.now() * 1000;
+ let visitCount = 1;
+
+ // Array of all possible transition types we could be redirected from.
+ let t = [
+ Ci.nsINavHistoryService.TRANSITION_LINK,
+ Ci.nsINavHistoryService.TRANSITION_TYPED,
+ Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ // Embed visits are not added to the database and we don't want redirects
+ // to them, thus just avoid addition.
+ // Ci.nsINavHistoryService.TRANSITION_EMBED,
+ Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
+ // Would make hard sorting by visit date because last_visit_date is actually
+ // calculated excluding download transitions, but the query includes
+ // downloads.
+ // Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ ];
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds - 1000;
+ return timeInMicroseconds;
+ }
+
+ // we add a visit for each of the above transition types.
+ t.forEach(transition =>
+ visits.push({
+ isVisit: true,
+ transType: transition,
+ uri: "http://" + transition + ".example.com/",
+ title: transition + "-example",
+ isRedirect: true,
+ lastVisit: newTimeInMicroseconds(),
+ visitCount:
+ transition == Ci.nsINavHistoryService.TRANSITION_EMBED ||
+ transition == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK
+ ? 0
+ : visitCount++,
+ isInQuery: true,
+ })
+ );
+
+ // Add a REDIRECT_TEMPORARY layer of visits for each of the above visits.
+ t.forEach(transition =>
+ visits.push({
+ isVisit: true,
+ transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY,
+ uri: "http://" + transition + ".redirect.temp.example.com/",
+ title: transition + "-redirect-temp-example",
+ lastVisit: newTimeInMicroseconds(),
+ isRedirect: true,
+ referrer: "http://" + transition + ".example.com/",
+ visitCount: visitCount++,
+ isInQuery: true,
+ })
+ );
+
+ // Add a REDIRECT_PERMANENT layer of visits for each of the above redirects.
+ t.forEach(transition =>
+ visits.push({
+ isVisit: true,
+ transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
+ uri: "http://" + transition + ".redirect.perm.example.com/",
+ title: transition + "-redirect-perm-example",
+ lastVisit: newTimeInMicroseconds(),
+ isRedirect: true,
+ referrer: "http://" + transition + ".redirect.temp.example.com/",
+ visitCount: visitCount++,
+ isInQuery: true,
+ })
+ );
+
+ // Add a REDIRECT_PERMANENT layer of visits that loop to the first visit.
+ // These entries should not change visitCount or lastVisit, otherwise
+ // guessing an order would be a nightmare.
+ function getLastValue(aURI, aProperty) {
+ for (let i = 0; i < visits.length; i++) {
+ if (visits[i].uri == aURI) {
+ return visits[i][aProperty];
+ }
+ }
+ do_throw("Unknown uri.");
+ return null;
+ }
+ t.forEach(transition =>
+ visits.push({
+ isVisit: true,
+ transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
+ uri: "http://" + transition + ".example.com/",
+ title: getLastValue("http://" + transition + ".example.com/", "title"),
+ lastVisit: getLastValue(
+ "http://" + transition + ".example.com/",
+ "lastVisit"
+ ),
+ isRedirect: true,
+ referrer: "http://" + transition + ".redirect.perm.example.com/",
+ visitCount: getLastValue(
+ "http://" + transition + ".example.com/",
+ "visitCount"
+ ),
+ isInQuery: true,
+ })
+ );
+
+ // Add an unvisited bookmark in the database, it should never appear.
+ visits.push({
+ isBookmark: true,
+ uri: "http://unvisited.bookmark.com/",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "Unvisited Bookmark",
+ isInQuery: false,
+ });
+
+ // Put visits in the database.
+ await task_populateDB(visits);
+});
+
+add_task(async function test_redirects() {
+ // Frecency and hidden are updated asynchronously, wait for them.
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ // This array will be used by cartProd to generate a matrix of all possible
+ // combinations.
+ let includeHidden_options = [true, false];
+ let maxResults_options = [5, 10, 20, null];
+ // These sortingMode are choosen to toggle using special queries for history
+ // menu.
+ let sorting_options = [
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING,
+ ];
+ // Will execute check_results_callback() for each generated combination.
+ cartProd(
+ [includeHidden_options, maxResults_options, sorting_options],
+ check_results_callback
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await PlacesUtils.history.clear();
+});
diff --git a/toolkit/components/places/tests/queries/test_result_observeHistoryDetails.js b/toolkit/components/places/tests/queries/test_result_observeHistoryDetails.js
new file mode 100644
index 0000000000..83531ee2c4
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_result_observeHistoryDetails.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that skipHistoryDetailsNotifications works as expected.
+
+function accumulateNotifications(
+ result,
+ skipHistoryDetailsNotifications = false
+) {
+ let notifications = [];
+ let resultObserver = new Proxy(NavHistoryResultObserver, {
+ get(target, name) {
+ if (name == "check") {
+ result.removeObserver(resultObserver, false);
+ return expectedNotifications =>
+ Assert.deepEqual(notifications, expectedNotifications);
+ }
+ if (name == "skipHistoryDetailsNotifications") {
+ return skipHistoryDetailsNotifications;
+ }
+ // ignore a few uninteresting notifications.
+ if (["QueryInterface", "containerStateChanged"].includes(name)) {
+ return () => {};
+ }
+ return () => {
+ notifications.push(name);
+ };
+ },
+ });
+ result.addObserver(resultObserver, false);
+ return resultObserver;
+}
+
+add_task(async function test_history_query_observe() {
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let notifications = accumulateNotifications(result);
+ let root = PlacesUtils.asContainer(result.root);
+ root.containerOpen = true;
+
+ await PlacesTestUtils.addVisits({
+ uri: "http://mozilla.org",
+ title: "test",
+ });
+ notifications.check([
+ "nodeHistoryDetailsChanged",
+ "nodeInserted",
+ "nodeTitleChanged",
+ ]);
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_history_query_no_observe() {
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let result = PlacesUtils.history.executeQuery(query, options);
+ // Even if we opt-out of notifications, this is an history query, thus the
+ // setting is pretty much ignored.
+ let notifications = accumulateNotifications(result, true);
+ let root = PlacesUtils.asContainer(result.root);
+ root.containerOpen = true;
+
+ await PlacesTestUtils.addVisits({
+ uri: "http://mozilla.org",
+ title: "test",
+ });
+ await PlacesTestUtils.addVisits({
+ uri: "http://mozilla2.org",
+ title: "test",
+ });
+
+ notifications.check([
+ "nodeHistoryDetailsChanged",
+ "nodeInserted",
+ "nodeTitleChanged",
+ "nodeHistoryDetailsChanged",
+ "nodeInserted",
+ "nodeTitleChanged",
+ ]);
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_bookmarks_query_observe() {
+ let query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let notifications = accumulateNotifications(result);
+ let root = PlacesUtils.asContainer(result.root);
+ root.containerOpen = true;
+
+ await PlacesUtils.bookmarks.insert({
+ url: "http://mozilla.org",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "test",
+ });
+ await PlacesTestUtils.addVisits({
+ uri: "http://mozilla.org",
+ title: "title",
+ });
+
+ notifications.check([
+ "nodeHistoryDetailsChanged",
+ "nodeInserted",
+ "nodeHistoryDetailsChanged",
+ ]);
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_bookmarks_query_no_observe() {
+ let query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let notifications = accumulateNotifications(result, true);
+ let root = PlacesUtils.asContainer(result.root);
+ root.containerOpen = true;
+
+ await PlacesUtils.bookmarks.insert({
+ url: "http://mozilla.org",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "test",
+ });
+ await PlacesTestUtils.addVisits({
+ uri: "http://mozilla.org",
+ title: "title",
+ });
+
+ notifications.check(["nodeInserted"]);
+
+ info("Change the sorting mode to one that is based on history");
+ notifications = accumulateNotifications(result, true);
+ result.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
+ notifications.check(["invalidateContainer"]);
+
+ notifications = accumulateNotifications(result, true);
+ await PlacesTestUtils.addVisits({
+ uri: "http://mozilla.org",
+ title: "title",
+ });
+ notifications.check(["nodeHistoryDetailsChanged"]);
+
+ root.containerOpen = false;
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/queries/test_results-as-left-pane.js b/toolkit/components/places/tests/queries/test_results-as-left-pane.js
new file mode 100644
index 0000000000..6cec733758
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_results-as-left-pane.js
@@ -0,0 +1,83 @@
+"use strict";
+
+const expectedRoots = [
+ {
+ title: "OrganizerQueryHistory",
+ uri: `place:sort=${Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING}&type=${Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY}`,
+ guid: "history____v",
+ },
+ {
+ title: "OrganizerQueryDownloads",
+ uri: `place:transition=${Ci.nsINavHistoryService.TRANSITION_DOWNLOAD}&sort=${Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING}`,
+ guid: "downloads__v",
+ },
+ {
+ title: "TagsFolderTitle",
+ uri: `place:sort=${Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING}&type=${Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT}`,
+ guid: "tags_______v",
+ },
+ {
+ title: "OrganizerQueryAllBookmarks",
+ uri: `place:type=${Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY}`,
+ guid: "allbms_____v",
+ },
+];
+
+const placesStrings = Services.strings.createBundle(
+ "chrome://places/locale/places.properties"
+);
+
+function getLeftPaneQuery() {
+ var query = PlacesUtils.history.getNewQuery();
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_LEFT_PANE_QUERY;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ return result.root;
+}
+
+function assertExpectedChildren(root, expectedChildren) {
+ Assert.equal(
+ root.childCount,
+ expectedChildren.length,
+ "Should have the expected number of children."
+ );
+
+ for (let i = 0; i < root.childCount; i++) {
+ Assert.ok(
+ PlacesTestUtils.ComparePlacesURIs(
+ root.getChild(i).uri,
+ expectedChildren[i].uri
+ ),
+ "Should have the correct uri for root ${i}"
+ );
+ Assert.equal(
+ root.getChild(i).title,
+ placesStrings.GetStringFromName(expectedChildren[i].title),
+ "Should have the correct title for root ${i}"
+ );
+ Assert.equal(root.getChild(i).bookmarkGuid, expectedChildren[i].guid);
+ }
+}
+
+/**
+ * This test will test the basic RESULTS_AS_ROOTS_QUERY, that simply returns,
+ * the existing bookmark roots.
+ */
+add_task(async function test_results_as_root() {
+ let root = getLeftPaneQuery();
+ root.containerOpen = true;
+
+ Assert.equal(
+ PlacesUtils.asQuery(root).queryOptions.queryType,
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS,
+ "Should have a query type of QUERY_TYPE_BOOKMARKS"
+ );
+
+ assertExpectedChildren(root, expectedRoots);
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_results-as-roots.js b/toolkit/components/places/tests/queries/test_results-as-roots.js
new file mode 100644
index 0000000000..2f082d3e0b
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_results-as-roots.js
@@ -0,0 +1,114 @@
+"use strict";
+
+const MOBILE_BOOKMARKS_PREF = "browser.bookmarks.showMobileBookmarks";
+
+const expectedRoots = [
+ {
+ title: "BookmarksToolbarFolderTitle",
+ uri: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`,
+ guid: PlacesUtils.bookmarks.virtualToolbarGuid,
+ },
+ {
+ title: "BookmarksMenuFolderTitle",
+ uri: `place:parent=${PlacesUtils.bookmarks.menuGuid}`,
+ guid: PlacesUtils.bookmarks.virtualMenuGuid,
+ },
+ {
+ title: "OtherBookmarksFolderTitle",
+ uri: `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`,
+ guid: PlacesUtils.bookmarks.virtualUnfiledGuid,
+ },
+];
+
+const expectedRootsWithMobile = [
+ ...expectedRoots,
+ {
+ title: "MobileBookmarksFolderTitle",
+ uri: `place:parent=${PlacesUtils.bookmarks.mobileGuid}`,
+ guid: PlacesUtils.bookmarks.virtualMobileGuid,
+ },
+];
+
+const placesStrings = Services.strings.createBundle(
+ "chrome://places/locale/places.properties"
+);
+
+function getAllBookmarksQuery() {
+ var query = PlacesUtils.history.getNewQuery();
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_ASCENDING;
+ options.resultType = options.RESULTS_AS_ROOTS_QUERY;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ return result.root;
+}
+
+function assertExpectedChildren(root, expectedChildren) {
+ Assert.equal(
+ root.childCount,
+ expectedChildren.length,
+ "Should have the expected number of children."
+ );
+
+ for (let i = 0; i < root.childCount; i++) {
+ Assert.equal(
+ root.getChild(i).uri,
+ expectedChildren[i].uri,
+ "Should have the correct uri for root ${i}"
+ );
+ Assert.equal(
+ root.getChild(i).title,
+ placesStrings.GetStringFromName(expectedChildren[i].title),
+ "Should have the correct title for root ${i}"
+ );
+ Assert.equal(root.getChild(i).bookmarkGuid, expectedChildren[i].guid);
+ }
+}
+
+/**
+ * This test will test the basic RESULTS_AS_ROOTS_QUERY, that simply returns,
+ * the existing bookmark roots.
+ */
+add_task(async function test_results_as_root() {
+ let root = getAllBookmarksQuery();
+ root.containerOpen = true;
+
+ Assert.equal(
+ PlacesUtils.asQuery(root).queryOptions.queryType,
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS,
+ "Should have a query type of QUERY_TYPE_BOOKMARKS"
+ );
+
+ assertExpectedChildren(root, expectedRoots);
+
+ root.containerOpen = false;
+});
+
+add_task(async function test_results_as_root_with_mobile() {
+ Services.prefs.setBoolPref(MOBILE_BOOKMARKS_PREF, true);
+
+ let root = getAllBookmarksQuery();
+ root.containerOpen = true;
+
+ assertExpectedChildren(root, expectedRootsWithMobile);
+
+ root.containerOpen = false;
+ Services.prefs.clearUserPref(MOBILE_BOOKMARKS_PREF);
+});
+
+add_task(async function test_results_as_root_remove_mobile_dynamic() {
+ Services.prefs.setBoolPref(MOBILE_BOOKMARKS_PREF, true);
+
+ let root = getAllBookmarksQuery();
+ root.containerOpen = true;
+
+ // Now un-set the pref, and poke the database to update the query.
+ Services.prefs.clearUserPref(MOBILE_BOOKMARKS_PREF);
+
+ assertExpectedChildren(root, expectedRoots);
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_results-as-tag-query.js b/toolkit/components/places/tests/queries/test_results-as-tag-query.js
new file mode 100644
index 0000000000..0d4670b658
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_results-as-tag-query.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const testData = {
+ "http://foo.com/": ["tag1", "tag 2", "Space ☺️ Between"].sort(),
+ "http://bar.com/": ["tag1", "tag 2"].sort(),
+ "http://baz.com/": ["tag 2", "Space ☺️ Between"].sort(),
+ "http://qux.com/": ["Space ☺️ Between"],
+};
+
+const formattedTestData = [];
+for (const [uri, tagArray] of Object.entries(testData)) {
+ formattedTestData.push({
+ title: `Title of ${uri}`,
+ uri,
+ isBookmark: true,
+ isTag: true,
+ tagArray,
+ });
+}
+
+add_task(async function test_results_as_tags_root() {
+ await task_populateDB(formattedTestData);
+
+ // Construct URL - tag mapping from tag query.
+ const actualData = {};
+ for (const uri in testData) {
+ if (testData.hasOwnProperty(uri)) {
+ actualData[uri] = [];
+ }
+ }
+
+ const options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_TAGS_ROOT;
+ const query = PlacesUtils.history.getNewQuery();
+ const root = PlacesUtils.history.executeQuery(query, options).root;
+
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 3, "We should get as many results as tags.");
+ displayResultSet(root);
+
+ for (let i = 0; i < root.childCount; ++i) {
+ const node = root.getChild(i);
+ const tagName = node.title;
+ Assert.equal(
+ node.type,
+ node.RESULT_TYPE_QUERY,
+ "Result type should be RESULT_TYPE_QUERY."
+ );
+ const subRoot = node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ subRoot.containerOpen = true;
+ for (let j = 0; j < subRoot.childCount; ++j) {
+ actualData[subRoot.getChild(j).uri].push(tagName);
+ actualData[subRoot.getChild(j).uri].sort();
+ }
+ }
+
+ Assert.deepEqual(
+ actualData,
+ testData,
+ "URI-tag mapping should be same from query and initial data."
+ );
+});
diff --git a/toolkit/components/places/tests/queries/test_results-as-visit.js b/toolkit/components/places/tests/queries/test_results-as-visit.js
new file mode 100644
index 0000000000..256e756c98
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_results-as-visit.js
@@ -0,0 +1,158 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+var testData = [];
+var timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+}
+
+function createTestData() {
+ function generateVisits(aPage) {
+ for (var i = 0; i < aPage.visitCount; i++) {
+ testData.push({
+ isInQuery: aPage.inQuery,
+ isVisit: true,
+ title: aPage.title,
+ uri: aPage.uri,
+ lastVisit: newTimeInMicroseconds(),
+ isTag: aPage.tags && !!aPage.tags.length,
+ tagArray: aPage.tags,
+ });
+ }
+ }
+
+ var pages = [
+ {
+ uri: "http://foo.com/",
+ title: "amo",
+ tags: ["moz"],
+ visitCount: 3,
+ inQuery: false,
+ },
+ {
+ uri: "http://moilla.com/",
+ title: "bMoz",
+ tags: ["bugzilla"],
+ visitCount: 5,
+ inQuery: true,
+ },
+ {
+ uri: "http://foo.mail.com/changeme1.html",
+ title: "c Moz",
+ visitCount: 7,
+ inQuery: true,
+ },
+ {
+ uri: "http://foo.mail.com/changeme2.html",
+ tags: ["moz"],
+ title: "",
+ visitCount: 1,
+ inQuery: false,
+ },
+ {
+ uri: "http://foo.mail.com/changeme3.html",
+ title: "zydeco",
+ visitCount: 5,
+ inQuery: false,
+ },
+ ];
+ pages.forEach(generateVisits);
+}
+
+/**
+ * This test will test Queries that use relative search terms and URI options
+ */
+add_task(async function test_results_as_visit() {
+ createTestData();
+ await task_populateDB(testData);
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "moz";
+ query.minVisits = 2;
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_ASCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ info("Number of items in result set: " + root.childCount);
+ for (let i = 0; i < root.childCount; ++i) {
+ info(
+ "result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title
+ );
+ }
+
+ // Check our inital result set
+ compareArrayToResult(testData, root);
+
+ // If that passes, check liveupdate
+ // Add to the query set
+ info("Adding item to query");
+ var tmp = [];
+ for (let i = 0; i < 2; i++) {
+ tmp.push({
+ isVisit: true,
+ uri: "http://foo.com/added.html",
+ title: "ab moz",
+ });
+ }
+ await task_populateDB(tmp);
+ for (let i = 0; i < 2; i++) {
+ Assert.equal(root.getChild(i).title, "ab moz");
+ }
+
+ // Update an existing URI
+ info("Updating Item");
+ var change2 = [
+ { isVisit: true, title: "moz", uri: "http://foo.mail.com/changeme2.html" },
+ ];
+ await task_populateDB(change2);
+ Assert.ok(nodeInResult(change2, root));
+
+ // Update some visits - add one and take one out of query set, and simply
+ // change one so that it still applies to the query.
+ info("Updating More Items");
+ var change3 = [
+ {
+ isVisit: true,
+ lastVisit: newTimeInMicroseconds(),
+ uri: "http://foo.mail.com/changeme1.html",
+ title: "foo",
+ },
+ {
+ isVisit: true,
+ lastVisit: newTimeInMicroseconds(),
+ uri: "http://foo.mail.com/changeme3.html",
+ title: "moz",
+ isTag: true,
+ tagArray: ["foo", "moz"],
+ },
+ ];
+ await task_populateDB(change3);
+ Assert.ok(!nodeInResult({ uri: "http://foo.mail.com/changeme1.html" }, root));
+ Assert.ok(nodeInResult({ uri: "http://foo.mail.com/changeme3.html" }, root));
+
+ // And now, delete one
+ info("Delete item outside of batch");
+ var change4 = [
+ {
+ isVisit: true,
+ lastVisit: newTimeInMicroseconds(),
+ uri: "http://moilla.com/",
+ title: "mo,z",
+ },
+ ];
+ await task_populateDB(change4);
+ Assert.ok(!nodeInResult(change4, root));
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js b/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js
new file mode 100644
index 0000000000..224feb4f0c
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// Tests the interaction of includeHidden and searchTerms search options.
+
+var timeInMicroseconds = Date.now() * 1000;
+
+const VISITS = [
+ {
+ isVisit: true,
+ transType: TRANSITION_TYPED,
+ uri: "http://redirect.example.com/",
+ title: "example",
+ isRedirect: true,
+ lastVisit: timeInMicroseconds--,
+ },
+ {
+ isVisit: true,
+ transType: TRANSITION_TYPED,
+ uri: "http://target.example.com/",
+ title: "example",
+ lastVisit: timeInMicroseconds--,
+ },
+];
+
+const HIDDEN_VISITS = [
+ {
+ isVisit: true,
+ transType: TRANSITION_FRAMED_LINK,
+ uri: "http://hidden.example.com/",
+ title: "red",
+ lastVisit: timeInMicroseconds--,
+ },
+];
+
+const TEST_DATA = [
+ { searchTerms: "example", includeHidden: true, expectedResults: 2 },
+ { searchTerms: "example", includeHidden: false, expectedResults: 1 },
+ { searchTerms: "red", includeHidden: true, expectedResults: 1 },
+];
+
+add_task(async function test_initalize() {
+ await task_populateDB(VISITS);
+});
+
+add_task(async function test_searchTerms_includeHidden() {
+ for (let data of TEST_DATA) {
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = data.searchTerms;
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.includeHidden = data.includeHidden;
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ let cc = root.childCount;
+ // Live update with hidden visits.
+ await task_populateDB(HIDDEN_VISITS);
+ let cc_update = root.childCount;
+
+ root.containerOpen = false;
+
+ Assert.equal(cc, data.expectedResults);
+ Assert.equal(
+ cc_update,
+ data.expectedResults + (data.includeHidden ? 1 : 0)
+ );
+
+ await PlacesUtils.history.remove("http://hidden.example.com/");
+ }
+});
diff --git a/toolkit/components/places/tests/queries/test_searchTerms_time.js b/toolkit/components/places/tests/queries/test_searchTerms_time.js
new file mode 100644
index 0000000000..39fd1353eb
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchTerms_time.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that visitis are correctly live-updated in a history
+// query filtered on searchterms and time.
+
+const USEC_PER_DAY = 86400000000;
+const now = PlacesUtils.toPRTime(new Date());
+
+add_task(async function pages_query() {
+ let query = PlacesUtils.history.getNewQuery();
+ query.beginTime = now - 15 * USEC_PER_DAY;
+ query.endTime = now - 5 * USEC_PER_DAY;
+ query.beginTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH;
+ query.endTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH;
+ query.searchTerms = "mo";
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_URI_ASCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+ await testQuery(query, options);
+
+ options.sortingMode = options.SORT_BY_DATE_ASCENDING;
+ options.resultType = options.RESULTS_AS_VISITS;
+ await testQuery(query, options);
+});
+
+async function testQuery(query, options) {
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ Assert.equal(root.childCount, 0, "There should be zero results initially");
+
+ await PlacesTestUtils.addVisits([
+ // SearchTerms matching URL but out of the timeframe.
+ {
+ url: "https://test.moz.org",
+ title: "abc",
+ visitDate: now - 2 * USEC_PER_DAY,
+ },
+ // In the timeframe but no searchTerms match.
+ {
+ url: "https://test.def.org",
+ title: "def",
+ visitDate: now - 10 * USEC_PER_DAY,
+ },
+ // In the timeframe, matching title.
+ {
+ url: "https://test.ghi.org",
+ title: "amo",
+ visitDate: now - 10 * USEC_PER_DAY,
+ },
+ ]);
+
+ Assert.equal(root.childCount, 1, "Check matching results");
+ let node = root.getChild(0);
+ Assert.equal(node.title, "amo");
+
+ // Change title so it's no longer matching.
+ await PlacesTestUtils.addVisits({
+ url: "https://test.ghi.org",
+ title: "ghi",
+ visitDate: now - 10 * USEC_PER_DAY,
+ });
+
+ Assert.equal(root.childCount, 0, "Check matching results");
+
+ // Add visit in the timeframe.
+ await PlacesTestUtils.addVisits({
+ url: "https://test.moz.org",
+ title: "abc",
+ visitDate: now - 10 * USEC_PER_DAY,
+ });
+
+ Assert.equal(root.childCount, 1, "Check matching results");
+ node = root.getChild(0);
+ Assert.equal(node.title, "abc");
+
+ // Remove visit in the timeframe.
+ await PlacesUtils.history.removeVisitsByFilter({
+ beginDate: PlacesUtils.toDate(now - 15 * USEC_PER_DAY),
+ endDate: PlacesUtils.toDate(now - 5 * USEC_PER_DAY),
+ });
+ await PlacesTestUtils.dumpTable({
+ table: "moz_places",
+ columns: ["id", "url"],
+ });
+ await PlacesTestUtils.dumpTable({
+ table: "moz_historyvisits",
+ columns: ["place_id", "visit_date"],
+ });
+
+ Assert.equal(root.childCount, 0, "Check matching results");
+
+ // Add matching visit out of the timeframe.
+ await PlacesTestUtils.addVisits(
+ // SearchTerms matching URL but out of the timeframe.
+ {
+ url: "https://test.mozilla.org",
+ title: "mozilla",
+ visitDate: now - 2 * USEC_PER_DAY,
+ }
+ );
+
+ Assert.equal(root.childCount, 0, "Check matching results");
+
+ root.containerOpen = false;
+ await PlacesUtils.history.clear();
+}
diff --git a/toolkit/components/places/tests/queries/test_search_tags.js b/toolkit/components/places/tests/queries/test_search_tags.js
new file mode 100644
index 0000000000..4f8cface07
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_search_tags.js
@@ -0,0 +1,73 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+add_task(async function test_search_for_tagged_bookmarks() {
+ const testURI = "http://a1.com";
+
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "bug 395101 test",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ title: "1 title",
+ url: testURI,
+ });
+
+ // tag the bookmarked URI
+ PlacesUtils.tagging.tagURI(uri(testURI), [
+ "elephant",
+ "walrus",
+ "giraffe",
+ "turkey",
+ "hiPPo",
+ "BABOON",
+ "alf",
+ ]);
+
+ // search for the bookmark, using a tag
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "elephant";
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ query.setParents([folder.guid]);
+
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var rootNode = result.root;
+ rootNode.containerOpen = true;
+
+ Assert.equal(rootNode.childCount, 1);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, bookmark.guid);
+ rootNode.containerOpen = false;
+
+ // partial matches are okay
+ query.searchTerms = "wal";
+ result = PlacesUtils.history.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ Assert.equal(rootNode.childCount, 1);
+ rootNode.containerOpen = false;
+
+ // case insensitive search term
+ query.searchTerms = "WALRUS";
+ result = PlacesUtils.history.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ Assert.equal(rootNode.childCount, 1);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, bookmark.guid);
+ rootNode.containerOpen = false;
+
+ // case insensitive tag
+ query.searchTerms = "baboon";
+ result = PlacesUtils.history.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ Assert.equal(rootNode.childCount, 1);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, bookmark.guid);
+ rootNode.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js b/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js
new file mode 100644
index 0000000000..8eebf68cad
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check that bookmarklets are returned by searches with searchTerms.
+
+var testData = [
+ {
+ isInQuery: true,
+ isBookmark: true,
+ title: "bookmark 1",
+ uri: "http://mozilla.org/script/",
+ },
+
+ {
+ isInQuery: true,
+ isBookmark: true,
+ title: "bookmark 2",
+ uri: "javascript:alert('moz');",
+ },
+];
+
+add_task(async function test_initalize() {
+ await task_populateDB(testData);
+});
+
+add_test(function test_search_by_title() {
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "bookmark";
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult(testData, root);
+ root.containerOpen = false;
+
+ run_next_test();
+});
+
+add_test(function test_search_by_schemeToken() {
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "script";
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult(testData, root);
+ root.containerOpen = false;
+
+ run_next_test();
+});
+
+add_test(function test_search_by_uriAndTitle() {
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "moz";
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult(testData, root);
+ root.containerOpen = false;
+
+ run_next_test();
+});
diff --git a/toolkit/components/places/tests/queries/test_searchterms-domain.js b/toolkit/components/places/tests/queries/test_searchterms-domain.js
new file mode 100644
index 0000000000..45a0a7d542
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchterms-domain.js
@@ -0,0 +1,197 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// The test data for our database, note that the ordering of the results that
+// will be returned by the query (the isInQuery: true objects) is IMPORTANT.
+// see compareArrayToResult in head_queries.js for more info.
+var testData = [
+ // Test ftp protocol - vary the title length, embed search term
+ {
+ isInQuery: true,
+ isVisit: true,
+ isDetails: true,
+ uri: "ftp://foo.com/ftp",
+ lastVisit: lastweek,
+ title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo",
+ },
+
+ // Test flat domain with annotation, search term in sentence
+ {
+ isInQuery: true,
+ isVisit: true,
+ isDetails: true,
+ isPageAnnotation: true,
+ uri: "http://foo.com/",
+ annoName: "moz/test",
+ annoVal: "val",
+ lastVisit: lastweek,
+ title: "you know, moz is cool",
+ },
+
+ // Test subdomain included with isRedirect=true, different transtype
+ {
+ isInQuery: true,
+ isVisit: true,
+ isDetails: true,
+ title: "amozzie",
+ isRedirect: true,
+ uri: "http://mail.foo.com/redirect",
+ lastVisit: old,
+ referrer: "http://myreferrer.com",
+ transType: PlacesUtils.history.TRANSITION_LINK,
+ },
+
+ // Test subdomain inclued, search term at end
+ {
+ isInQuery: true,
+ isVisit: true,
+ isDetails: true,
+ uri: "http://mail.foo.com/yiihah",
+ title: "blahmoz",
+ lastVisit: daybefore,
+ },
+
+ // Test www. style URI is included, with a tag
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ isTag: true,
+ uri: "http://www.foo.com/yiihah",
+ tagArray: ["moz"],
+ lastVisit: yesterday,
+ title: "foo",
+ },
+
+ // Test https protocol
+ {
+ isInQuery: true,
+ isVisit: true,
+ isDetails: true,
+ title: "moz",
+ uri: "https://foo.com/",
+ lastVisit: today,
+ },
+
+ // Begin the invalid queries: wrong search term
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ title: "m o z",
+ uri: "http://foo.com/tooearly.php",
+ lastVisit: today,
+ },
+
+ // Test bad URI
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ title: "moz",
+ uri: "http://sffoo.com/justwrong.htm",
+ lastVisit: yesterday,
+ },
+
+ // Test what we do with escaping in titles
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ title: "m%0o%0z",
+ uri: "http://foo.com/changeme1.htm",
+ lastVisit: yesterday,
+ },
+
+ // Test another invalid title - for updating later
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ title: "m,oz",
+ uri: "http://foo.com/changeme2.htm",
+ lastVisit: yesterday,
+ },
+];
+
+/**
+ * This test will test Queries that use relative search terms and domain options
+ */
+add_task(async function test_searchterms_domain() {
+ await task_populateDB(testData);
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "moz";
+ query.domain = "foo.com";
+ query.domainIsHost = false;
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_ASCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ info("Number of items in result set: " + root.childCount);
+ for (var i = 0; i < root.childCount; ++i) {
+ info(
+ "result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title
+ );
+ }
+
+ // Check our inital result set
+ compareArrayToResult(testData, root);
+
+ // If that passes, check liveupdate
+ // Add to the query set
+ info("Adding item to query");
+ var change1 = [
+ {
+ isVisit: true,
+ isDetails: true,
+ uri: "http://foo.com/added.htm",
+ title: "moz",
+ transType: PlacesUtils.history.TRANSITION_LINK,
+ },
+ ];
+ await task_populateDB(change1);
+ Assert.ok(nodeInResult(change1, root));
+
+ // Update an existing URI
+ info("Updating Item");
+ var change2 = [
+ { isDetails: true, uri: "http://foo.com/changeme1.htm", title: "moz" },
+ ];
+ await task_populateDB(change2);
+ Assert.ok(nodeInResult(change2, root));
+
+ // Add one and take one out of query set, and simply change one so that it
+ // still applies to the query.
+ info("Updating More Items");
+ var change3 = [
+ { isDetails: true, uri: "http://foo.com/changeme2.htm", title: "moz" },
+ {
+ isDetails: true,
+ uri: "http://mail.foo.com/yiihah",
+ title: "moz now updated",
+ },
+ { isDetails: true, uri: "ftp://foo.com/ftp", title: "gone" },
+ ];
+ await task_populateDB(change3);
+ Assert.ok(nodeInResult({ uri: "http://foo.com/changeme2.htm" }, root));
+ Assert.ok(nodeInResult({ uri: "http://mail.foo.com/yiihah" }, root));
+ Assert.ok(!nodeInResult({ uri: "ftp://foo.com/ftp" }, root));
+
+ // And now, delete one
+ info("Deleting items");
+ var change4 = [{ isDetails: true, uri: "https://foo.com/", title: "mo,z" }];
+ await task_populateDB(change4);
+ Assert.ok(!nodeInResult(change4, root));
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_searchterms-uri.js b/toolkit/components/places/tests/queries/test_searchterms-uri.js
new file mode 100644
index 0000000000..27b9a28c71
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchterms-uri.js
@@ -0,0 +1,125 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// The test data for our database, note that the ordering of the results that
+// will be returned by the query (the isInQuery: true objects) is IMPORTANT.
+// see compareArrayToResult in head_queries.js for more info.
+var testData = [
+ // Test flat domain with annotation, search term in sentence
+ {
+ isInQuery: true,
+ isVisit: true,
+ isDetails: true,
+ isPageAnnotation: true,
+ uri: "http://foo.com/",
+ annoName: "moz/test",
+ annoVal: "val",
+ lastVisit: lastweek,
+ title: "you know, moz is cool",
+ },
+
+ // Test https protocol
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ title: "moz",
+ uri: "https://foo.com/",
+ lastVisit: today,
+ },
+
+ // Begin the invalid queries: wrong search term
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ title: "m o z",
+ uri: "http://foo.com/wrongsearch.php",
+ lastVisit: today,
+ },
+
+ // Test subdomain inclued, search term at end
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ uri: "http://mail.foo.com/yiihah",
+ title: "blahmoz",
+ lastVisit: daybefore,
+ },
+
+ // Test ftp protocol - vary the title length, embed search term
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ uri: "ftp://foo.com/ftp",
+ lastVisit: lastweek,
+ title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo",
+ },
+
+ // Test what we do with escaping in titles
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ title: "m%0o%0z",
+ uri: "http://foo.com/changeme1.htm",
+ lastVisit: yesterday,
+ },
+
+ // Test another invalid title - for updating later
+ {
+ isInQuery: false,
+ isVisit: true,
+ isDetails: true,
+ title: "m,oz",
+ uri: "http://foo.com/changeme2.htm",
+ lastVisit: yesterday,
+ },
+];
+
+/**
+ * This test will test Queries that use relative search terms and URI options
+ */
+add_task(async function test_searchterms_uri() {
+ await task_populateDB(testData);
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "moz";
+ query.uri = uri("http://foo.com");
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_ASCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ info("Number of items in result set: " + root.childCount);
+ for (var i = 0; i < root.childCount; ++i) {
+ info(
+ "result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title
+ );
+ }
+
+ // Check our inital result set
+ compareArrayToResult(testData, root);
+
+ // live update.
+ info("change title");
+ var change1 = [{ isDetails: true, uri: "http://foo.com/", title: "mo" }];
+ await task_populateDB(change1);
+
+ Assert.ok(!nodeInResult({ uri: "http://foo.com/" }, root));
+ var change2 = [{ isDetails: true, uri: "http://foo.com/", title: "moz" }];
+ await task_populateDB(change2);
+ Assert.ok(nodeInResult({ uri: "http://foo.com/" }, root));
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js b/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js
new file mode 100644
index 0000000000..358ab45fdb
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js
@@ -0,0 +1,223 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ * ***** END LICENSE BLOCK ***** */
+
+// This test ensures that the date and site type of |place:| query maintains
+// its quantifications correctly. Namely, it ensures that the date part of the
+// query is not lost when the domain queries are made.
+
+// We specifically craft these entries so that if a by Date and Site sorting is
+// applied, we find one domain in the today range, and two domains in the older
+// than six months range.
+// The correspondence between item in |testData| and date range is stored in
+// leveledTestData.
+var testData = [
+ {
+ isVisit: true,
+ uri: "file:///directory/1",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true,
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/1",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true,
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/2",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true,
+ },
+ {
+ isVisit: true,
+ uri: "file:///directory/2",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true,
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/3",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true,
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/4",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true,
+ },
+ {
+ isVisit: true,
+ uri: "http://example.net/1",
+ lastVisit: olderthansixmonths + 1000,
+ title: "test visit",
+ isInQuery: true,
+ },
+];
+var leveledTestData = [
+ // Today
+ [
+ [0], // Today, local files
+ [1, 2],
+ ], // Today, example.com
+ // Older than six months
+ [
+ [3], // Older than six months, local files
+ [4, 5], // Older than six months, example.com
+ [6], // Older than six months, example.net
+ ],
+];
+
+// This test data is meant for live updating. The |levels| property indicates
+// date range index and then domain index.
+var testDataAddedLater = [
+ {
+ isVisit: true,
+ uri: "http://example.com/5",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true,
+ levels: [1, 1],
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/6",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true,
+ levels: [1, 1],
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/7",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true,
+ levels: [0, 1],
+ },
+ {
+ isVisit: true,
+ uri: "file:///directory/3",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true,
+ levels: [0, 0],
+ },
+];
+
+add_task(async function test_sort_date_site_grouping() {
+ await task_populateDB(testData);
+
+ // On Linux, the (local files) folder is shown after sites unlike Mac/Windows.
+ // Thus, we avoid running this test on Linux but this should be re-enabled
+ // after bug 624024 is resolved.
+ let isLinux = "@mozilla.org/gnome-gconf-service;1" in Cc;
+ if (isLinux) {
+ return;
+ }
+
+ // In this test, there are three levels of results:
+ // 1st: Date queries. e.g., today, last week, or older than 6 months.
+ // 2nd: Domain queries restricted to a date. e.g. mozilla.com today.
+ // 3rd: Actual visits. e.g. mozilla.com/index.html today.
+ //
+ // We store all the third level result roots so that we can easily close all
+ // containers and test live updating into specific results.
+ let roots = [];
+
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY;
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ // This corresponds to the number of date ranges.
+ Assert.equal(root.childCount, leveledTestData.length);
+
+ // We pass off to |checkFirstLevel| to check the first level of results.
+ for (let index = 0; index < leveledTestData.length; index++) {
+ let node = root.getChild(index);
+ checkFirstLevel(index, node, roots);
+ }
+
+ // Test live updating.
+ for (let visit of testDataAddedLater) {
+ await task_populateDB([visit]);
+ let oldLength = testData.length;
+ let i = visit.levels[0];
+ let j = visit.levels[1];
+ testData.push(visit);
+ leveledTestData[i][j].push(oldLength);
+ compareArrayToResult(
+ leveledTestData[i][j].map(x => testData[x]),
+ roots[i][j]
+ );
+ }
+
+ for (let i = 0; i < roots.length; i++) {
+ for (let j = 0; j < roots[i].length; j++) {
+ roots[i][j].containerOpen = false;
+ }
+ }
+
+ root.containerOpen = false;
+});
+
+function checkFirstLevel(index, node, roots) {
+ PlacesUtils.asContainer(node).containerOpen = true;
+
+ Assert.ok(PlacesUtils.nodeIsDay(node));
+ PlacesUtils.asQuery(node);
+ let query = node.query;
+ let options = node.queryOptions;
+
+ Assert.ok(query.hasBeginTime && query.hasEndTime);
+
+ // Here we check the second level of results.
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ roots.push([]);
+ root.containerOpen = true;
+
+ Assert.equal(root.childCount, leveledTestData[index].length);
+ for (var secondIndex = 0; secondIndex < root.childCount; secondIndex++) {
+ let child = PlacesUtils.asQuery(root.getChild(secondIndex));
+ checkSecondLevel(index, secondIndex, child, roots);
+ }
+ root.containerOpen = false;
+ node.containerOpen = false;
+}
+
+function checkSecondLevel(index, secondIndex, child, roots) {
+ let query = child.query;
+ let options = child.queryOptions;
+
+ Assert.ok(query.hasDomain);
+ Assert.ok(query.hasBeginTime && query.hasEndTime);
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ // We should now have that roots[index][secondIndex] is set to the second
+ // level's results root.
+ roots[index].push(root);
+
+ // We pass off to compareArrayToResult to check the third level of
+ // results.
+ root.containerOpen = true;
+ compareArrayToResult(
+ leveledTestData[index][secondIndex].map(x => testData[x]),
+ root
+ );
+ // We close |root|'s container later so that we can test live
+ // updates into it.
+}
diff --git a/toolkit/components/places/tests/queries/test_sorting.js b/toolkit/components/places/tests/queries/test_sorting.js
new file mode 100644
index 0000000000..41059c0823
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_sorting.js
@@ -0,0 +1,961 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+var tests = [];
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_NONE,
+
+ async setup() {
+ info("Sorting test 1: SORT BY NONE");
+
+ this._unsortedData = [
+ {
+ isBookmark: true,
+ uri: "http://example.com/b",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "y",
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "z",
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "x",
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = this._unsortedData;
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ },
+
+ check() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ // no reverse sorting for SORT BY NONE
+ },
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING,
+
+ async setup() {
+ info("Sorting test 2: SORT BY TITLE");
+
+ this._unsortedData = [
+ {
+ isBookmark: true,
+ uri: "http://example.com/b1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "y",
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "z",
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "x",
+ isInQuery: true,
+ },
+
+ // if titles are equal, should fall back to URI
+ {
+ isBookmark: true,
+ uri: "http://example.com/b2",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "y",
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[2],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[1],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ },
+
+ check() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ },
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING,
+
+ async setup() {
+ info("Sorting test 3: SORT BY DATE");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ {
+ isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ uri: "http://example.com/c1",
+ lastVisit: timeInMicroseconds - 2000,
+ title: "x1",
+ isInQuery: true,
+ },
+
+ {
+ isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ uri: "http://example.com/a",
+ lastVisit: timeInMicroseconds - 1000,
+ title: "z",
+ isInQuery: true,
+ },
+
+ {
+ isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ uri: "http://example.com/b",
+ lastVisit: timeInMicroseconds - 3000,
+ title: "y",
+ isInQuery: true,
+ },
+
+ // if dates are equal, should fall back to title
+ {
+ isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ uri: "http://example.com/c2",
+ lastVisit: timeInMicroseconds - 2000,
+ title: "x2",
+ isInQuery: true,
+ },
+
+ // if dates and title are equal, should fall back to bookmark index
+ {
+ isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ uri: "http://example.com/c2",
+ lastVisit: timeInMicroseconds - 2000,
+ title: "x2",
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[2],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[1],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ },
+
+ check() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ },
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING,
+
+ async setup() {
+ info("Sorting test 4: SORT BY URI");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ {
+ isBookmark: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds,
+ uri: "http://example.com/b",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ title: "y",
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ title: "x",
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ title: "z",
+ isInQuery: true,
+ },
+
+ // if URIs are equal, should fall back to date
+ {
+ isBookmark: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds + 1000,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ title: "x",
+ isInQuery: true,
+ },
+
+ // if no URI (e.g., node is a folder), should fall back to title
+ {
+ isFolder: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ title: "y",
+ isInQuery: true,
+ },
+
+ // if URIs and dates are equal, should fall back to bookmark index
+ {
+ isBookmark: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds + 1000,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 5,
+ title: "x",
+ isInQuery: true,
+ },
+
+ // if no URI and titles are equal, should fall back to bookmark index
+ {
+ isFolder: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 6,
+ title: "y",
+ isInQuery: true,
+ },
+
+ // if no URI and titles are equal, should fall back to title
+ {
+ isFolder: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 7,
+ title: "z",
+ isInQuery: true,
+ },
+
+ // Separator should go after folders.
+ {
+ isSeparator: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 8,
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[4],
+ this._unsortedData[6],
+ this._unsortedData[7],
+ this._unsortedData[8],
+ this._unsortedData[2],
+ this._unsortedData[0],
+ this._unsortedData[1],
+ this._unsortedData[3],
+ this._unsortedData[5],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ },
+
+ check() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ },
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING,
+
+ async setup() {
+ info("Sorting test 5: SORT BY VISITCOUNT");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ {
+ isBookmark: true,
+ uri: "http://example.com/a",
+ lastVisit: timeInMicroseconds,
+ title: "z",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/c",
+ lastVisit: timeInMicroseconds,
+ title: "x",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/b1",
+ lastVisit: timeInMicroseconds,
+ title: "y1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ isInQuery: true,
+ },
+
+ // if visitCounts are equal, should fall back to date
+ {
+ isBookmark: true,
+ uri: "http://example.com/b2",
+ lastVisit: timeInMicroseconds + 1000,
+ title: "y2a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ isInQuery: true,
+ },
+
+ // if visitCounts and dates are equal, should fall back to bookmark index
+ {
+ isBookmark: true,
+ uri: "http://example.com/b2",
+ lastVisit: timeInMicroseconds + 1000,
+ title: "y2b",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[0],
+ this._unsortedData[2],
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[1],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ // add visits to increase visit count
+ await PlacesTestUtils.addVisits([
+ {
+ uri: uri("http://example.com/a"),
+ transition: TRANSITION_TYPED,
+ visitDate: timeInMicroseconds,
+ },
+ {
+ uri: uri("http://example.com/b1"),
+ transition: TRANSITION_TYPED,
+ visitDate: timeInMicroseconds,
+ },
+ {
+ uri: uri("http://example.com/b1"),
+ transition: TRANSITION_TYPED,
+ visitDate: timeInMicroseconds,
+ },
+ {
+ uri: uri("http://example.com/b2"),
+ transition: TRANSITION_TYPED,
+ visitDate: timeInMicroseconds + 1000,
+ },
+ {
+ uri: uri("http://example.com/b2"),
+ transition: TRANSITION_TYPED,
+ visitDate: timeInMicroseconds + 1000,
+ },
+ {
+ uri: uri("http://example.com/c"),
+ transition: TRANSITION_TYPED,
+ visitDate: timeInMicroseconds,
+ },
+ {
+ uri: uri("http://example.com/c"),
+ transition: TRANSITION_TYPED,
+ visitDate: timeInMicroseconds,
+ },
+ {
+ uri: uri("http://example.com/c"),
+ transition: TRANSITION_TYPED,
+ visitDate: timeInMicroseconds,
+ },
+ ]);
+ },
+
+ check() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ this._sortingMode =
+ Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ },
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING,
+
+ async setup() {
+ info("Sorting test 7: SORT BY DATEADDED");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ {
+ isBookmark: true,
+ uri: "http://example.com/b1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ title: "y1",
+ dateAdded: timeInMicroseconds - 1000,
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ title: "z",
+ dateAdded: timeInMicroseconds - 2000,
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ title: "x",
+ dateAdded: timeInMicroseconds,
+ isInQuery: true,
+ },
+
+ // if dateAddeds are equal, should fall back to title
+ {
+ isBookmark: true,
+ uri: "http://example.com/b2",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ title: "y2",
+ dateAdded: timeInMicroseconds - 1000,
+ isInQuery: true,
+ },
+
+ // if dateAddeds and titles are equal, should fall back to bookmark index
+ {
+ isBookmark: true,
+ uri: "http://example.com/b3",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ title: "y3",
+ dateAdded: timeInMicroseconds - 1000,
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[1],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ },
+
+ check() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ this._sortingMode =
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ },
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING,
+
+ async setup() {
+ info("Sorting test 8: SORT BY LASTMODIFIED");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ var timeAddedInMicroseconds = timeInMicroseconds - 10000;
+
+ this._unsortedData = [
+ {
+ isBookmark: true,
+ uri: "http://example.com/b1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ title: "y1",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds - 1000,
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ title: "z",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds - 2000,
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ title: "x",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds,
+ isInQuery: true,
+ },
+
+ // if lastModifieds are equal, should fall back to title
+ {
+ isBookmark: true,
+ uri: "http://example.com/b2",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ title: "y2",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds - 1000,
+ isInQuery: true,
+ },
+
+ // if lastModifieds and titles are equal, should fall back to bookmark
+ // index
+ {
+ isBookmark: true,
+ uri: "http://example.com/b3",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ title: "y3",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds - 1000,
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[1],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ },
+
+ check() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ this._sortingMode =
+ Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ },
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING,
+
+ async setup() {
+ info("Sorting test 9: SORT BY TAGS");
+
+ this._unsortedData = [
+ {
+ isBookmark: true,
+ uri: "http://url2.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title x",
+ isTag: true,
+ tagArray: ["x", "y", "z"],
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://url1a.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title y1",
+ isTag: true,
+ tagArray: ["a", "b"],
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://url3a.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title w1",
+ isInQuery: true,
+ },
+
+ {
+ isBookmark: true,
+ uri: "http://url0.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title z",
+ isTag: true,
+ tagArray: ["a", "y", "z"],
+ isInQuery: true,
+ },
+
+ // if tags are equal, should fall back to title
+ {
+ isBookmark: true,
+ uri: "http://url1b.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title y2",
+ isTag: true,
+ tagArray: ["b", "a"],
+ isInQuery: true,
+ },
+
+ // if tags are equal, should fall back to title
+ {
+ isBookmark: true,
+ uri: "http://url3b.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title w2",
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[2],
+ this._unsortedData[5],
+ this._unsortedData[1],
+ this._unsortedData[4],
+ this._unsortedData[3],
+ this._unsortedData[0],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ },
+
+ check() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ },
+});
+
+// SORT_BY_FRECENCY_*
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_ASCENDING,
+
+ async setup() {
+ info("Sorting test 13: SORT BY FRECENCY ");
+
+ let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ this._unsortedData = [
+ {
+ isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "I",
+ isInQuery: true,
+ },
+
+ {
+ isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "I",
+ isInQuery: true,
+ },
+
+ {
+ isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "I",
+ isInQuery: true,
+ },
+
+ {
+ isVisit: true,
+ isDetails: true,
+ uri: "http://is.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "love",
+ isInQuery: true,
+ },
+
+ {
+ isVisit: true,
+ isDetails: true,
+ uri: "http://best.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "moz",
+ isInQuery: true,
+ },
+
+ {
+ isVisit: true,
+ isDetails: true,
+ uri: "http://best.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "moz",
+ isInQuery: true,
+ },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[3],
+ this._unsortedData[5],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ await task_populateDB(this._unsortedData);
+ },
+
+ check() {
+ var query = PlacesUtils.history.getNewQuery();
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ var root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse() {
+ this._sortingMode =
+ Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ },
+});
+
+add_task(async function test_sorting() {
+ for (let test of tests) {
+ await test.setup();
+ await PlacesTestUtils.promiseAsyncUpdates();
+ test.check();
+ // sorting reversed, usually SORT_BY have ASC and DESC
+ test.check_reverse();
+ // Execute cleanup tasks
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ }
+});
diff --git a/toolkit/components/places/tests/queries/test_tags.js b/toolkit/components/places/tests/queries/test_tags.js
new file mode 100644
index 0000000000..17ad3478ce
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_tags.js
@@ -0,0 +1,626 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests bookmark queries with tags. See bug 399799.
+ */
+
+"use strict";
+
+add_task(async function tags_getter_setter() {
+ info("Tags getter/setter should work correctly");
+ info("Without setting tags, tags getter should return empty array");
+ var [query] = makeQuery();
+ Assert.equal(query.tags.length, 0);
+
+ info("Setting tags to an empty array, tags getter should return empty array");
+ [query] = makeQuery([]);
+ Assert.equal(query.tags.length, 0);
+
+ info("Setting a few tags, tags getter should return correct array");
+ var tags = ["bar", "baz", "foo"];
+ [query] = makeQuery(tags);
+ setsAreEqual(query.tags, tags, true);
+
+ info("Setting some dupe tags, tags getter return unique tags");
+ [query] = makeQuery(["foo", "foo", "bar", "foo", "baz", "bar"]);
+ setsAreEqual(query.tags, ["bar", "baz", "foo"], true);
+});
+
+add_task(async function invalid_setter_calls() {
+ info("Invalid calls to tags setter should fail");
+ try {
+ var query = PlacesUtils.history.getNewQuery();
+ query.tags = null;
+ do_throw("Passing null to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ query = PlacesUtils.history.getNewQuery();
+ query.tags = "this should not work";
+ do_throw("Passing a string to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ makeQuery([null]);
+ do_throw("Passing one-element array with null to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ makeQuery([undefined]);
+ do_throw("Passing one-element array with undefined to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ makeQuery(["foo", null, "bar"]);
+ do_throw("Passing mixture of tags and null to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ makeQuery(["foo", undefined, "bar"]);
+ do_throw("Passing mixture of tags and undefined to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ makeQuery([1, 2, 3]);
+ do_throw("Passing numbers to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ makeQuery(["foo", 1, 2, 3]);
+ do_throw("Passing mixture of tags and numbers to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ var str = PlacesUtils.toISupportsString("foo");
+ query = PlacesUtils.history.getNewQuery();
+ query.tags = str;
+ do_throw("Passing nsISupportsString to SetTags should fail");
+ } catch (exc) {}
+
+ try {
+ makeQuery([str]);
+ do_throw("Passing array of nsISupportsStrings to SetTags should fail");
+ } catch (exc) {}
+});
+
+add_task(async function not_setting_tags() {
+ info("Not setting tags at all should not affect query URI");
+ checkQueryURI();
+});
+
+add_task(async function empty_array_tags() {
+ info("Setting tags with an empty array should not affect query URI");
+ checkQueryURI([]);
+});
+
+add_task(async function set_tags() {
+ info("Setting some tags should result in correct query URI");
+ checkQueryURI([
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ]);
+});
+
+add_task(async function no_tags_tagsAreNot() {
+ info(
+ "Not setting tags at all but setting tagsAreNot should " +
+ "affect query URI"
+ );
+ checkQueryURI(null, true);
+});
+
+add_task(async function empty_array_tags_tagsAreNot() {
+ info(
+ "Setting tags with an empty array and setting tagsAreNot " +
+ "should affect query URI"
+ );
+ checkQueryURI([], true);
+});
+
+add_task(async function () {
+ info(
+ "Setting some tags and setting tagsAreNot should result in " +
+ "correct query URI"
+ );
+ checkQueryURI(
+ [
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ],
+ true
+ );
+});
+
+add_task(async function tag() {
+ info("Querying on tag associated with a URI should return that URI");
+ await task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["bar"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(async function many_tags() {
+ info("Querying on many tags associated with a URI should return that URI");
+ await task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "bar"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["bar", "baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "bar", "baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(async function repeated_tag() {
+ info("Specifying the same tag multiple times should not matter");
+ await task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "foo"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(async function many_tags_no_bookmark() {
+ info(
+ "Querying on many tags associated with a URI and tags not associated " +
+ "with that URI should not return that URI"
+ );
+ await task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "bogus"]);
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["foo", "bar", "bogus"]);
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]);
+ executeAndCheckQueryResults(query, opts, []);
+ });
+});
+
+add_task(async function nonexistent_tags() {
+ info("Querying on nonexistent tag should return no results");
+ await task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["bogus"]);
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["bogus", "gnarly"]);
+ executeAndCheckQueryResults(query, opts, []);
+ });
+});
+
+add_task(async function tagsAreNot() {
+ info("Querying bookmarks using tagsAreNot should work correctly");
+ var urisAndTags = {
+ "http://example.com/1": ["foo", "bar"],
+ "http://example.com/2": ["baz", "qux"],
+ "http://example.com/3": null,
+ };
+
+ info("Add bookmarks and tag the URIs");
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ await addBookmark(nsiuri);
+ if (tags) {
+ PlacesUtils.tagging.tagURI(nsiuri, tags);
+ }
+ }
+
+ info(' Querying for "foo" should match only /2 and /3');
+ var [query, opts] = makeQuery(["foo"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
+ "http://example.com/2",
+ "http://example.com/3",
+ ]);
+
+ info(' Querying for "foo" and "bar" should match only /2 and /3');
+ [query, opts] = makeQuery(["foo", "bar"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
+ "http://example.com/2",
+ "http://example.com/3",
+ ]);
+
+ info(' Querying for "foo" and "bogus" should match only /2 and /3');
+ [query, opts] = makeQuery(["foo", "bogus"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
+ "http://example.com/2",
+ "http://example.com/3",
+ ]);
+
+ info(' Querying for "foo" and "baz" should match only /3');
+ [query, opts] = makeQuery(["foo", "baz"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
+ "http://example.com/3",
+ ]);
+
+ info(' Querying for "bogus" should match all');
+ [query, opts] = makeQuery(["bogus"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
+ "http://example.com/1",
+ "http://example.com/2",
+ "http://example.com/3",
+ ]);
+
+ // Clean up.
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ if (tags) {
+ PlacesUtils.tagging.untagURI(nsiuri, tags);
+ }
+ }
+ await task_cleanDatabase();
+});
+
+add_task(async function duplicate_tags() {
+ info(
+ "Duplicate existing tags (i.e., multiple tag folders with " +
+ "same name) should not throw off query results"
+ );
+ var tagName = "foo";
+
+ info("Add bookmark and tag it normally");
+ await addBookmark(TEST_URI);
+ PlacesUtils.tagging.tagURI(TEST_URI, [tagName]);
+
+ info("Manually create tag folder with same name as tag and insert bookmark");
+ let dupTag = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.tagsGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: tagName,
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: dupTag.guid,
+ title: "title",
+ url: TEST_URI,
+ });
+
+ info("Querying for tag should match URI");
+ var [query, opts] = makeQuery([tagName]);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
+ TEST_URI.spec,
+ ]);
+
+ PlacesUtils.tagging.untagURI(TEST_URI, [tagName]);
+ await task_cleanDatabase();
+});
+
+add_task(async function folder_named_as_tag() {
+ info(
+ "Regular folders with the same name as tag should not throw " +
+ "off query results"
+ );
+ var tagName = "foo";
+
+ info("Add bookmark and tag it");
+ await addBookmark(TEST_URI);
+ PlacesUtils.tagging.tagURI(TEST_URI, [tagName]);
+
+ info("Create folder with same name as tag");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: tagName,
+ });
+
+ info("Querying for tag should match URI");
+ var [query, opts] = makeQuery([tagName]);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
+ TEST_URI.spec,
+ ]);
+
+ PlacesUtils.tagging.untagURI(TEST_URI, [tagName]);
+ await task_cleanDatabase();
+});
+
+add_task(async function ORed_queries() {
+ info("Multiple queries ORed together should work");
+ var urisAndTags = {
+ "http://example.com/1": [],
+ "http://example.com/2": [],
+ };
+
+ // Search with lots of tags to make sure tag parameter substitution in SQL
+ // can handle it with more than one query.
+ for (let i = 0; i < 11; i++) {
+ urisAndTags["http://example.com/1"].push("/1 tag " + i);
+ urisAndTags["http://example.com/2"].push("/2 tag " + i);
+ }
+
+ info("Add bookmarks and tag the URIs");
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ await addBookmark(nsiuri);
+ if (tags) {
+ PlacesUtils.tagging.tagURI(nsiuri, tags);
+ }
+ }
+
+ // Clean up.
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ if (tags) {
+ PlacesUtils.tagging.untagURI(nsiuri, tags);
+ }
+ }
+ await task_cleanDatabase();
+});
+
+add_task(async function tag_casing() {
+ info(
+ "Querying on associated tags should return " +
+ "correct results irrespective of casing of tags."
+ );
+ await task_doWithBookmark(["fOo", "bAr"], function (aURI) {
+ let [query, opts] = makeQuery(["Foo"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["Foo", "Bar"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["Foo"], true);
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["Bogus"], true);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(async function tag_casing_l10n() {
+ info(
+ "Querying on associated tags should return " +
+ "correct results irrespective of casing of tags with international strings."
+ );
+ // \u041F is a lowercase \u043F
+ await task_doWithBookmark(
+ ["\u041F\u0442\u0438\u0446\u044B"],
+ function (aURI) {
+ let [query, opts] = makeQuery(["\u041F\u0442\u0438\u0446\u044B"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["\u043F\u0442\u0438\u0446\u044B"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ }
+ );
+ await task_doWithBookmark(
+ ["\u043F\u0442\u0438\u0446\u044B"],
+ function (aURI) {
+ let [query, opts] = makeQuery(["\u041F\u0442\u0438\u0446\u044B"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["\u043F\u0442\u0438\u0446\u044B"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ }
+ );
+});
+
+add_task(async function tag_special_char() {
+ info(
+ "Querying on associated tags should return " +
+ "correct results even if tags contain special characters."
+ );
+ await task_doWithBookmark(["Space ☺️ Between"], function (aURI) {
+ let [query, opts] = makeQuery(["Space ☺️ Between"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["Space ☺️ Between"], true);
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["Bogus"], true);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+// The tag keys in query URIs, i.e., "place:tag=foo&!tags=1"
+// --- -----
+const QUERY_KEY_TAG = "tag";
+const QUERY_KEY_NOT_TAGS = "!tags";
+
+const TEST_URI = uri("http://example.com/");
+
+/**
+ * Adds a bookmark.
+ *
+ * @param aURI
+ * URI of the page (an nsIURI)
+ */
+function addBookmark(aURI) {
+ return PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: aURI.spec,
+ url: aURI,
+ });
+}
+
+/**
+ * Asynchronous task that removes all pages from history and bookmarks.
+ */
+async function task_cleanDatabase(aCallback) {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Sets up a query with the specified tags, converts it to a URI, and makes sure
+ * the URI is what we expect it to be.
+ *
+ * @param aTags
+ * The query's tags will be set to those in this array
+ * @param aTagsAreNot
+ * The query's tagsAreNot property will be set to this
+ */
+function checkQueryURI(aTags, aTagsAreNot) {
+ var pairs = (aTags || []).sort().map(t => QUERY_KEY_TAG + "=" + encodeTag(t));
+ if (aTagsAreNot) {
+ pairs.push(QUERY_KEY_NOT_TAGS + "=1");
+ }
+ var expURI = "place:" + pairs.join("&");
+ var [query, opts] = makeQuery(aTags, aTagsAreNot);
+ var actualURI = queryURI(query, opts);
+ info("Query URI should be what we expect for the given tags");
+ Assert.equal(actualURI, expURI);
+}
+
+/**
+ * Asynchronous task that executes a callback task in a "scoped" database state.
+ * A bookmark is added and tagged before the callback is called, and afterward
+ * the database is cleared.
+ *
+ * @param aTags
+ * A bookmark will be added and tagged with this array of tags
+ * @param aCallback
+ * A task function that will be called after the bookmark has been tagged
+ */
+async function task_doWithBookmark(aTags, aCallback) {
+ await addBookmark(TEST_URI);
+ PlacesUtils.tagging.tagURI(TEST_URI, aTags);
+ await aCallback(TEST_URI);
+ PlacesUtils.tagging.untagURI(TEST_URI, aTags);
+ await task_cleanDatabase();
+}
+
+/**
+ * queryToQueryString() encodes every character in the query URI that doesn't
+ * match /[a-zA-Z]/. There's no simple JavaScript function that does the same,
+ * but encodeURIComponent() comes close, only missing some punctuation. This
+ * function takes care of all of that.
+ *
+ * @param aTag
+ * A tag name to encode
+ * @return A UTF-8 escaped string suitable for inclusion in a query URI
+ */
+function encodeTag(aTag) {
+ return encodeURIComponent(aTag).replace(
+ /[-_.!~*'()]/g, // '
+ s => "%" + s.charCodeAt(0).toString(16)
+ );
+}
+
+/**
+ * Executes the given query and compares the results to the given URIs.
+ * See queryResultsAre().
+ *
+ * @param aQuery
+ * An nsINavHistoryQuery
+ * @param aQueryOpts
+ * An nsINavHistoryQueryOptions
+ * @param aExpectedURIs
+ * Array of URIs (as strings) that aResultRoot should contain
+ */
+function executeAndCheckQueryResults(aQuery, aQueryOpts, aExpectedURIs) {
+ var root = PlacesUtils.history.executeQuery(aQuery, aQueryOpts).root;
+ root.containerOpen = true;
+ queryResultsAre(root, aExpectedURIs);
+ root.containerOpen = false;
+}
+
+/**
+ * Returns new query and query options objects. The query's tags will be
+ * set to aTags. aTags may be null, in which case setTags() is not called at
+ * all on the query.
+ *
+ * @param aTags
+ * The query's tags will be set to those in this array
+ * @param aTagsAreNot
+ * The query's tagsAreNot property will be set to this
+ * @return [query, queryOptions]
+ */
+function makeQuery(aTags, aTagsAreNot) {
+ aTagsAreNot = !!aTagsAreNot;
+ info(
+ "Making a query " +
+ (aTags
+ ? "with tags " + aTags.toSource()
+ : "without calling setTags() at all") +
+ " and with tagsAreNot=" +
+ aTagsAreNot
+ );
+ var query = PlacesUtils.history.getNewQuery();
+ query.tagsAreNot = aTagsAreNot;
+ if (aTags) {
+ query.tags = aTags;
+ var uniqueTags = [];
+ aTags.forEach(function (t) {
+ if (typeof t === "string" && !uniqueTags.includes(t)) {
+ uniqueTags.push(t);
+ }
+ });
+ uniqueTags.sort();
+ }
+
+ info("Made query should be correct for tags and tagsAreNot");
+ if (uniqueTags) {
+ setsAreEqual(query.tags, uniqueTags, true);
+ }
+ var expCount = uniqueTags ? uniqueTags.length : 0;
+ Assert.equal(query.tags.length, expCount);
+ Assert.equal(query.tagsAreNot, aTagsAreNot);
+
+ return [query, PlacesUtils.history.getNewQueryOptions()];
+}
+
+/**
+ * Ensures that the URIs of aResultRoot are the same as those in aExpectedURIs.
+ *
+ * @param aResultRoot
+ * The nsINavHistoryContainerResultNode root of an nsINavHistoryResult
+ * @param aExpectedURIs
+ * Array of URIs (as strings) that aResultRoot should contain
+ */
+function queryResultsAre(aResultRoot, aExpectedURIs) {
+ var rootWasOpen = aResultRoot.containerOpen;
+ if (!rootWasOpen) {
+ aResultRoot.containerOpen = true;
+ }
+ var actualURIs = [];
+ for (let i = 0; i < aResultRoot.childCount; i++) {
+ actualURIs.push(aResultRoot.getChild(i).uri);
+ }
+ setsAreEqual(actualURIs, aExpectedURIs);
+ if (!rootWasOpen) {
+ aResultRoot.containerOpen = false;
+ }
+}
+
+/**
+ * Converts the given query into its query URI.
+ *
+ * @param aQuery
+ * An nsINavHistoryQuery
+ * @param aQueryOpts
+ * An nsINavHistoryQueryOptions
+ * @return The query's URI
+ */
+function queryURI(aQuery, aQueryOpts) {
+ return PlacesUtils.history.queryToQueryString(aQuery, aQueryOpts);
+}
+
+/**
+ * Ensures that the arrays contain the same elements and, optionally, in the
+ * same order.
+ */
+function setsAreEqual(aArr1, aArr2, aIsOrdered) {
+ Assert.equal(aArr1.length, aArr2.length);
+ if (aIsOrdered) {
+ for (let i = 0; i < aArr1.length; i++) {
+ Assert.equal(aArr1[i], aArr2[i]);
+ }
+ } else {
+ aArr1.forEach(u => Assert.ok(aArr2.includes(u)));
+ aArr2.forEach(u => Assert.ok(aArr1.includes(u)));
+ }
+}
diff --git a/toolkit/components/places/tests/queries/test_transitions.js b/toolkit/components/places/tests/queries/test_transitions.js
new file mode 100644
index 0000000000..3055f28e9f
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_transitions.js
@@ -0,0 +1,175 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ * ***** END LICENSE BLOCK ***** */
+var testData = [
+ {
+ isVisit: true,
+ title: "page 0",
+ uri: "http://mozilla.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ isVisit: true,
+ title: "page 1",
+ uri: "http://google.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ },
+ {
+ isVisit: true,
+ title: "page 2",
+ uri: "http://microsoft.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ },
+ {
+ isVisit: true,
+ title: "page 3",
+ uri: "http://en.wikipedia.org/",
+ transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ },
+ {
+ isVisit: true,
+ title: "page 4",
+ uri: "http://fr.wikipedia.org/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ },
+ {
+ isVisit: true,
+ title: "page 5",
+ uri: "http://apple.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ isVisit: true,
+ title: "page 6",
+ uri: "http://campus-bike-store.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ },
+ {
+ isVisit: true,
+ title: "page 7",
+ uri: "http://uwaterloo.ca/",
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ isVisit: true,
+ title: "page 8",
+ uri: "http://pugcleaner.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ },
+ {
+ isVisit: true,
+ title: "page 9",
+ uri: "http://de.wikipedia.org/",
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ isVisit: true,
+ title: "arewefastyet",
+ uri: "http://arewefastyet.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ },
+ {
+ isVisit: true,
+ title: "arewefastyet",
+ uri: "http://arewefastyet.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ },
+];
+// sets of indices of testData array by transition type
+var testDataTyped = [0, 5, 7, 9];
+var testDataDownload = [1, 2, 4, 6, 10];
+var testDataBookmark = [3, 8, 11];
+
+add_task(async function test_transitions() {
+ let timeNow = Date.now();
+ for (let item of testData) {
+ await PlacesTestUtils.addVisits({
+ uri: uri(item.uri),
+ transition: item.transType,
+ visitDate: timeNow++ * 1000,
+ title: item.title,
+ });
+ }
+
+ // dump_table("moz_places");
+ // dump_table("moz_historyvisits");
+
+ var numSortFunc = function (a, b) {
+ return a - b;
+ };
+ var arrs = testDataTyped
+ .concat(testDataDownload)
+ .concat(testDataBookmark)
+ .sort(numSortFunc);
+
+ // Four tests which compare the result of a query to an expected set.
+ var data = arrs.filter(function (index) {
+ return (
+ testData[index].uri.match(/arewefastyet\.com/) &&
+ testData[index].transType == Ci.nsINavHistoryService.TRANSITION_DOWNLOAD
+ );
+ });
+
+ compareQueryToTestData(
+ "place:domain=arewefastyet.com&transition=" +
+ Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ data.slice()
+ );
+
+ compareQueryToTestData(
+ "place:transition=" + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ testDataDownload.slice()
+ );
+
+ compareQueryToTestData(
+ "place:transition=" + Ci.nsINavHistoryService.TRANSITION_TYPED,
+ testDataTyped.slice()
+ );
+
+ compareQueryToTestData(
+ "place:transition=" +
+ Ci.nsINavHistoryService.TRANSITION_DOWNLOAD +
+ "&transition=" +
+ Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ data
+ );
+
+ // Tests the live update property of transitions.
+ var query = {};
+ var options = {};
+ PlacesUtils.history.queryStringToQuery(
+ "place:transition=" + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ query,
+ options
+ );
+ var result = PlacesUtils.history.executeQuery(query.value, options.value);
+ var root = result.root;
+ root.containerOpen = true;
+ Assert.equal(testDataDownload.length, root.childCount);
+ await PlacesTestUtils.addVisits({
+ uri: uri("http://getfirefox.com"),
+ transition: TRANSITION_DOWNLOAD,
+ });
+ Assert.equal(testDataDownload.length + 1, root.childCount);
+ root.containerOpen = false;
+});
+
+/*
+ * Takes a query and a set of indices. The indices correspond to elements
+ * of testData that are the result of the query.
+ */
+function compareQueryToTestData(queryStr, data) {
+ var query = {};
+ var options = {};
+ PlacesUtils.history.queryStringToQuery(queryStr, query, options);
+ var result = PlacesUtils.history.executeQuery(query.value, options.value);
+ var root = result.root;
+ for (var i = 0; i < data.length; i++) {
+ data[i] = testData[data[i]];
+ data[i].isInQuery = true;
+ }
+ compareArrayToResult(data, root);
+}
diff --git a/toolkit/components/places/tests/queries/xpcshell.toml b/toolkit/components/places/tests/queries/xpcshell.toml
new file mode 100644
index 0000000000..b171df8c00
--- /dev/null
+++ b/toolkit/components/places/tests/queries/xpcshell.toml
@@ -0,0 +1,57 @@
+[DEFAULT]
+head = "head_queries.js"
+skip-if = ["os == 'android'"]
+
+["test_async.js"]
+
+["test_bookmarks.js"]
+
+["test_containersQueries_sorting.js"]
+
+["test_downloadHistory_liveUpdate.js"]
+
+["test_excludeQueries.js"]
+
+["test_history_queries_tags_liveUpdate.js"]
+
+["test_history_queries_titles_liveUpdate.js"]
+
+["test_options_inherit.js"]
+
+["test_queryMultipleFolder.js"]
+
+["test_querySerialization.js"]
+
+["test_query_uri_liveupdate.js"]
+
+["test_redirects.js"]
+
+["test_result_observeHistoryDetails.js"]
+
+["test_results-as-left-pane.js"]
+
+["test_results-as-roots.js"]
+
+["test_results-as-tag-query.js"]
+
+["test_results-as-visit.js"]
+
+["test_searchTerms_includeHidden.js"]
+
+["test_searchTerms_time.js"]
+
+["test_search_tags.js"]
+
+["test_searchterms-bookmarklets.js"]
+
+["test_searchterms-domain.js"]
+
+["test_searchterms-uri.js"]
+
+["test_sort-date-site-grouping.js"]
+
+["test_sorting.js"]
+
+["test_tags.js"]
+
+["test_transitions.js"]