michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: let tmp = {}; michael@0: Cu.import("resource://gre/modules/PageThumbs.jsm", tmp); michael@0: Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", tmp); michael@0: Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp); michael@0: Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp); michael@0: Cu.import("resource://gre/modules/FileUtils.jsm", tmp); michael@0: Cu.import("resource://gre/modules/osfile.jsm", tmp); michael@0: let {PageThumbs, BackgroundPageThumbs, NewTabUtils, PageThumbsStorage, SessionStore, FileUtils, OS} = tmp; michael@0: michael@0: Cu.import("resource://gre/modules/PlacesUtils.jsm"); michael@0: michael@0: let oldEnabledPref = Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled"); michael@0: Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", false); michael@0: michael@0: registerCleanupFunction(function () { michael@0: while (gBrowser.tabs.length > 1) michael@0: gBrowser.removeTab(gBrowser.tabs[1]); michael@0: Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", oldEnabledPref) michael@0: }); michael@0: michael@0: /** michael@0: * Provide the default test function to start our test runner. michael@0: */ michael@0: function test() { michael@0: TestRunner.run(); michael@0: } michael@0: michael@0: /** michael@0: * The test runner that controls the execution flow of our tests. michael@0: */ michael@0: let TestRunner = { michael@0: /** michael@0: * Starts the test runner. michael@0: */ michael@0: run: function () { michael@0: waitForExplicitFinish(); michael@0: michael@0: SessionStore.promiseInitialized.then(function () { michael@0: this._iter = runTests(); michael@0: if (this._iter) { michael@0: this.next(); michael@0: } else { michael@0: finish(); michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Runs the next available test or finishes if there's no test left. michael@0: * @param aValue This value will be passed to the yielder via the runner's michael@0: * iterator. michael@0: */ michael@0: next: function (aValue) { michael@0: try { michael@0: let value = TestRunner._iter.send(aValue); michael@0: if (value && typeof value.then == "function") { michael@0: value.then(result => { michael@0: next(result); michael@0: }, error => { michael@0: ok(false, error + "\n" + error.stack); michael@0: }); michael@0: } michael@0: } catch (e if e instanceof StopIteration) { michael@0: finish(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Continues the current test execution. michael@0: * @param aValue This value will be passed to the yielder via the runner's michael@0: * iterator. michael@0: */ michael@0: function next(aValue) { michael@0: TestRunner.next(aValue); michael@0: } michael@0: michael@0: /** michael@0: * Creates a new tab with the given URI. michael@0: * @param aURI The URI that's loaded in the tab. michael@0: * @param aCallback The function to call when the tab has loaded. michael@0: */ michael@0: function addTab(aURI, aCallback) { michael@0: let tab = gBrowser.selectedTab = gBrowser.addTab(aURI); michael@0: whenLoaded(tab.linkedBrowser, aCallback); michael@0: } michael@0: michael@0: /** michael@0: * Loads a new URI into the currently selected tab. michael@0: * @param aURI The URI to load. michael@0: */ michael@0: function navigateTo(aURI) { michael@0: let browser = gBrowser.selectedTab.linkedBrowser; michael@0: whenLoaded(browser); michael@0: browser.loadURI(aURI); michael@0: } michael@0: michael@0: /** michael@0: * Continues the current test execution when a load event for the given element michael@0: * has been received. michael@0: * @param aElement The DOM element to listen on. michael@0: * @param aCallback The function to call when the load event was dispatched. michael@0: */ michael@0: function whenLoaded(aElement, aCallback = next) { michael@0: aElement.addEventListener("load", function onLoad() { michael@0: aElement.removeEventListener("load", onLoad, true); michael@0: executeSoon(aCallback); michael@0: }, true); michael@0: } michael@0: michael@0: /** michael@0: * Captures a screenshot for the currently selected tab, stores it in the cache, michael@0: * retrieves it from the cache and compares pixel color values. michael@0: * @param aRed The red component's intensity. michael@0: * @param aGreen The green component's intensity. michael@0: * @param aBlue The blue component's intensity. michael@0: * @param aMessage The info message to print when comparing the pixel color. michael@0: */ michael@0: function captureAndCheckColor(aRed, aGreen, aBlue, aMessage) { michael@0: let browser = gBrowser.selectedBrowser; michael@0: // We'll get oranges if the expiration filter removes the file during the michael@0: // test. michael@0: dontExpireThumbnailURLs([browser.currentURI.spec]); michael@0: michael@0: // Capture the screenshot. michael@0: PageThumbs.captureAndStore(browser, function () { michael@0: retrieveImageDataForURL(browser.currentURI.spec, function ([r, g, b]) { michael@0: is("" + [r,g,b], "" + [aRed, aGreen, aBlue], aMessage); michael@0: next(); michael@0: }); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * For a given URL, loads the corresponding thumbnail michael@0: * to a canvas and passes its image data to the callback. michael@0: * @param aURL The url associated with the thumbnail. michael@0: * @param aCallback The function to pass the image data to. michael@0: */ michael@0: function retrieveImageDataForURL(aURL, aCallback) { michael@0: let width = 100, height = 100; michael@0: let thumb = PageThumbs.getThumbnailURL(aURL, width, height); michael@0: // create a tab with a chrome:// URL so it can host the thumbnail image. michael@0: // Note that we tried creating the element directly in the top-level chrome michael@0: // document, but this caused a strange problem: michael@0: // * call this with the url of an image. michael@0: // * immediately change the image content. michael@0: // * call this again with the same url (now holding different content) michael@0: // The original image data would be used. Maybe the img hadn't been michael@0: // collected yet and the platform noticed the same URL, so reused the michael@0: // content? Not sure - but this solves the problem. michael@0: addTab("chrome://global/content/mozilla.xhtml", () => { michael@0: let doc = gBrowser.selectedBrowser.contentDocument; michael@0: let htmlns = "http://www.w3.org/1999/xhtml"; michael@0: let img = doc.createElementNS(htmlns, "img"); michael@0: img.setAttribute("src", thumb); michael@0: michael@0: whenLoaded(img, function () { michael@0: let canvas = document.createElementNS(htmlns, "canvas"); michael@0: canvas.setAttribute("width", width); michael@0: canvas.setAttribute("height", height); michael@0: michael@0: // Draw the image to a canvas and compare the pixel color values. michael@0: let ctx = canvas.getContext("2d"); michael@0: ctx.drawImage(img, 0, 0, width, height); michael@0: let result = ctx.getImageData(0, 0, 100, 100).data; michael@0: gBrowser.removeTab(gBrowser.selectedTab); michael@0: aCallback(result); michael@0: }); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Returns the file of the thumbnail with the given URL. michael@0: * @param aURL The URL of the thumbnail. michael@0: */ michael@0: function thumbnailFile(aURL) { michael@0: return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL)); michael@0: } michael@0: michael@0: /** michael@0: * Checks if a thumbnail for the given URL exists. michael@0: * @param aURL The url associated to the thumbnail. michael@0: */ michael@0: function thumbnailExists(aURL) { michael@0: let file = thumbnailFile(aURL); michael@0: return file.exists() && file.fileSize; michael@0: } michael@0: michael@0: /** michael@0: * Removes the thumbnail for the given URL. michael@0: * @param aURL The URL associated with the thumbnail. michael@0: */ michael@0: function removeThumbnail(aURL) { michael@0: let file = thumbnailFile(aURL); michael@0: file.remove(false); michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously adds visits to a page, invoking a callback function when done. michael@0: * michael@0: * @param aPlaceInfo michael@0: * One of the following: a string spec, an nsIURI, an object describing michael@0: * the Place as described below, or an array of any such types. An michael@0: * object describing a Place must look like this: michael@0: * { uri: nsIURI of the page, michael@0: * [optional] transition: one of the TRANSITION_* from michael@0: * nsINavHistoryService, michael@0: * [optional] title: title of the page, michael@0: * [optional] visitDate: visit date in microseconds from the epoch michael@0: * [optional] referrer: nsIURI of the referrer for this visit michael@0: * } michael@0: * @param [optional] aCallback michael@0: * Function to be invoked on completion. michael@0: */ michael@0: function addVisits(aPlaceInfo, aCallback) { michael@0: let places = []; michael@0: if (aPlaceInfo instanceof Ci.nsIURI) { michael@0: places.push({ uri: aPlaceInfo }); michael@0: } michael@0: else if (Array.isArray(aPlaceInfo)) { michael@0: places = places.concat(aPlaceInfo); michael@0: } else { michael@0: places.push(aPlaceInfo) michael@0: } michael@0: michael@0: // Create mozIVisitInfo for each entry. michael@0: let now = Date.now(); michael@0: for (let i = 0; i < places.length; i++) { michael@0: if (typeof(places[i] == "string")) { michael@0: places[i] = { uri: Services.io.newURI(places[i], "", null) }; michael@0: } michael@0: if (!places[i].title) { michael@0: places[i].title = "test visit for " + places[i].uri.spec; michael@0: } michael@0: places[i].visits = [{ michael@0: transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK michael@0: : places[i].transition, michael@0: visitDate: places[i].visitDate || (now++) * 1000, michael@0: referrerURI: places[i].referrer michael@0: }]; michael@0: } michael@0: michael@0: PlacesUtils.asyncHistory.updatePlaces( michael@0: places, michael@0: { michael@0: handleError: function AAV_handleError() { michael@0: throw("Unexpected error in adding visit."); michael@0: }, michael@0: handleResult: function () {}, michael@0: handleCompletion: function UP_handleCompletion() { michael@0: if (aCallback) michael@0: aCallback(); michael@0: } michael@0: } michael@0: ); michael@0: } michael@0: michael@0: /** michael@0: * Calls addVisits, and then forces the newtab module to repopulate its links. michael@0: * See addVisits for parameter descriptions. michael@0: */ michael@0: function addVisitsAndRepopulateNewTabLinks(aPlaceInfo, aCallback) { michael@0: addVisits(aPlaceInfo, () => NewTabUtils.links.populateCache(aCallback, true)); michael@0: } michael@0: michael@0: /** michael@0: * Calls a given callback when the thumbnail for a given URL has been found michael@0: * on disk. Keeps trying until the thumbnail has been created. michael@0: * michael@0: * @param aURL The URL of the thumbnail's page. michael@0: * @param [optional] aCallback michael@0: * Function to be invoked on completion. michael@0: */ michael@0: function whenFileExists(aURL, aCallback = next) { michael@0: let callback = aCallback; michael@0: if (!thumbnailExists(aURL)) { michael@0: callback = function () whenFileExists(aURL, aCallback); michael@0: } michael@0: michael@0: executeSoon(callback); michael@0: } michael@0: michael@0: /** michael@0: * Calls a given callback when the given file has been removed. michael@0: * Keeps trying until the file is removed. michael@0: * michael@0: * @param aFile The file that is being removed michael@0: * @param [optional] aCallback michael@0: * Function to be invoked on completion. michael@0: */ michael@0: function whenFileRemoved(aFile, aCallback) { michael@0: let callback = aCallback; michael@0: if (aFile.exists()) { michael@0: callback = function () whenFileRemoved(aFile, aCallback); michael@0: } michael@0: michael@0: executeSoon(callback || next); michael@0: } michael@0: michael@0: function wait(aMillis) { michael@0: setTimeout(next, aMillis); michael@0: } michael@0: michael@0: /** michael@0: * Makes sure that a given list of URLs is not implicitly expired. michael@0: * michael@0: * @param aURLs The list of URLs that should not be expired. michael@0: */ michael@0: function dontExpireThumbnailURLs(aURLs) { michael@0: let dontExpireURLs = (cb) => cb(aURLs); michael@0: PageThumbs.addExpirationFilter(dontExpireURLs); michael@0: michael@0: registerCleanupFunction(function () { michael@0: PageThumbs.removeExpirationFilter(dontExpireURLs); michael@0: }); michael@0: } michael@0: michael@0: function bgCapture(aURL, aOptions) { michael@0: bgCaptureWithMethod("capture", aURL, aOptions); michael@0: } michael@0: michael@0: function bgCaptureIfMissing(aURL, aOptions) { michael@0: bgCaptureWithMethod("captureIfMissing", aURL, aOptions); michael@0: } michael@0: michael@0: function bgCaptureWithMethod(aMethodName, aURL, aOptions = {}) { michael@0: // We'll get oranges if the expiration filter removes the file during the michael@0: // test. michael@0: dontExpireThumbnailURLs([aURL]); michael@0: if (!aOptions.onDone) michael@0: aOptions.onDone = next; michael@0: BackgroundPageThumbs[aMethodName](aURL, aOptions); michael@0: } michael@0: michael@0: function bgTestPageURL(aOpts = {}) { michael@0: let TEST_PAGE_URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_background.sjs"; michael@0: return TEST_PAGE_URL + "?" + encodeURIComponent(JSON.stringify(aOpts)); michael@0: } michael@0: michael@0: function bgAddCrashObserver() { michael@0: let crashed = false; michael@0: Services.obs.addObserver(function crashObserver(subject, topic, data) { michael@0: is(topic, 'ipc:content-shutdown', 'Received correct observer topic.'); michael@0: ok(subject instanceof Components.interfaces.nsIPropertyBag2, michael@0: 'Subject implements nsIPropertyBag2.'); michael@0: // we might see this called as the process terminates due to previous tests. michael@0: // We are only looking for "abnormal" exits... michael@0: if (!subject.hasKey("abnormal")) { michael@0: info("This is a normal termination and isn't the one we are looking for..."); michael@0: return; michael@0: } michael@0: Services.obs.removeObserver(crashObserver, 'ipc:content-shutdown'); michael@0: crashed = true; michael@0: michael@0: var dumpID; michael@0: if ('nsICrashReporter' in Components.interfaces) { michael@0: dumpID = subject.getPropertyAsAString('dumpID'); michael@0: ok(dumpID, "dumpID is present and not an empty string"); michael@0: } michael@0: michael@0: if (dumpID) { michael@0: var minidumpDirectory = getMinidumpDirectory(); michael@0: removeFile(minidumpDirectory, dumpID + '.dmp'); michael@0: removeFile(minidumpDirectory, dumpID + '.extra'); michael@0: } michael@0: }, 'ipc:content-shutdown', false); michael@0: return { michael@0: get crashed() crashed michael@0: }; michael@0: } michael@0: michael@0: function bgInjectCrashContentScript() { michael@0: const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/toolkit/components/thumbnails/test/thumbnails_crash_content_helper.js"; michael@0: let thumbnailBrowser = BackgroundPageThumbs._thumbBrowser; michael@0: let mm = thumbnailBrowser.messageManager; michael@0: mm.loadFrameScript(TEST_CONTENT_HELPER, false); michael@0: return mm; michael@0: } michael@0: michael@0: function getMinidumpDirectory() { michael@0: var dir = Services.dirsvc.get('ProfD', Components.interfaces.nsIFile); michael@0: dir.append("minidumps"); michael@0: return dir; michael@0: } michael@0: michael@0: function removeFile(directory, filename) { michael@0: var file = directory.clone(); michael@0: file.append(filename); michael@0: if (file.exists()) { michael@0: file.remove(false); michael@0: } michael@0: }