Thu, 22 Jan 2015 13:21:57 +0100
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 }