Bug 1265835 - Implement browser.history.getVisits, r?aswan
MozReview-Commit-ID: lhFdMTHYUl
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -1,75 +1,95 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyGetter(this, "History", () => {
- Cu.import("resource://gre/modules/PlacesUtils.jsm");
- return PlacesUtils.history;
-});
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
normalizeTime,
SingletonEventManager,
} = ExtensionUtils;
-let historySvc = Ci.nsINavHistoryService;
+const History = PlacesUtils.history;
const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
- ["link", historySvc.TRANSITION_LINK],
- ["typed", historySvc.TRANSITION_TYPED],
- ["auto_bookmark", historySvc.TRANSITION_BOOKMARK],
- ["auto_subframe", historySvc.TRANSITION_EMBED],
- ["manual_subframe", historySvc.TRANSITION_FRAMED_LINK],
+ ["link", History.TRANSITION_LINK],
+ ["typed", History.TRANSITION_TYPED],
+ ["auto_bookmark", History.TRANSITION_BOOKMARK],
+ ["auto_subframe", History.TRANSITION_EMBED],
+ ["manual_subframe", History.TRANSITION_FRAMED_LINK],
]);
+let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map();
+for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) {
+ TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition);
+}
+
function getTransitionType(transition) {
// cannot set a default value for the transition argument as the framework sets it to null
transition = transition || "link";
let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition);
if (!transitionType) {
throw new Error(`|${transition}| is not a supported transition for history`);
}
return transitionType;
}
+function getTransition(transitionType) {
+ return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link";
+}
+
/*
- * Converts a nsINavHistoryResultNode into a plain object
+ * Converts a nsINavHistoryResultNode into a HistoryItem
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
*/
-function convertNavHistoryResultNode(node) {
+function convertNodeToHistoryItem(node) {
return {
id: node.pageGuid,
url: node.uri,
title: node.title,
lastVisitTime: PlacesUtils.toDate(node.time).getTime(),
visitCount: node.accessCount,
};
}
/*
+ * Converts a nsINavHistoryResultNode into a VisitItem
+ *
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
+ */
+function convertNodeToVisitItem(node) {
+ return {
+ id: node.pageGuid,
+ visitId: node.visitId,
+ visitTime: PlacesUtils.toDate(node.time).getTime(),
+ referringVisitId: node.fromVisitId,
+ transition: getTransition(node.visitType),
+ };
+}
+
+/*
* Converts a nsINavHistoryContainerResultNode into an array of objects
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryContainerResultNode
*/
-function convertNavHistoryContainerResultNode(container) {
+function convertNavHistoryContainerResultNode(container, converter) {
let results = [];
container.containerOpen = true;
for (let i = 0; i < container.childCount; i++) {
let node = container.getChild(i);
- results.push(convertNavHistoryResultNode(node));
+ results.push(converter(node));
}
container.containerOpen = false;
return results;
}
var _observer;
function getObserver() {
@@ -158,17 +178,33 @@ extensions.registerSchemaAPI("history",
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
options.maxResults = query.maxResults || 100;
let historyQuery = History.getNewQuery();
historyQuery.searchTerms = query.text;
historyQuery.beginTime = beginTime;
historyQuery.endTime = endTime;
let queryResult = History.executeQuery(historyQuery, options).root;
- let results = convertNavHistoryContainerResultNode(queryResult);
+ let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToHistoryItem);
+ return Promise.resolve(results);
+ },
+ getVisits: function(details) {
+ let url = details.url;
+ if (!url) {
+ return Promise.reject({message: "A URL must be provided for getVisits"});
+ }
+
+ let options = History.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+
+ let historyQuery = History.getNewQuery();
+ historyQuery.uri = NetUtil.newURI(url);
+ let queryResult = History.executeQuery(historyQuery, options).root;
+ let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
return Promise.resolve(results);
},
onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => {
let listener = (event, data) => {
context.runSafe(fire, data);
};
--- a/browser/components/extensions/schemas/history.json
+++ b/browser/components/extensions/schemas/history.json
@@ -150,17 +150,16 @@
}
}
]
}
]
},
{
"name": "getVisits",
- "unsupported": true,
"type": "function",
"description": "Retrieves information about visits to a URL.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
--- a/browser/components/extensions/test/browser/browser_ext_history.js
+++ b/browser/components/extensions/test/browser/browser_ext_history.js
@@ -290,8 +290,73 @@ add_task(function* test_add_url() {
for (let data of failTestData) {
extension.sendMessage("expect-failure", data);
yield extension.awaitMessage("add-failed");
}
yield extension.unload();
});
+
+add_task(function* test_get_visits() {
+ function background() {
+ const TEST_DOMAIN = "http://example.com/";
+ const FIRST_DATE = Date.now();
+ const INITIAL_DETAILS = {
+ url: TEST_DOMAIN,
+ visitTime: FIRST_DATE,
+ transition: "link",
+ };
+
+ let visitIds = new Set();
+
+ function checkVisit(visit, expected) {
+ visitIds.add(visit.visitId);
+ browser.test.assertEq(expected.visitTime, visit.visitTime, "visit has the correct visitTime");
+ browser.test.assertEq(expected.transition, visit.transition, "visit has the correct transition");
+ browser.history.search({text: expected.url}).then(results => {
+ // all results will have the same id, so we only need to use the first one
+ browser.test.assertEq(results[0].id, visit.id, "visit has the correct id");
+ });
+ }
+
+ let details = Object.assign({}, INITIAL_DETAILS);
+
+ browser.history.addUrl(details).then(() => {
+ return browser.history.getVisits({url: details.url});
+ }).then(results => {
+ browser.test.assertEq(1, results.length, "the expected number of visits were returned");
+ checkVisit(results[0], details);
+ details.url = `${TEST_DOMAIN}/1/`;
+ return browser.history.addUrl(details);
+ }).then(() => {
+ return browser.history.getVisits({url: details.url});
+ }).then(results => {
+ browser.test.assertEq(1, results.length, "the expected number of visits were returned");
+ checkVisit(results[0], details);
+ details.visitTime = FIRST_DATE - 1000;
+ details.transition = "typed";
+ return browser.history.addUrl(details);
+ }).then(() => {
+ return browser.history.getVisits({url: details.url});
+ }).then(results => {
+ browser.test.assertEq(2, results.length, "the expected number of visits were returned");
+ checkVisit(results[0], INITIAL_DETAILS);
+ checkVisit(results[1], details);
+ }).then(() => {
+ browser.test.assertEq(3, visitIds.size, "each visit has a unique visitId");
+ browser.test.notifyPass("get-visits");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ yield PlacesTestUtils.clearHistory();
+ yield extension.startup();
+
+ yield extension.awaitFinish("get-visits");
+ yield extension.unload();
+});