/* -*- 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 hs = Cc["@mozilla.org/browser/nav-history-service;1"]. getService(Ci.nsINavHistoryService); var bh = hs.QueryInterface(Ci.nsIBrowserHistory); var tagging = Cc["@mozilla.org/browser/tagging-service;1"]. getService(Ci.nsITaggingService); 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_TAG_QUERY, name: "RESULTS_AS_TAG_QUERY"}, ]; 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) { do_check_eq(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_TAG_QUERY && (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_TAG_QUERY) { // 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 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); // 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; } 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); do_check_eq(sortedResults[i].title, results[i].title); } } // Main function run_test() { run_next_test(); } add_task(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 })); yield task_populateDB(visits); cartProd([resultTypes, sortingModes], test_query_callback); });