From d6090a825fa44657d3e6e0476fe102510d9fd928 Mon Sep 17 00:00:00 2001 From: "sspitzer@mozilla.org" Date: Sat, 24 Nov 2007 18:38:17 -0800 Subject: [PATCH] fix for bug #395452: handle multiple tags in url bar autocomplete fix for bug #395462: handle multiple tags in bookmark search fix for bug #403847: places organizer search won't find items tagged with multi-word tags fix for bug #403849: fix URIHasTag() to take a string, not a nsIURI r=dietrich, a=blocking-firefox-3+ --- .../components/places/src/nsNavHistory.cpp | 109 ++++++++++-- toolkit/components/places/src/nsNavHistory.h | 4 +- .../places/src/nsNavHistoryAutoComplete.cpp | 105 +++++++++-- .../unit/test_history_autocomplete_tags.js | 163 ++++++++++++++++++ .../places/tests/unit/test_multi_word_tags.js | 162 +++++++++++++++++ 5 files changed, 506 insertions(+), 37 deletions(-) create mode 100755 toolkit/components/places/tests/unit/test_history_autocomplete_tags.js create mode 100644 toolkit/components/places/tests/unit/test_multi_word_tags.js diff --git a/toolkit/components/places/src/nsNavHistory.cpp b/toolkit/components/places/src/nsNavHistory.cpp index 9baf9955e83f..9fe10e96b82a 100644 --- a/toolkit/components/places/src/nsNavHistory.cpp +++ b/toolkit/components/places/src/nsNavHistory.cpp @@ -4237,23 +4237,17 @@ nsNavHistory::GroupByFolder(nsNavHistoryQueryResultNode *aResultNode, } PRBool -nsNavHistory::URIHasTag(nsIURI* aURI, const nsAString& aTag) +nsNavHistory::URIHasTag(const nsACString& aURISpec, const nsAString& aTag) { mozStorageStatementScoper scoper(mDBURIHasTag); - nsCAutoString spec; - nsresult rv = aURI->GetSpec(spec); - NS_ENSURE_SUCCESS(rv, rv); - rv = mDBURIHasTag->BindUTF8StringParameter(0, spec); + nsresult rv = mDBURIHasTag->BindUTF8StringParameter(0, aURISpec); NS_ENSURE_SUCCESS(rv, rv); rv = mDBURIHasTag->BindStringParameter(1, aTag); NS_ENSURE_SUCCESS(rv, rv); - nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService(); - NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY); - PRInt64 tagsFolder = GetTagsFolder(); - rv = mDBURIHasTag->BindInt64Parameter(2, tagsFolder); + rv = mDBURIHasTag->BindInt64Parameter(2, GetTagsFolder()); NS_ENSURE_SUCCESS(rv, rv); PRBool hasTag = PR_FALSE; @@ -4262,6 +4256,87 @@ nsNavHistory::URIHasTag(nsIURI* aURI, const nsAString& aTag) return hasTag; } +void +nsNavHistory::CreateTermsFromTokens(const nsStringArray& aTagTokens, nsStringArray &aTerms) +{ + PRUint32 tagTokensCount = aTagTokens.Count(); + + // from our tokens, build up all possible tags + // for example: ("a b c") -> ("a","b","c","a b","b c","a b c") + for (PRUint32 numCon = 1; numCon <= tagTokensCount; numCon++) { + for (PRUint32 i = 0; i < tagTokensCount; i++) { + if (i + numCon > tagTokensCount) + continue; + + // after certain number of tokens (30 if SQLITE_MAX_EXPR_DEPTH is the default of 1000) + // we'll generate a query with an expression tree that is too large. + // if we exceed this limit, CreateStatement() will fail. + // 30 tokens == 465 terms + if (aTerms.Count() == 465) { + NS_WARNING("hitting SQLITE_MAX_EXPR_DEPTH, not generating any more terms"); + return; + } + + nsAutoString currentValue; + for (PRUint32 j = i; j < i + numCon; j++) { + if (!currentValue.IsEmpty()) + currentValue += NS_LITERAL_STRING(" "); + currentValue += *(aTagTokens.StringAt(j)); + } + + aTerms.AppendString(currentValue); + } + } +} + +PRBool +nsNavHistory::URIHasAnyTagFromTerms(const nsACString& aURISpec, const nsStringArray& aTerms) +{ + PRUint32 termsCount = aTerms.Count(); + + if (termsCount == 1) + return URIHasTag(aURISpec, *(aTerms.StringAt(0))); + + nsCString tagQuery = NS_LITERAL_CSTRING( + "SELECT b.id FROM moz_bookmarks b " + "JOIN moz_places p ON b.fk = p.id " + "WHERE p.url = ?1 " + "AND (SELECT b1.parent FROM moz_bookmarks b1 WHERE " + "b1.id = b.parent AND ("); + + for (PRUint32 i=0; i uriHasAnyTagQuery; + + nsresult rv = mDBConn->CreateStatement(tagQuery, getter_AddRefs(uriHasAnyTagQuery)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = uriHasAnyTagQuery->BindUTF8StringParameter(0, aURISpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = uriHasAnyTagQuery->BindInt64Parameter(1, GetTagsFolder()); + NS_ENSURE_SUCCESS(rv, rv); + + for (PRUint32 i=0; iBindStringParameter(i+2, *(aTerms.StringAt(i))); + NS_ENSURE_SUCCESS(rv, rv); + } + + PRBool hasAnyTag = PR_FALSE; + rv = uriHasAnyTagQuery->ExecuteStep(&hasAnyTag); + NS_ENSURE_SUCCESS(rv, rv); + return hasAnyTag; +} // nsNavHistory::FilterResultSet // @@ -4404,6 +4479,9 @@ nsNavHistory::FilterResultSet(nsNavHistoryQueryResultNode* aQueryNode, } } + nsStringArray tagTerms; + CreateTermsFromTokens(*terms[queryIndex], tagTerms); + // search terms // XXXmano/dietrich: when bug 331487 is fixed, bookmark queries can group // by folder or not regardless of specified folders or search terms. @@ -4419,19 +4497,12 @@ nsNavHistory::FilterResultSet(nsNavHistoryQueryResultNode* aQueryNode, NS_ConvertUTF8toUTF16(aSet[nodeIndex]->mURI)))) termFound = PR_TRUE; - // tags - if (!termFound) { - nsCOMPtr itemURI; - rv = NS_NewURI(getter_AddRefs(itemURI), aSet[nodeIndex]->mURI); - NS_ENSURE_SUCCESS(rv, rv); - termFound = URIHasTag(itemURI, *terms[queryIndex]->StringAt(termIndex)); - } - if (!termFound) allTermsFound = PR_FALSE; } - if (!allTermsFound) - continue; + + if (!allTermsFound && !URIHasAnyTagFromTerms(aSet[nodeIndex]->mURI, tagTerms)) + continue; appendNode = PR_TRUE; } diff --git a/toolkit/components/places/src/nsNavHistory.h b/toolkit/components/places/src/nsNavHistory.h index be0f2e4a683e..49e9f4dea17a 100644 --- a/toolkit/components/places/src/nsNavHistory.h +++ b/toolkit/components/places/src/nsNavHistory.h @@ -524,7 +524,9 @@ protected: const nsCOMArray& aSource, nsCOMArray* aDest); - PRBool URIHasTag(nsIURI* aURI, const nsAString& aTag); + PRBool URIHasTag(const nsACString& aURISpec, const nsAString& aTag); + PRBool URIHasAnyTagFromTerms(const nsACString& aURISpec, const nsStringArray& aTerms); + void CreateTermsFromTokens(const nsStringArray& aTagTokens, nsStringArray& aTerms); nsresult FilterResultSet(nsNavHistoryQueryResultNode *aParentNode, const nsCOMArray& aSet, diff --git a/toolkit/components/places/src/nsNavHistoryAutoComplete.cpp b/toolkit/components/places/src/nsNavHistoryAutoComplete.cpp index 9fb37c6325db..505ed71df0ed 100644 --- a/toolkit/components/places/src/nsNavHistoryAutoComplete.cpp +++ b/toolkit/components/places/src/nsNavHistoryAutoComplete.cpp @@ -62,6 +62,7 @@ #include "nsFaviconService.h" #include "nsUnicharUtils.h" #include "nsNavBookmarks.h" +#include "nsPrintfCString.h" #define NS_AUTOCOMPLETESIMPLERESULT_CONTRACTID \ "@mozilla.org/autocomplete/simple-result;1" @@ -124,7 +125,7 @@ nsNavHistory::CreateAutoCompleteQueries() "LEFT OUTER JOIN moz_historyvisits v ON h.id = v.place_id " "LEFT OUTER JOIN moz_favicons f ON h.favicon_id = f.id " "WHERE " - "(b.parent = (SELECT t.id FROM moz_bookmarks t WHERE t.parent = ?1 and t.title LIKE ?2 ESCAPE '/')) " + "(b.parent = (SELECT t.id FROM moz_bookmarks t WHERE t.parent = ?1 AND LOWER(t.title) = LOWER(?2))) " "GROUP BY h.id ORDER BY h.visit_count DESC, MAX(v.visit_date) DESC;"); rv = mDBConn->CreateStatement(sql, getter_AddRefs(mDBTagAutoCompleteQuery)); NS_ENSURE_SUCCESS(rv, rv); @@ -384,6 +385,7 @@ nsNavHistory::StartSearch(const nsAString & aSearchString, else if (!mCurrentSearchString.IsEmpty()) { // reset to mCurrentChunkEndTime mCurrentChunkEndTime = PR_Now(); + mCurrentOldestVisit = 0; mFirstChunk = PR_TRUE; // determine our earliest visit @@ -400,7 +402,8 @@ nsNavHistory::StartSearch(const nsAString & aSearchString, rv = dbSelectStatement->GetInt64(0, &mCurrentOldestVisit); NS_ENSURE_SUCCESS(rv, rv); } - else { + + if (!mCurrentOldestVisit) { // if we have no visits, use a reasonable value mCurrentOldestVisit = PR_Now() - USECS_PER_DAY; } @@ -488,42 +491,110 @@ nsresult nsNavHistory::AutoCompleteTypedSearch() nsresult nsNavHistory::AutoCompleteTagsSearch() { - mozStorageStatementScoper scope(mDBTagAutoCompleteQuery); - nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService(); NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY); + nsresult rv; PRInt64 tagsFolder = GetTagsFolder(); - nsresult rv = mDBTagAutoCompleteQuery->BindInt64Parameter(0, tagsFolder); - NS_ENSURE_SUCCESS(rv, rv); + nsString::const_iterator strStart, strEnd; + mCurrentSearchString.BeginReading(strStart); + mCurrentSearchString.EndReading(strEnd); + nsString::const_iterator start = strStart, end = strEnd; - nsString escapedSearchString; - rv = mDBTagAutoCompleteQuery->EscapeStringForLIKE(mCurrentSearchString, PRUnichar('/'), escapedSearchString); - NS_ENSURE_SUCCESS(rv, rv); + nsStringArray tagTokens; - rv = mDBTagAutoCompleteQuery->BindStringParameter(1, escapedSearchString); - NS_ENSURE_SUCCESS(rv, rv); + // check if we have any delimiters + while (FindInReadable(NS_LITERAL_STRING(" "), start, end, + nsDefaultStringComparator())) { + nsAutoString currentMatch(Substring(strStart, start)); + currentMatch.Trim("\r\n\t\b"); + if (!currentMatch.IsEmpty()) + tagTokens.AppendString(currentMatch); + strStart = start = end; + end = strEnd; + } + + nsCOMPtr tagAutoCompleteQuery; + + // we didn't find any spaces, so we only have one possible tag, which is + // the search string. this is the common case, so we use + // our pre-compiled query + if (!tagTokens.Count()) { + tagAutoCompleteQuery = mDBTagAutoCompleteQuery; + + rv = tagAutoCompleteQuery->BindInt64Parameter(0, tagsFolder); + NS_ENSURE_SUCCESS(rv, rv); + + rv = tagAutoCompleteQuery->BindStringParameter(1, mCurrentSearchString); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + // add in the last match (if it is non-empty) + nsAutoString lastMatch(Substring(strStart, strEnd)); + lastMatch.Trim("\r\n\t\b"); + if (!lastMatch.IsEmpty()) + tagTokens.AppendString(lastMatch); + + nsCString tagQuery = NS_LITERAL_CSTRING( + "SELECT h.url, h.title, f.url, b.id, b.parent " + "FROM moz_places h " + "JOIN moz_bookmarks b ON b.fk = h.id " + "LEFT OUTER JOIN moz_historyvisits v ON h.id = v.place_id " + "LEFT OUTER JOIN moz_favicons f ON h.favicon_id = f.id " + "WHERE " + "(b.parent in " + " (SELECT t.id FROM moz_bookmarks t WHERE t.parent = ?1 AND ("); + + nsStringArray terms; + CreateTermsFromTokens(tagTokens, terms); + + for (PRUint32 i=0; iCreateStatement(tagQuery, getter_AddRefs(tagAutoCompleteQuery)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = tagAutoCompleteQuery->BindInt64Parameter(0, tagsFolder); + NS_ENSURE_SUCCESS(rv, rv); + + for (PRUint32 i=0; iBindStringParameter(i+1, *(terms.StringAt(i))); + NS_ENSURE_SUCCESS(rv, rv); + } + } nsFaviconService* faviconService = nsFaviconService::GetFaviconService(); NS_ENSURE_TRUE(faviconService, NS_ERROR_OUT_OF_MEMORY); + mozStorageStatementScoper scope(tagAutoCompleteQuery); + PRBool hasMore = PR_FALSE; // Determine the result of the search - while (NS_SUCCEEDED(mDBTagAutoCompleteQuery->ExecuteStep(&hasMore)) && hasMore) { + while (NS_SUCCEEDED(tagAutoCompleteQuery->ExecuteStep(&hasMore)) && hasMore) { nsAutoString entryURL, entryTitle, entryFavicon; - rv = mDBTagAutoCompleteQuery->GetString(kAutoCompleteIndex_URL, entryURL); + rv = tagAutoCompleteQuery->GetString(kAutoCompleteIndex_URL, entryURL); NS_ENSURE_SUCCESS(rv, rv); - rv = mDBTagAutoCompleteQuery->GetString(kAutoCompleteIndex_Title, entryTitle); + rv = tagAutoCompleteQuery->GetString(kAutoCompleteIndex_Title, entryTitle); NS_ENSURE_SUCCESS(rv, rv); - rv = mDBTagAutoCompleteQuery->GetString(kAutoCompleteIndex_FaviconURL, entryFavicon); + rv = tagAutoCompleteQuery->GetString(kAutoCompleteIndex_FaviconURL, entryFavicon); NS_ENSURE_SUCCESS(rv, rv); PRInt64 itemId = 0; - rv = mDBTagAutoCompleteQuery->GetInt64(kAutoCompleteIndex_ItemId, &itemId); + rv = tagAutoCompleteQuery->GetInt64(kAutoCompleteIndex_ItemId, &itemId); NS_ENSURE_SUCCESS(rv, rv); PRInt64 parentId = 0; - rv = mDBTagAutoCompleteQuery->GetInt64(kAutoCompleteIndex_ParentId, &parentId); + rv = tagAutoCompleteQuery->GetInt64(kAutoCompleteIndex_ParentId, &parentId); NS_ENSURE_SUCCESS(rv, rv); PRBool dummy; diff --git a/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js b/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js new file mode 100755 index 000000000000..b88bbbb00bbd --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js @@ -0,0 +1,163 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bug 378079 unit test code. + * + * The Initial Developer of the Original Code is POTI Inc. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Matt Crocker + * Seth Spitzer + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var current_test = 0; + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + searches: null, + + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt: function(aIndex) { + return this.searches[aIndex]; + }, + + onSearchComplete: function() {}, + + popupOpen: false, + + popup: { + setSelectedIndex: function(aIndex) {}, + invalidate: function() {}, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompletePopup)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompleteInput)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +} catch(ex) { + do_throw("Could not get tagging service\n"); +} + +function ensure_tag_results(uris, searchTerm) +{ + var controller = Components.classes["@mozilla.org/autocomplete/controller;1"]. + getService(Components.interfaces.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete + var input = new AutoCompleteInput(["history"]); + + controller.input = input; + + // Search is asynchronous, so don't let the test finish immediately + do_test_pending(); + + input.onSearchComplete = function() { + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH); + do_check_eq(controller.matchCount, uris.length); + for (var i=0; i (Original Author) + * Seth Spitzer + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// Get history service +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +} catch(ex) { + do_throw("Could not get history service\n"); +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +} catch(ex) { + do_throw("Could not get tagging service\n"); +} + +// main +function run_test() { + var uri1 = uri("http://site.tld/1"); + var uri2 = uri("http://site.tld/2"); + var uri3 = uri("http://site.tld/3"); + var uri4 = uri("http://site.tld/4"); + var uri5 = uri("http://site.tld/5"); + var uri6 = uri("http://site.tld/6"); + + tagssvc.tagURI(uri1, ["foo"]); + tagssvc.tagURI(uri2, ["bar"]); + tagssvc.tagURI(uri3, ["cheese"]); + tagssvc.tagURI(uri4, ["foo bar"]); + tagssvc.tagURI(uri5, ["bar cheese"]); + tagssvc.tagURI(uri6, ["foo bar cheese"]); + + // exclude livemark items, search for "item", should get one result + var options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + var query = histsvc.getNewQuery(); + query.searchTerms = "foo"; + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + do_check_eq(root.getChild(0).uri, "http://site.tld/1"); + + query.searchTerms = "bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + do_check_eq(root.getChild(0).uri, "http://site.tld/2"); + + query.searchTerms = "cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + do_check_eq(root.getChild(0).uri, "http://site.tld/3"); + + query.searchTerms = "foo bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 3); + do_check_eq(root.getChild(0).uri, "http://site.tld/1"); + do_check_eq(root.getChild(1).uri, "http://site.tld/2"); + do_check_eq(root.getChild(2).uri, "http://site.tld/4"); + + query.searchTerms = "bar foo"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 2); + do_check_eq(root.getChild(0).uri, "http://site.tld/1"); + do_check_eq(root.getChild(1).uri, "http://site.tld/2"); + + query.searchTerms = "bar cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 3); + do_check_eq(root.getChild(0).uri, "http://site.tld/2"); + do_check_eq(root.getChild(1).uri, "http://site.tld/3"); + do_check_eq(root.getChild(2).uri, "http://site.tld/5"); + + query.searchTerms = "cheese bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 2); + do_check_eq(root.getChild(0).uri, "http://site.tld/2"); + do_check_eq(root.getChild(1).uri, "http://site.tld/3"); + + query.searchTerms = "foo bar cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 6); + do_check_eq(root.getChild(0).uri, "http://site.tld/1"); + do_check_eq(root.getChild(1).uri, "http://site.tld/2"); + do_check_eq(root.getChild(2).uri, "http://site.tld/3"); + do_check_eq(root.getChild(3).uri, "http://site.tld/4"); + do_check_eq(root.getChild(4).uri, "http://site.tld/5"); + do_check_eq(root.getChild(5).uri, "http://site.tld/6"); + + query.searchTerms = "cheese foo bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 4); + do_check_eq(root.getChild(0).uri, "http://site.tld/1"); + do_check_eq(root.getChild(1).uri, "http://site.tld/2"); + do_check_eq(root.getChild(2).uri, "http://site.tld/3"); + do_check_eq(root.getChild(3).uri, "http://site.tld/4"); + + query.searchTerms = "cheese bar foo"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 3); + do_check_eq(root.getChild(0).uri, "http://site.tld/1"); + do_check_eq(root.getChild(1).uri, "http://site.tld/2"); + do_check_eq(root.getChild(2).uri, "http://site.tld/3"); +}