1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,572 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80: */ 1.6 +/* Any copyright is dedicated to the Public Domain. 1.7 + * http://creativecommons.org/publicdomain/zero/1.0/ */ 1.8 + 1.9 +/** 1.10 + * Tests the DownloadList object. 1.11 + */ 1.12 + 1.13 +"use strict"; 1.14 + 1.15 +//////////////////////////////////////////////////////////////////////////////// 1.16 +//// Globals 1.17 + 1.18 +/** 1.19 + * Returns a PRTime in the past usable to add expirable visits. 1.20 + * 1.21 + * @note Expiration ignores any visit added in the last 7 days, but it's 1.22 + * better be safe against DST issues, by going back one day more. 1.23 + */ 1.24 +function getExpirablePRTime() 1.25 +{ 1.26 + let dateObj = new Date(); 1.27 + // Normalize to midnight 1.28 + dateObj.setHours(0); 1.29 + dateObj.setMinutes(0); 1.30 + dateObj.setSeconds(0); 1.31 + dateObj.setMilliseconds(0); 1.32 + dateObj = new Date(dateObj.getTime() - 8 * 86400000); 1.33 + return dateObj.getTime() * 1000; 1.34 +} 1.35 + 1.36 +/** 1.37 + * Adds an expirable history visit for a download. 1.38 + * 1.39 + * @param aSourceUrl 1.40 + * String containing the URI for the download source, or null to use 1.41 + * httpUrl("source.txt"). 1.42 + * 1.43 + * @return {Promise} 1.44 + * @rejects JavaScript exception. 1.45 + */ 1.46 +function promiseExpirableDownloadVisit(aSourceUrl) 1.47 +{ 1.48 + let deferred = Promise.defer(); 1.49 + PlacesUtils.asyncHistory.updatePlaces( 1.50 + { 1.51 + uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")), 1.52 + visits: [{ 1.53 + transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, 1.54 + visitDate: getExpirablePRTime(), 1.55 + }] 1.56 + }, 1.57 + { 1.58 + handleError: function handleError(aResultCode, aPlaceInfo) { 1.59 + let ex = new Components.Exception("Unexpected error in adding visits.", 1.60 + aResultCode); 1.61 + deferred.reject(ex); 1.62 + }, 1.63 + handleResult: function () {}, 1.64 + handleCompletion: function handleCompletion() { 1.65 + deferred.resolve(); 1.66 + } 1.67 + }); 1.68 + return deferred.promise; 1.69 +} 1.70 + 1.71 +//////////////////////////////////////////////////////////////////////////////// 1.72 +//// Tests 1.73 + 1.74 +/** 1.75 + * Checks the testing mechanism used to build different download lists. 1.76 + */ 1.77 +add_task(function test_construction() 1.78 +{ 1.79 + let downloadListOne = yield promiseNewList(); 1.80 + let downloadListTwo = yield promiseNewList(); 1.81 + let privateDownloadListOne = yield promiseNewList(true); 1.82 + let privateDownloadListTwo = yield promiseNewList(true); 1.83 + 1.84 + do_check_neq(downloadListOne, downloadListTwo); 1.85 + do_check_neq(privateDownloadListOne, privateDownloadListTwo); 1.86 + do_check_neq(downloadListOne, privateDownloadListOne); 1.87 +}); 1.88 + 1.89 +/** 1.90 + * Checks the methods to add and retrieve items from the list. 1.91 + */ 1.92 +add_task(function test_add_getAll() 1.93 +{ 1.94 + let list = yield promiseNewList(); 1.95 + 1.96 + let downloadOne = yield promiseNewDownload(); 1.97 + yield list.add(downloadOne); 1.98 + 1.99 + let itemsOne = yield list.getAll(); 1.100 + do_check_eq(itemsOne.length, 1); 1.101 + do_check_eq(itemsOne[0], downloadOne); 1.102 + 1.103 + let downloadTwo = yield promiseNewDownload(); 1.104 + yield list.add(downloadTwo); 1.105 + 1.106 + let itemsTwo = yield list.getAll(); 1.107 + do_check_eq(itemsTwo.length, 2); 1.108 + do_check_eq(itemsTwo[0], downloadOne); 1.109 + do_check_eq(itemsTwo[1], downloadTwo); 1.110 + 1.111 + // The first snapshot should not have been modified. 1.112 + do_check_eq(itemsOne.length, 1); 1.113 +}); 1.114 + 1.115 +/** 1.116 + * Checks the method to remove items from the list. 1.117 + */ 1.118 +add_task(function test_remove() 1.119 +{ 1.120 + let list = yield promiseNewList(); 1.121 + 1.122 + yield list.add(yield promiseNewDownload()); 1.123 + yield list.add(yield promiseNewDownload()); 1.124 + 1.125 + let items = yield list.getAll(); 1.126 + yield list.remove(items[0]); 1.127 + 1.128 + // Removing an item that was never added should not raise an error. 1.129 + yield list.remove(yield promiseNewDownload()); 1.130 + 1.131 + items = yield list.getAll(); 1.132 + do_check_eq(items.length, 1); 1.133 +}); 1.134 + 1.135 +/** 1.136 + * Tests that the "add", "remove", and "getAll" methods on the global 1.137 + * DownloadCombinedList object combine the contents of the global DownloadList 1.138 + * objects for public and private downloads. 1.139 + */ 1.140 +add_task(function test_DownloadCombinedList_add_remove_getAll() 1.141 +{ 1.142 + let publicList = yield promiseNewList(); 1.143 + let privateList = yield Downloads.getList(Downloads.PRIVATE); 1.144 + let combinedList = yield Downloads.getList(Downloads.ALL); 1.145 + 1.146 + let publicDownload = yield promiseNewDownload(); 1.147 + let privateDownload = yield Downloads.createDownload({ 1.148 + source: { url: httpUrl("source.txt"), isPrivate: true }, 1.149 + target: getTempFile(TEST_TARGET_FILE_NAME).path, 1.150 + }); 1.151 + 1.152 + yield publicList.add(publicDownload); 1.153 + yield privateList.add(privateDownload); 1.154 + 1.155 + do_check_eq((yield combinedList.getAll()).length, 2); 1.156 + 1.157 + yield combinedList.remove(publicDownload); 1.158 + yield combinedList.remove(privateDownload); 1.159 + 1.160 + do_check_eq((yield combinedList.getAll()).length, 0); 1.161 + 1.162 + yield combinedList.add(publicDownload); 1.163 + yield combinedList.add(privateDownload); 1.164 + 1.165 + do_check_eq((yield publicList.getAll()).length, 1); 1.166 + do_check_eq((yield privateList.getAll()).length, 1); 1.167 + do_check_eq((yield combinedList.getAll()).length, 2); 1.168 + 1.169 + yield publicList.remove(publicDownload); 1.170 + yield privateList.remove(privateDownload); 1.171 + 1.172 + do_check_eq((yield combinedList.getAll()).length, 0); 1.173 +}); 1.174 + 1.175 +/** 1.176 + * Checks that views receive the download add and remove notifications, and that 1.177 + * adding and removing views works as expected, both for a normal and a combined 1.178 + * list. 1.179 + */ 1.180 +add_task(function test_notifications_add_remove() 1.181 +{ 1.182 + for (let isCombined of [false, true]) { 1.183 + // Force creating a new list for both the public and combined cases. 1.184 + let list = yield promiseNewList(); 1.185 + if (isCombined) { 1.186 + list = yield Downloads.getList(Downloads.ALL); 1.187 + } 1.188 + 1.189 + let downloadOne = yield promiseNewDownload(); 1.190 + let downloadTwo = yield Downloads.createDownload({ 1.191 + source: { url: httpUrl("source.txt"), isPrivate: true }, 1.192 + target: getTempFile(TEST_TARGET_FILE_NAME).path, 1.193 + }); 1.194 + yield list.add(downloadOne); 1.195 + yield list.add(downloadTwo); 1.196 + 1.197 + // Check that we receive add notifications for existing elements. 1.198 + let addNotifications = 0; 1.199 + let viewOne = { 1.200 + onDownloadAdded: function (aDownload) { 1.201 + // The first download to be notified should be the first that was added. 1.202 + if (addNotifications == 0) { 1.203 + do_check_eq(aDownload, downloadOne); 1.204 + } else if (addNotifications == 1) { 1.205 + do_check_eq(aDownload, downloadTwo); 1.206 + } 1.207 + addNotifications++; 1.208 + }, 1.209 + }; 1.210 + yield list.addView(viewOne); 1.211 + do_check_eq(addNotifications, 2); 1.212 + 1.213 + // Check that we receive add notifications for new elements. 1.214 + yield list.add(yield promiseNewDownload()); 1.215 + do_check_eq(addNotifications, 3); 1.216 + 1.217 + // Check that we receive remove notifications. 1.218 + let removeNotifications = 0; 1.219 + let viewTwo = { 1.220 + onDownloadRemoved: function (aDownload) { 1.221 + do_check_eq(aDownload, downloadOne); 1.222 + removeNotifications++; 1.223 + }, 1.224 + }; 1.225 + yield list.addView(viewTwo); 1.226 + yield list.remove(downloadOne); 1.227 + do_check_eq(removeNotifications, 1); 1.228 + 1.229 + // We should not receive remove notifications after the view is removed. 1.230 + yield list.removeView(viewTwo); 1.231 + yield list.remove(downloadTwo); 1.232 + do_check_eq(removeNotifications, 1); 1.233 + 1.234 + // We should not receive add notifications after the view is removed. 1.235 + yield list.removeView(viewOne); 1.236 + yield list.add(yield promiseNewDownload()); 1.237 + do_check_eq(addNotifications, 3); 1.238 + } 1.239 +}); 1.240 + 1.241 +/** 1.242 + * Checks that views receive the download change notifications, both for a 1.243 + * normal and a combined list. 1.244 + */ 1.245 +add_task(function test_notifications_change() 1.246 +{ 1.247 + for (let isCombined of [false, true]) { 1.248 + // Force creating a new list for both the public and combined cases. 1.249 + let list = yield promiseNewList(); 1.250 + if (isCombined) { 1.251 + list = yield Downloads.getList(Downloads.ALL); 1.252 + } 1.253 + 1.254 + let downloadOne = yield promiseNewDownload(); 1.255 + let downloadTwo = yield Downloads.createDownload({ 1.256 + source: { url: httpUrl("source.txt"), isPrivate: true }, 1.257 + target: getTempFile(TEST_TARGET_FILE_NAME).path, 1.258 + }); 1.259 + yield list.add(downloadOne); 1.260 + yield list.add(downloadTwo); 1.261 + 1.262 + // Check that we receive change notifications. 1.263 + let receivedOnDownloadChanged = false; 1.264 + yield list.addView({ 1.265 + onDownloadChanged: function (aDownload) { 1.266 + do_check_eq(aDownload, downloadOne); 1.267 + receivedOnDownloadChanged = true; 1.268 + }, 1.269 + }); 1.270 + yield downloadOne.start(); 1.271 + do_check_true(receivedOnDownloadChanged); 1.272 + 1.273 + // We should not receive change notifications after a download is removed. 1.274 + receivedOnDownloadChanged = false; 1.275 + yield list.remove(downloadTwo); 1.276 + yield downloadTwo.start(); 1.277 + do_check_false(receivedOnDownloadChanged); 1.278 + } 1.279 +}); 1.280 + 1.281 +/** 1.282 + * Checks that the reference to "this" is correct in the view callbacks. 1.283 + */ 1.284 +add_task(function test_notifications_this() 1.285 +{ 1.286 + let list = yield promiseNewList(); 1.287 + 1.288 + // Check that we receive change notifications. 1.289 + let receivedOnDownloadAdded = false; 1.290 + let receivedOnDownloadChanged = false; 1.291 + let receivedOnDownloadRemoved = false; 1.292 + let view = { 1.293 + onDownloadAdded: function () { 1.294 + do_check_eq(this, view); 1.295 + receivedOnDownloadAdded = true; 1.296 + }, 1.297 + onDownloadChanged: function () { 1.298 + // Only do this check once. 1.299 + if (!receivedOnDownloadChanged) { 1.300 + do_check_eq(this, view); 1.301 + receivedOnDownloadChanged = true; 1.302 + } 1.303 + }, 1.304 + onDownloadRemoved: function () { 1.305 + do_check_eq(this, view); 1.306 + receivedOnDownloadRemoved = true; 1.307 + }, 1.308 + }; 1.309 + yield list.addView(view); 1.310 + 1.311 + let download = yield promiseNewDownload(); 1.312 + yield list.add(download); 1.313 + yield download.start(); 1.314 + yield list.remove(download); 1.315 + 1.316 + // Verify that we executed the checks. 1.317 + do_check_true(receivedOnDownloadAdded); 1.318 + do_check_true(receivedOnDownloadChanged); 1.319 + do_check_true(receivedOnDownloadRemoved); 1.320 +}); 1.321 + 1.322 +/** 1.323 + * Checks that download is removed on history expiration. 1.324 + */ 1.325 +add_task(function test_history_expiration() 1.326 +{ 1.327 + mustInterruptResponses(); 1.328 + 1.329 + function cleanup() { 1.330 + Services.prefs.clearUserPref("places.history.expiration.max_pages"); 1.331 + } 1.332 + do_register_cleanup(cleanup); 1.333 + 1.334 + // Set max pages to 0 to make the download expire. 1.335 + Services.prefs.setIntPref("places.history.expiration.max_pages", 0); 1.336 + 1.337 + let list = yield promiseNewList(); 1.338 + let downloadOne = yield promiseNewDownload(); 1.339 + let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt")); 1.340 + 1.341 + let deferred = Promise.defer(); 1.342 + let removeNotifications = 0; 1.343 + let downloadView = { 1.344 + onDownloadRemoved: function (aDownload) { 1.345 + if (++removeNotifications == 2) { 1.346 + deferred.resolve(); 1.347 + } 1.348 + }, 1.349 + }; 1.350 + yield list.addView(downloadView); 1.351 + 1.352 + // Work with one finished download and one canceled download. 1.353 + yield downloadOne.start(); 1.354 + downloadTwo.start(); 1.355 + yield downloadTwo.cancel(); 1.356 + 1.357 + // We must replace the visits added while executing the downloads with visits 1.358 + // that are older than 7 days, otherwise they will not be expired. 1.359 + yield promiseClearHistory(); 1.360 + yield promiseExpirableDownloadVisit(); 1.361 + yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt")); 1.362 + 1.363 + // After clearing history, we can add the downloads to be removed to the list. 1.364 + yield list.add(downloadOne); 1.365 + yield list.add(downloadTwo); 1.366 + 1.367 + // Force a history expiration. 1.368 + Cc["@mozilla.org/places/expiration;1"] 1.369 + .getService(Ci.nsIObserver).observe(null, "places-debug-start-expiration", -1); 1.370 + 1.371 + // Wait for both downloads to be removed. 1.372 + yield deferred.promise; 1.373 + 1.374 + cleanup(); 1.375 +}); 1.376 + 1.377 +/** 1.378 + * Checks all downloads are removed after clearing history. 1.379 + */ 1.380 +add_task(function test_history_clear() 1.381 +{ 1.382 + let list = yield promiseNewList(); 1.383 + let downloadOne = yield promiseNewDownload(); 1.384 + let downloadTwo = yield promiseNewDownload(); 1.385 + yield list.add(downloadOne); 1.386 + yield list.add(downloadTwo); 1.387 + 1.388 + let deferred = Promise.defer(); 1.389 + let removeNotifications = 0; 1.390 + let downloadView = { 1.391 + onDownloadRemoved: function (aDownload) { 1.392 + if (++removeNotifications == 2) { 1.393 + deferred.resolve(); 1.394 + } 1.395 + }, 1.396 + }; 1.397 + yield list.addView(downloadView); 1.398 + 1.399 + yield downloadOne.start(); 1.400 + yield downloadTwo.start(); 1.401 + 1.402 + yield promiseClearHistory(); 1.403 + 1.404 + // Wait for the removal notifications that may still be pending. 1.405 + yield deferred.promise; 1.406 +}); 1.407 + 1.408 +/** 1.409 + * Tests the removeFinished method to ensure that it only removes 1.410 + * finished downloads. 1.411 + */ 1.412 +add_task(function test_removeFinished() 1.413 +{ 1.414 + let list = yield promiseNewList(); 1.415 + let downloadOne = yield promiseNewDownload(); 1.416 + let downloadTwo = yield promiseNewDownload(); 1.417 + let downloadThree = yield promiseNewDownload(); 1.418 + let downloadFour = yield promiseNewDownload(); 1.419 + yield list.add(downloadOne); 1.420 + yield list.add(downloadTwo); 1.421 + yield list.add(downloadThree); 1.422 + yield list.add(downloadFour); 1.423 + 1.424 + let deferred = Promise.defer(); 1.425 + let removeNotifications = 0; 1.426 + let downloadView = { 1.427 + onDownloadRemoved: function (aDownload) { 1.428 + do_check_true(aDownload == downloadOne || 1.429 + aDownload == downloadTwo || 1.430 + aDownload == downloadThree); 1.431 + do_check_true(removeNotifications < 3); 1.432 + if (++removeNotifications == 3) { 1.433 + deferred.resolve(); 1.434 + } 1.435 + }, 1.436 + }; 1.437 + yield list.addView(downloadView); 1.438 + 1.439 + // Start three of the downloads, but don't start downloadTwo, then set 1.440 + // downloadFour to have partial data. All downloads except downloadFour 1.441 + // should be removed. 1.442 + yield downloadOne.start(); 1.443 + yield downloadThree.start(); 1.444 + yield downloadFour.start(); 1.445 + downloadFour.hasPartialData = true; 1.446 + 1.447 + list.removeFinished(); 1.448 + yield deferred.promise; 1.449 + 1.450 + let downloads = yield list.getAll() 1.451 + do_check_eq(downloads.length, 1); 1.452 +}); 1.453 + 1.454 +/** 1.455 + * Tests the global DownloadSummary objects for the public, private, and 1.456 + * combined download lists. 1.457 + */ 1.458 +add_task(function test_DownloadSummary() 1.459 +{ 1.460 + mustInterruptResponses(); 1.461 + 1.462 + let publicList = yield promiseNewList(); 1.463 + let privateList = yield Downloads.getList(Downloads.PRIVATE); 1.464 + 1.465 + let publicSummary = yield Downloads.getSummary(Downloads.PUBLIC); 1.466 + let privateSummary = yield Downloads.getSummary(Downloads.PRIVATE); 1.467 + let combinedSummary = yield Downloads.getSummary(Downloads.ALL); 1.468 + 1.469 + // Add a public download that has succeeded. 1.470 + let succeededPublicDownload = yield promiseNewDownload(); 1.471 + yield succeededPublicDownload.start(); 1.472 + yield publicList.add(succeededPublicDownload); 1.473 + 1.474 + // Add a public download that has been canceled midway. 1.475 + let canceledPublicDownload = 1.476 + yield promiseNewDownload(httpUrl("interruptible.txt")); 1.477 + canceledPublicDownload.start(); 1.478 + yield promiseDownloadMidway(canceledPublicDownload); 1.479 + yield canceledPublicDownload.cancel(); 1.480 + yield publicList.add(canceledPublicDownload); 1.481 + 1.482 + // Add a public download that is in progress. 1.483 + let inProgressPublicDownload = 1.484 + yield promiseNewDownload(httpUrl("interruptible.txt")); 1.485 + inProgressPublicDownload.start(); 1.486 + yield promiseDownloadMidway(inProgressPublicDownload); 1.487 + yield publicList.add(inProgressPublicDownload); 1.488 + 1.489 + // Add a private download that is in progress. 1.490 + let inProgressPrivateDownload = yield Downloads.createDownload({ 1.491 + source: { url: httpUrl("interruptible.txt"), isPrivate: true }, 1.492 + target: getTempFile(TEST_TARGET_FILE_NAME).path, 1.493 + }); 1.494 + inProgressPrivateDownload.start(); 1.495 + yield promiseDownloadMidway(inProgressPrivateDownload); 1.496 + yield privateList.add(inProgressPrivateDownload); 1.497 + 1.498 + // Verify that the summary includes the total number of bytes and the 1.499 + // currently transferred bytes only for the downloads that are not stopped. 1.500 + // For simplicity, we assume that after a download is added to the list, its 1.501 + // current state is immediately propagated to the summary object, which is 1.502 + // true in the current implementation, though it is not guaranteed as all the 1.503 + // download operations may happen asynchronously. 1.504 + do_check_false(publicSummary.allHaveStopped); 1.505 + do_check_eq(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); 1.506 + do_check_eq(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length); 1.507 + 1.508 + do_check_false(privateSummary.allHaveStopped); 1.509 + do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); 1.510 + do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length); 1.511 + 1.512 + do_check_false(combinedSummary.allHaveStopped); 1.513 + do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 4); 1.514 + do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length * 2); 1.515 + 1.516 + yield inProgressPublicDownload.cancel(); 1.517 + 1.518 + // Stopping the download should have excluded it from the summary. 1.519 + do_check_true(publicSummary.allHaveStopped); 1.520 + do_check_eq(publicSummary.progressTotalBytes, 0); 1.521 + do_check_eq(publicSummary.progressCurrentBytes, 0); 1.522 + 1.523 + do_check_false(privateSummary.allHaveStopped); 1.524 + do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); 1.525 + do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length); 1.526 + 1.527 + do_check_false(combinedSummary.allHaveStopped); 1.528 + do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); 1.529 + do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length); 1.530 + 1.531 + yield inProgressPrivateDownload.cancel(); 1.532 + 1.533 + // All the downloads should be stopped now. 1.534 + do_check_true(publicSummary.allHaveStopped); 1.535 + do_check_eq(publicSummary.progressTotalBytes, 0); 1.536 + do_check_eq(publicSummary.progressCurrentBytes, 0); 1.537 + 1.538 + do_check_true(privateSummary.allHaveStopped); 1.539 + do_check_eq(privateSummary.progressTotalBytes, 0); 1.540 + do_check_eq(privateSummary.progressCurrentBytes, 0); 1.541 + 1.542 + do_check_true(combinedSummary.allHaveStopped); 1.543 + do_check_eq(combinedSummary.progressTotalBytes, 0); 1.544 + do_check_eq(combinedSummary.progressCurrentBytes, 0); 1.545 +}); 1.546 + 1.547 +/** 1.548 + * Checks that views receive the summary change notification. This is tested on 1.549 + * the combined summary when adding a public download, as we assume that if we 1.550 + * pass the test in this case we will also pass it in the others. 1.551 + */ 1.552 +add_task(function test_DownloadSummary_notifications() 1.553 +{ 1.554 + let list = yield promiseNewList(); 1.555 + let summary = yield Downloads.getSummary(Downloads.ALL); 1.556 + 1.557 + let download = yield promiseNewDownload(); 1.558 + yield list.add(download); 1.559 + 1.560 + // Check that we receive change notifications. 1.561 + let receivedOnSummaryChanged = false; 1.562 + yield summary.addView({ 1.563 + onSummaryChanged: function () { 1.564 + receivedOnSummaryChanged = true; 1.565 + }, 1.566 + }); 1.567 + yield download.start(); 1.568 + do_check_true(receivedOnSummaryChanged); 1.569 +}); 1.570 + 1.571 +//////////////////////////////////////////////////////////////////////////////// 1.572 +//// Termination 1.573 + 1.574 +let tailFile = do_get_file("tail.js"); 1.575 +Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec);