1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/thumbnails/test/head.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,396 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +let tmp = {}; 1.8 +Cu.import("resource://gre/modules/PageThumbs.jsm", tmp); 1.9 +Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", tmp); 1.10 +Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp); 1.11 +Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp); 1.12 +Cu.import("resource://gre/modules/FileUtils.jsm", tmp); 1.13 +Cu.import("resource://gre/modules/osfile.jsm", tmp); 1.14 +let {PageThumbs, BackgroundPageThumbs, NewTabUtils, PageThumbsStorage, SessionStore, FileUtils, OS} = tmp; 1.15 + 1.16 +Cu.import("resource://gre/modules/PlacesUtils.jsm"); 1.17 + 1.18 +let oldEnabledPref = Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled"); 1.19 +Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", false); 1.20 + 1.21 +registerCleanupFunction(function () { 1.22 + while (gBrowser.tabs.length > 1) 1.23 + gBrowser.removeTab(gBrowser.tabs[1]); 1.24 + Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", oldEnabledPref) 1.25 +}); 1.26 + 1.27 +/** 1.28 + * Provide the default test function to start our test runner. 1.29 + */ 1.30 +function test() { 1.31 + TestRunner.run(); 1.32 +} 1.33 + 1.34 +/** 1.35 + * The test runner that controls the execution flow of our tests. 1.36 + */ 1.37 +let TestRunner = { 1.38 + /** 1.39 + * Starts the test runner. 1.40 + */ 1.41 + run: function () { 1.42 + waitForExplicitFinish(); 1.43 + 1.44 + SessionStore.promiseInitialized.then(function () { 1.45 + this._iter = runTests(); 1.46 + if (this._iter) { 1.47 + this.next(); 1.48 + } else { 1.49 + finish(); 1.50 + } 1.51 + }.bind(this)); 1.52 + }, 1.53 + 1.54 + /** 1.55 + * Runs the next available test or finishes if there's no test left. 1.56 + * @param aValue This value will be passed to the yielder via the runner's 1.57 + * iterator. 1.58 + */ 1.59 + next: function (aValue) { 1.60 + try { 1.61 + let value = TestRunner._iter.send(aValue); 1.62 + if (value && typeof value.then == "function") { 1.63 + value.then(result => { 1.64 + next(result); 1.65 + }, error => { 1.66 + ok(false, error + "\n" + error.stack); 1.67 + }); 1.68 + } 1.69 + } catch (e if e instanceof StopIteration) { 1.70 + finish(); 1.71 + } 1.72 + } 1.73 +}; 1.74 + 1.75 +/** 1.76 + * Continues the current test execution. 1.77 + * @param aValue This value will be passed to the yielder via the runner's 1.78 + * iterator. 1.79 + */ 1.80 +function next(aValue) { 1.81 + TestRunner.next(aValue); 1.82 +} 1.83 + 1.84 +/** 1.85 + * Creates a new tab with the given URI. 1.86 + * @param aURI The URI that's loaded in the tab. 1.87 + * @param aCallback The function to call when the tab has loaded. 1.88 + */ 1.89 +function addTab(aURI, aCallback) { 1.90 + let tab = gBrowser.selectedTab = gBrowser.addTab(aURI); 1.91 + whenLoaded(tab.linkedBrowser, aCallback); 1.92 +} 1.93 + 1.94 +/** 1.95 + * Loads a new URI into the currently selected tab. 1.96 + * @param aURI The URI to load. 1.97 + */ 1.98 +function navigateTo(aURI) { 1.99 + let browser = gBrowser.selectedTab.linkedBrowser; 1.100 + whenLoaded(browser); 1.101 + browser.loadURI(aURI); 1.102 +} 1.103 + 1.104 +/** 1.105 + * Continues the current test execution when a load event for the given element 1.106 + * has been received. 1.107 + * @param aElement The DOM element to listen on. 1.108 + * @param aCallback The function to call when the load event was dispatched. 1.109 + */ 1.110 +function whenLoaded(aElement, aCallback = next) { 1.111 + aElement.addEventListener("load", function onLoad() { 1.112 + aElement.removeEventListener("load", onLoad, true); 1.113 + executeSoon(aCallback); 1.114 + }, true); 1.115 +} 1.116 + 1.117 +/** 1.118 + * Captures a screenshot for the currently selected tab, stores it in the cache, 1.119 + * retrieves it from the cache and compares pixel color values. 1.120 + * @param aRed The red component's intensity. 1.121 + * @param aGreen The green component's intensity. 1.122 + * @param aBlue The blue component's intensity. 1.123 + * @param aMessage The info message to print when comparing the pixel color. 1.124 + */ 1.125 +function captureAndCheckColor(aRed, aGreen, aBlue, aMessage) { 1.126 + let browser = gBrowser.selectedBrowser; 1.127 + // We'll get oranges if the expiration filter removes the file during the 1.128 + // test. 1.129 + dontExpireThumbnailURLs([browser.currentURI.spec]); 1.130 + 1.131 + // Capture the screenshot. 1.132 + PageThumbs.captureAndStore(browser, function () { 1.133 + retrieveImageDataForURL(browser.currentURI.spec, function ([r, g, b]) { 1.134 + is("" + [r,g,b], "" + [aRed, aGreen, aBlue], aMessage); 1.135 + next(); 1.136 + }); 1.137 + }); 1.138 +} 1.139 + 1.140 +/** 1.141 + * For a given URL, loads the corresponding thumbnail 1.142 + * to a canvas and passes its image data to the callback. 1.143 + * @param aURL The url associated with the thumbnail. 1.144 + * @param aCallback The function to pass the image data to. 1.145 + */ 1.146 +function retrieveImageDataForURL(aURL, aCallback) { 1.147 + let width = 100, height = 100; 1.148 + let thumb = PageThumbs.getThumbnailURL(aURL, width, height); 1.149 + // create a tab with a chrome:// URL so it can host the thumbnail image. 1.150 + // Note that we tried creating the element directly in the top-level chrome 1.151 + // document, but this caused a strange problem: 1.152 + // * call this with the url of an image. 1.153 + // * immediately change the image content. 1.154 + // * call this again with the same url (now holding different content) 1.155 + // The original image data would be used. Maybe the img hadn't been 1.156 + // collected yet and the platform noticed the same URL, so reused the 1.157 + // content? Not sure - but this solves the problem. 1.158 + addTab("chrome://global/content/mozilla.xhtml", () => { 1.159 + let doc = gBrowser.selectedBrowser.contentDocument; 1.160 + let htmlns = "http://www.w3.org/1999/xhtml"; 1.161 + let img = doc.createElementNS(htmlns, "img"); 1.162 + img.setAttribute("src", thumb); 1.163 + 1.164 + whenLoaded(img, function () { 1.165 + let canvas = document.createElementNS(htmlns, "canvas"); 1.166 + canvas.setAttribute("width", width); 1.167 + canvas.setAttribute("height", height); 1.168 + 1.169 + // Draw the image to a canvas and compare the pixel color values. 1.170 + let ctx = canvas.getContext("2d"); 1.171 + ctx.drawImage(img, 0, 0, width, height); 1.172 + let result = ctx.getImageData(0, 0, 100, 100).data; 1.173 + gBrowser.removeTab(gBrowser.selectedTab); 1.174 + aCallback(result); 1.175 + }); 1.176 + }); 1.177 +} 1.178 + 1.179 +/** 1.180 + * Returns the file of the thumbnail with the given URL. 1.181 + * @param aURL The URL of the thumbnail. 1.182 + */ 1.183 +function thumbnailFile(aURL) { 1.184 + return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL)); 1.185 +} 1.186 + 1.187 +/** 1.188 + * Checks if a thumbnail for the given URL exists. 1.189 + * @param aURL The url associated to the thumbnail. 1.190 + */ 1.191 +function thumbnailExists(aURL) { 1.192 + let file = thumbnailFile(aURL); 1.193 + return file.exists() && file.fileSize; 1.194 +} 1.195 + 1.196 +/** 1.197 + * Removes the thumbnail for the given URL. 1.198 + * @param aURL The URL associated with the thumbnail. 1.199 + */ 1.200 +function removeThumbnail(aURL) { 1.201 + let file = thumbnailFile(aURL); 1.202 + file.remove(false); 1.203 +} 1.204 + 1.205 +/** 1.206 + * Asynchronously adds visits to a page, invoking a callback function when done. 1.207 + * 1.208 + * @param aPlaceInfo 1.209 + * One of the following: a string spec, an nsIURI, an object describing 1.210 + * the Place as described below, or an array of any such types. An 1.211 + * object describing a Place must look like this: 1.212 + * { uri: nsIURI of the page, 1.213 + * [optional] transition: one of the TRANSITION_* from 1.214 + * nsINavHistoryService, 1.215 + * [optional] title: title of the page, 1.216 + * [optional] visitDate: visit date in microseconds from the epoch 1.217 + * [optional] referrer: nsIURI of the referrer for this visit 1.218 + * } 1.219 + * @param [optional] aCallback 1.220 + * Function to be invoked on completion. 1.221 + */ 1.222 +function addVisits(aPlaceInfo, aCallback) { 1.223 + let places = []; 1.224 + if (aPlaceInfo instanceof Ci.nsIURI) { 1.225 + places.push({ uri: aPlaceInfo }); 1.226 + } 1.227 + else if (Array.isArray(aPlaceInfo)) { 1.228 + places = places.concat(aPlaceInfo); 1.229 + } else { 1.230 + places.push(aPlaceInfo) 1.231 + } 1.232 + 1.233 + // Create mozIVisitInfo for each entry. 1.234 + let now = Date.now(); 1.235 + for (let i = 0; i < places.length; i++) { 1.236 + if (typeof(places[i] == "string")) { 1.237 + places[i] = { uri: Services.io.newURI(places[i], "", null) }; 1.238 + } 1.239 + if (!places[i].title) { 1.240 + places[i].title = "test visit for " + places[i].uri.spec; 1.241 + } 1.242 + places[i].visits = [{ 1.243 + transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK 1.244 + : places[i].transition, 1.245 + visitDate: places[i].visitDate || (now++) * 1000, 1.246 + referrerURI: places[i].referrer 1.247 + }]; 1.248 + } 1.249 + 1.250 + PlacesUtils.asyncHistory.updatePlaces( 1.251 + places, 1.252 + { 1.253 + handleError: function AAV_handleError() { 1.254 + throw("Unexpected error in adding visit."); 1.255 + }, 1.256 + handleResult: function () {}, 1.257 + handleCompletion: function UP_handleCompletion() { 1.258 + if (aCallback) 1.259 + aCallback(); 1.260 + } 1.261 + } 1.262 + ); 1.263 +} 1.264 + 1.265 +/** 1.266 + * Calls addVisits, and then forces the newtab module to repopulate its links. 1.267 + * See addVisits for parameter descriptions. 1.268 + */ 1.269 +function addVisitsAndRepopulateNewTabLinks(aPlaceInfo, aCallback) { 1.270 + addVisits(aPlaceInfo, () => NewTabUtils.links.populateCache(aCallback, true)); 1.271 +} 1.272 + 1.273 +/** 1.274 + * Calls a given callback when the thumbnail for a given URL has been found 1.275 + * on disk. Keeps trying until the thumbnail has been created. 1.276 + * 1.277 + * @param aURL The URL of the thumbnail's page. 1.278 + * @param [optional] aCallback 1.279 + * Function to be invoked on completion. 1.280 + */ 1.281 +function whenFileExists(aURL, aCallback = next) { 1.282 + let callback = aCallback; 1.283 + if (!thumbnailExists(aURL)) { 1.284 + callback = function () whenFileExists(aURL, aCallback); 1.285 + } 1.286 + 1.287 + executeSoon(callback); 1.288 +} 1.289 + 1.290 +/** 1.291 + * Calls a given callback when the given file has been removed. 1.292 + * Keeps trying until the file is removed. 1.293 + * 1.294 + * @param aFile The file that is being removed 1.295 + * @param [optional] aCallback 1.296 + * Function to be invoked on completion. 1.297 + */ 1.298 +function whenFileRemoved(aFile, aCallback) { 1.299 + let callback = aCallback; 1.300 + if (aFile.exists()) { 1.301 + callback = function () whenFileRemoved(aFile, aCallback); 1.302 + } 1.303 + 1.304 + executeSoon(callback || next); 1.305 +} 1.306 + 1.307 +function wait(aMillis) { 1.308 + setTimeout(next, aMillis); 1.309 +} 1.310 + 1.311 +/** 1.312 + * Makes sure that a given list of URLs is not implicitly expired. 1.313 + * 1.314 + * @param aURLs The list of URLs that should not be expired. 1.315 + */ 1.316 +function dontExpireThumbnailURLs(aURLs) { 1.317 + let dontExpireURLs = (cb) => cb(aURLs); 1.318 + PageThumbs.addExpirationFilter(dontExpireURLs); 1.319 + 1.320 + registerCleanupFunction(function () { 1.321 + PageThumbs.removeExpirationFilter(dontExpireURLs); 1.322 + }); 1.323 +} 1.324 + 1.325 +function bgCapture(aURL, aOptions) { 1.326 + bgCaptureWithMethod("capture", aURL, aOptions); 1.327 +} 1.328 + 1.329 +function bgCaptureIfMissing(aURL, aOptions) { 1.330 + bgCaptureWithMethod("captureIfMissing", aURL, aOptions); 1.331 +} 1.332 + 1.333 +function bgCaptureWithMethod(aMethodName, aURL, aOptions = {}) { 1.334 + // We'll get oranges if the expiration filter removes the file during the 1.335 + // test. 1.336 + dontExpireThumbnailURLs([aURL]); 1.337 + if (!aOptions.onDone) 1.338 + aOptions.onDone = next; 1.339 + BackgroundPageThumbs[aMethodName](aURL, aOptions); 1.340 +} 1.341 + 1.342 +function bgTestPageURL(aOpts = {}) { 1.343 + let TEST_PAGE_URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_background.sjs"; 1.344 + return TEST_PAGE_URL + "?" + encodeURIComponent(JSON.stringify(aOpts)); 1.345 +} 1.346 + 1.347 +function bgAddCrashObserver() { 1.348 + let crashed = false; 1.349 + Services.obs.addObserver(function crashObserver(subject, topic, data) { 1.350 + is(topic, 'ipc:content-shutdown', 'Received correct observer topic.'); 1.351 + ok(subject instanceof Components.interfaces.nsIPropertyBag2, 1.352 + 'Subject implements nsIPropertyBag2.'); 1.353 + // we might see this called as the process terminates due to previous tests. 1.354 + // We are only looking for "abnormal" exits... 1.355 + if (!subject.hasKey("abnormal")) { 1.356 + info("This is a normal termination and isn't the one we are looking for..."); 1.357 + return; 1.358 + } 1.359 + Services.obs.removeObserver(crashObserver, 'ipc:content-shutdown'); 1.360 + crashed = true; 1.361 + 1.362 + var dumpID; 1.363 + if ('nsICrashReporter' in Components.interfaces) { 1.364 + dumpID = subject.getPropertyAsAString('dumpID'); 1.365 + ok(dumpID, "dumpID is present and not an empty string"); 1.366 + } 1.367 + 1.368 + if (dumpID) { 1.369 + var minidumpDirectory = getMinidumpDirectory(); 1.370 + removeFile(minidumpDirectory, dumpID + '.dmp'); 1.371 + removeFile(minidumpDirectory, dumpID + '.extra'); 1.372 + } 1.373 + }, 'ipc:content-shutdown', false); 1.374 + return { 1.375 + get crashed() crashed 1.376 + }; 1.377 +} 1.378 + 1.379 +function bgInjectCrashContentScript() { 1.380 + const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/toolkit/components/thumbnails/test/thumbnails_crash_content_helper.js"; 1.381 + let thumbnailBrowser = BackgroundPageThumbs._thumbBrowser; 1.382 + let mm = thumbnailBrowser.messageManager; 1.383 + mm.loadFrameScript(TEST_CONTENT_HELPER, false); 1.384 + return mm; 1.385 +} 1.386 + 1.387 +function getMinidumpDirectory() { 1.388 + var dir = Services.dirsvc.get('ProfD', Components.interfaces.nsIFile); 1.389 + dir.append("minidumps"); 1.390 + return dir; 1.391 +} 1.392 + 1.393 +function removeFile(directory, filename) { 1.394 + var file = directory.clone(); 1.395 + file.append(filename); 1.396 + if (file.exists()) { 1.397 + file.remove(false); 1.398 + } 1.399 +}