Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* -*- 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.sys.mjs
"use strict";
add_task(async 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 = async function (options) {
info("Remover with options " + JSON.stringify(options));
let SAMPLE_SIZE = options.sampleSize;
await PlacesUtils.history.clear();
await 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 rankingChangePromises = [];
let uriDeletePromises = new Map();
let getURL = options.url
? i =>
Math.floor(i / (SAMPLE_SIZE / 5)) +
"/"
: i =>
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);
}
info("Generating " + uri.spec + ", " + dbDate);
let visit = {
uri,
title: "visit " + i,
visitDate: dbDate,
test: {
// `visitDate`, as a Date
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,
},
};
visits.push(visit);
if (hasOwnBookmark) {
info("Adding a bookmark to visit " + i);
await PlacesUtils.bookmarks.insert({
url: uri,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
title: "test bookmark",
});
info("Bookmark added");
}
}
info("Adding visits");
await PlacesTestUtils.addVisits(visits);
info("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 => 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;
info("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))
) {
rankingChangePromises.push(Promise.withResolvers());
} else if (!options.url || i == 0) {
uriDeletePromises.set(
removedItems[i].uri.spec,
Promise.withResolvers()
);
}
}
const placesEventListener = events => {
for (const event of events) {
switch (event.type) {
case "page-title-changed": {
this.deferred.reject(
"Unexpected page-title-changed event happens on " + event.url
);
break;
}
case "history-cleared": {
info("history-cleared");
this.deferred.reject("Unexpected history-cleared event happens");
break;
}
case "pages-rank-changed": {
info("pages-rank-changed");
for (const deferred of rankingChangePromises) {
deferred.resolve();
}
break;
}
}
}
};
PlacesObservers.addListener(
["page-title-changed", "history-cleared", "pages-rank-changed"],
placesEventListener
);
let cbarg;
if (options.useCallback) {
info("Setting up callback");
cbarg = [
info => {
for (let visit of visits) {
info("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 {
info("No callback");
cbarg = [];
}
let result = await 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];
info("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.
info("Checking URI delete promises.");
await Promise.all(Array.from(uriDeletePromises.values()));
info("Checking frecency change promises.");
await Promise.all(rankingChangePromises);
PlacesObservers.removeListener(
["page-title-changed", "history-cleared", "pages-rank-changed"],
placesEventListener
);
};
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,
};
if ("begin" in range) {
options.begin = range.begin;
}
if ("end" in range) {
options.end = range.end;
}
if ("limit" in range) {
options.limit = range.limit;
}
await remover(options);
options.url = 1;
await remover(options);
options.url = 2;
await remover(options);
options.url = 3;
await remover(options);
}
}
await PlacesUtils.history.clear();
});
// Test the various error cases
add_task(async 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 valid Date/
);
Assert.throws(
() => PlacesUtils.history.removeVisitsByFilter({ beginDate: Date.now() }),
/TypeError: Expected a valid Date/
);
Assert.throws(
() =>
PlacesUtils.history.removeVisitsByFilter({ beginDate: new Date(NaN) }),
/TypeError: Expected a valid 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/
);
Assert.throws(
() => PlacesUtils.history.removeVisitsByFilter({ transition: -1 }),
/TypeError: `transition` should be valid/
);
});
add_task(async function test_orphans() {
let uri = NetUtil.newURI("http://moz.org/");
await PlacesTestUtils.addVisits({ uri });
PlacesUtils.favicons.setAndFetchFaviconForPage(
uri,
SMALLPNG_DATA_URI,
true,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
null,
Services.scriptSecurityManager.getSystemPrincipal()
);
await PlacesUtils.history.update({
url: uri,
annotations: new Map([["test", "restval"]]),
});
await PlacesUtils.history.removeVisitsByFilter({
beginDate: new Date(1999, 9, 9, 9, 9),
endDate: new Date(),
});
Assert.ok(
!(await PlacesTestUtils.isPageInDB(uri)),
"Page should have been removed"
);
let db = await PlacesUtils.promiseDBConnection();
let rows = await db.execute(`SELECT (SELECT count(*) FROM moz_annos) +
(SELECT count(*) FROM moz_icons) +
(SELECT count(*) FROM moz_pages_w_icons) +
(SELECT count(*) FROM moz_icons_to_pages) AS count`);
Assert.equal(rows[0].getResultByName("count"), 0, "Should not find orphans");
});