Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* -*- 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 () {
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 () {
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"],
};
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, [
]);
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, [
]);
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, [
]);
info(' Querying for "foo" and "baz" should match only /3');
[query, opts] = makeQuery(["foo", "baz"], true);
queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
]);
info(' Querying for "bogus" should match all');
[query, opts] = makeQuery(["bogus"], true);
queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [
]);
// 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 = {
};
// 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() {
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)));
}
}