summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/history
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/history')
-rw-r--r--toolkit/components/places/tests/history/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/history/head_history.js19
-rw-r--r--toolkit/components/places/tests/history/test_insert.js257
-rw-r--r--toolkit/components/places/tests/history/test_remove.js360
-rw-r--r--toolkit/components/places/tests/history/test_removeVisits.js316
-rw-r--r--toolkit/components/places/tests/history/test_removeVisitsByFilter.js345
-rw-r--r--toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js52
-rw-r--r--toolkit/components/places/tests/history/xpcshell.ini9
8 files changed, 1365 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/history/.eslintrc.js b/toolkit/components/places/tests/history/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/places/tests/history/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/history/head_history.js b/toolkit/components/places/tests/history/head_history.js
new file mode 100644
index 000000000..870802dc1
--- /dev/null
+++ b/toolkit/components/places/tests/history/head_history.js
@@ -0,0 +1,19 @@
+/* -*- 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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Import common head.
+{
+ let commonFile = do_get_file("../head_common.js", false);
+ let uri = Services.io.newFileURI(commonFile);
+ Services.scriptloader.loadSubScript(uri.spec, this);
+}
diff --git a/toolkit/components/places/tests/history/test_insert.js b/toolkit/components/places/tests/history/test_insert.js
new file mode 100644
index 000000000..e2884af8c
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_insert.js
@@ -0,0 +1,257 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+// Tests for `History.insert` and `History.insertMany`, as implemented in History.jsm
+
+"use strict";
+
+add_task(function* test_insert_error_cases() {
+ const TEST_URL = "http://mozilla.com";
+
+ Assert.throws(
+ () => PlacesUtils.history.insert(),
+ /TypeError: pageInfo must be an object/,
+ "passing a null into History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert(1),
+ /TypeError: pageInfo must be an object/,
+ "passing a non object into History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({}),
+ /TypeError: PageInfo object must have a url property/,
+ "passing an object without a url to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({url: 123}),
+ /TypeError: Invalid url or guid: 123/,
+ "passing an object with an invalid url to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({url: TEST_URL}),
+ /TypeError: PageInfo object must have an array of visits/,
+ "passing an object without a visits property to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({url: TEST_URL, visits: 1}),
+ /TypeError: PageInfo object must have an array of visits/,
+ "passing an object with a non-array visits property to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({url: TEST_URL, visits: []}),
+ /TypeError: PageInfo object must have an array of visits/,
+ "passing an object with an empty array as the visits property to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({
+ url: TEST_URL,
+ visits: [
+ {
+ transition: TRANSITION_LINK,
+ date: "a"
+ }
+ ]}),
+ /TypeError: Expected a Date, got a/,
+ "passing a visit object with an invalid date to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({
+ url: TEST_URL,
+ visits: [
+ {
+ transition: TRANSITION_LINK
+ },
+ {
+ transition: TRANSITION_LINK,
+ date: "a"
+ }
+ ]}),
+ /TypeError: Expected a Date, got a/,
+ "passing a second visit object with an invalid date to History.insert should throw a TypeError"
+ );
+ let futureDate = new Date();
+ futureDate.setDate(futureDate.getDate() + 1000);
+ Assert.throws(
+ () => PlacesUtils.history.insert({
+ url: TEST_URL,
+ visits: [
+ {
+ transition: TRANSITION_LINK,
+ date: futureDate,
+ }
+ ]}),
+ `TypeError: date: ${futureDate} is not a valid date`,
+ "passing a visit object with a future date to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({
+ url: TEST_URL,
+ visits: [
+ {transition: "a"}
+ ]}),
+ /TypeError: transition: a is not a valid transition type/,
+ "passing a visit object with an invalid transition to History.insert should throw a TypeError"
+ );
+});
+
+add_task(function* test_history_insert() {
+ const TEST_URL = "http://mozilla.com/";
+
+ let inserter = Task.async(function*(name, filter, referrer, date, transition) {
+ do_print(name);
+ do_print(`filter: ${filter}, referrer: ${referrer}, date: ${date}, transition: ${transition}`);
+
+ let uri = NetUtil.newURI(TEST_URL + Math.random());
+ let title = "Visit " + Math.random();
+
+ let pageInfo = {
+ title,
+ visits: [
+ {transition: transition, referrer: referrer, date: date, }
+ ]
+ };
+
+ pageInfo.url = yield filter(uri);
+
+ let result = yield PlacesUtils.history.insert(pageInfo);
+
+ Assert.ok(PlacesUtils.isValidGuid(result.guid), "guid for pageInfo object is valid");
+ Assert.equal(uri.spec, result.url.href, "url is correct for pageInfo object");
+ Assert.equal(title, result.title, "title is correct for pageInfo object");
+ Assert.equal(TRANSITION_LINK, result.visits[0].transition, "transition is correct for pageInfo object");
+ if (referrer) {
+ Assert.equal(referrer, result.visits[0].referrer.href, "url of referrer for visit is correct");
+ } else {
+ Assert.equal(null, result.visits[0].referrer, "url of referrer for visit is correct");
+ }
+ if (date) {
+ Assert.equal(Number(date),
+ Number(result.visits[0].date),
+ "date of visit is correct");
+ }
+
+ Assert.ok(yield PlacesTestUtils.isPageInDB(uri), "Page was added");
+ Assert.ok(yield PlacesTestUtils.visitsInDB(uri), "Visit was added");
+ });
+
+ try {
+ for (let referrer of [TEST_URL, null]) {
+ for (let date of [new Date(), null]) {
+ for (let transition of [TRANSITION_LINK, null]) {
+ yield inserter("Testing History.insert() with an nsIURI", x => x, referrer, date, transition);
+ yield inserter("Testing History.insert() with a string url", x => x.spec, referrer, date, transition);
+ yield inserter("Testing History.insert() with a URL object", x => new URL(x.spec), referrer, date, transition);
+ }
+ }
+ }
+ } finally {
+ yield PlacesTestUtils.clearHistory();
+ }
+});
+
+add_task(function* test_insert_multiple_error_cases() {
+ let validPageInfo = {
+ url: "http://mozilla.com",
+ visits: [
+ {transition: TRANSITION_LINK}
+ ]
+ };
+
+ Assert.throws(
+ () => PlacesUtils.history.insertMany(),
+ /TypeError: pageInfos must be an array/,
+ "passing a null into History.insertMany should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insertMany([]),
+ /TypeError: pageInfos may not be an empty array/,
+ "passing an empty array into History.insertMany should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insertMany([validPageInfo, {}]),
+ /TypeError: PageInfo object must have a url property/,
+ "passing a second invalid PageInfo object to History.insertMany should throw a TypeError"
+ );
+});
+
+add_task(function* test_history_insertMany() {
+ const BAD_URLS = ["about:config", "chrome://browser/content/browser.xul"];
+ const GOOD_URLS = [1, 2, 3].map(x => { return `http://mozilla.com/${x}`; });
+
+ let makePageInfos = Task.async(function*(urls, filter = x => x) {
+ let pageInfos = [];
+ for (let url of urls) {
+ let uri = NetUtil.newURI(url);
+
+ let pageInfo = {
+ title: `Visit to ${url}`,
+ visits: [
+ {transition: TRANSITION_LINK}
+ ]
+ };
+
+ pageInfo.url = yield filter(uri);
+ pageInfos.push(pageInfo);
+ }
+ return pageInfos;
+ });
+
+ let inserter = Task.async(function*(name, filter, useCallbacks) {
+ do_print(name);
+ do_print(`filter: ${filter}`);
+ do_print(`useCallbacks: ${useCallbacks}`);
+ yield PlacesTestUtils.clearHistory();
+
+ let result;
+ let allUrls = GOOD_URLS.concat(BAD_URLS);
+ let pageInfos = yield makePageInfos(allUrls, filter);
+
+ if (useCallbacks) {
+ let onResultUrls = [];
+ let onErrorUrls = [];
+ result = yield PlacesUtils.history.insertMany(pageInfos, pageInfo => {
+ let url = pageInfo.url.href;
+ Assert.ok(GOOD_URLS.includes(url), "onResult callback called for correct url");
+ onResultUrls.push(url);
+ Assert.equal(`Visit to ${url}`, pageInfo.title, "onResult callback provides the correct title");
+ Assert.ok(PlacesUtils.isValidGuid(pageInfo.guid), "onResult callback provides a valid guid");
+ }, pageInfo => {
+ let url = pageInfo.url.href;
+ Assert.ok(BAD_URLS.includes(url), "onError callback called for correct uri");
+ onErrorUrls.push(url);
+ Assert.equal(undefined, pageInfo.title, "onError callback provides the correct title");
+ Assert.equal(undefined, pageInfo.guid, "onError callback provides the expected guid");
+ });
+ Assert.equal(GOOD_URLS.sort().toString(), onResultUrls.sort().toString(), "onResult callback was called for each good url");
+ Assert.equal(BAD_URLS.sort().toString(), onErrorUrls.sort().toString(), "onError callback was called for each bad url");
+ } else {
+ result = yield PlacesUtils.history.insertMany(pageInfos);
+ }
+
+ Assert.equal(undefined, result, "insertMany returned undefined");
+
+ for (let url of allUrls) {
+ let expected = GOOD_URLS.includes(url);
+ Assert.equal(expected, yield PlacesTestUtils.isPageInDB(url), `isPageInDB for ${url} is ${expected}`);
+ Assert.equal(expected, yield PlacesTestUtils.visitsInDB(url), `visitsInDB for ${url} is ${expected}`);
+ }
+ });
+
+ try {
+ for (let useCallbacks of [false, true]) {
+ yield inserter("Testing History.insertMany() with an nsIURI", x => x, useCallbacks);
+ yield inserter("Testing History.insertMany() with a string url", x => x.spec, useCallbacks);
+ yield inserter("Testing History.insertMany() with a URL object", x => new URL(x.spec), useCallbacks);
+ }
+ // Test rejection when no items added
+ let pageInfos = yield makePageInfos(BAD_URLS);
+ PlacesUtils.history.insertMany(pageInfos).then(() => {
+ Assert.ok(false, "History.insertMany rejected promise with all bad URLs");
+ }, error => {
+ Assert.equal("No items were added to history.", error.message, "History.insertMany rejected promise with all bad URLs");
+ });
+ } finally {
+ yield PlacesTestUtils.clearHistory();
+ }
+});
diff --git a/toolkit/components/places/tests/history/test_remove.js b/toolkit/components/places/tests/history/test_remove.js
new file mode 100644
index 000000000..7423f6464
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_remove.js
@@ -0,0 +1,360 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+// Tests for `History.remove`, as implemented in History.jsm
+
+"use strict";
+
+Cu.importGlobalProperties(["URL"]);
+
+
+// Test removing a single page
+add_task(function* test_remove_single() {
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+
+ let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
+ yield PlacesTestUtils.addVisits(WITNESS_URI);
+ Assert.ok(page_in_database(WITNESS_URI));
+
+ let remover = Task.async(function*(name, filter, options) {
+ do_print(name);
+ do_print(JSON.stringify(options));
+ do_print("Setting up visit");
+
+ let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
+ let title = "Visit " + Math.random();
+ yield PlacesTestUtils.addVisits({uri: uri, title: title});
+ Assert.ok(visits_in_database(uri), "History entry created");
+
+ let removeArg = yield filter(uri);
+
+ if (options.addBookmark) {
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test bookmark");
+ }
+
+ let shouldRemove = !options.addBookmark;
+ let observer;
+ let promiseObserved = new Promise((resolve, reject) => {
+ observer = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function(aUri) {
+ reject(new Error("Unexpected call to onVisit " + aUri.spec));
+ },
+ onTitleChanged: function(aUri) {
+ reject(new Error("Unexpected call to onTitleChanged " + aUri.spec));
+ },
+ onClearHistory: function() {
+ reject("Unexpected call to onClearHistory");
+ },
+ onPageChanged: function(aUri) {
+ reject(new Error("Unexpected call to onPageChanged " + aUri.spec));
+ },
+ onFrecencyChanged: function(aURI) {
+ try {
+ Assert.ok(!shouldRemove, "Observing onFrecencyChanged");
+ Assert.equal(aURI.spec, uri.spec, "Observing effect on the right uri");
+ } finally {
+ resolve();
+ }
+ },
+ onManyFrecenciesChanged: function() {
+ try {
+ Assert.ok(!shouldRemove, "Observing onManyFrecenciesChanged");
+ } finally {
+ resolve();
+ }
+ },
+ onDeleteURI: function(aURI) {
+ try {
+ Assert.ok(shouldRemove, "Observing onDeleteURI");
+ Assert.equal(aURI.spec, uri.spec, "Observing effect on the right uri");
+ } finally {
+ resolve();
+ }
+ },
+ onDeleteVisits: function(aURI) {
+ Assert.equal(aURI.spec, uri.spec, "Observing onDeleteVisits on the right uri");
+ }
+ };
+ });
+ PlacesUtils.history.addObserver(observer, false);
+
+ do_print("Performing removal");
+ let removed = false;
+ if (options.useCallback) {
+ let onRowCalled = false;
+ let guid = do_get_guid_for_uri(uri);
+ removed = yield PlacesUtils.history.remove(removeArg, page => {
+ Assert.equal(onRowCalled, false, "Callback has not been called yet");
+ onRowCalled = true;
+ Assert.equal(page.url.href, uri.spec, "Callback provides the correct url");
+ Assert.equal(page.guid, guid, "Callback provides the correct guid");
+ Assert.equal(page.title, title, "Callback provides the correct title");
+ });
+ Assert.ok(onRowCalled, "Callback has been called");
+ } else {
+ removed = yield PlacesUtils.history.remove(removeArg);
+ }
+
+ yield promiseObserved;
+ PlacesUtils.history.removeObserver(observer);
+
+ Assert.equal(visits_in_database(uri), 0, "History entry has disappeared");
+ Assert.notEqual(visits_in_database(WITNESS_URI), 0, "Witness URI still has visits");
+ Assert.notEqual(page_in_database(WITNESS_URI), 0, "Witness URI is still here");
+ if (shouldRemove) {
+ Assert.ok(removed, "Something was removed");
+ Assert.equal(page_in_database(uri), 0, "Page has disappeared");
+ } else {
+ Assert.ok(!removed, "The page was not removed, as there was a bookmark");
+ Assert.notEqual(page_in_database(uri), 0, "The page is still present");
+ }
+ });
+
+ try {
+ for (let useCallback of [false, true]) {
+ for (let addBookmark of [false, true]) {
+ let options = { useCallback: useCallback, addBookmark: addBookmark };
+ yield remover("Testing History.remove() with a single URI", x => x, options);
+ yield remover("Testing History.remove() with a single string url", x => x.spec, options);
+ yield remover("Testing History.remove() with a single string guid", x => do_get_guid_for_uri(x), options);
+ yield remover("Testing History.remove() with a single URI in an array", x => [x], options);
+ yield remover("Testing History.remove() with a single string url in an array", x => [x.spec], options);
+ yield remover("Testing History.remove() with a single string guid in an array", x => [do_get_guid_for_uri(x)], options);
+ }
+ }
+ } finally {
+ yield PlacesTestUtils.clearHistory();
+ }
+ return;
+});
+
+// Test removing a list of pages
+add_task(function* test_remove_many() {
+ const SIZE = 10;
+
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ do_print("Adding a witness page");
+ let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
+ yield PlacesTestUtils.addVisits(WITNESS_URI);
+ Assert.ok(page_in_database(WITNESS_URI), "Witness page added");
+
+ do_print("Generating samples");
+ let pages = [];
+ for (let i = 0; i < SIZE; ++i) {
+ let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove?sample=" + i + "&salt=" + Math.random());
+ let title = "Visit " + i + ", " + Math.random();
+ let hasBookmark = i % 3 == 0;
+ let page = {
+ uri: uri,
+ title: title,
+ hasBookmark: hasBookmark,
+ // `true` once `onResult` has been called for this page
+ onResultCalled: false,
+ // `true` once `onDeleteVisits` has been called for this page
+ onDeleteVisitsCalled: false,
+ // `true` once `onFrecencyChangedCalled` has been called for this page
+ onFrecencyChangedCalled: false,
+ // `true` once `onDeleteURI` has been called for this page
+ onDeleteURICalled: false,
+ };
+ do_print("Pushing: " + uri.spec);
+ pages.push(page);
+
+ yield PlacesTestUtils.addVisits(page);
+ page.guid = do_get_guid_for_uri(uri);
+ if (hasBookmark) {
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test bookmark " + i);
+ }
+ Assert.ok(page_in_database(uri), "Page added");
+ }
+
+ do_print("Mixing key types and introducing dangling keys");
+ let keys = [];
+ for (let i = 0; i < SIZE; ++i) {
+ if (i % 4 == 0) {
+ keys.push(pages[i].uri);
+ keys.push(NetUtil.newURI("http://example.org/dangling/nsIURI/" + i));
+ } else if (i % 4 == 1) {
+ keys.push(new URL(pages[i].uri.spec));
+ keys.push(new URL("http://example.org/dangling/URL/" + i));
+ } else if (i % 4 == 2) {
+ keys.push(pages[i].uri.spec);
+ keys.push("http://example.org/dangling/stringuri/" + i);
+ } else {
+ keys.push(pages[i].guid);
+ keys.push(("guid_" + i + "_01234567890").substr(0, 12));
+ }
+ }
+
+ let observer = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function(aURI) {
+ Assert.ok(false, "Unexpected call to onVisit " + aURI.spec);
+ },
+ onTitleChanged: function(aURI) {
+ Assert.ok(false, "Unexpected call to onTitleChanged " + aURI.spec);
+ },
+ onClearHistory: function() {
+ Assert.ok(false, "Unexpected call to onClearHistory");
+ },
+ onPageChanged: function(aURI) {
+ Assert.ok(false, "Unexpected call to onPageChanged " + aURI.spec);
+ },
+ onFrecencyChanged: function(aURI) {
+ let origin = pages.find(x => x.uri.spec == aURI.spec);
+ Assert.ok(origin);
+ Assert.ok(origin.hasBookmark, "Observing onFrecencyChanged on a page with a bookmark");
+ origin.onFrecencyChangedCalled = true;
+ // We do not make sure that `origin.onFrecencyChangedCalled` is `false`, as
+ },
+ onManyFrecenciesChanged: function() {
+ Assert.ok(false, "Observing onManyFrecenciesChanges, this is most likely correct but not covered by this test");
+ },
+ onDeleteURI: function(aURI) {
+ let origin = pages.find(x => x.uri.spec == aURI.spec);
+ Assert.ok(origin);
+ Assert.ok(!origin.hasBookmark, "Observing onDeleteURI on a page without a bookmark");
+ Assert.ok(!origin.onDeleteURICalled, "Observing onDeleteURI for the first time");
+ origin.onDeleteURICalled = true;
+ },
+ onDeleteVisits: function(aURI) {
+ let origin = pages.find(x => x.uri.spec == aURI.spec);
+ Assert.ok(origin);
+ Assert.ok(!origin.onDeleteVisitsCalled, "Observing onDeleteVisits for the first time");
+ origin.onDeleteVisitsCalled = true;
+ }
+ };
+ PlacesUtils.history.addObserver(observer, false);
+
+ do_print("Removing the pages and checking the callbacks");
+ let removed = yield PlacesUtils.history.remove(keys, page => {
+ let origin = pages.find(candidate => candidate.uri.spec == page.url.href);
+
+ Assert.ok(origin, "onResult has a valid page");
+ Assert.ok(!origin.onResultCalled, "onResult has not seen this page yet");
+ origin.onResultCalled = true;
+ Assert.equal(page.guid, origin.guid, "onResult has the right guid");
+ Assert.equal(page.title, origin.title, "onResult has the right title");
+ });
+ Assert.ok(removed, "Something was removed");
+
+ PlacesUtils.history.removeObserver(observer);
+
+ do_print("Checking out results");
+ // By now the observers should have been called.
+ for (let i = 0; i < pages.length; ++i) {
+ let page = pages[i];
+ do_print("Page: " + i);
+ Assert.ok(page.onResultCalled, "We have reached the page from the callback");
+ Assert.ok(visits_in_database(page.uri) == 0, "History entry has disappeared");
+ Assert.equal(page_in_database(page.uri) != 0, page.hasBookmark, "Page is present only if it also has bookmarks");
+ Assert.equal(page.onFrecencyChangedCalled, page.onDeleteVisitsCalled, "onDeleteVisits was called iff onFrecencyChanged was called");
+ Assert.ok(page.onFrecencyChangedCalled ^ page.onDeleteURICalled, "Either onFrecencyChanged or onDeleteURI was called");
+ }
+
+ Assert.notEqual(visits_in_database(WITNESS_URI), 0, "Witness URI still has visits");
+ Assert.notEqual(page_in_database(WITNESS_URI), 0, "Witness URI is still here");
+});
+
+add_task(function* cleanup() {
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+// Test the various error cases
+add_task(function* test_error_cases() {
+ Assert.throws(
+ () => PlacesUtils.history.remove(),
+ /TypeError: Invalid url/,
+ "History.remove with no argument should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(null),
+ /TypeError: Invalid url/,
+ "History.remove with `null` should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(undefined),
+ /TypeError: Invalid url/,
+ "History.remove with `undefined` should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove("not a guid, obviously"),
+ /TypeError: .* is not a valid URL/,
+ "History.remove with an ill-formed guid/url argument should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove({"not the kind of object we know how to handle": true}),
+ /TypeError: Invalid url/,
+ "History.remove with an unexpected object should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove([]),
+ /TypeError: Expected at least one page/,
+ "History.remove with an empty array should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove([null]),
+ /TypeError: Invalid url or guid/,
+ "History.remove with an array containing null should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(["http://example.org", "not a guid, obviously"]),
+ /TypeError: .* is not a valid URL/,
+ "History.remove with an array containing an ill-formed guid/url argument should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(["0123456789ab"/* valid guid*/, null]),
+ /TypeError: Invalid url or guid: null/,
+ "History.remove with an array containing a guid and a second argument that is null should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(["http://example.org", {"not the kind of object we know how to handle": true}]),
+ /TypeError: Invalid url/,
+ "History.remove with an array containing an unexpected objecgt should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove("http://example.org", "not a function, obviously"),
+ /TypeError: Invalid function/,
+ "History.remove with a second argument that is not a function argument should throw a TypeError"
+ );
+ try {
+ PlacesUtils.history.remove("http://example.org/I/have/clearly/not/been/added", null);
+ Assert.ok(true, "History.remove should ignore `null` as a second argument");
+ } catch (ex) {
+ Assert.ok(false, "History.remove should ignore `null` as a second argument");
+ }
+});
+
+add_task(function* test_orphans() {
+ let uri = NetUtil.newURI("http://moz.org/");
+ yield PlacesTestUtils.addVisits({ uri });
+
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ uri, SMALLPNG_DATA_URI, true, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null, Services.scriptSecurityManager.getSystemPrincipal());
+ PlacesUtils.annotations.setPageAnnotation(uri, "test", "restval", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ yield PlacesUtils.history.remove(uri);
+ Assert.ok(!(yield PlacesTestUtils.isPageInDB(uri)), "Page should have been removed");
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT (SELECT count(*) FROM moz_annos) +
+ (SELECT count(*) FROM moz_favicons) AS count`);
+ Assert.equal(rows[0].getResultByName("count"), 0, "Should not find orphans");
+});
diff --git a/toolkit/components/places/tests/history/test_removeVisits.js b/toolkit/components/places/tests/history/test_removeVisits.js
new file mode 100644
index 000000000..8df0c81a9
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_removeVisits.js
@@ -0,0 +1,316 @@
+const JS_NOW = Date.now();
+const DB_NOW = JS_NOW * 1000;
+const TEST_URI = uri("http://example.com/");
+const PLACE_URI = uri("place:queryType=0&sort=8&maxResults=10");
+
+function* cleanup() {
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+ // This is needed to remove place: entries.
+ DBConn().executeSimpleSQL("DELETE FROM moz_places");
+}
+
+add_task(function* remove_visits_outside_unbookmarked_uri() {
+ do_print("*** TEST: Remove some visits outside valid timeframe from an unbookmarked URI");
+
+ do_print("Add 10 visits for the URI from way in the past.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - 100000 - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+
+ do_print("Remove visits using timerange outside the URI's visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that all visits still exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 10);
+ for (let i = 0; i < root.childCount; i++) {
+ let visitTime = root.getChild(i).time;
+ do_check_eq(visitTime, DB_NOW - 100000 - (i * 1000));
+ }
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return true.");
+ do_check_true(yield promiseIsURIVisited(TEST_URI));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Frecency should be positive.")
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ yield cleanup();
+});
+
+add_task(function* remove_visits_outside_bookmarked_uri() {
+ do_print("*** TEST: Remove some visits outside valid timeframe from a bookmarked URI");
+
+ do_print("Add 10 visits for the URI from way in the past.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - 100000 - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+ do_print("Bookmark the URI.");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Remove visits using timerange outside the URI's visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that all visits still exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 10);
+ for (let i = 0; i < root.childCount; i++) {
+ let visitTime = root.getChild(i).time;
+ do_check_eq(visitTime, DB_NOW - 100000 - (i * 1000));
+ }
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return true.");
+ do_check_true(yield promiseIsURIVisited(TEST_URI));
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Frecency should be positive.")
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ yield cleanup();
+});
+
+add_task(function* remove_visits_unbookmarked_uri() {
+ do_print("*** TEST: Remove some visits from an unbookmarked URI");
+
+ do_print("Add 10 visits for the URI from now to 9 usecs in the past.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+
+ do_print("Remove the 5 most recent visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 4),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that only the older 5 visits still exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 5);
+ for (let i = 0; i < root.childCount; i++) {
+ let visitTime = root.getChild(i).time;
+ do_check_eq(visitTime, DB_NOW - (i * 1000) - 5000);
+ }
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return true.");
+ do_check_true(yield promiseIsURIVisited(TEST_URI));
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Frecency should be positive.")
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ yield cleanup();
+});
+
+add_task(function* remove_visits_bookmarked_uri() {
+ do_print("*** TEST: Remove some visits from a bookmarked URI");
+
+ do_print("Add 10 visits for the URI from now to 9 usecs in the past.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+ do_print("Bookmark the URI.");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Remove the 5 most recent visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 4),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that only the older 5 visits still exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 5);
+ for (let i = 0; i < root.childCount; i++) {
+ let visitTime = root.getChild(i).time;
+ do_check_eq(visitTime, DB_NOW - (i * 1000) - 5000);
+ }
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return true.");
+ do_check_true(yield promiseIsURIVisited(TEST_URI));
+ yield PlacesTestUtils.promiseAsyncUpdates()
+
+ do_print("Frecency should be positive.")
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ yield cleanup();
+});
+
+add_task(function* remove_all_visits_unbookmarked_uri() {
+ do_print("*** TEST: Remove all visits from an unbookmarked URI");
+
+ do_print("Add some visits for the URI.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+
+ do_print("Remove all visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should no longer exist in moz_places.");
+ do_check_false(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that no visits exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return false.");
+ do_check_false(yield promiseIsURIVisited(TEST_URI));
+
+ yield cleanup();
+});
+
+add_task(function* remove_all_visits_bookmarked_uri() {
+ do_print("*** TEST: Remove all visits from a bookmarked URI");
+
+ do_print("Add some visits for the URI.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+ do_print("Bookmark the URI.");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ let initialFrecency = frecencyForUrl(TEST_URI);
+
+ do_print("Remove all visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that no visits exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return false.");
+ do_check_false(yield promiseIsURIVisited(TEST_URI));
+
+ do_print("nsINavBookmarksService.isBookmarked should return true.");
+ do_check_true(PlacesUtils.bookmarks.isBookmarked(TEST_URI));
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Frecency should be smaller.")
+ do_check_true(frecencyForUrl(TEST_URI) < initialFrecency);
+
+ yield cleanup();
+});
+
+add_task(function* remove_all_visits_bookmarked_uri() {
+ do_print("*** TEST: Remove some visits from a zero frecency URI retains zero frecency");
+
+ do_print("Add some visits for the URI.");
+ yield PlacesTestUtils.addVisits([
+ { uri: TEST_URI, transition: TRANSITION_FRAMED_LINK, visitDate: (DB_NOW - 86400000000000) },
+ { uri: TEST_URI, transition: TRANSITION_FRAMED_LINK, visitDate: DB_NOW }
+ ]);
+
+ do_print("Remove newer visit.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+ do_print("Frecency should be zero.")
+ do_check_eq(frecencyForUrl(TEST_URI), 0);
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/history/test_removeVisitsByFilter.js b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js
new file mode 100644
index 000000000..699420e43
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js
@@ -0,0 +1,345 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+// Tests for `History.removeVisitsByFilter`, as implemented in History.jsm
+
+"use strict";
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+
+add_task(function* test_removeVisitsByFilter() {
+ let referenceDate = new Date(1999, 9, 9, 9, 9);
+
+ // Populate a database with 20 entries, remove a subset of entries,
+ // ensure consistency.
+ let remover = Task.async(function*(options) {
+ do_print("Remover with options " + JSON.stringify(options));
+ let SAMPLE_SIZE = options.sampleSize;
+
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Populate the database.
+ // Create `SAMPLE_SIZE` visits, from the oldest to the newest.
+
+ let bookmarkIndices = new Set(options.bookmarks);
+ let visits = [];
+ let frecencyChangePromises = new Map();
+ let uriDeletePromises = new Map();
+ let getURL = options.url ?
+ i => "http://mozilla.com/test_browserhistory/test_removeVisitsByFilter/removeme/byurl/" + Math.floor(i / (SAMPLE_SIZE / 5)) + "/" :
+ i => "http://mozilla.com/test_browserhistory/test_removeVisitsByFilter/removeme/" + i + "/" + Math.random();
+ for (let i = 0; i < SAMPLE_SIZE; ++i) {
+ let spec = getURL(i);
+ let uri = NetUtil.newURI(spec);
+ let jsDate = new Date(Number(referenceDate) + 3600 * 1000 * i);
+ let dbDate = jsDate * 1000;
+ let hasBookmark = bookmarkIndices.has(i);
+ let hasOwnBookmark = hasBookmark;
+ if (!hasOwnBookmark && options.url) {
+ // Also mark as bookmarked if one of the earlier bookmarked items has the same URL.
+ hasBookmark =
+ options.bookmarks.filter(n => n < i).some(n => visits[n].uri.spec == spec && visits[n].test.hasBookmark);
+ }
+ do_print("Generating " + uri.spec + ", " + dbDate);
+ let visit = {
+ uri,
+ title: "visit " + i,
+ visitDate: dbDate,
+ test: {
+ // `visitDate`, as a Date
+ jsDate: jsDate,
+ // `true` if we expect that the visit will be removed
+ toRemove: false,
+ // `true` if `onRow` informed of the removal of this visit
+ announcedByOnRow: false,
+ // `true` if there is a bookmark for this URI, i.e. of the page
+ // should not be entirely removed.
+ hasBookmark: hasBookmark,
+ onFrecencyChanged: null,
+ onDeleteURI: null,
+ },
+ };
+ visits.push(visit);
+ if (hasOwnBookmark) {
+ do_print("Adding a bookmark to visit " + i);
+ yield PlacesUtils.bookmarks.insert({
+ url: uri,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test bookmark"
+ });
+ do_print("Bookmark added");
+ }
+ }
+
+ do_print("Adding visits");
+ yield PlacesTestUtils.addVisits(visits);
+
+ do_print("Preparing filters");
+ let filter = {
+ };
+ let beginIndex = 0;
+ let endIndex = visits.length - 1;
+ if ("begin" in options) {
+ let ms = Number(visits[options.begin].test.jsDate) - 1000;
+ filter.beginDate = new Date(ms);
+ beginIndex = options.begin;
+ }
+ if ("end" in options) {
+ let ms = Number(visits[options.end].test.jsDate) + 1000;
+ filter.endDate = new Date(ms);
+ endIndex = options.end;
+ }
+ if ("limit" in options) {
+ endIndex = beginIndex + options.limit - 1; // -1 because the start index is inclusive.
+ filter.limit = options.limit;
+ }
+ let removedItems = visits.slice(beginIndex);
+ endIndex -= beginIndex;
+ if (options.url) {
+ let rawURL = "";
+ switch (options.url) {
+ case 1:
+ filter.url = new URL(removedItems[0].uri.spec);
+ rawURL = filter.url.href;
+ break;
+ case 2:
+ filter.url = removedItems[0].uri;
+ rawURL = filter.url.spec;
+ break;
+ case 3:
+ filter.url = removedItems[0].uri.spec;
+ rawURL = filter.url;
+ break;
+ }
+ endIndex = Math.min(endIndex, removedItems.findIndex((v, index) => v.uri.spec != rawURL) - 1);
+ }
+ removedItems.splice(endIndex + 1);
+ let remainingItems = visits.filter(v => !removedItems.includes(v));
+ for (let i = 0; i < removedItems.length; i++) {
+ let test = removedItems[i].test;
+ do_print("Marking visit " + (beginIndex + i) + " as expecting removal");
+ test.toRemove = true;
+ if (test.hasBookmark ||
+ (options.url && remainingItems.some(v => v.uri.spec == removedItems[i].uri.spec))) {
+ frecencyChangePromises.set(removedItems[i].uri.spec, PromiseUtils.defer());
+ } else if (!options.url || i == 0) {
+ uriDeletePromises.set(removedItems[i].uri.spec, PromiseUtils.defer());
+ }
+ }
+
+ let observer = {
+ deferred: PromiseUtils.defer(),
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function(uri) {
+ this.deferred.reject(new Error("Unexpected call to onVisit " + uri.spec));
+ },
+ onTitleChanged: function(uri) {
+ this.deferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
+ },
+ onClearHistory: function() {
+ this.deferred.reject("Unexpected call to onClearHistory");
+ },
+ onPageChanged: function(uri) {
+ this.deferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec));
+ },
+ onFrecencyChanged: function(aURI) {
+ do_print("onFrecencyChanged " + aURI.spec);
+ let deferred = frecencyChangePromises.get(aURI.spec);
+ Assert.ok(!!deferred, "Observing onFrecencyChanged");
+ deferred.resolve();
+ },
+ onManyFrecenciesChanged: function() {
+ do_print("Many frecencies changed");
+ for (let [, deferred] of frecencyChangePromises) {
+ deferred.resolve();
+ }
+ },
+ onDeleteURI: function(aURI) {
+ do_print("onDeleteURI " + aURI.spec);
+ let deferred = uriDeletePromises.get(aURI.spec);
+ Assert.ok(!!deferred, "Observing onDeleteURI");
+ deferred.resolve();
+ },
+ onDeleteVisits: function(aURI) {
+ // Not sure we can test anything.
+ }
+ };
+ PlacesUtils.history.addObserver(observer, false);
+
+ let cbarg;
+ if (options.useCallback) {
+ do_print("Setting up callback");
+ cbarg = [info => {
+ for (let visit of visits) {
+ do_print("Comparing " + info.date + " and " + visit.test.jsDate);
+ if (Math.abs(visit.test.jsDate - info.date) < 100) { // Assume rounding errors
+ Assert.ok(!visit.test.announcedByOnRow,
+ "This is the first time we announce the removal of this visit");
+ Assert.ok(visit.test.toRemove,
+ "This is a visit we intended to remove");
+ visit.test.announcedByOnRow = true;
+ return;
+ }
+ }
+ Assert.ok(false, "Could not find the visit we attempt to remove");
+ }];
+ } else {
+ do_print("No callback");
+ cbarg = [];
+ }
+ let result = yield PlacesUtils.history.removeVisitsByFilter(filter, ...cbarg);
+
+ Assert.ok(result, "Removal succeeded");
+
+ // Make sure that we have eliminated exactly the entries we expected
+ // to eliminate.
+ for (let i = 0; i < visits.length; ++i) {
+ let visit = visits[i];
+ do_print("Controlling the results on visit " + i);
+ let remainingVisitsForURI = remainingItems.filter(v => visit.uri.spec == v.uri.spec).length;
+ Assert.equal(
+ visits_in_database(visit.uri),
+ remainingVisitsForURI,
+ "Visit is still present iff expected");
+ if (options.useCallback) {
+ Assert.equal(
+ visit.test.toRemove,
+ visit.test.announcedByOnRow,
+ "Visit removal has been announced by onResult iff expected");
+ }
+ if (visit.test.hasBookmark || remainingVisitsForURI) {
+ Assert.notEqual(page_in_database(visit.uri), 0, "The page should still appear in the db");
+ } else {
+ Assert.equal(page_in_database(visit.uri), 0, "The page should have been removed from the db");
+ }
+ }
+
+ // Make sure that the observer has been called wherever applicable.
+ do_print("Checking URI delete promises.");
+ yield Promise.all(Array.from(uriDeletePromises.values()));
+ do_print("Checking frecency change promises.");
+ yield Promise.all(Array.from(frecencyChangePromises.values()));
+ PlacesUtils.history.removeObserver(observer);
+ });
+
+ let size = 20;
+ for (let range of [
+ {begin: 0},
+ {end: 19},
+ {begin: 0, end: 10},
+ {begin: 3, end: 4},
+ {begin: 5, end: 8, limit: 2},
+ {begin: 10, end: 18, limit: 5},
+ ]) {
+ for (let bookmarks of [[], [5, 6]]) {
+ let options = {
+ sampleSize: size,
+ bookmarks: bookmarks,
+ };
+ if ("begin" in range) {
+ options.begin = range.begin;
+ }
+ if ("end" in range) {
+ options.end = range.end;
+ }
+ if ("limit" in range) {
+ options.limit = range.limit;
+ }
+ yield remover(options);
+ options.url = 1;
+ yield remover(options);
+ options.url = 2;
+ yield remover(options);
+ options.url = 3;
+ yield remover(options);
+ }
+ }
+ yield PlacesTestUtils.clearHistory();
+});
+
+// Test the various error cases
+add_task(function* test_error_cases() {
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter(),
+ /TypeError: Expected a filter/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter("obviously, not a filter"),
+ /TypeError: Expected a filter/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({}),
+ /TypeError: Expected a non-empty filter/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: "now"}),
+ /TypeError: Expected a Date/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: Date.now()}),
+ /TypeError: Expected a Date/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date()}, "obviously, not a callback"),
+ /TypeError: Invalid function/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}),
+ /TypeError: `beginDate` should be at least as old/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({limit: {}}),
+ /Expected a non-zero positive integer as a limit/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({limit: -1}),
+ /Expected a non-zero positive integer as a limit/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({limit: 0.1}),
+ /Expected a non-zero positive integer as a limit/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({limit: Infinity}),
+ /Expected a non-zero positive integer as a limit/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({url: {}}),
+ /Expected a valid URL for `url`/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({url: 0}),
+ /Expected a valid URL for `url`/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}),
+ /TypeError: `beginDate` should be at least as old/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}),
+ /TypeError: `beginDate` should be at least as old/
+ );
+});
+
+add_task(function* test_orphans() {
+ let uri = NetUtil.newURI("http://moz.org/");
+ yield PlacesTestUtils.addVisits({ uri });
+
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ uri, SMALLPNG_DATA_URI, true, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null, Services.scriptSecurityManager.getSystemPrincipal());
+ PlacesUtils.annotations.setPageAnnotation(uri, "test", "restval", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ yield PlacesUtils.history.removeVisitsByFilter({ beginDate: new Date(1999, 9, 9, 9, 9),
+ endDate: new Date() });
+ Assert.ok(!(yield PlacesTestUtils.isPageInDB(uri)), "Page should have been removed");
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT (SELECT count(*) FROM moz_annos) +
+ (SELECT count(*) FROM moz_favicons) AS count`);
+ Assert.equal(rows[0].getResultByName("count"), 0, "Should not find orphans");
+});
diff --git a/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js b/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js
new file mode 100644
index 000000000..832df9d9a
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js
@@ -0,0 +1,52 @@
+// Test that repeated additions of the same URI through updatePlaces, properly
+// update from_visit and notify titleChanged.
+
+add_task(function* test() {
+ let uri = "http://test.com/";
+
+ let promiseTitleChangedNotifications = new Promise(resolve => {
+ let historyObserver = {
+ _count: 0,
+ __proto__: NavHistoryObserver.prototype,
+ onTitleChanged(aURI, aTitle, aGUID) {
+ Assert.equal(aURI.spec, uri, "Should notify the proper url");
+ if (++this._count == 2) {
+ PlacesUtils.history.removeObserver(historyObserver);
+ resolve();
+ }
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+ });
+
+ // This repeats the url on purpose, don't merge it into a single place entry.
+ yield PlacesTestUtils.addVisits([
+ { uri, title: "test" },
+ { uri, referrer: uri, title: "test2" },
+ ]);
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let query = PlacesUtils.history.getNewQuery();
+ query.uri = NetUtil.newURI(uri);
+ options.resultType = options.RESULTS_AS_VISIT;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ Assert.equal(root.childCount, 2);
+
+ let child = root.getChild(0);
+ Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
+ Assert.equal(child.visitId, 1, "Visit ID should be 1");
+ Assert.equal(child.fromVisitId, -1, "Should have no referrer visit ID");
+ Assert.equal(child.title, "test2", "Should have the correct title");
+
+ child = root.getChild(1);
+ Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
+ Assert.equal(child.visitId, 2, "Visit ID should be 2");
+ Assert.equal(child.fromVisitId, 1, "First visit should be the referring visit");
+ Assert.equal(child.title, "test2", "Should have the correct title");
+
+ root.containerOpen = false;
+
+ yield promiseTitleChangedNotifications;
+});
diff --git a/toolkit/components/places/tests/history/xpcshell.ini b/toolkit/components/places/tests/history/xpcshell.ini
new file mode 100644
index 000000000..ee182e090
--- /dev/null
+++ b/toolkit/components/places/tests/history/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head = head_history.js
+tail =
+
+[test_insert.js]
+[test_remove.js]
+[test_removeVisits.js]
+[test_removeVisitsByFilter.js]
+[test_updatePlaces_sameUri_titleChanged.js]