Bug 1265835 - Implement browser.history.getVisits, r?aswan draft
authorBob Silverberg <bsilverberg@mozilla.com>
Wed, 08 Jun 2016 08:50:08 -0400
changeset 376688 d91dd8704a1cd539ae4ff3a98611d6acfeb8720a
parent 376687 71d424246fe1149948c8054a4392feea87dfd0ad
child 523207 d97e7c69338e463a366a0a64fa40c989aceafa82
push id20639
push userbmo:bob.silverberg@gmail.com
push dateWed, 08 Jun 2016 12:52:00 +0000
reviewersaswan
bugs1265835
milestone50.0a1
Bug 1265835 - Implement browser.history.getVisits, r?aswan MozReview-Commit-ID: lhFdMTHYUl
browser/components/extensions/ext-history.js
browser/components/extensions/schemas/history.json
browser/components/extensions/test/browser/browser_ext_history.js
--- 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();
+});