Bug 1249263 - add a removeByFilter method to filter by host and time,r=mak

Added a method in History to filter by host and timeframe, which is designed
to act as a replacement for `RemovePagesByTimeFrame` and `RemovePagesFromHost`
in the old API. The `filter` object accepts both a host argument, as well as
a timeframe, and filters as per one or both of them.
This also moves certain code (the method `validatePageInfo` and methods it
uses) from History to PlacesUtils such that we can use it for testing as well,
and modifies the method to take another parameter which decides whether
the visits inside the pageInfo need to be validated as well (since the pageInfo
returned from History.jsm::`remove` and History.jsm::`removeByFilter` do not pass
a visits array in their callback functions.

Shifts `ensureDate` and `isValidTransitionType`(now renamed to `isValidTransition`)
inside the history object.

MozReview-Commit-ID: EQAHmjf7131

--HG--
extra : rebase_source : d5992a1bd3c297c84dd0ecbf47111e8f914a58a0
This commit is contained in:
i.milind.luthra+590334 2017-05-11 17:22:27 +02:00
parent f55150cdcf
commit d3ba94cc11
4 changed files with 620 additions and 105 deletions

View File

@ -94,9 +94,6 @@ const ONRESULT_CHUNK_SIZE = 300;
const REMOVE_PAGES_CHUNKLEN = 300;
// Timers resolution is not always good, it can have a 16ms precision on Win.
const TIMERS_RESOLUTION_SKEW_MS = 16;
/**
* Sends a bookmarks notification through the given observers.
*
@ -182,7 +179,7 @@ this.History = Object.freeze({
throw new TypeError("pageInfo must be an object");
}
let info = validatePageInfo(pageInfo);
let info = PlacesUtils.validatePageInfo(pageInfo);
return PlacesUtils.withConnectionWrapper("History.jsm: insert",
db => insert(db, info));
@ -249,7 +246,7 @@ this.History = Object.freeze({
}
for (let pageInfo of pageInfos) {
let info = validatePageInfo(pageInfo);
let info = PlacesUtils.validatePageInfo(pageInfo);
infos.push(info);
}
@ -296,7 +293,7 @@ this.History = Object.freeze({
for (let page of pages) {
// Normalize to URL or GUID, or throw if `page` cannot
// be normalized.
let normalized = normalizeToURLOrGUID(page);
let normalized = PlacesUtils.normalizeToURLOrGUID(page);
if (typeof normalized === "string") {
guids.push(normalized);
} else {
@ -381,10 +378,10 @@ this.History = Object.freeze({
let hasURL = "url" in filter;
let hasLimit = "limit" in filter;
if (hasBeginDate) {
ensureDate(filter.beginDate);
this.ensureDate(filter.beginDate);
}
if (hasEndDate) {
ensureDate(filter.endDate);
this.ensureDate(filter.endDate);
}
if (hasBeginDate && hasEndDate && filter.beginDate > filter.endDate) {
throw new TypeError("`beginDate` should be at least as old as `endDate`");
@ -414,6 +411,88 @@ this.History = Object.freeze({
);
},
/**
* Remove pages from the database based on a filter.
*
* Any change may be observed through nsINavHistoryObserver
*
*
* @param filter: An object containing a non empty subset of the following
* properties:
* - host: (string)
* Hostname with subhost wildcard (at most one *), or empty for local files.
* The * can be used only if it is the first character in the url, and not the host.
* For example, *.mozilla.org is allowed, *.org, www.*.org or * is not allowed.
* - beginDate: (Date)
* The first time the page was visited (inclusive)
* - endDate: (Date)
* The last time the page was visited (inclusive)
* @param [optional] onResult: (function(PageInfo))
* A callback invoked for each page found.
*
* @note This removes pages with at least one visit inside the timeframe.
* Any visits outside the timeframe will also be removed with the page.
* @return (Promise)
* A promise resolved once the operation is complete.
* @resolve (bool)
* `true` if at least one page was removed, `false` otherwise.
* @throws (TypeError)
* if `filter` does not have the expected type, in particular
* if the `object` is empty, or its components do not satisfy the
* criteria given above
*/
removeByFilter(filter, onResult) {
if (!filter || typeof filter !== "object") {
throw new TypeError("Expected a filter object");
}
let hasHost = "host" in filter;
if (hasHost) {
if (typeof filter.host !== "string") {
throw new TypeError("`host` should be a string");
}
filter.host = filter.host.toLowerCase();
}
let hasBeginDate = "beginDate" in filter;
if (hasBeginDate) {
this.ensureDate(filter.beginDate);
}
let hasEndDate = "endDate" in filter;
if (hasEndDate) {
this.ensureDate(filter.endDate);
}
if (hasBeginDate && hasEndDate && filter.beginDate > filter.endDate) {
throw new TypeError("`beginDate` should be at least as old as `endDate`");
}
if (!hasBeginDate && !hasEndDate && !hasHost) {
throw new TypeError("Expected a non-empty filter");
}
// Host should follow one of these formats
// The first one matches `localhost` or any other custom set in hostsfile
// The second one matches *.mozilla.org or mozilla.com etc
// The third one is for local files
if (hasHost &&
!((/^[a-z0-9-]+$/).test(filter.host)) &&
!((/^(\*\.)?([a-z0-9-]+)(\.[a-z0-9-]+)+$/).test(filter.host)) &&
(filter.host !== "")) {
throw new TypeError("Expected well formed hostname string for `host` with atmost 1 wildcard.");
}
if (onResult && typeof onResult != "function") {
throw new TypeError("Invalid function: " + onResult);
}
return PlacesUtils.withConnectionWrapper(
"History.jsm: removeByFilter",
db => removeByFilter(db, filter, onResult)
);
},
/**
* Determine if a page has been visited.
*
@ -446,6 +525,25 @@ this.History = Object.freeze({
);
},
/**
* Is a value a valid transition type?
*
* @param transitionType: (String)
* @return (Boolean)
*/
isValidTransition(transitionType) {
return Object.values(History.TRANSITIONS).includes(transitionType);
},
/**
* Throw if an object is not a Date object.
*/
ensureDate(arg) {
if (!arg || typeof arg != "object" || arg.constructor.name != "Date") {
throw new TypeError("Expected a Date, got " + arg);
}
},
/**
* Possible values for the `transition` property of `VisitInfo`
* objects.
@ -505,63 +603,11 @@ this.History = Object.freeze({
},
});
/**
* Validate an input PageInfo object, returning a valid PageInfo object.
*
* @param pageInfo: (PageInfo)
* @return (PageInfo)
*/
function validatePageInfo(pageInfo) {
let info = {
visits: [],
};
if (!pageInfo.url) {
throw new TypeError("PageInfo object must have a url property");
}
info.url = normalizeToURLOrGUID(pageInfo.url);
if (typeof pageInfo.title === "string") {
info.title = pageInfo.title;
} else if (pageInfo.title != null && pageInfo.title != undefined) {
throw new TypeError(`title property of PageInfo object: ${pageInfo.title} must be a string if provided`);
}
if (!pageInfo.visits || !Array.isArray(pageInfo.visits) || !pageInfo.visits.length) {
throw new TypeError("PageInfo object must have an array of visits");
}
for (let inVisit of pageInfo.visits) {
let visit = {
date: new Date(),
transition: inVisit.transition || History.TRANSITIONS.LINK,
};
if (!isValidTransitionType(visit.transition)) {
throw new TypeError(`transition: ${visit.transition} is not a valid transition type`);
}
if (inVisit.date) {
ensureDate(inVisit.date);
if (inVisit.date > (Date.now() + TIMERS_RESOLUTION_SKEW_MS)) {
throw new TypeError(`date: ${inVisit.date} cannot be a future date`);
}
visit.date = inVisit.date;
}
if (inVisit.referrer) {
visit.referrer = normalizeToURLOrGUID(inVisit.referrer);
}
info.visits.push(visit);
}
return info;
}
/**
* Convert a PageInfo object into the format expected by updatePlaces.
*
* Note: this assumes that the PageInfo object has already been validated
* via validatePageInfo.
* via PlacesUtils.validatePageInfo.
*
* @param pageInfo: (PageInfo)
* @return (info)
@ -584,50 +630,6 @@ function convertForUpdatePlaces(pageInfo) {
return info;
}
/**
* Is a value a valid transition type?
*
* @param transitionType: (String)
* @return (Boolean)
*/
function isValidTransitionType(transitionType) {
return Object.values(History.TRANSITIONS).includes(transitionType);
}
/**
* Normalize a key to either a string (if it is a valid GUID) or an
* instance of `URL` (if it is a `URL`, `nsIURI`, or a string
* representing a valid url).
*
* @throws (TypeError)
* If the key is neither a valid guid nor a valid url.
*/
function normalizeToURLOrGUID(key) {
if (typeof key === "string") {
// A string may be a URL or a guid
if (PlacesUtils.isValidGuid(key)) {
return key;
}
return new URL(key);
}
if (key instanceof URL) {
return key;
}
if (key instanceof Ci.nsIURI) {
return new URL(key.spec);
}
throw new TypeError("Invalid url or guid: " + key);
}
/**
* Throw if an object is not a Date object.
*/
function ensureDate(arg) {
if (!arg || typeof arg != "object" || arg.constructor.name != "Date") {
throw new TypeError("Expected a Date, got " + arg);
}
}
/**
* Convert a list of strings or numbers to its SQL
* representation as a string.
@ -921,6 +923,112 @@ var removeVisitsByFilter = Task.async(function*(db, filter, onResult = null) {
return visitsToRemove.length != 0;
});
// Inner implementation of History.removeByFilter
var removeByFilter = Task.async(function*(db, filter, onResult = null) {
// 1. Create fragment for date filtration
let dateFilterSQLFragment = "";
let conditions = [];
let params = {};
if ("beginDate" in filter) {
conditions.push("v.visit_date >= :begin");
params.begin = PlacesUtils.toPRTime(filter.beginDate);
}
if ("endDate" in filter) {
conditions.push("v.visit_date <= :end");
params.end = PlacesUtils.toPRTime(filter.endDate);
}
if (conditions.length !== 0) {
dateFilterSQLFragment =
`EXISTS
(SELECT id FROM moz_historyvisits v WHERE v.place_id = h.id AND
${ conditions.join(" AND ") }
LIMIT 1)`;
}
// 2. Create fragment for host and subhost filtering
let hostFilterSQLFragment = "";
if (filter.host || filter.host === "") {
// There are four cases that we need to consider,
// mozilla.org, *.mozilla.org, localhost, and local files
if (filter.host.indexOf("*") === 0) {
// Case 1: subhost wildcard is specified (*.mozilla.org)
let revHost = filter.host.slice(2).split("").reverse().join("");
hostFilterSQLFragment =
`h.rev_host between :revHostStart and :revHostEnd`;
params.revHostStart = revHost + ".";
params.revHostEnd = revHost + "/";
} else {
// This covers the rest (mozilla.org, localhost and local files)
let revHost = filter.host.split("").reverse().join("") + ".";
hostFilterSQLFragment =
`h.rev_host = :hostName`;
params.hostName = revHost;
}
}
// 3. Find out what needs to be removed
let fragmentArray = [hostFilterSQLFragment, dateFilterSQLFragment];
let query =
`SELECT h.id, url, rev_host, guid, title, frecency, foreign_count
FROM moz_places h WHERE
(${ fragmentArray.filter(f => f !== "").join(") AND (") })`;
let onResultData = onResult ? [] : null;
let pages = [];
let hasPagesToRemove = false;
yield db.executeCached(
query,
params,
row => {
let hasForeign = row.getResultByName("foreign_count") != 0;
if (!hasForeign) {
hasPagesToRemove = true;
}
let id = row.getResultByName("id");
let guid = row.getResultByName("guid");
let url = row.getResultByName("url");
let page = {
id,
guid,
hasForeign,
hasVisits: false,
url: new URL(url)
};
pages.push(page);
if (onResult) {
onResultData.push({
guid,
title: row.getResultByName("title"),
frecency: row.getResultByName("frecency"),
url: new URL(url)
});
}
});
if (pages.length === 0) {
// Nothing to do
return false;
}
try {
yield db.executeTransaction(Task.async(function*() {
// 4. Actually remove visits
yield db.execute(`DELETE FROM moz_historyvisits
WHERE place_id IN(${ sqlList(pages.map(p => p.id)) })`);
// 5. Clean up and notify
yield cleanupPages(db, pages);
}));
notifyCleanup(db, pages);
notifyOnResult(onResultData, onResult);
} finally {
PlacesUtils.history.clearEmbedVisits();
}
return hasPagesToRemove;
});
// Inner implementation of History.remove.
var remove = Task.async(function*(db, {guids, urls}, onResult = null) {

View File

@ -64,6 +64,9 @@ const MIN_TRANSACTIONS_FOR_BATCH = 5;
// converts "\r\n" to "\n".
const NEWLINE = AppConstants.platform == "macosx" ? "\n" : "\r\n";
// Timers resolution is not always good, it can have a 16ms precision on Win.
const TIMERS_RESOLUTION_SKEW_MS = 16;
function QI_node(aNode, aIID) {
var result = null;
try {
@ -976,6 +979,88 @@ this.PlacesUtils = {
return nodes;
},
/**
* Validate an input PageInfo object, returning a valid PageInfo object.
*
* @param pageInfo: (PageInfo)
* @return (PageInfo)
*/
validatePageInfo(pageInfo, validateVisits = true) {
let info = {
visits: [],
};
if (!pageInfo.url) {
throw new TypeError("PageInfo object must have a url property");
}
info.url = this.normalizeToURLOrGUID(pageInfo.url);
if (!validateVisits) {
return info;
}
if (typeof pageInfo.title === "string") {
info.title = pageInfo.title;
} else if (pageInfo.title != null && pageInfo.title != undefined) {
throw new TypeError(`title property of PageInfo object: ${pageInfo.title} must be a string if provided`);
}
if (!pageInfo.visits || !Array.isArray(pageInfo.visits) || !pageInfo.visits.length) {
throw new TypeError("PageInfo object must have an array of visits");
}
for (let inVisit of pageInfo.visits) {
let visit = {
date: new Date(),
transition: inVisit.transition || History.TRANSITIONS.LINK,
};
if (!PlacesUtils.history.isValidTransition(visit.transition)) {
throw new TypeError(`transition: ${visit.transition} is not a valid transition type`);
}
if (inVisit.date) {
PlacesUtils.history.ensureDate(inVisit.date);
if (inVisit.date > (Date.now() + TIMERS_RESOLUTION_SKEW_MS)) {
throw new TypeError(`date: ${inVisit.date} cannot be a future date`);
}
visit.date = inVisit.date;
}
if (inVisit.referrer) {
visit.referrer = this.normalizeToURLOrGUID(inVisit.referrer);
}
info.visits.push(visit);
}
return info;
},
/**
* Normalize a key to either a string (if it is a valid GUID) or an
* instance of `URL` (if it is a `URL`, `nsIURI`, or a string
* representing a valid url).
*
* @throws (TypeError)
* If the key is neither a valid guid nor a valid url.
*/
normalizeToURLOrGUID(key) {
if (typeof key === "string") {
// A string may be a URL or a guid
if (this.isValidGuid(key)) {
return key;
}
return new URL(key);
}
if (key instanceof URL) {
return key;
}
if (key instanceof Ci.nsIURI) {
return new URL(key.spec);
}
throw new TypeError("Invalid url or guid: " + key);
},
/**
* Generates a nsINavHistoryResult for the contents of a folder.
* @param folderId

View File

@ -0,0 +1,321 @@
"use strict";
/* This test will ideally test the following cases
(each with and without a callback associated with it)
* Case A: Tests which should remove pages (Positives)
* Case A 1: Page has multiple visits both in/out of timeframe, all get deleted
* Case A 2: Page has single uri, removed by host
* Case A 3: Page has random subhost, with same host, removed by wildcard
* Case A 4: Page is localhost and localhost:port, removed by host
* Case A 5: Page is a `file://` type address, removed by empty host
* Cases A 1,2,3 will be tried with and without bookmarks added (which prevent page deletion)
* Case B: Tests in which no pages are removed (Inverses)
* Case B 1 (inverse): Page has no visits in timeframe, and nothing is deleted
* Case B 2: Page has single uri, not removed since hostname is different
* Case B 3: Page has multiple subhosts, not removed since wildcard doesn't match
* Case C: Combinations tests
* Case C 1: Single hostname, multiple visits, at least one in timeframe and hostname
* Case C 2: Random subhosts, multiple visits, at least one in timeframe and hostname-wildcard
*/
add_task(function* test_removeByFilter() {
// Cleanup
yield PlacesTestUtils.clearHistory();
yield PlacesUtils.bookmarks.eraseEverything();
// Adding a witness URI
let witnessURI = NetUtil.newURI("http://witnessmozilla.org/test_browserhistory/test_removeByFilter" + Math.random());
yield PlacesTestUtils.addVisits(witnessURI);
Assert.ok((yield PlacesTestUtils.isPageInDB(witnessURI)), "Witness URI is in database");
let removeByFilterTester = Task.async(function*(visits, filter, checkBeforeRemove, checkAfterRemove, useCallback, bookmarkedUri) {
// Add visits for URIs
yield PlacesTestUtils.addVisits(visits);
if (bookmarkedUri !== null && visits.map(v => v.uri).includes(bookmarkedUri)) {
yield PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: bookmarkedUri,
title: "test bookmark"
});
}
checkBeforeRemove();
// Take care of any observers (due to bookmarks)
let { observer, promiseObserved } = getObserverPromise(bookmarkedUri);
if (observer) {
PlacesUtils.history.addObserver(observer, false);
}
// Perfom delete operation on database
let removed = false;
if (useCallback) {
// The amount of callbacks will be the unique URIs to remove from the database
let netCallbacksRequired = (new Set(visits.map(v => v.uri))).size;
removed = yield PlacesUtils.history.removeByFilter(filter, pageInfo => {
Assert.ok(PlacesUtils.validatePageInfo(pageInfo, false), "pageInfo should follow a basic format");
Assert.ok(netCallbacksRequired > 0, "Callback called as many times as required");
netCallbacksRequired--;
});
} else {
removed = yield PlacesUtils.history.removeByFilter(filter);
}
checkAfterRemove();
yield promiseObserved;
if (observer) {
PlacesUtils.history.removeObserver(observer);
// Remove the added bookmarks as they interfere with following tests
PlacesUtils.bookmarks.eraseEverything();
}
Assert.ok((yield PlacesTestUtils.isPageInDB(witnessURI)), "Witness URI is still in database");
return removed;
});
const remoteUriList = [ "http://mozilla.org/test_browserhistory/test_removeByFilter/" + Math.random(),
"http://subdomain1.mozilla.org/test_browserhistory/test_removeByFilter/" + Math.random(),
"http://subdomain2.mozilla.org/test_browserhistory/test_removeByFilter/" + Math.random()
];
const localhostUriList = [ "http://localhost:4500/" + Math.random(), "http://localhost/" + Math.random() ];
const fileUriList = [ "file:///home/user/files" + Math.random() ];
const title = "Title " + Math.random();
let sameHostVisits = [
{
uri: remoteUriList[0],
title,
visitDate: new Date(2005, 1, 1) * 1000
},
{
uri: remoteUriList[0],
title,
visitDate: new Date(2005, 3, 3) * 1000
},
{
uri: remoteUriList[0],
title,
visitDate: new Date(2007, 1, 1) * 1000
}
];
let randomHostVisits = [
{
uri: remoteUriList[0],
title,
visitDate: new Date(2005, 1, 1) * 1000
},
{
uri: remoteUriList[1],
title,
visitDate: new Date(2005, 3, 3) * 1000
},
{
uri: remoteUriList[2],
title,
visitDate: new Date(2007, 1, 1) * 1000
}
];
let localhostVisits = [
{
uri: localhostUriList[0],
title
},
{
uri: localhostUriList[1],
title
}
];
let fileVisits = [
{
uri: fileUriList[0],
title
}
];
let assertInDB = function*(aUri) {
Assert.ok((yield PlacesTestUtils.isPageInDB(aUri)));
};
let assertNotInDB = function*(aUri) {
Assert.ok(!(yield PlacesTestUtils.isPageInDB(aUri)));
};
for (let callbackUse of [true, false]) {
// Case A Positives
for (let bookmarkUse of [true, false]) {
let bookmarkedUri = (arr) => undefined;
let checkableArray = (arr) => arr;
let checkClosure = assertNotInDB;
if (bookmarkUse) {
bookmarkedUri = (arr) => arr[0];
checkableArray = (arr) => arr.slice(1);
checkClosure = function(aUri) { };
}
// Case A 1: Dates
yield removeByFilterTester(sameHostVisits,
{ beginDate: new Date(2004, 1, 1), endDate: new Date(2006, 1, 1) },
() => assertInDB(remoteUriList[0]),
() => checkClosure(remoteUriList[0]),
callbackUse, bookmarkedUri(remoteUriList));
// Case A 2: Single Sub-host
yield removeByFilterTester(sameHostVisits, { host: "mozilla.org" },
() => assertInDB(remoteUriList[0]),
() => checkClosure(remoteUriList[0]),
callbackUse, bookmarkedUri(remoteUriList));
// Case A 3: Multiple subhost
yield removeByFilterTester(randomHostVisits, { host: "*.mozilla.org" },
() => remoteUriList.forEach(assertInDB),
() => checkableArray(remoteUriList).forEach(checkClosure),
callbackUse, bookmarkedUri(remoteUriList));
}
// Case A 4: Localhost
yield removeByFilterTester(localhostVisits, { host: "localhost" },
() => localhostUriList.forEach(assertInDB),
() => localhostUriList.forEach(assertNotInDB),
callbackUse);
// Case A 5: Local Files
yield removeByFilterTester(fileVisits, { host: "" },
() => fileUriList.forEach(assertInDB),
() => fileUriList.forEach(assertNotInDB),
callbackUse);
// Case B: Tests which do not remove anything (inverses)
// Case B 1: Date
yield removeByFilterTester(sameHostVisits,
{ beginDate: new Date(2001, 1, 1), endDate: new Date(2002, 1, 1) },
() => assertInDB(remoteUriList[0]),
() => assertInDB(remoteUriList[0]),
callbackUse);
// Case B 2 : Single subhost
yield removeByFilterTester(sameHostVisits, { host: "notthere.org" },
() => assertInDB(remoteUriList[0]),
() => assertInDB(remoteUriList[0]),
callbackUse);
// Case B 3 : Multiple subhosts
yield removeByFilterTester(randomHostVisits, { host: "*.notthere.org" },
() => remoteUriList.forEach(assertInDB),
() => remoteUriList.forEach(assertInDB),
callbackUse);
// Case C: Combination Cases
// Case C 1: single subhost
yield removeByFilterTester(sameHostVisits,
{ host: "mozilla.org",
beginDate: new Date(2004, 1, 1),
endDate: new Date(2006, 1, 1) },
() => assertInDB(remoteUriList[0]),
() => assertNotInDB(remoteUriList[0]),
callbackUse);
// Case C 2: multiple subhost
yield removeByFilterTester(randomHostVisits,
{ host: "*.mozilla.org",
beginDate: new Date(2005, 1, 1),
endDate: new Date(2017, 1, 1) },
() => remoteUriList.forEach(assertInDB),
() => remoteUriList.forEach(assertNotInDB),
callbackUse);
}
});
// Test various error cases
add_task(function* test_error_cases() {
Assert.throws(
() => PlacesUtils.history.removeByFilter(),
/TypeError: Expected a filter/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter("obviously, not a filter"),
/TypeError: Expected a filter/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({}),
/TypeError: Expected a non-empty filter/
);
Assert.throws(
() => PlacesUtils.history.removeVisitsByFilter({beginDate: "now"}),
/TypeError: Expected a Date/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({beginDate: Date.now()}),
/TypeError: Expected a Date/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({beginDate: new Date()}, "obviously, not a callback"),
/TypeError: Invalid function/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({beginDate: new Date(1000), endDate: new Date(0)}),
/TypeError: `beginDate` should be at least as old/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({host: "#"}),
/TypeError: Expected well formed hostname string for/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({host: "*.org"}),
/TypeError: Expected well formed hostname string for/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({host: "www.*.org"}),
/TypeError: Expected well formed hostname string for/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({host: {}}),
/TypeError: `host` should be a string/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({host: ".mozilla.org"}),
/TypeError: Expected well formed hostname string for/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({host: "*"}),
/TypeError: Expected well formed hostname string for/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({host: "local.host."}),
/TypeError: Expected well formed hostname string for/
);
Assert.throws(
() => PlacesUtils.history.removeByFilter({host: "(local files)"}),
/TypeError: Expected well formed hostname string for/
);
});
// Helper functions
function getObserverPromise(bookmarkedUri) {
if (!bookmarkedUri) {
return { observer: null, promiseObserved: Promise.resolve() };
}
let observer;
let promiseObserved = new Promise((resolve, reject) => {
observer = {
onBeginUpdateBatch() {},
onEndUpdateBatch() {},
onVisit(aUri) {
reject(new Error("Unexpected call to onVisit"));
},
onTitleChanged(aUri) {
reject(new Error("Unexpected call to onTitleChanged"));
},
onClearHistory() {
reject(new Error("Unexpected call to onClearHistory"));
},
onPageChanged(aUri) {
reject(new Error("Unexpected call to onPageChanged"));
},
onFrecencyChanged(aURI) {},
onManyFrecenciesChanged() {},
onDeleteURI(aURI) {
try {
Assert.notEqual(aURI.spec, bookmarkedUri, "Bookmarked URI should not be deleted");
} finally {
resolve();
}
},
onDeleteVisits(aURI, aVisitTime) {
try {
Assert.equal(aVisitTime, PlacesUtils.toPRTime(new Date(0)), "Observing onDeleteVisits deletes all visits");
Assert.equal(aURI.spec, bookmarkedUri, "Bookmarked URI should have all visits removed but not the page itself");
} finally {
resolve();
}
}
};
});
return { observer, promiseObserved };
}

View File

@ -7,6 +7,7 @@ head = head_history.js
[test_remove.js]
[test_removeMany.js]
[test_removeVisits.js]
[test_removeByFilter.js]
[test_removeVisitsByFilter.js]
[test_sameUri_titleChanged.js]
[test_updatePlaces_embed.js]