Bug 1352502 - Part 2. Add API to update the page meta info for Places. r?mak draft
authorNan Jiang <najiang@mozilla.com>
Fri, 23 Jun 2017 14:30:27 -0400
changeset 599911 e9004db90f47d9a5951dd4f6c90cd5e241f8e180
parent 584479 45467fe4017403ffb6ac5927fdb102453e236ba7
child 634865 3fb6585b165cda1959d76ff4c94809fef3e4b5f4
push id65618
push usernajiang@mozilla.com
push dateFri, 23 Jun 2017 18:32:49 +0000
reviewersmak
bugs1352502
milestone55.0a1
Bug 1352502 - Part 2. Add API to update the page meta info for Places. r?mak MozReview-Commit-ID: K3SjQr3ayjS
toolkit/components/places/History.jsm
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/tests/history/test_fetch.js
toolkit/components/places/tests/history/test_update.js
toolkit/components/places/tests/history/xpcshell.ini
toolkit/components/places/tests/migration/test_current_from_v38.js
--- a/toolkit/components/places/History.jsm
+++ b/toolkit/components/places/History.jsm
@@ -18,16 +18,22 @@
  *     or (nsIURI)
  *     or (string)
  *     The full URI of the page. Note that `PageInfo` values passed as
  *     argument may hold `nsIURI` or `string` values for property `url`,
  *     but `PageInfo` objects returned by this module always hold `URL`
  *     values.
  * - title: (string)
  *     The title associated with the page, if any.
+ * - description: (string)
+ *     The description of the page, if any.
+ * - previewImageURL: (URL)
+ *     or (nsIURI)
+ *     or (string)
+ *     The preview image URL of the page, if any.
  * - frecency: (number)
  *     The frecency of the page, if any.
  *     See https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Places/Frecency_algorithm
  *     Note that this property may not be used to change the actualy frecency
  *     score of a page, only to retrieve it. In other words, any `frecency` field
  *     passed as argument to a function of this API will be ignored.
  *  - visits: (Array<VisitInfo>)
  *     All the visits for this page, if any.
@@ -86,17 +92,16 @@ Cu.importGlobalProperties(["URL"]);
  * may emit before we yield.
  */
 const NOTIFICATION_CHUNK_SIZE = 300;
 const ONRESULT_CHUNK_SIZE = 300;
 
 // This constant determines the maximum number of remove pages before we cycle.
 const REMOVE_PAGES_CHUNKLEN = 300;
 
-
 /**
  * Sends a bookmarks notification through the given observers.
  *
  * @param observers
  *        array of nsINavBookmarkObserver objects.
  * @param notification
  *        the notification name.
  * @param args
@@ -116,16 +121,18 @@ this.History = Object.freeze({
    *
    * @param guidOrURI: (string) or (URL, nsIURI or href)
    *      Either the full URI of the page or the GUID of the page.
    * @param [optional] options (object)
    *      An optional object whose properties describe options:
    *        - `includeVisits` (boolean) set this to true if `visits` in the
    *           PageInfo needs to contain VisitInfo in a reverse chronological order.
    *           By default, `visits` is undefined inside the returned `PageInfo`.
+   *        - `includeMeta` (boolean) set this to true to fetch page meta fields,
+   *           i.e. `description` and `preview_image_url`.
    *
    * @return (Promise)
    *      A promise resolved once the operation is complete.
    * @resolves (PageInfo | null) If the page could be found, the information
    *      on that page.
    * @note the VisitInfo objects returned while fetching visits do not
    *       contain the property `referrer`.
    *       TODO: Add `referrer` to VisitInfo. See Bug #1365913.
@@ -144,16 +151,21 @@ this.History = Object.freeze({
       throw new TypeError("options should be an object and not null");
     }
 
     let hasIncludeVisits = "includeVisits" in options;
     if (hasIncludeVisits && typeof options.includeVisits !== "boolean") {
       throw new TypeError("includeVisits should be a boolean if exists");
     }
 
+    let hasIncludeMeta = "includeMeta" in options;
+    if (hasIncludeMeta && typeof options.includeMeta !== "boolean") {
+      throw new TypeError("includeMeta should be a boolean if exists");
+    }
+
     return PlacesUtils.promiseDBConnection()
                       .then(db => fetch(db, guidOrURI, options));
   },
 
   /**
    * Adds a number of visits for a single page.
    *
    * Any change may be observed through nsINavHistoryObserver
@@ -188,20 +200,16 @@ this.History = Object.freeze({
    *      If `pageInfo` does not have a `visits` property or if the
    *      value of `visits` is ill-typed or is an empty array.
    * @throws (Error)
    *      If an element of `visits` has an invalid `date`.
    * @throws (Error)
    *      If an element of `visits` has an invalid `transition`.
    */
   insert(pageInfo) {
-    if (typeof pageInfo != "object" || !pageInfo) {
-      throw new TypeError("pageInfo must be an object");
-    }
-
     let info = PlacesUtils.validatePageInfo(pageInfo);
 
     return PlacesUtils.withConnectionWrapper("History.jsm: insert",
       db => insert(db, info));
   },
 
   /**
    * Adds a number of visits for a number of pages.
@@ -557,16 +565,71 @@ this.History = Object.freeze({
    * Throw if an object is not a Date object.
    */
   ensureDate(arg) {
     if (!arg || typeof arg != "object" || arg.constructor.name != "Date") {
       throw new TypeError("Expected a Date, got " + arg);
     }
   },
 
+   /**
+   * Update information for a page.
+   *
+   * Currently, it supports updating the description and the preview image URL
+   * for a page, any other fields will be ignored.
+   *
+   * Note that this function will ignore the update if the target page has not
+   * yet been stored in the database. `History.fetch` could be used to check
+   * whether the page and its meta information exist or not. Beware that
+   * fetch&update might fail as they are not executed in a single transaction.
+   *
+   * @param pageInfo: (PageInfo)
+   *      pageInfo must contain a URL of the target page. It will be ignored
+   *      if a valid page `guid` is also provided.
+   *
+   *      If a property `description` is provided, the description of the
+   *      page is updated. Note that:
+   *      1). An empty string or null `description` will clear the existing
+   *          value in the database.
+   *      2). Descriptions longer than DB_DESCRIPTION_LENGTH_MAX will be
+   *          truncated.
+   *
+   *      If a property `previewImageURL` is provided, the preview image
+   *      URL of the page is updated. Note that:
+   *      1). A null `previewImageURL` will clear the existing value in the
+   *          database.
+   *      2). It throws if its length is greater than DB_URL_LENGTH_MAX
+   *          defined in PlacesUtils.jsm.
+   *
+   * @return (Promise)
+   *      A promise resolved once the update is complete.
+   * @rejects (Error)
+   *      Rejects if the update was unsuccessful.
+   *
+   * @throws (Error)
+   *      If `pageInfo` has an unexpected type.
+   * @throws (Error)
+   *      If `pageInfo` has an invalid `url` or an invalid `guid`.
+   * @throws (Error)
+   *      If `pageInfo` has neither `description` nor `previewImageURL`.
+   * @throws (Error)
+   *      If the length of `pageInfo.previewImageURL` is greater than
+   *      DB_URL_LENGTH_MAX defined in PlacesUtils.jsm.
+   */
+  update(pageInfo) {
+    let info = PlacesUtils.validatePageInfo(pageInfo, false);
+
+    if (info.description === undefined && info.previewImageURL === undefined) {
+      throw new TypeError("pageInfo object must at least have either a description or a previewImageURL property");
+    }
+
+    return PlacesUtils.withConnectionWrapper("History.jsm: update", db => update(db, info));
+  },
+
+
   /**
    * Possible values for the `transition` property of `VisitInfo`
    * objects.
    */
 
   TRANSITIONS: {
     /**
      * The user followed a link and got a new toplevel window.
@@ -855,17 +918,23 @@ var fetch = async function(db, guidOrURL
   let joinFragment = "";
   let visitOrderFragment = ""
   if (options.includeVisits) {
     visitSelectionFragment = ", v.visit_date, v.visit_type";
     joinFragment = "JOIN moz_historyvisits v ON h.id = v.place_id";
     visitOrderFragment = "ORDER BY v.visit_date DESC";
   }
 
-  let query = `SELECT h.id, guid, url, title, frecency ${visitSelectionFragment}
+  let pageMetaSelectionFragment = "";
+  if (options.includeMeta) {
+    pageMetaSelectionFragment = ", description, preview_image_url";
+  }
+
+  let query = `SELECT h.id, guid, url, title, frecency
+               ${pageMetaSelectionFragment} ${visitSelectionFragment}
                FROM moz_places h ${joinFragment}
                ${whereClauseFragment}
                ${visitOrderFragment}`;
   let pageInfo = null;
   await db.executeCached(
     query,
     params,
     row => {
@@ -873,16 +942,21 @@ var fetch = async function(db, guidOrURL
         // This means we're on the first row, so we need to get all the page info.
         pageInfo = {
           guid: row.getResultByName("guid"),
           url: new URL(row.getResultByName("url")),
           frecency: row.getResultByName("frecency"),
           title: row.getResultByName("title") || ""
         };
       }
+      if (options.includeMeta) {
+        pageInfo.description = row.getResultByName("description") || ""
+        let previewImageURL = row.getResultByName("preview_image_url");
+        pageInfo.previewImageURL = previewImageURL ? new URL(previewImageURL) : null;
+      }
       if (options.includeVisits) {
         // On every row (not just the first), we need to collect visit data.
         if (!("visits" in pageInfo)) {
           pageInfo.visits = [];
         }
         let date = PlacesUtils.toDate(row.getResultByName("visit_date"));
         let transition = row.getResultByName("visit_type");
 
@@ -1248,8 +1322,37 @@ var insertMany = async function(db, page
           resolve();
         } else {
           reject({message: "No items were added to history."})
         }
       }
     }, true);
   });
 };
+
+// Inner implementation of History.update.
+var update = async function(db, pageInfo) {
+  let updateFragments = [];
+  let whereClauseFragment = "";
+  let info = {};
+
+  // Prefer GUID over url if it's present
+  if (typeof pageInfo.guid === "string") {
+    whereClauseFragment = "WHERE guid = :guid";
+    info.guid = pageInfo.guid;
+  } else {
+    whereClauseFragment = "WHERE url_hash = hash(:url) AND url = :url";
+    info.url = pageInfo.url.href;
+  }
+
+  if (pageInfo.description || pageInfo.description === null) {
+    updateFragments.push("description = :description");
+    info.description = pageInfo.description;
+  }
+  if (pageInfo.previewImageURL || pageInfo.previewImageURL === null) {
+    updateFragments.push("preview_image_url = :previewImageURL");
+    info.previewImageURL = pageInfo.previewImageURL ? pageInfo.previewImageURL.href : null;
+  }
+  let query = `UPDATE moz_places
+               SET ${updateFragments.join(", ")}
+               ${whereClauseFragment}`;
+  await db.execute(query, info);
+}
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -207,16 +207,17 @@ function serializeNode(aNode, aIsLivemar
   }
 
   return JSON.stringify(data);
 }
 
 // Imposed to limit database size.
 const DB_URL_LENGTH_MAX = 65536;
 const DB_TITLE_LENGTH_MAX = 4096;
+const DB_DESCRIPTION_LENGTH_MAX = 1024;
 
 /**
  * List of bookmark object validators, one per each known property.
  * Validators must throw if the property value is invalid and return a fixed up
  * version of the value, if needed.
  */
 const BOOKMARK_VALIDATORS = Object.freeze({
   guid: simpleValidateFunc(v => PlacesUtils.isValidGuid(v)),
@@ -983,32 +984,64 @@ this.PlacesUtils = {
    * @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 (!validateVisits) {
-      return info;
+    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 (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 (pageInfo.previewImageURL || pageInfo.previewImageURL === null) {
+      let previewImageURL = pageInfo.previewImageURL;
+
+      if (previewImageURL === null) {
+        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 (!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,
--- a/toolkit/components/places/tests/history/test_fetch.js
+++ b/toolkit/components/places/tests/history/test_fetch.js
@@ -72,16 +72,45 @@ add_task(async function test_fetch_exist
           Assert.lessOrEqual(pageInfo.visits[i + 1].date.getTime(), pageInfo.visits[i].date.getTime());
         }
       }
       Assert.deepEqual(idealPageInfo, pageInfo);
     }
   }
 });
 
+add_task(async function test_fetch_page_meta_info() {
+  await PlacesTestUtils.clearHistory();
+
+  let TEST_URI = NetUtil.newURI("http://mozilla.com/test_fetch_page_meta_info");
+  await PlacesTestUtils.addVisits(TEST_URI);
+  Assert.ok(page_in_database(TEST_URI));
+
+  // Test fetching the null values
+  let includeMeta = true;
+  let pageInfo = await PlacesUtils.history.fetch(TEST_URI, {includeMeta});
+  Assert.strictEqual(null, pageInfo.previewImageURL, "fetch should return a null previewImageURL");
+  Assert.equal("", pageInfo.description, "fetch should return a empty string description");
+
+  // Now set the pageMetaInfo for this page
+  let description = "Test description";
+  let previewImageURL = "http://mozilla.com/test_preview_image.png";
+  await PlacesUtils.history.update({ url: TEST_URI, description, previewImageURL });
+
+  includeMeta = true;
+  pageInfo = await PlacesUtils.history.fetch(TEST_URI, {includeMeta});
+  Assert.equal(previewImageURL, pageInfo.previewImageURL.href, "fetch should return a previewImageURL");
+  Assert.equal(description, pageInfo.description, "fetch should return a description");
+
+  includeMeta = false;
+  pageInfo = await PlacesUtils.history.fetch(TEST_URI, {includeMeta});
+  Assert.ok(!("description" in pageInfo), "fetch should not return a description if includeMeta is false")
+  Assert.ok(!("previewImageURL" in pageInfo), "fetch should not return a previewImageURL if includeMeta is false")
+});
+
 add_task(async function test_fetch_nonexistent() {
   await PlacesTestUtils.clearHistory();
   await PlacesUtils.bookmarks.eraseEverything();
 
   let uri = NetUtil.newURI("http://doesntexist.in.db");
   let pageInfo = await PlacesUtils.history.fetch(uri);
   Assert.equal(pageInfo, null);
 });
@@ -99,12 +128,16 @@ add_task(async function test_error_cases
     () => PlacesUtils.history.fetch("http://valid.uri.com", "not an object"),
       /TypeError: options should be/
   );
   Assert.throws(
     () => PlacesUtils.history.fetch("http://valid.uri.com", null),
       /TypeError: options should be/
   );
   Assert.throws(
-    () => PlacesUtils.history.fetch("http://valid.uri.come", { includeVisits: "not a boolean"}),
+    () => PlacesUtils.history.fetch("http://valid.uri.come", {includeVisits: "not a boolean"}),
       /TypeError: includeVisits should be a/
   );
+  Assert.throws(
+    () => PlacesUtils.history.fetch("http://valid.uri.come", {includeMeta: "not a boolean"}),
+      /TypeError: includeMeta should be a/
+  );
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_update.js
@@ -0,0 +1,152 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// 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"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.update(null),
+    /TypeError: pageInfo must be/,
+    "passing a null as pageInfo should throw a TypeError"
+  );
+  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"
+  );
+  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"
+  );
+  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"
+  );
+  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"
+  );
+  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"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.update({ url: "http://valid.uri.com" }),
+    /TypeError: pageInfo object must at least/,
+    "passing a pageInfo with neither description nor previewImageURL should throw a TypeError"
+  );
+});
+
+add_task(async function test_description_change_saved() {
+  await PlacesTestUtils.clearHistory();
+
+  let TEST_URL = NetUtil.newURI("http://mozilla.org/test_description_change_saved");
+  await PlacesTestUtils.addVisits(TEST_URL);
+  Assert.ok(page_in_database(TEST_URL));
+
+  let description = "Test description";
+  await PlacesUtils.history.update({ url: TEST_URL, description });
+  let descriptionInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "description");
+  Assert.equal(description, descriptionInDB, "description should be updated via URL as expected");
+
+  description = "";
+  await PlacesUtils.history.update({ url: TEST_URL, description });
+  descriptionInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "description");
+  Assert.strictEqual(null, descriptionInDB, "an empty description should set it to null in the database");
+
+  let guid = await PlacesTestUtils.fieldInDB(TEST_URL, "guid");
+  description = "Test description";
+  await PlacesUtils.history.update({ url: TEST_URL, guid, description });
+  descriptionInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "description");
+  Assert.equal(description, descriptionInDB, "description should be updated via GUID as expected");
+
+  description = "Test descipriton".repeat(1000);
+  await PlacesUtils.history.update({ url: TEST_URL, description });
+  descriptionInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "description");
+  Assert.ok(0 < descriptionInDB.length < description.length, "a long description should be truncated");
+
+  description = null;
+  await PlacesUtils.history.update({ url: TEST_URL, description});
+  descriptionInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "description");
+  Assert.strictEqual(description, descriptionInDB, "a null description should set it to null in the database");
+});
+
+add_task(async function test_previewImageURL_change_saved() {
+  await PlacesTestUtils.clearHistory();
+
+  let TEST_URL = NetUtil.newURI("http://mozilla.org/test_previewImageURL_change_saved");
+  let IMAGE_URL = "http://mozilla.org/test_preview_image.png";
+  await PlacesTestUtils.addVisits(TEST_URL);
+  Assert.ok(page_in_database(TEST_URL));
+
+  let previewImageURL = IMAGE_URL;
+  await PlacesUtils.history.update({ url: TEST_URL, previewImageURL });
+  let previewImageURLInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "preview_image_url");
+  Assert.equal(previewImageURL, previewImageURLInDB, "previewImageURL should be updated via URL as expected");
+
+  previewImageURL = null;
+  await PlacesUtils.history.update({ url: TEST_URL, previewImageURL});
+  previewImageURLInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "preview_image_url");
+  Assert.strictEqual(null, previewImageURLInDB, "a null previewImageURL should set it to null in the database");
+
+  let guid = await PlacesTestUtils.fieldInDB(TEST_URL, "guid");
+  previewImageURL = IMAGE_URL;
+  await PlacesUtils.history.update({ url: TEST_URL, guid, previewImageURL });
+  previewImageURLInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "preview_image_url");
+  Assert.equal(previewImageURL, previewImageURLInDB, "previewImageURL should be updated via GUID as expected");
+});
+
+add_task(async function test_change_both_saved() {
+  await PlacesTestUtils.clearHistory();
+
+  let TEST_URL = NetUtil.newURI("http://mozilla.org/test_change_both_saved");
+  await PlacesTestUtils.addVisits(TEST_URL);
+  Assert.ok(page_in_database(TEST_URL));
+
+  let description = "Test description";
+  let previewImageURL = "http://mozilla.org/test_preview_image.png";
+
+  await PlacesUtils.history.update({ url: TEST_URL, description, previewImageURL });
+  let descriptionInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "description");
+  let previewImageURLInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "preview_image_url");
+  Assert.equal(description, descriptionInDB, "description should be updated via URL as expected");
+  Assert.equal(previewImageURL, previewImageURLInDB, "previewImageURL should be updated via URL as expected");
+
+  // Update description should not touch other fields
+  description = null;
+  await PlacesUtils.history.update({ url: TEST_URL, description });
+  descriptionInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "description");
+  previewImageURLInDB = await PlacesTestUtils.fieldInDB(TEST_URL, "preview_image_url");
+  Assert.strictEqual(description, descriptionInDB, "description should be updated via URL as expected");
+  Assert.equal(previewImageURL, previewImageURLInDB, "previewImageURL should not be updated");
+});
--- a/toolkit/components/places/tests/history/xpcshell.ini
+++ b/toolkit/components/places/tests/history/xpcshell.ini
@@ -6,9 +6,10 @@ head = head_history.js
 [test_insert.js]
 [test_insertMany.js]
 [test_remove.js]
 [test_removeMany.js]
 [test_removeVisits.js]
 [test_removeByFilter.js]
 [test_removeVisitsByFilter.js]
 [test_sameUri_titleChanged.js]
+[test_update.js]
 [test_updatePlaces_embed.js]
--- a/toolkit/components/places/tests/migration/test_current_from_v38.js
+++ b/toolkit/components/places/tests/migration/test_current_from_v38.js
@@ -6,38 +6,13 @@ add_task(function* database_is_valid() {
   // Accessing the database for the first time triggers migration.
   Assert.equal(PlacesUtils.history.databaseStatus,
                PlacesUtils.history.DATABASE_STATUS_UPGRADED);
 
   let db = yield PlacesUtils.promiseDBConnection();
   Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
 });
 
-add_task(function* test_new_fields() {
-  let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
-  let db = yield Sqlite.openConnection({ path });
-
-  // Manually update these two fields for a places record.
-  yield db.execute(`
-    UPDATE moz_places
-    SET description = :description, preview_image_url = :previewImageURL
-    WHERE id = 1`, { description: "Page description",
-                     previewImageURL: "https://example.com/img.png" });
-  let rows = yield db.execute(`SELECT description FROM moz_places
-                               WHERE description IS NOT NULL
-                               AND preview_image_url IS NOT NULL`);
-  Assert.equal(rows.length, 1,
-    "should fetch one record with not null description and preview_image_url");
-
-  // Reset them to the default value
-  yield db.execute(`
-    UPDATE moz_places
-    SET description = NULL,
-        preview_image_url = NULL
-    WHERE id = 1`);
-  rows = yield db.execute(`SELECT description FROM moz_places
-                           WHERE description IS NOT NULL
-                           AND preview_image_url IS NOT NULL`);
-  Assert.equal(rows.length, 0,
-    "should fetch 0 record with not null description and preview_image_url");
-
-  yield db.close();
+add_task(function* test_select_new_fields() {
+  let db = yield PlacesUtils.promiseDBConnection();
+  yield db.execute(`SELECT description, preview_image_url FROM moz_places`);
+  Assert.ok(true, "should be able to select description and preview_image_url");
 });