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+

This commit is contained in:
sspitzer@mozilla.org 2007-11-24 18:38:17 -08:00
Родитель 809618b9cc
Коммит 2d64e1edf7
5 изменённых файлов: 506 добавлений и 37 удалений

Просмотреть файл

@ -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<termsCount; i++) {
if (i)
tagQuery += NS_LITERAL_CSTRING(" OR");
// +3 to skip over the "?2", which is the tag root parameter
tagQuery += NS_LITERAL_CSTRING(" LOWER(b1.title) = ") +
nsPrintfCString("LOWER(?%d)", i+3);
}
tagQuery += NS_LITERAL_CSTRING(")) = ?2 LIMIT 1");
nsCOMPtr<mozIStorageStatement> 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; i<termsCount; i++) {
// +2 to skip over the "?2", which is the tag root parameter
rv = uriHasAnyTagQuery->BindStringParameter(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<nsIURI> 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;
}

Просмотреть файл

@ -524,7 +524,9 @@ protected:
const nsCOMArray<nsNavHistoryResultNode>& aSource,
nsCOMArray<nsNavHistoryResultNode>* 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<nsNavHistoryResultNode>& aSet,

Просмотреть файл

@ -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<mozIStorageStatement> 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; i<terms.Count(); i++) {
if (i)
tagQuery += NS_LITERAL_CSTRING(" OR");
// +2 to skip over the "?1", which is the tag root parameter
tagQuery += NS_LITERAL_CSTRING(" LOWER(t.title) = ") +
nsPrintfCString("LOWER(?%d)", i+2);
}
tagQuery += NS_LITERAL_CSTRING("))) "
"GROUP BY h.id ORDER BY h.visit_count DESC, MAX(v.visit_date) DESC;");
rv = mDBConn->CreateStatement(tagQuery, getter_AddRefs(tagAutoCompleteQuery));
NS_ENSURE_SUCCESS(rv, rv);
rv = tagAutoCompleteQuery->BindInt64Parameter(0, tagsFolder);
NS_ENSURE_SUCCESS(rv, rv);
for (PRUint32 i=0; i<terms.Count(); i++) {
// +1 to skip over the "?1", which is the tag root parameter
rv = tagAutoCompleteQuery->BindStringParameter(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;

Просмотреть файл

@ -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 <matt@songbirdnest.com>
* Seth Spitzer <sspitzer@mozilla.org>
*
* 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<controller.matchCount; i++) {
do_check_eq(controller.getValueAt(i), uris[i].spec);
do_check_eq(controller.getStyleAt(i), "tag");
}
if (current_test < (tests.length - 1)) {
current_test++;
tests[current_test]();
}
do_test_finished();
};
controller.startSearch(searchTerm);
}
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");
var tests = [function() { ensure_tag_results([uri1], "foo"); },
function() { ensure_tag_results([uri2], "bar"); },
function() { ensure_tag_results([uri3], "cheese"); },
function() { ensure_tag_results([uri1, uri2, uri4], "foo bar"); },
function() { ensure_tag_results([uri1, uri2], "bar foo"); },
function() { ensure_tag_results([uri2, uri3, uri5], "bar cheese"); },
function() { ensure_tag_results([uri2, uri3], "cheese bar"); },
function() { ensure_tag_results([uri1, uri2, uri3, uri4, uri5, uri6], "foo bar cheese"); }];
/**
* Test history autocomplete
*/
function run_test() {
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"]);
tests[0]();
}

Просмотреть файл

@ -0,0 +1,162 @@
/* -*- 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 Places Tagging Service unit test code.
*
* The Initial Developer of the Original Code is
* Mozilla Corporation.
* Portions created by the Initial Developer are Copyright (C) 2007
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Asaf Romano <mano@mozilla.com> (Original Author)
* Seth Spitzer <sspitzer@mozilla.com>
*
* 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");
}