Bug 1265836 - Part 4: Implement browser.history.addUrl, r?aswan draft
authorBob Silverberg <bsilverberg@mozilla.com>
Thu, 19 May 2016 21:55:37 -0400
changeset 374251 bc58756420ecbeea1631664eb411e26e2862958f
parent 374250 02ab9d5a8fe84552ae2a7ea34c89dc116737dfc4
child 522581 10c2ac76d6b070b37c00cd5b1ce9cbbe191accb1
push id19960
push userbmo:bob.silverberg@gmail.com
push dateThu, 02 Jun 2016 02:42:32 +0000
reviewersaswan
bugs1265836
milestone49.0a1
Bug 1265836 - Part 4: Implement browser.history.addUrl, r?aswan MozReview-Commit-ID: CBcKLvRLj3w
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
@@ -9,16 +9,35 @@ XPCOMUtils.defineLazyGetter(this, "Histo
   return PlacesUtils.history;
 });
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 const {
   normalizeTime,
 } = ExtensionUtils;
 
+let historySvc = Ci.nsINavHistoryService;
+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],
+]);
+
+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;
+}
+
 /*
  * Converts a nsINavHistoryResultNode into a plain object
  *
  * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
  */
 function convertNavHistoryResultNode(node) {
   return {
     id: node.pageGuid,
@@ -43,16 +62,42 @@ function convertNavHistoryContainerResul
   }
   container.containerOpen = false;
   return results;
 }
 
 extensions.registerSchemaAPI("history", "history", (extension, context) => {
   return {
     history: {
+      addUrl: function(details) {
+        let transition, date;
+        try {
+          transition = getTransitionType(details.transition);
+        } catch (error) {
+          return Promise.reject({message: error.message});
+        }
+        if (details.visitTime) {
+          date = normalizeTime(details.visitTime);
+        }
+        let pageInfo = {
+          title: details.title,
+          url: details.url,
+          visits: [
+            {
+              transition,
+              date,
+            },
+          ],
+        };
+        try {
+          return History.insert(pageInfo).then(() => undefined);
+        } catch (error) {
+          return Promise.reject({message: error.message});
+        }
+      },
       deleteAll: function() {
         return History.clear();
       },
       deleteRange: function(filter) {
         let newFilter = {
           beginDate: normalizeTime(filter.startTime),
           endDate: normalizeTime(filter.endTime),
         };
--- a/browser/components/extensions/schemas/history.json
+++ b/browser/components/extensions/schemas/history.json
@@ -182,28 +182,42 @@
                 }
               }
             ]
           }
         ]
       },
       {
         "name": "addUrl",
-        "unsupported": true,
         "type": "function",
-        "description": "Adds a URL to the history at the current time with a $(topic:transition-types)[transition type] of \"link\".",
+        "description": "Adds a URL to the history with a default visitTime of the current time and a default $(topic:transition-types)[transition type] of \"link\".",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "url": {
                 "type": "string",
-                "description": "The URL to add."
+                "description": "The URL to add. Must be a valid URL that can be added to history."
+              },
+              "title": {
+                "type": "string",
+                "optional": true,
+                "description": "The title of the page."
+              },
+              "transition": {
+                "$ref": "TransitionType",
+                "optional": true,
+                "description": "The $(topic:transition-types)[transition type] for this visit from its referrer."
+              },
+              "visitTime": {
+                "$ref": "HistoryTime",
+                "optional": true,
+                "description": "The date when this visit occurred."
               }
             }
           },
           {
             "name": "callback",
             "type": "function",
             "optional": true,
             "parameters": []
--- a/browser/components/extensions/test/browser/browser_ext_history.js
+++ b/browser/components/extensions/test/browser/browser_ext_history.js
@@ -1,14 +1,18 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
                                   "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
+                                  "resource://gre/modules/ExtensionUtils.jsm");
 
 add_task(function* test_delete() {
   function background() {
     browser.test.onMessage.addListener((msg, arg) => {
       if (msg === "delete-url") {
         browser.history.deleteUrl({url: arg}).then(result => {
           browser.test.assertEq(undefined, result, "browser.history.deleteUrl returns nothing");
           browser.test.sendMessage("url-deleted");
@@ -25,17 +29,16 @@ add_task(function* test_delete() {
         });
       }
     });
 
     browser.test.sendMessage("ready");
   }
 
   const REFERENCE_DATE = new Date(1999, 9, 9, 9, 9);
-  let {PlacesUtils} = Cu.import("resource://gre/modules/PlacesUtils.jsm", {});
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["history"],
     },
     background: `(${background})()`,
   });
 
@@ -186,8 +189,91 @@ add_task(function* test_search() {
   results = yield extension.awaitMessage("max-results-search");
   is(results.length, 1, "history.search returned 1 result");
   checkResult(results, DOUBLE_VISIT_URL, 2);
 
   yield extension.awaitFinish("search");
   yield extension.unload();
   yield PlacesTestUtils.clearHistory();
 });
+
+add_task(function* test_add_url() {
+  function background() {
+    const TEST_DOMAIN = "http://example.com/";
+
+    browser.test.onMessage.addListener((msg, testData) => {
+      let [details, type] = testData;
+      details.url = details.url || `${TEST_DOMAIN}${type}`;
+      if (msg === "add-url") {
+        details.title = `Title for ${type}`;
+        browser.history.addUrl(details).then(() => {
+          return browser.history.search({text: details.url});
+        }).then(results => {
+          browser.test.assertEq(1, results.length, "1 result found when searching for added URL");
+          browser.test.sendMessage("url-added", {details, result: results[0]});
+        });
+      } else if (msg === "expect-failure") {
+        let expectedMsg = testData[2];
+        browser.history.addUrl(details).then(() => {
+          browser.test.fail(`Expected error thrown for ${type}`);
+        }, error => {
+          browser.test.assertTrue(
+            error.message.includes(expectedMsg),
+            `"Expected error thrown when trying to add a URL with  ${type}`
+          );
+          browser.test.sendMessage("add-failed");
+        });
+      }
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  let addTestData = [
+    [{}, "default"],
+    [{visitTime: new Date()}, "with_date"],
+    [{visitTime: Date.now()}, "with_ms_number"],
+    [{visitTime: Date.now().toString()}, "with_ms_string"],
+    [{visitTime: new Date().toISOString()}, "with_iso_string"],
+    [{transition: "typed"}, "valid_transition"],
+  ];
+
+  let failTestData = [
+    [{transition: "generated"}, "an invalid transition", "|generated| is not a supported transition for history"],
+    [{visitTime: Date.now() + 1000000}, "a future date", "cannot be a future date"],
+    [{url: "about.config"}, "an invalid url", "about.config is not a valid URL"],
+  ];
+
+  function* checkUrl(results) {
+    ok(yield PlacesTestUtils.isPageInDB(results.details.url), `${results.details.url} found in history database`);
+    ok(PlacesUtils.isValidGuid(results.result.id), "URL was added with a valid id");
+    is(results.result.title, results.details.title, "URL was added with the correct title");
+    if (results.details.visitTime) {
+      is(results.result.lastVisitTime,
+         Number(ExtensionUtils.normalizeTime(results.details.visitTime)),
+         "URL was added with the correct date");
+    }
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["history"],
+    },
+    background: `(${background})()`,
+  });
+
+  yield PlacesTestUtils.clearHistory();
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+
+  for (let data of addTestData) {
+    extension.sendMessage("add-url", data);
+    let results = yield extension.awaitMessage("url-added");
+    yield checkUrl(results);
+  }
+
+  for (let data of failTestData) {
+    extension.sendMessage("expect-failure", data);
+    yield extension.awaitMessage("add-failed");
+  }
+
+  yield extension.unload();
+});