Merge mozilla-central to inbound. a=merge CLOSED TREE
authorTiberius Oros <toros@mozilla.com>
Mon, 08 Oct 2018 12:54:57 +0300
changeset 498407 556ba29c2e6c5af0ba72aaf67f8c81f96e2a6672
parent 498406 a5f6d07b35d1906a135741a12bd31fb701132c6c (current diff)
parent 498388 fb8edc9e0e00097813febdb64a64c82666c812be (diff)
child 498408 15fe5851f113defcc86285e94ed79687031a5d56
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to inbound. a=merge CLOSED TREE
--- a/browser/components/extensions/test/xpcshell/test_ext_history.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_history.js
@@ -293,18 +293,18 @@ add_task(async function test_add_url() {
     [{visitTime: new Date()}, "with_date"],
     [{visitTime: Date.now()}, "with_ms_number"],
     [{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"],
+    [{visitTime: Date.now() + 1000000}, "a future date", "Invalid value"],
+    [{url: "about.config"}, "an invalid url", "Invalid value"],
   ];
 
   async function checkUrl(results) {
     ok((await 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");
     equal(results.result.title, results.details.title, "URL was added with the correct title");
     if (results.details.visitTime) {
       equal(results.result.lastVisitTime,
--- a/devtools/client/shared/components/menu/MenuButton.js
+++ b/devtools/client/shared/components/menu/MenuButton.js
@@ -65,25 +65,27 @@ class MenuButton extends PureComponent {
     super(props);
 
     this.showMenu = this.showMenu.bind(this);
     this.hideMenu = this.hideMenu.bind(this);
     this.toggleMenu = this.toggleMenu.bind(this);
     this.onHidden = this.onHidden.bind(this);
     this.onClick = this.onClick.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
+    this.onTouchStart = this.onTouchStart.bind(this);
 
     this.buttonRef = createRef();
 
     this.state = {
       expanded: false,
       // In tests, initialize the menu immediately.
       isMenuInitialized: flags.testing || false,
       win: props.doc.defaultView.top,
     };
+    this.ignoreNextClick = false;
 
     this.initializeTooltip();
   }
 
   componentDidMount() {
     if (!this.state.isMenuInitialized) {
       // Initialize the menu when the button is focused or moused over.
       for (const event of ["focus", "mousemove"]) {
@@ -191,16 +193,54 @@ class MenuButton extends PureComponent {
     }
 
     this.tooltip.updateContainerBounds(this.buttonRef.current, {
       position: this.props.menuPosition,
       y: this.props.menuOffset,
     });
   }
 
+  // When we are closing the menu we will get a 'hidden' event before we get
+  // a 'click' event. We want to re-enable the pointer-events: auto setting we
+  // use on the button while the menu is visible, but we don't want to do it
+  // until after the subsequent click event since otherwise we will end up
+  // re-opening the menu.
+  //
+  // For mouse events, we achieve this by using setTimeout(..., 0) to schedule
+  // a separate task to run after the click event, but in the case of touch
+  // events the event order differs and the setTimeout callback will run before
+  // the click event.
+  //
+  // In order to prevent that we detect touch events and set a flag to ignore
+  // the next click event. However, we need to differentiate between touch drag
+  // events and long press events (which don't generate a 'click') and "taps"
+  // (which do). We do that by looking for a 'touchmove' event and clearing the
+  // flag if we get one.
+  onTouchStart(evt) {
+    const touchend = () => {
+      const anchorRect = this.buttonRef.current.getClientRects()[0];
+      const { clientX, clientY } = evt.changedTouches[0];
+      // We need to check that the click is inside the bounds since when the
+      // menu is being closed the button will currently have
+      // pointer-events: none (and if we don't check the bounds we will end up
+      // ignoring unrelated clicks).
+      if (anchorRect.x <= clientX && clientX <= anchorRect.x + anchorRect.width &&
+          anchorRect.y <= clientY && clientY <= anchorRect.y + anchorRect.height) {
+        this.ignoreNextClick = true;
+      }
+    };
+
+    const touchmove = () => {
+      this.state.win.removeEventListener("touchend", touchend);
+    };
+
+    this.state.win.addEventListener("touchend", touchend, { once: true });
+    this.state.win.addEventListener("touchmove", touchmove, { once: true });
+  }
+
   onHidden() {
     this.setState({ expanded: false });
     // While the menu is open, if we click _anywhere_ outside the menu, it will
     // automatically close. This is performed by the XUL wrapper before we get
     // any chance to see any event. To avoid immediately re-opening the menu
     // when we process the subsequent click event on this button, we set
     // 'pointer-events: none' on the button while the menu is open.
     //
@@ -208,24 +248,34 @@ class MenuButton extends PureComponent {
     // the button works again) but we don't want to do it immediately since the
     // "popuphidden" event which triggers this callback might be dispatched
     // before the "click" event that we want to ignore.  As a result, we queue
     // up a task using setTimeout() to run after the "click" event.
     this.state.win.setTimeout(() => {
       if (this.buttonRef.current) {
         this.buttonRef.current.style.pointerEvents = "auto";
       }
+      this.state.win.removeEventListener("touchstart",
+                                         this.onTouchStart,
+                                         true);
     }, 0);
 
+    this.state.win.addEventListener("touchstart", this.onTouchStart, true);
+
     if (this.props.onCloseButton) {
       this.props.onCloseButton();
     }
   }
 
   async onClick(e) {
+    if (this.ignoreNextClick) {
+      this.ignoreNextClick = false;
+      return;
+    }
+
     if (e.target === this.buttonRef.current) {
       // On Mac, even after clicking the button it doesn't get focus.
       // Force focus to the button so that our keydown handlers get called.
       this.buttonRef.current.focus();
 
       if (this.props.onClick) {
         this.props.onClick(e);
       }
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -210,18 +210,19 @@ HistoryStore.prototype = {
     let failed = [];
     let toAdd = [];
     let toRemove = [];
     for await (let record of Async.yieldingIterator(records)) {
       if (record.deleted) {
         toRemove.push(record);
       } else {
         try {
-          if (await this._recordToPlaceInfo(record)) {
-            toAdd.push(record);
+          let pageInfo = await this._recordToPlaceInfo(record);
+          if (pageInfo) {
+            toAdd.push(pageInfo);
           }
         } catch (ex) {
           if (Async.isShutdownException(ex)) {
             throw ex;
           }
           this._log.error("Failed to create a place info", ex);
           this._log.trace("The record that failed", record);
           failed.push(record.id);
@@ -325,35 +326,35 @@ HistoryStore.prototype = {
   _canAddURI(uri) {
     return PlacesUtils.history.canAddURI(uri);
   },
 
   /**
    * Converts a Sync history record to a mozIPlaceInfo.
    *
    * Throws if an invalid record is encountered (invalid URI, etc.),
-   * returns true if the record is to be applied, false otherwise
-   * (no visits to add, etc.),
+   * returns a new PageInfo object if the record is to be applied, null
+   * otherwise (no visits to add, etc.),
    */
   async _recordToPlaceInfo(record) {
     // Sort out invalid URIs and ones Places just simply doesn't want.
     record.url = PlacesUtils.normalizeToURLOrGUID(record.histUri);
     record.uri = CommonUtils.makeURI(record.histUri);
 
     if (!Utils.checkGUID(record.id)) {
       this._log.warn("Encountered record with invalid GUID: " + record.id);
-      return false;
+      return null;
     }
     record.guid = record.id;
 
     if (!this._canAddURI(record.uri) ||
         !this.engine.shouldSyncURL(record.uri.spec)) {
       this._log.trace("Ignoring record " + record.id + " with URI "
                       + record.uri.spec + ": can't add this URI.");
-      return false;
+      return null;
     }
 
     // We dupe visits by date and type. So an incoming visit that has
     // the same timestamp and type as a local one won't get applied.
     // To avoid creating new objects, we rewrite the query result so we
     // can simply check for containment below.
     let curVisitsAsArray = [];
     let curVisits = new Set();
@@ -424,20 +425,30 @@ HistoryStore.prototype = {
 
     // No update if there aren't any visits to apply.
     // History wants at least one visit.
     // In any case, the only thing we could change would be the title
     // and that shouldn't change without a visit.
     if (!record.visits.length) {
       this._log.trace("Ignoring record " + record.id + " with URI "
                       + record.uri.spec + ": no visits to add.");
-      return false;
+      return null;
     }
 
-    return true;
+    // PageInfo is validated using validateItemProperties which does a shallow
+    // copy of the properties. Since record uses getters some of the properties
+    // are not copied over. Thus we create and return a new object.
+    let pageInfo = {
+      title: record.title,
+      url: record.url,
+      guid: record.guid,
+      visits: record.visits,
+    };
+
+    return pageInfo;
   },
 
   async remove(record) {
     this._log.trace("Removing page: " + record.id);
     let removed = await PlacesUtils.history.remove(record.id);
     if (removed) {
       this._log.trace("Removed page: " + record.id);
     } else {
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -299,16 +299,98 @@ const SYNC_BOOKMARK_VALIDATORS = Object.
 const SYNC_CHANGE_RECORD_VALIDATORS = Object.freeze({
   modified: simpleValidateFunc(v => typeof v == "number" && v >= 0),
   counter: simpleValidateFunc(v => typeof v == "number" && v >= 0),
   status: simpleValidateFunc(v => typeof v == "number" &&
                                   Object.values(PlacesUtils.bookmarks.SYNC_STATUS).includes(v)),
   tombstone: simpleValidateFunc(v => v === true || v === false),
   synced: simpleValidateFunc(v => v === true || v === false),
 });
+/**
+ * List PageInfo bookmark object validators.
+ */
+const PAGEINFO_VALIDATORS = Object.freeze({
+  guid: BOOKMARK_VALIDATORS.guid,
+  url: BOOKMARK_VALIDATORS.url,
+  title: v => {
+    if (v == null || v == undefined) {
+      return undefined;
+    } else if (typeof v === "string") {
+      return v;
+    }
+    throw new TypeError(`title property of PageInfo object: ${v} must be a string if provided`);
+  },
+  previewImageURL: v => {
+    if (!v) {
+      return null;
+    }
+    return BOOKMARK_VALIDATORS.url(v);
+  },
+  description: v => {
+    if (typeof v === "string" || v === null) {
+      return v ? v.slice(0, DB_DESCRIPTION_LENGTH_MAX) : null;
+    }
+    throw new TypeError(`description property of pageInfo object: ${v} must be either a string or null if provided`);
+  },
+  annotations: v => {
+    if (typeof v != "object" ||
+        v.constructor.name != "Map") {
+        throw new TypeError("annotations must be a Map");
+      }
+
+      if (v.size == 0) {
+        throw new TypeError("there must be at least one annotation");
+      }
+
+      for (let [key, value] of v.entries()) {
+        if (typeof key != "string") {
+          throw new TypeError("all annotation keys must be strings");
+        }
+        if (typeof value != "string" &&
+            typeof value != "number" &&
+            typeof value != "boolean" &&
+            value !== null &&
+            value !== undefined) {
+          throw new TypeError("all annotation values must be Boolean, Numbers or Strings");
+        }
+      }
+      return v;
+  },
+  visits: v => {
+    if (!Array.isArray(v) || !v.length) {
+      throw new TypeError("PageInfo object must have an array of visits");
+    }
+    let visits = [];
+    for (let inVisit of v) {
+      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 = PlacesUtils.normalizeToURLOrGUID(inVisit.referrer);
+      }
+      visits.push(visit);
+    }
+    return visits;
+  },
+});
+
 
 var PlacesUtils = {
   // Place entries that are containers, e.g. bookmark folders or queries.
   TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
   // Place entries that are bookmark separators.
   TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
   // Place entries that are not containers or separators
   TYPE_X_MOZ_PLACE: "text/x-moz-place",
@@ -628,17 +710,17 @@ var PlacesUtils = {
    *         - fixup: a function invoked when validation fails, takes the input
    *                  object as argument and must fix the property.
    *
    * @return a validated and normalized item.
    * @throws if the object contains invalid data.
    * @note any unknown properties are pass-through.
    */
   validateItemProperties(name, validators, props, behavior = {}) {
-    if (!props)
+    if (typeof props != "object" || !props)
       throw new Error(`${name}: Input should be a valid object`);
     // Make a shallow copy of `props` to avoid mutating the original object
     // when filling in defaults.
     let input = Object.assign({}, props);
     let normalizedInput = {};
     let required = new Set();
     for (let prop in behavior) {
       if (behavior[prop].hasOwnProperty("required") && behavior[prop].required) {
@@ -1031,124 +1113,22 @@ var PlacesUtils = {
 
   /**
    * Validate an input PageInfo object, returning a valid PageInfo object.
    *
    * @param pageInfo: (PageInfo)
    * @return (PageInfo)
    */
   validatePageInfo(pageInfo, validateVisits = true) {
-    let info = {
-      visits: [],
-    };
-
-    if (typeof pageInfo != "object" || !pageInfo) {
-      throw new TypeError("pageInfo must be an object");
-    }
-
-    if (!pageInfo.url) {
-      throw new TypeError("PageInfo object must have a url property");
-    }
-
-    info.url = this.normalizeToURLOrGUID(pageInfo.url);
-
-    if (typeof pageInfo.guid === "string" && this.isValidGuid(pageInfo.guid)) {
-      info.guid = pageInfo.guid;
-    } else if (pageInfo.guid) {
-      throw new TypeError(`guid property of PageInfo object: ${pageInfo.guid} is invalid`);
-    }
-
-    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 ("description" in pageInfo && (typeof pageInfo.description === "string" || pageInfo.description === null)) {
-      info.description = pageInfo.description ? pageInfo.description.slice(0, DB_DESCRIPTION_LENGTH_MAX) : null;
-    } else if (pageInfo.description !== undefined) {
-      throw new TypeError(`description property of pageInfo object: ${pageInfo.description} must be either a string or null if provided`);
-    }
-
-    if ("previewImageURL" in pageInfo) {
-      let previewImageURL = pageInfo.previewImageURL;
-
-      if (!previewImageURL) {
-        info.previewImageURL = null;
-      } else if (typeof(previewImageURL) === "string" && previewImageURL.length <= DB_URL_LENGTH_MAX) {
-        info.previewImageURL = new URL(previewImageURL);
-      } else if (previewImageURL instanceof Ci.nsIURI && previewImageURL.spec.length <= DB_URL_LENGTH_MAX) {
-        info.previewImageURL = new URL(previewImageURL.spec);
-      } else if (previewImageURL instanceof URL && previewImageURL.href.length <= DB_URL_LENGTH_MAX) {
-        info.previewImageURL = previewImageURL;
-      } else {
-        throw new TypeError("previewImageURL property of pageInfo object: ${previewImageURL} is invalid");
-      }
-    }
-
-    if (pageInfo.annotations) {
-      if (typeof pageInfo.annotations != "object" ||
-          pageInfo.annotations.constructor.name != "Map") {
-        throw new TypeError("annotations must be a Map");
-      }
-
-      if (pageInfo.annotations.size == 0) {
-        throw new TypeError("there must be at least one annotation");
-      }
-
-      for (let [key, value] of pageInfo.annotations.entries()) {
-        if (typeof key != "string") {
-          throw new TypeError("all annotation keys must be strings");
-        }
-        if (typeof value != "string" &&
-            typeof value != "number" &&
-            typeof value != "boolean" &&
-            value !== null &&
-            value !== undefined) {
-          throw new TypeError("all annotation values must be Boolean, Numbers or Strings");
-        }
-      }
-
-      info.annotations = pageInfo.annotations;
-    }
-
-    if (!validateVisits) {
-      return info;
-    }
-
-    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;
+    return this.validateItemProperties("PageInfo", PAGEINFO_VALIDATORS, pageInfo,
+      { url: { requiredIf: b => { typeof b.guid != "string"; } },
+        guid: { requiredIf: b => { typeof b.url != "string"; } },
+        visits: { requiredIf: b => validateVisits  },
+      });
   },
-
   /**
    * 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.
    */
--- a/toolkit/components/places/tests/history/test_insert.js
+++ b/toolkit/components/places/tests/history/test_insert.js
@@ -5,98 +5,98 @@
 
 "use strict";
 
 add_task(async function test_insert_error_cases() {
   const TEST_URL = "http://mozilla.com";
 
   Assert.throws(
     () => PlacesUtils.history.insert(),
-    /TypeError: pageInfo must be an object/,
-    "passing a null into History.insert should throw a TypeError"
+    /Error: PageInfo: Input should be /,
+    "passing a null into History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert(1),
-    /TypeError: pageInfo must be an object/,
-    "passing a non object into History.insert should throw a TypeError"
+    /Error: PageInfo: Input should be/,
+    "passing a non object into History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert({}),
-    /TypeError: PageInfo object must have a url property/,
-    "passing an object without a url to History.insert should throw a TypeError"
+    /Error: PageInfo: The following properties were expected/,
+    "passing an object without a url to History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert({url: 123}),
-    /TypeError: Invalid url or guid: 123/,
-    "passing an object with an invalid url to History.insert should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing an object with an invalid url to History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert({url: TEST_URL}),
-    /TypeError: PageInfo object must have an array of visits/,
-    "passing an object without a visits property to History.insert should throw a TypeError"
+    /Error: PageInfo: The following properties were expected/,
+    "passing an object without a visits property to History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert({url: TEST_URL, visits: 1}),
-    /TypeError: PageInfo object must have an array of visits/,
-    "passing an object with a non-array visits property to History.insert should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing an object with a non-array visits property to History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert({url: TEST_URL, visits: []}),
-    /TypeError: PageInfo object must have an array of visits/,
-    "passing an object with an empty array as the visits property to History.insert should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing an object with an empty array as the visits property to History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert({
       url: TEST_URL,
       visits: [
         {
           transition: TRANSITION_LINK,
           date: "a",
         },
       ]}),
-    /TypeError: Expected a Date, got a/,
-    "passing a visit object with an invalid date to History.insert should throw a TypeError"
+    /PageInfo: Invalid value for property/,
+    "passing a visit object with an invalid date to History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert({
       url: TEST_URL,
       visits: [
         {
           transition: TRANSITION_LINK,
         },
         {
           transition: TRANSITION_LINK,
           date: "a",
         },
       ]}),
-    /TypeError: Expected a Date, got a/,
-    "passing a second visit object with an invalid date to History.insert should throw a TypeError"
+    /PageInfo: Invalid value for property/,
+    "passing a second visit object with an invalid date to History.insert should throw an Error"
   );
   let futureDate = new Date();
   futureDate.setDate(futureDate.getDate() + 1000);
   Assert.throws(
     () => PlacesUtils.history.insert({
       url: TEST_URL,
       visits: [
         {
           transition: TRANSITION_LINK,
           date: futureDate,
         },
       ]}),
-    /cannot be a future date/,
-    "passing a visit object with a future date to History.insert should throw a TypeError"
+    /PageInfo: Invalid value for property/,
+    "passing a visit object with a future date to History.insert should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.insert({
       url: TEST_URL,
       visits: [
         {transition: "a"},
       ]}),
-    /TypeError: transition: a is not a valid transition type/,
-    "passing a visit object with an invalid transition to History.insert should throw a TypeError"
+    /PageInfo: Invalid value for property/,
+    "passing a visit object with an invalid transition to History.insert should throw an Error"
   );
 });
 
 add_task(async function test_history_insert() {
   const TEST_URL = "http://mozilla.com/";
 
   let inserter = async function(name, filter, referrer, date, transition) {
     info(name);
--- a/toolkit/components/places/tests/history/test_insertMany.js
+++ b/toolkit/components/places/tests/history/test_insertMany.js
@@ -20,18 +20,18 @@ add_task(async function test_error_cases
   );
   Assert.throws(
     () => PlacesUtils.history.insertMany([]),
     /TypeError: pageInfos may not be an empty array/,
     "passing an empty array into History.insertMany should throw a TypeError"
   );
   Assert.throws(
     () => PlacesUtils.history.insertMany([validPageInfo, {}]),
-    /TypeError: PageInfo object must have a url property/,
-    "passing a second invalid PageInfo object to History.insertMany should throw a TypeError"
+    /Error: PageInfo: The following properties were expected/,
+    "passing a second invalid PageInfo object to History.insertMany should throw an Error"
   );
 });
 
 add_task(async function test_insertMany() {
   const BAD_URLS = ["about:config", "chrome://browser/content/browser.xul"];
   const GOOD_URLS = [1, 2, 3].map(x => { return `http://mozilla.com/${x}`; });
 
   let makePageInfos = async function(urls, filter = x => x) {
--- a/toolkit/components/places/tests/history/test_update.js
+++ b/toolkit/components/places/tests/history/test_update.js
@@ -5,104 +5,104 @@
 
 // Tests for `History.update` as implemented in History.jsm
 
 "use strict";
 
 add_task(async function test_error_cases() {
   Assert.throws(
     () => PlacesUtils.history.update("not an object"),
-    /TypeError: pageInfo must be/,
-    "passing a string as pageInfo should throw a TypeError"
+    /Error: PageInfo: Input should be a valid object/,
+    "passing a string as pageInfo should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update(null),
-    /TypeError: pageInfo must be/,
-    "passing a null as pageInfo should throw a TypeError"
+    /Error: PageInfo: Input should be/,
+    "passing a null as pageInfo should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({url: "not a valid url string"}),
-    /TypeError: not a valid url string is/,
-    "passing an invalid url should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing an invalid url should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({
       url: "http://valid.uri.com",
       description: 123,
     }),
-    /TypeError: description property of/,
-    "passing a non-string description in pageInfo should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing a non-string description in pageInfo should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({
       url: "http://valid.uri.com",
       guid: "invalid guid",
       description: "Test description",
     }),
-    /TypeError: guid property of/,
-    "passing a invalid guid in pageInfo should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing a invalid guid in pageInfo should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({
       url: "http://valid.uri.com",
       previewImageURL: "not a valid url string",
     }),
-    /TypeError: not a valid url string is/,
-    "passing an invlid preview image url in pageInfo should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing an invlid preview image url in pageInfo should throw an Error"
   );
   Assert.throws(
     () => {
       let imageName = "a-very-long-string".repeat(10000);
       let previewImageURL = `http://valid.uri.com/${imageName}.png`;
       PlacesUtils.history.update({
         url: "http://valid.uri.com",
         previewImageURL,
       });
     },
-    /TypeError: previewImageURL property of/,
-    "passing an oversized previewImageURL in pageInfo should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing an oversized previewImageURL in pageInfo should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({ url: "http://valid.uri.com" }),
     /TypeError: pageInfo object must at least/,
     "passing a pageInfo with neither description, previewImageURL, nor annotations should throw a TypeError"
   );
   Assert.throws(
     () => PlacesUtils.history.update({ url: "http://valid.uri.com", annotations: "asd" }),
-    /TypeError: annotations must be a Map/,
-    "passing a pageInfo with incorrect annotations type should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing a pageInfo with incorrect annotations type should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({ url: "http://valid.uri.com", annotations: new Map() }),
-    /TypeError: there must be at least one annotation/,
-    "passing a pageInfo with an empty annotations type should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing a pageInfo with an empty annotations type should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({
       url: "http://valid.uri.com",
       annotations: new Map([[1234, "value"]]),
     }),
-    /TypeError: all annotation keys must be strings/,
-    "passing a pageInfo with an invalid key type should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing a pageInfo with an invalid key type should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({
       url: "http://valid.uri.com",
       annotations: new Map([["test", ["myarray"]]]),
     }),
-    /TypeError: all annotation values must be Boolean, Numbers or Strings/,
-    "passing a pageInfo with an invalid key type should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing a pageInfo with an invalid key type should throw an Error"
   );
   Assert.throws(
     () => PlacesUtils.history.update({
       url: "http://valid.uri.com",
       annotations: new Map([["test", {anno: "value"}]]),
     }),
-    /TypeError: all annotation values must be Boolean, Numbers or Strings/,
-    "passing a pageInfo with an invalid key type should throw a TypeError"
+    /Error: PageInfo: Invalid value for property/,
+    "passing a pageInfo with an invalid key type should throw an Error"
   );
 });
 
 add_task(async function test_description_change_saved() {
   await PlacesUtils.history.clear();
 
   let TEST_URL = "http://mozilla.org/test_description_change_saved";
   await PlacesTestUtils.addVisits(TEST_URL);
@@ -376,8 +376,18 @@ add_task(async function test_annotations
   info("Adding annotations to a non existing page should be silent");
   await PlacesUtils.history.update({
     url: "http://nonexisting.moz/",
     annotations: new Map([
       ["test/annotation", null],
     ]),
   });
 });
+
+add_task(async function test_annotations_nonexisting_page() {
+  info("Adding annotations to a non existing page should be silent");
+  await PlacesUtils.history.update({
+    url: "http://nonexisting.moz/",
+    annotations: new Map([
+      ["test/annotation", null],
+    ]),
+  });
+});