toolkit/components/places/tests/browser/head.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* Any copyright is dedicated to the Public Domain.
     2    http://creativecommons.org/publicdomain/zero/1.0/ */
     3 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
     4 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
     5 const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK;
     6 const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT;
     7 const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY;
     8 const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED;
     9 const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK;
    10 const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD;
    12 Components.utils.import("resource://gre/modules/NetUtil.jsm");
    14 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    15                                   "resource://gre/modules/Promise.jsm");
    16 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    17                                   "resource://gre/modules/Task.jsm");
    19 /**
    20  * Allows waiting for an observer notification once.
    21  *
    22  * @param aTopic
    23  *        Notification topic to observe.
    24  *
    25  * @return {Promise}
    26  * @resolves The array [aSubject, aData] from the observed notification.
    27  * @rejects Never.
    28  */
    29 function promiseTopicObserved(aTopic)
    30 {
    31   let deferred = Promise.defer();
    33   Services.obs.addObserver(
    34     function PTO_observe(aSubject, aTopic, aData) {
    35       Services.obs.removeObserver(PTO_observe, aTopic);
    36       deferred.resolve([aSubject, aData]);
    37     }, aTopic, false);
    39   return deferred.promise;
    40 }
    42 /**
    43  * Clears history asynchronously.
    44  *
    45  * @return {Promise}
    46  * @resolves When history has been cleared.
    47  * @rejects Never.
    48  */
    49 function promiseClearHistory() {
    50   let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
    51   PlacesUtils.bhistory.removeAllPages();
    52   return promise;
    53 }
    55 /**
    56  * Waits for all pending async statements on the default connection.
    57  *
    58  * @return {Promise}
    59  * @resolves When all pending async statements finished.
    60  * @rejects Never.
    61  *
    62  * @note The result is achieved by asynchronously executing a query requiring
    63  *       a write lock.  Since all statements on the same connection are
    64  *       serialized, the end of this write operation means that all writes are
    65  *       complete.  Note that WAL makes so that writers don't block readers, but
    66  *       this is a problem only across different connections.
    67  */
    68 function promiseAsyncUpdates()
    69 {
    70   let deferred = Promise.defer();
    72   let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
    73                               .DBConnection;
    74   let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
    75   begin.executeAsync();
    76   begin.finalize();
    78   let commit = db.createAsyncStatement("COMMIT");
    79   commit.executeAsync({
    80     handleResult: function() {},
    81     handleError: function() {},
    82     handleCompletion: function(aReason)
    83     {
    84       deferred.resolve();
    85     }
    86   });
    87   commit.finalize();
    89   return deferred.promise;
    90 }
    92 /**
    93  * Returns a moz_places field value for a url.
    94  *
    95  * @param aURI
    96  *        The URI or spec to get field for.
    97  * param aCallback
    98  *        Callback function that will get the property value.
    99  */
   100 function fieldForUrl(aURI, aFieldName, aCallback)
   101 {
   102   let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
   103   let stmt = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   104                                 .DBConnection.createAsyncStatement(
   105     "SELECT " + aFieldName + " FROM moz_places WHERE url = :page_url"
   106   );
   107   stmt.params.page_url = url;
   108   stmt.executeAsync({
   109     _value: -1,
   110     handleResult: function(aResultSet) {
   111       let row = aResultSet.getNextRow();
   112       if (!row)
   113         ok(false, "The page should exist in the database");
   114       this._value = row.getResultByName(aFieldName);
   115     },
   116     handleError: function() {},
   117     handleCompletion: function(aReason) {
   118       if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
   119          ok(false, "The statement should properly succeed");
   120       aCallback(this._value);
   121     }
   122   });
   123   stmt.finalize();
   124 }
   126 /**
   127  * Generic nsINavHistoryObserver that doesn't implement anything, but provides
   128  * dummy methods to prevent errors about an object not having a certain method.
   129  */
   130 function NavHistoryObserver() {}
   132 NavHistoryObserver.prototype = {
   133   onBeginUpdateBatch: function () {},
   134   onEndUpdateBatch: function () {},
   135   onVisit: function () {},
   136   onTitleChanged: function () {},
   137   onDeleteURI: function () {},
   138   onClearHistory: function () {},
   139   onPageChanged: function () {},
   140   onDeleteVisits: function () {},
   141   QueryInterface: XPCOMUtils.generateQI([
   142     Ci.nsINavHistoryObserver,
   143   ])
   144 };
   146 /**
   147  * Waits for the first OnPageChanged notification for ATTRIBUTE_FAVICON, and
   148  * verifies that it matches the expected page URI and associated favicon URI.
   149  *
   150  * This function also double-checks the GUID parameter of the notification.
   151  *
   152  * @param aExpectedPageURI
   153  *        nsIURI object of the page whose favicon should change.
   154  * @param aExpectedFaviconURI
   155  *        nsIURI object of the newly associated favicon.
   156  * @param aCallback
   157  *        This function is called after the check finished.
   158  */
   159 function waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, aWindow,
   160                                aCallback) {
   161   let historyObserver = {
   162     __proto__: NavHistoryObserver.prototype,
   163     onPageChanged: function WFFC_onPageChanged(aURI, aWhat, aValue, aGUID) {
   164       if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) {
   165         return;
   166       }
   167       aWindow.PlacesUtils.history.removeObserver(this);
   169       ok(aURI.equals(aExpectedPageURI),
   170         "Check URIs are equal for the page which favicon changed");
   171       is(aValue, aExpectedFaviconURI.spec,
   172         "Check changed favicon URI is the expected");
   173       checkGuidForURI(aURI, aGUID);
   175       if (aCallback) {
   176         aCallback();
   177       }
   178     }
   179   };
   180   aWindow.PlacesUtils.history.addObserver(historyObserver, false);
   181 }
   183 /**
   184  * Asynchronously adds visits to a page, invoking a callback function when done.
   185  *
   186  * @param aPlaceInfo
   187  *        Either an nsIURI, in such a case a single LINK visit will be added.
   188  *        Or can be an object describing the visit to add, or an array
   189  *        of these objects:
   190  *          { uri: nsIURI of the page,
   191  *            transition: one of the TRANSITION_* from nsINavHistoryService,
   192  *            [optional] title: title of the page,
   193  *            [optional] visitDate: visit date in microseconds from the epoch
   194  *            [optional] referrer: nsIURI of the referrer for this visit
   195  *          }
   196  * @param [optional] aCallback
   197  *        Function to be invoked on completion.
   198  * @param [optional] aStack
   199  *        The stack frame used to report errors.
   200  */
   201 function addVisits(aPlaceInfo, aWindow, aCallback, aStack) {
   202   let stack = aStack || Components.stack.caller;
   203   let places = [];
   204   if (aPlaceInfo instanceof Ci.nsIURI) {
   205     places.push({ uri: aPlaceInfo });
   206   }
   207   else if (Array.isArray(aPlaceInfo)) {
   208     places = places.concat(aPlaceInfo);
   209   } else {
   210     places.push(aPlaceInfo)
   211   }
   213   // Create mozIVisitInfo for each entry.
   214   let now = Date.now();
   215   for (let place of places) {
   216     if (!place.title) {
   217       place.title = "test visit for " + place.uri.spec;
   218     }
   219     place.visits = [{
   220       transitionType: place.transition === undefined ? TRANSITION_LINK
   221                                                      : place.transition,
   222       visitDate: place.visitDate || (now++) * 1000,
   223       referrerURI: place.referrer
   224     }];
   225   }
   227   aWindow.PlacesUtils.asyncHistory.updatePlaces(
   228     places,
   229     {
   230       handleError: function AAV_handleError() {
   231         throw("Unexpected error in adding visit.");
   232       },
   233       handleResult: function () {},
   234       handleCompletion: function UP_handleCompletion() {
   235         if (aCallback)
   236           aCallback();
   237       }
   238     }
   239   );
   240 }
   242 /**
   243  * Asynchronously adds visits to a page.
   244  *
   245  * @param aPlaceInfo
   246  *        Can be an nsIURI, in such a case a single LINK visit will be added.
   247  *        Otherwise can be an object describing the visit to add, or an array
   248  *        of these objects:
   249  *          { uri: nsIURI of the page,
   250  *            transition: one of the TRANSITION_* from nsINavHistoryService,
   251  *            [optional] title: title of the page,
   252  *            [optional] visitDate: visit date in microseconds from the epoch
   253  *            [optional] referrer: nsIURI of the referrer for this visit
   254  *          }
   255  *
   256  * @return {Promise}
   257  * @resolves When all visits have been added successfully.
   258  * @rejects JavaScript exception.
   259  */
   260 function promiseAddVisits(aPlaceInfo)
   261 {
   262   let deferred = Promise.defer();
   263   let places = [];
   264   if (aPlaceInfo instanceof Ci.nsIURI) {
   265     places.push({ uri: aPlaceInfo });
   266   }
   267   else if (Array.isArray(aPlaceInfo)) {
   268     places = places.concat(aPlaceInfo);
   269   } else {
   270     places.push(aPlaceInfo)
   271   }
   273   // Create mozIVisitInfo for each entry.
   274   let now = Date.now();
   275   for (let i = 0; i < places.length; i++) {
   276     if (!places[i].title) {
   277       places[i].title = "test visit for " + places[i].uri.spec;
   278     }
   279     places[i].visits = [{
   280       transitionType: places[i].transition === undefined ? TRANSITION_LINK
   281                                                          : places[i].transition,
   282       visitDate: places[i].visitDate || (now++) * 1000,
   283       referrerURI: places[i].referrer
   284     }];
   285   }
   287   PlacesUtils.asyncHistory.updatePlaces(
   288     places,
   289     {
   290       handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
   291         let ex = new Components.Exception("Unexpected error in adding visits.",
   292                                           aResultCode);
   293         deferred.reject(ex);
   294       },
   295       handleResult: function () {},
   296       handleCompletion: function UP_handleCompletion() {
   297         deferred.resolve();
   298       }
   299     }
   300   );
   302   return deferred.promise;
   303 }
   305 /**
   306  * Checks that the favicon for the given page matches the provided data.
   307  *
   308  * @param aPageURI
   309  *        nsIURI object for the page to check.
   310  * @param aExpectedMimeType
   311  *        Expected MIME type of the icon, for example "image/png".
   312  * @param aExpectedData
   313  *        Expected icon data, expressed as an array of byte values.
   314  * @param aCallback
   315  *        This function is called after the check finished.
   316  */
   317 function checkFaviconDataForPage(aPageURI, aExpectedMimeType, aExpectedData,
   318   aWindow, aCallback) {
   319   aWindow.PlacesUtils.favicons.getFaviconDataForPage(aPageURI,
   320     function (aURI, aDataLen, aData, aMimeType) {
   321       is(aExpectedMimeType, aMimeType, "Check expected MimeType");
   322       is(aExpectedData.length, aData.length,
   323         "Check favicon data for the given page matches the provided data");
   324       checkGuidForURI(aPageURI);
   325       aCallback();
   326     });
   327 }
   329 /**
   330  * Tests that a guid was set in moz_places for a given uri.
   331  *
   332  * @param aURI
   333  *        The uri to check.
   334  * @param [optional] aGUID
   335  *        The expected guid in the database.
   336  */
   337 function checkGuidForURI(aURI, aGUID) {
   338   let guid = doGetGuidForURI(aURI);
   339   if (aGUID) {
   340     doCheckValidPlacesGuid(aGUID);
   341     is(guid, aGUID, "Check equal guid for URIs");
   342   }
   343 }
   345 /**
   346  * Retrieves the guid for a given uri.
   347  *
   348  * @param aURI
   349  *        The uri to check.
   350  * @return the associated the guid.
   351  */
   352 function doGetGuidForURI(aURI) {
   353   let stmt = DBConn().createStatement(
   354     "SELECT guid "
   355     + "FROM moz_places "
   356     + "WHERE url = :url "
   357   );
   358   stmt.params.url = aURI.spec;
   359   ok(stmt.executeStep(), "Check get guid for uri from moz_places");
   360   let guid = stmt.row.guid;
   361   stmt.finalize();
   362   doCheckValidPlacesGuid(guid);
   363   return guid;
   364 }
   366 /**
   367  * Tests if a given guid is valid for use in Places or not.
   368  *
   369  * @param aGuid
   370  *        The guid to test.
   371  */
   372 function doCheckValidPlacesGuid(aGuid) {
   373   ok(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), "Check guid for valid places");
   374 }
   376 /**
   377  * Gets the database connection.  If the Places connection is invalid it will
   378  * try to create a new connection.
   379  *
   380  * @param [optional] aForceNewConnection
   381  *        Forces creation of a new connection to the database.  When a
   382  *        connection is asyncClosed it cannot anymore schedule async statements,
   383  *        though connectionReady will keep returning true (Bug 726990).
   384  *
   385  * @return The database connection or null if unable to get one.
   386  */
   387 function DBConn(aForceNewConnection) {
   388   let gDBConn;
   389   if (!aForceNewConnection) {
   390     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   391       .DBConnection;
   392     if (db.connectionReady)
   393       return db;
   394   }
   396   // If the Places database connection has been closed, create a new connection.
   397   if (!gDBConn || aForceNewConnection) {
   398     let file = Services.dirsvc.get('ProfD', Ci.nsIFile);
   399     file.append("places.sqlite");
   400     let dbConn = gDBConn = Services.storage.openDatabase(file);
   402     // Be sure to cleanly close this connection.
   403     Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) {
   404       Services.obs.removeObserver(DBCloseCallback, aTopic);
   405       dbConn.asyncClose();
   406     }, "profile-before-change", false);
   407   }
   409   return gDBConn.connectionReady ? gDBConn : null;
   410 }
   412 function whenDelayedStartupFinished(aWindow, aCallback) {
   413   Services.obs.addObserver(function observer(aSubject, aTopic) {
   414     if (aWindow == aSubject) {
   415       Services.obs.removeObserver(observer, aTopic);
   416       executeSoon(function() { aCallback(aWindow); });
   417     }
   418   }, "browser-delayed-startup-finished", false);
   419 }
   421 function whenNewWindowLoaded(aOptions, aCallback) {
   422   let win = OpenBrowserWindow(aOptions);
   423   whenDelayedStartupFinished(win, aCallback);
   424 }
   426 /**
   427  * Asynchronously check a url is visited.
   428  *
   429  * @param aURI The URI.
   430  * @param aExpectedValue The expected value.
   431  * @return {Promise}
   432  * @resolves When the check has been added successfully.
   433  * @rejects JavaScript exception.
   434  */
   435 function promiseIsURIVisited(aURI, aExpectedValue) {
   436   let deferred = Promise.defer();
   438   PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
   439     deferred.resolve(aIsVisited);
   440   });
   442   return deferred.promise;
   443 }
   445 function waitForCondition(condition, nextTest, errorMsg) {
   446   let tries = 0;
   447   let interval = setInterval(function() {
   448     if (tries >= 30) {
   449       ok(false, errorMsg);
   450       moveOn();
   451     }
   452     let conditionPassed;
   453     try {
   454       conditionPassed = condition();
   455     } catch (e) {
   456       ok(false, e + "\n" + e.stack);
   457       conditionPassed = false;
   458     }
   459     if (conditionPassed) {
   460       moveOn();
   461     }
   462     tries++;
   463   }, 200);
   464   function moveOn() {
   465     clearInterval(interval);
   466     nextTest();
   467   };
   468 }

mercurial