|
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; |
|
11 |
|
12 Components.utils.import("resource://gre/modules/NetUtil.jsm"); |
|
13 |
|
14 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
15 "resource://gre/modules/Promise.jsm"); |
|
16 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
17 "resource://gre/modules/Task.jsm"); |
|
18 |
|
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(); |
|
32 |
|
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); |
|
38 |
|
39 return deferred.promise; |
|
40 } |
|
41 |
|
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 } |
|
54 |
|
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(); |
|
71 |
|
72 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
|
73 .DBConnection; |
|
74 let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); |
|
75 begin.executeAsync(); |
|
76 begin.finalize(); |
|
77 |
|
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(); |
|
88 |
|
89 return deferred.promise; |
|
90 } |
|
91 |
|
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 } |
|
125 |
|
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() {} |
|
131 |
|
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 }; |
|
145 |
|
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); |
|
168 |
|
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); |
|
174 |
|
175 if (aCallback) { |
|
176 aCallback(); |
|
177 } |
|
178 } |
|
179 }; |
|
180 aWindow.PlacesUtils.history.addObserver(historyObserver, false); |
|
181 } |
|
182 |
|
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 } |
|
212 |
|
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 } |
|
226 |
|
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 } |
|
241 |
|
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 } |
|
272 |
|
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 } |
|
286 |
|
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 ); |
|
301 |
|
302 return deferred.promise; |
|
303 } |
|
304 |
|
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 } |
|
328 |
|
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 } |
|
344 |
|
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 } |
|
365 |
|
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 } |
|
375 |
|
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 } |
|
395 |
|
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); |
|
401 |
|
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 } |
|
408 |
|
409 return gDBConn.connectionReady ? gDBConn : null; |
|
410 } |
|
411 |
|
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 } |
|
420 |
|
421 function whenNewWindowLoaded(aOptions, aCallback) { |
|
422 let win = OpenBrowserWindow(aOptions); |
|
423 whenDelayedStartupFinished(win, aCallback); |
|
424 } |
|
425 |
|
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(); |
|
437 |
|
438 PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { |
|
439 deferred.resolve(aIsVisited); |
|
440 }); |
|
441 |
|
442 return deferred.promise; |
|
443 } |
|
444 |
|
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 } |