|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* Any copyright is dedicated to the Public Domain. |
|
4 * http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
5 |
|
6 /** |
|
7 * Tests the DownloadList object. |
|
8 */ |
|
9 |
|
10 "use strict"; |
|
11 |
|
12 //////////////////////////////////////////////////////////////////////////////// |
|
13 //// Globals |
|
14 |
|
15 /** |
|
16 * Returns a PRTime in the past usable to add expirable visits. |
|
17 * |
|
18 * @note Expiration ignores any visit added in the last 7 days, but it's |
|
19 * better be safe against DST issues, by going back one day more. |
|
20 */ |
|
21 function getExpirablePRTime() |
|
22 { |
|
23 let dateObj = new Date(); |
|
24 // Normalize to midnight |
|
25 dateObj.setHours(0); |
|
26 dateObj.setMinutes(0); |
|
27 dateObj.setSeconds(0); |
|
28 dateObj.setMilliseconds(0); |
|
29 dateObj = new Date(dateObj.getTime() - 8 * 86400000); |
|
30 return dateObj.getTime() * 1000; |
|
31 } |
|
32 |
|
33 /** |
|
34 * Adds an expirable history visit for a download. |
|
35 * |
|
36 * @param aSourceUrl |
|
37 * String containing the URI for the download source, or null to use |
|
38 * httpUrl("source.txt"). |
|
39 * |
|
40 * @return {Promise} |
|
41 * @rejects JavaScript exception. |
|
42 */ |
|
43 function promiseExpirableDownloadVisit(aSourceUrl) |
|
44 { |
|
45 let deferred = Promise.defer(); |
|
46 PlacesUtils.asyncHistory.updatePlaces( |
|
47 { |
|
48 uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")), |
|
49 visits: [{ |
|
50 transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, |
|
51 visitDate: getExpirablePRTime(), |
|
52 }] |
|
53 }, |
|
54 { |
|
55 handleError: function handleError(aResultCode, aPlaceInfo) { |
|
56 let ex = new Components.Exception("Unexpected error in adding visits.", |
|
57 aResultCode); |
|
58 deferred.reject(ex); |
|
59 }, |
|
60 handleResult: function () {}, |
|
61 handleCompletion: function handleCompletion() { |
|
62 deferred.resolve(); |
|
63 } |
|
64 }); |
|
65 return deferred.promise; |
|
66 } |
|
67 |
|
68 //////////////////////////////////////////////////////////////////////////////// |
|
69 //// Tests |
|
70 |
|
71 /** |
|
72 * Checks the testing mechanism used to build different download lists. |
|
73 */ |
|
74 add_task(function test_construction() |
|
75 { |
|
76 let downloadListOne = yield promiseNewList(); |
|
77 let downloadListTwo = yield promiseNewList(); |
|
78 let privateDownloadListOne = yield promiseNewList(true); |
|
79 let privateDownloadListTwo = yield promiseNewList(true); |
|
80 |
|
81 do_check_neq(downloadListOne, downloadListTwo); |
|
82 do_check_neq(privateDownloadListOne, privateDownloadListTwo); |
|
83 do_check_neq(downloadListOne, privateDownloadListOne); |
|
84 }); |
|
85 |
|
86 /** |
|
87 * Checks the methods to add and retrieve items from the list. |
|
88 */ |
|
89 add_task(function test_add_getAll() |
|
90 { |
|
91 let list = yield promiseNewList(); |
|
92 |
|
93 let downloadOne = yield promiseNewDownload(); |
|
94 yield list.add(downloadOne); |
|
95 |
|
96 let itemsOne = yield list.getAll(); |
|
97 do_check_eq(itemsOne.length, 1); |
|
98 do_check_eq(itemsOne[0], downloadOne); |
|
99 |
|
100 let downloadTwo = yield promiseNewDownload(); |
|
101 yield list.add(downloadTwo); |
|
102 |
|
103 let itemsTwo = yield list.getAll(); |
|
104 do_check_eq(itemsTwo.length, 2); |
|
105 do_check_eq(itemsTwo[0], downloadOne); |
|
106 do_check_eq(itemsTwo[1], downloadTwo); |
|
107 |
|
108 // The first snapshot should not have been modified. |
|
109 do_check_eq(itemsOne.length, 1); |
|
110 }); |
|
111 |
|
112 /** |
|
113 * Checks the method to remove items from the list. |
|
114 */ |
|
115 add_task(function test_remove() |
|
116 { |
|
117 let list = yield promiseNewList(); |
|
118 |
|
119 yield list.add(yield promiseNewDownload()); |
|
120 yield list.add(yield promiseNewDownload()); |
|
121 |
|
122 let items = yield list.getAll(); |
|
123 yield list.remove(items[0]); |
|
124 |
|
125 // Removing an item that was never added should not raise an error. |
|
126 yield list.remove(yield promiseNewDownload()); |
|
127 |
|
128 items = yield list.getAll(); |
|
129 do_check_eq(items.length, 1); |
|
130 }); |
|
131 |
|
132 /** |
|
133 * Tests that the "add", "remove", and "getAll" methods on the global |
|
134 * DownloadCombinedList object combine the contents of the global DownloadList |
|
135 * objects for public and private downloads. |
|
136 */ |
|
137 add_task(function test_DownloadCombinedList_add_remove_getAll() |
|
138 { |
|
139 let publicList = yield promiseNewList(); |
|
140 let privateList = yield Downloads.getList(Downloads.PRIVATE); |
|
141 let combinedList = yield Downloads.getList(Downloads.ALL); |
|
142 |
|
143 let publicDownload = yield promiseNewDownload(); |
|
144 let privateDownload = yield Downloads.createDownload({ |
|
145 source: { url: httpUrl("source.txt"), isPrivate: true }, |
|
146 target: getTempFile(TEST_TARGET_FILE_NAME).path, |
|
147 }); |
|
148 |
|
149 yield publicList.add(publicDownload); |
|
150 yield privateList.add(privateDownload); |
|
151 |
|
152 do_check_eq((yield combinedList.getAll()).length, 2); |
|
153 |
|
154 yield combinedList.remove(publicDownload); |
|
155 yield combinedList.remove(privateDownload); |
|
156 |
|
157 do_check_eq((yield combinedList.getAll()).length, 0); |
|
158 |
|
159 yield combinedList.add(publicDownload); |
|
160 yield combinedList.add(privateDownload); |
|
161 |
|
162 do_check_eq((yield publicList.getAll()).length, 1); |
|
163 do_check_eq((yield privateList.getAll()).length, 1); |
|
164 do_check_eq((yield combinedList.getAll()).length, 2); |
|
165 |
|
166 yield publicList.remove(publicDownload); |
|
167 yield privateList.remove(privateDownload); |
|
168 |
|
169 do_check_eq((yield combinedList.getAll()).length, 0); |
|
170 }); |
|
171 |
|
172 /** |
|
173 * Checks that views receive the download add and remove notifications, and that |
|
174 * adding and removing views works as expected, both for a normal and a combined |
|
175 * list. |
|
176 */ |
|
177 add_task(function test_notifications_add_remove() |
|
178 { |
|
179 for (let isCombined of [false, true]) { |
|
180 // Force creating a new list for both the public and combined cases. |
|
181 let list = yield promiseNewList(); |
|
182 if (isCombined) { |
|
183 list = yield Downloads.getList(Downloads.ALL); |
|
184 } |
|
185 |
|
186 let downloadOne = yield promiseNewDownload(); |
|
187 let downloadTwo = yield Downloads.createDownload({ |
|
188 source: { url: httpUrl("source.txt"), isPrivate: true }, |
|
189 target: getTempFile(TEST_TARGET_FILE_NAME).path, |
|
190 }); |
|
191 yield list.add(downloadOne); |
|
192 yield list.add(downloadTwo); |
|
193 |
|
194 // Check that we receive add notifications for existing elements. |
|
195 let addNotifications = 0; |
|
196 let viewOne = { |
|
197 onDownloadAdded: function (aDownload) { |
|
198 // The first download to be notified should be the first that was added. |
|
199 if (addNotifications == 0) { |
|
200 do_check_eq(aDownload, downloadOne); |
|
201 } else if (addNotifications == 1) { |
|
202 do_check_eq(aDownload, downloadTwo); |
|
203 } |
|
204 addNotifications++; |
|
205 }, |
|
206 }; |
|
207 yield list.addView(viewOne); |
|
208 do_check_eq(addNotifications, 2); |
|
209 |
|
210 // Check that we receive add notifications for new elements. |
|
211 yield list.add(yield promiseNewDownload()); |
|
212 do_check_eq(addNotifications, 3); |
|
213 |
|
214 // Check that we receive remove notifications. |
|
215 let removeNotifications = 0; |
|
216 let viewTwo = { |
|
217 onDownloadRemoved: function (aDownload) { |
|
218 do_check_eq(aDownload, downloadOne); |
|
219 removeNotifications++; |
|
220 }, |
|
221 }; |
|
222 yield list.addView(viewTwo); |
|
223 yield list.remove(downloadOne); |
|
224 do_check_eq(removeNotifications, 1); |
|
225 |
|
226 // We should not receive remove notifications after the view is removed. |
|
227 yield list.removeView(viewTwo); |
|
228 yield list.remove(downloadTwo); |
|
229 do_check_eq(removeNotifications, 1); |
|
230 |
|
231 // We should not receive add notifications after the view is removed. |
|
232 yield list.removeView(viewOne); |
|
233 yield list.add(yield promiseNewDownload()); |
|
234 do_check_eq(addNotifications, 3); |
|
235 } |
|
236 }); |
|
237 |
|
238 /** |
|
239 * Checks that views receive the download change notifications, both for a |
|
240 * normal and a combined list. |
|
241 */ |
|
242 add_task(function test_notifications_change() |
|
243 { |
|
244 for (let isCombined of [false, true]) { |
|
245 // Force creating a new list for both the public and combined cases. |
|
246 let list = yield promiseNewList(); |
|
247 if (isCombined) { |
|
248 list = yield Downloads.getList(Downloads.ALL); |
|
249 } |
|
250 |
|
251 let downloadOne = yield promiseNewDownload(); |
|
252 let downloadTwo = yield Downloads.createDownload({ |
|
253 source: { url: httpUrl("source.txt"), isPrivate: true }, |
|
254 target: getTempFile(TEST_TARGET_FILE_NAME).path, |
|
255 }); |
|
256 yield list.add(downloadOne); |
|
257 yield list.add(downloadTwo); |
|
258 |
|
259 // Check that we receive change notifications. |
|
260 let receivedOnDownloadChanged = false; |
|
261 yield list.addView({ |
|
262 onDownloadChanged: function (aDownload) { |
|
263 do_check_eq(aDownload, downloadOne); |
|
264 receivedOnDownloadChanged = true; |
|
265 }, |
|
266 }); |
|
267 yield downloadOne.start(); |
|
268 do_check_true(receivedOnDownloadChanged); |
|
269 |
|
270 // We should not receive change notifications after a download is removed. |
|
271 receivedOnDownloadChanged = false; |
|
272 yield list.remove(downloadTwo); |
|
273 yield downloadTwo.start(); |
|
274 do_check_false(receivedOnDownloadChanged); |
|
275 } |
|
276 }); |
|
277 |
|
278 /** |
|
279 * Checks that the reference to "this" is correct in the view callbacks. |
|
280 */ |
|
281 add_task(function test_notifications_this() |
|
282 { |
|
283 let list = yield promiseNewList(); |
|
284 |
|
285 // Check that we receive change notifications. |
|
286 let receivedOnDownloadAdded = false; |
|
287 let receivedOnDownloadChanged = false; |
|
288 let receivedOnDownloadRemoved = false; |
|
289 let view = { |
|
290 onDownloadAdded: function () { |
|
291 do_check_eq(this, view); |
|
292 receivedOnDownloadAdded = true; |
|
293 }, |
|
294 onDownloadChanged: function () { |
|
295 // Only do this check once. |
|
296 if (!receivedOnDownloadChanged) { |
|
297 do_check_eq(this, view); |
|
298 receivedOnDownloadChanged = true; |
|
299 } |
|
300 }, |
|
301 onDownloadRemoved: function () { |
|
302 do_check_eq(this, view); |
|
303 receivedOnDownloadRemoved = true; |
|
304 }, |
|
305 }; |
|
306 yield list.addView(view); |
|
307 |
|
308 let download = yield promiseNewDownload(); |
|
309 yield list.add(download); |
|
310 yield download.start(); |
|
311 yield list.remove(download); |
|
312 |
|
313 // Verify that we executed the checks. |
|
314 do_check_true(receivedOnDownloadAdded); |
|
315 do_check_true(receivedOnDownloadChanged); |
|
316 do_check_true(receivedOnDownloadRemoved); |
|
317 }); |
|
318 |
|
319 /** |
|
320 * Checks that download is removed on history expiration. |
|
321 */ |
|
322 add_task(function test_history_expiration() |
|
323 { |
|
324 mustInterruptResponses(); |
|
325 |
|
326 function cleanup() { |
|
327 Services.prefs.clearUserPref("places.history.expiration.max_pages"); |
|
328 } |
|
329 do_register_cleanup(cleanup); |
|
330 |
|
331 // Set max pages to 0 to make the download expire. |
|
332 Services.prefs.setIntPref("places.history.expiration.max_pages", 0); |
|
333 |
|
334 let list = yield promiseNewList(); |
|
335 let downloadOne = yield promiseNewDownload(); |
|
336 let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt")); |
|
337 |
|
338 let deferred = Promise.defer(); |
|
339 let removeNotifications = 0; |
|
340 let downloadView = { |
|
341 onDownloadRemoved: function (aDownload) { |
|
342 if (++removeNotifications == 2) { |
|
343 deferred.resolve(); |
|
344 } |
|
345 }, |
|
346 }; |
|
347 yield list.addView(downloadView); |
|
348 |
|
349 // Work with one finished download and one canceled download. |
|
350 yield downloadOne.start(); |
|
351 downloadTwo.start(); |
|
352 yield downloadTwo.cancel(); |
|
353 |
|
354 // We must replace the visits added while executing the downloads with visits |
|
355 // that are older than 7 days, otherwise they will not be expired. |
|
356 yield promiseClearHistory(); |
|
357 yield promiseExpirableDownloadVisit(); |
|
358 yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt")); |
|
359 |
|
360 // After clearing history, we can add the downloads to be removed to the list. |
|
361 yield list.add(downloadOne); |
|
362 yield list.add(downloadTwo); |
|
363 |
|
364 // Force a history expiration. |
|
365 Cc["@mozilla.org/places/expiration;1"] |
|
366 .getService(Ci.nsIObserver).observe(null, "places-debug-start-expiration", -1); |
|
367 |
|
368 // Wait for both downloads to be removed. |
|
369 yield deferred.promise; |
|
370 |
|
371 cleanup(); |
|
372 }); |
|
373 |
|
374 /** |
|
375 * Checks all downloads are removed after clearing history. |
|
376 */ |
|
377 add_task(function test_history_clear() |
|
378 { |
|
379 let list = yield promiseNewList(); |
|
380 let downloadOne = yield promiseNewDownload(); |
|
381 let downloadTwo = yield promiseNewDownload(); |
|
382 yield list.add(downloadOne); |
|
383 yield list.add(downloadTwo); |
|
384 |
|
385 let deferred = Promise.defer(); |
|
386 let removeNotifications = 0; |
|
387 let downloadView = { |
|
388 onDownloadRemoved: function (aDownload) { |
|
389 if (++removeNotifications == 2) { |
|
390 deferred.resolve(); |
|
391 } |
|
392 }, |
|
393 }; |
|
394 yield list.addView(downloadView); |
|
395 |
|
396 yield downloadOne.start(); |
|
397 yield downloadTwo.start(); |
|
398 |
|
399 yield promiseClearHistory(); |
|
400 |
|
401 // Wait for the removal notifications that may still be pending. |
|
402 yield deferred.promise; |
|
403 }); |
|
404 |
|
405 /** |
|
406 * Tests the removeFinished method to ensure that it only removes |
|
407 * finished downloads. |
|
408 */ |
|
409 add_task(function test_removeFinished() |
|
410 { |
|
411 let list = yield promiseNewList(); |
|
412 let downloadOne = yield promiseNewDownload(); |
|
413 let downloadTwo = yield promiseNewDownload(); |
|
414 let downloadThree = yield promiseNewDownload(); |
|
415 let downloadFour = yield promiseNewDownload(); |
|
416 yield list.add(downloadOne); |
|
417 yield list.add(downloadTwo); |
|
418 yield list.add(downloadThree); |
|
419 yield list.add(downloadFour); |
|
420 |
|
421 let deferred = Promise.defer(); |
|
422 let removeNotifications = 0; |
|
423 let downloadView = { |
|
424 onDownloadRemoved: function (aDownload) { |
|
425 do_check_true(aDownload == downloadOne || |
|
426 aDownload == downloadTwo || |
|
427 aDownload == downloadThree); |
|
428 do_check_true(removeNotifications < 3); |
|
429 if (++removeNotifications == 3) { |
|
430 deferred.resolve(); |
|
431 } |
|
432 }, |
|
433 }; |
|
434 yield list.addView(downloadView); |
|
435 |
|
436 // Start three of the downloads, but don't start downloadTwo, then set |
|
437 // downloadFour to have partial data. All downloads except downloadFour |
|
438 // should be removed. |
|
439 yield downloadOne.start(); |
|
440 yield downloadThree.start(); |
|
441 yield downloadFour.start(); |
|
442 downloadFour.hasPartialData = true; |
|
443 |
|
444 list.removeFinished(); |
|
445 yield deferred.promise; |
|
446 |
|
447 let downloads = yield list.getAll() |
|
448 do_check_eq(downloads.length, 1); |
|
449 }); |
|
450 |
|
451 /** |
|
452 * Tests the global DownloadSummary objects for the public, private, and |
|
453 * combined download lists. |
|
454 */ |
|
455 add_task(function test_DownloadSummary() |
|
456 { |
|
457 mustInterruptResponses(); |
|
458 |
|
459 let publicList = yield promiseNewList(); |
|
460 let privateList = yield Downloads.getList(Downloads.PRIVATE); |
|
461 |
|
462 let publicSummary = yield Downloads.getSummary(Downloads.PUBLIC); |
|
463 let privateSummary = yield Downloads.getSummary(Downloads.PRIVATE); |
|
464 let combinedSummary = yield Downloads.getSummary(Downloads.ALL); |
|
465 |
|
466 // Add a public download that has succeeded. |
|
467 let succeededPublicDownload = yield promiseNewDownload(); |
|
468 yield succeededPublicDownload.start(); |
|
469 yield publicList.add(succeededPublicDownload); |
|
470 |
|
471 // Add a public download that has been canceled midway. |
|
472 let canceledPublicDownload = |
|
473 yield promiseNewDownload(httpUrl("interruptible.txt")); |
|
474 canceledPublicDownload.start(); |
|
475 yield promiseDownloadMidway(canceledPublicDownload); |
|
476 yield canceledPublicDownload.cancel(); |
|
477 yield publicList.add(canceledPublicDownload); |
|
478 |
|
479 // Add a public download that is in progress. |
|
480 let inProgressPublicDownload = |
|
481 yield promiseNewDownload(httpUrl("interruptible.txt")); |
|
482 inProgressPublicDownload.start(); |
|
483 yield promiseDownloadMidway(inProgressPublicDownload); |
|
484 yield publicList.add(inProgressPublicDownload); |
|
485 |
|
486 // Add a private download that is in progress. |
|
487 let inProgressPrivateDownload = yield Downloads.createDownload({ |
|
488 source: { url: httpUrl("interruptible.txt"), isPrivate: true }, |
|
489 target: getTempFile(TEST_TARGET_FILE_NAME).path, |
|
490 }); |
|
491 inProgressPrivateDownload.start(); |
|
492 yield promiseDownloadMidway(inProgressPrivateDownload); |
|
493 yield privateList.add(inProgressPrivateDownload); |
|
494 |
|
495 // Verify that the summary includes the total number of bytes and the |
|
496 // currently transferred bytes only for the downloads that are not stopped. |
|
497 // For simplicity, we assume that after a download is added to the list, its |
|
498 // current state is immediately propagated to the summary object, which is |
|
499 // true in the current implementation, though it is not guaranteed as all the |
|
500 // download operations may happen asynchronously. |
|
501 do_check_false(publicSummary.allHaveStopped); |
|
502 do_check_eq(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); |
|
503 do_check_eq(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length); |
|
504 |
|
505 do_check_false(privateSummary.allHaveStopped); |
|
506 do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); |
|
507 do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length); |
|
508 |
|
509 do_check_false(combinedSummary.allHaveStopped); |
|
510 do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 4); |
|
511 do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length * 2); |
|
512 |
|
513 yield inProgressPublicDownload.cancel(); |
|
514 |
|
515 // Stopping the download should have excluded it from the summary. |
|
516 do_check_true(publicSummary.allHaveStopped); |
|
517 do_check_eq(publicSummary.progressTotalBytes, 0); |
|
518 do_check_eq(publicSummary.progressCurrentBytes, 0); |
|
519 |
|
520 do_check_false(privateSummary.allHaveStopped); |
|
521 do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); |
|
522 do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length); |
|
523 |
|
524 do_check_false(combinedSummary.allHaveStopped); |
|
525 do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); |
|
526 do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length); |
|
527 |
|
528 yield inProgressPrivateDownload.cancel(); |
|
529 |
|
530 // All the downloads should be stopped now. |
|
531 do_check_true(publicSummary.allHaveStopped); |
|
532 do_check_eq(publicSummary.progressTotalBytes, 0); |
|
533 do_check_eq(publicSummary.progressCurrentBytes, 0); |
|
534 |
|
535 do_check_true(privateSummary.allHaveStopped); |
|
536 do_check_eq(privateSummary.progressTotalBytes, 0); |
|
537 do_check_eq(privateSummary.progressCurrentBytes, 0); |
|
538 |
|
539 do_check_true(combinedSummary.allHaveStopped); |
|
540 do_check_eq(combinedSummary.progressTotalBytes, 0); |
|
541 do_check_eq(combinedSummary.progressCurrentBytes, 0); |
|
542 }); |
|
543 |
|
544 /** |
|
545 * Checks that views receive the summary change notification. This is tested on |
|
546 * the combined summary when adding a public download, as we assume that if we |
|
547 * pass the test in this case we will also pass it in the others. |
|
548 */ |
|
549 add_task(function test_DownloadSummary_notifications() |
|
550 { |
|
551 let list = yield promiseNewList(); |
|
552 let summary = yield Downloads.getSummary(Downloads.ALL); |
|
553 |
|
554 let download = yield promiseNewDownload(); |
|
555 yield list.add(download); |
|
556 |
|
557 // Check that we receive change notifications. |
|
558 let receivedOnSummaryChanged = false; |
|
559 yield summary.addView({ |
|
560 onSummaryChanged: function () { |
|
561 receivedOnSummaryChanged = true; |
|
562 }, |
|
563 }); |
|
564 yield download.start(); |
|
565 do_check_true(receivedOnSummaryChanged); |
|
566 }); |
|
567 |
|
568 //////////////////////////////////////////////////////////////////////////////// |
|
569 //// Termination |
|
570 |
|
571 let tailFile = do_get_file("tail.js"); |
|
572 Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec); |