|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 let tmp = {}; |
|
5 Cu.import("resource://gre/modules/PageThumbs.jsm", tmp); |
|
6 Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", tmp); |
|
7 Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp); |
|
8 Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp); |
|
9 Cu.import("resource://gre/modules/FileUtils.jsm", tmp); |
|
10 Cu.import("resource://gre/modules/osfile.jsm", tmp); |
|
11 let {PageThumbs, BackgroundPageThumbs, NewTabUtils, PageThumbsStorage, SessionStore, FileUtils, OS} = tmp; |
|
12 |
|
13 Cu.import("resource://gre/modules/PlacesUtils.jsm"); |
|
14 |
|
15 let oldEnabledPref = Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled"); |
|
16 Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", false); |
|
17 |
|
18 registerCleanupFunction(function () { |
|
19 while (gBrowser.tabs.length > 1) |
|
20 gBrowser.removeTab(gBrowser.tabs[1]); |
|
21 Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", oldEnabledPref) |
|
22 }); |
|
23 |
|
24 /** |
|
25 * Provide the default test function to start our test runner. |
|
26 */ |
|
27 function test() { |
|
28 TestRunner.run(); |
|
29 } |
|
30 |
|
31 /** |
|
32 * The test runner that controls the execution flow of our tests. |
|
33 */ |
|
34 let TestRunner = { |
|
35 /** |
|
36 * Starts the test runner. |
|
37 */ |
|
38 run: function () { |
|
39 waitForExplicitFinish(); |
|
40 |
|
41 SessionStore.promiseInitialized.then(function () { |
|
42 this._iter = runTests(); |
|
43 if (this._iter) { |
|
44 this.next(); |
|
45 } else { |
|
46 finish(); |
|
47 } |
|
48 }.bind(this)); |
|
49 }, |
|
50 |
|
51 /** |
|
52 * Runs the next available test or finishes if there's no test left. |
|
53 * @param aValue This value will be passed to the yielder via the runner's |
|
54 * iterator. |
|
55 */ |
|
56 next: function (aValue) { |
|
57 try { |
|
58 let value = TestRunner._iter.send(aValue); |
|
59 if (value && typeof value.then == "function") { |
|
60 value.then(result => { |
|
61 next(result); |
|
62 }, error => { |
|
63 ok(false, error + "\n" + error.stack); |
|
64 }); |
|
65 } |
|
66 } catch (e if e instanceof StopIteration) { |
|
67 finish(); |
|
68 } |
|
69 } |
|
70 }; |
|
71 |
|
72 /** |
|
73 * Continues the current test execution. |
|
74 * @param aValue This value will be passed to the yielder via the runner's |
|
75 * iterator. |
|
76 */ |
|
77 function next(aValue) { |
|
78 TestRunner.next(aValue); |
|
79 } |
|
80 |
|
81 /** |
|
82 * Creates a new tab with the given URI. |
|
83 * @param aURI The URI that's loaded in the tab. |
|
84 * @param aCallback The function to call when the tab has loaded. |
|
85 */ |
|
86 function addTab(aURI, aCallback) { |
|
87 let tab = gBrowser.selectedTab = gBrowser.addTab(aURI); |
|
88 whenLoaded(tab.linkedBrowser, aCallback); |
|
89 } |
|
90 |
|
91 /** |
|
92 * Loads a new URI into the currently selected tab. |
|
93 * @param aURI The URI to load. |
|
94 */ |
|
95 function navigateTo(aURI) { |
|
96 let browser = gBrowser.selectedTab.linkedBrowser; |
|
97 whenLoaded(browser); |
|
98 browser.loadURI(aURI); |
|
99 } |
|
100 |
|
101 /** |
|
102 * Continues the current test execution when a load event for the given element |
|
103 * has been received. |
|
104 * @param aElement The DOM element to listen on. |
|
105 * @param aCallback The function to call when the load event was dispatched. |
|
106 */ |
|
107 function whenLoaded(aElement, aCallback = next) { |
|
108 aElement.addEventListener("load", function onLoad() { |
|
109 aElement.removeEventListener("load", onLoad, true); |
|
110 executeSoon(aCallback); |
|
111 }, true); |
|
112 } |
|
113 |
|
114 /** |
|
115 * Captures a screenshot for the currently selected tab, stores it in the cache, |
|
116 * retrieves it from the cache and compares pixel color values. |
|
117 * @param aRed The red component's intensity. |
|
118 * @param aGreen The green component's intensity. |
|
119 * @param aBlue The blue component's intensity. |
|
120 * @param aMessage The info message to print when comparing the pixel color. |
|
121 */ |
|
122 function captureAndCheckColor(aRed, aGreen, aBlue, aMessage) { |
|
123 let browser = gBrowser.selectedBrowser; |
|
124 // We'll get oranges if the expiration filter removes the file during the |
|
125 // test. |
|
126 dontExpireThumbnailURLs([browser.currentURI.spec]); |
|
127 |
|
128 // Capture the screenshot. |
|
129 PageThumbs.captureAndStore(browser, function () { |
|
130 retrieveImageDataForURL(browser.currentURI.spec, function ([r, g, b]) { |
|
131 is("" + [r,g,b], "" + [aRed, aGreen, aBlue], aMessage); |
|
132 next(); |
|
133 }); |
|
134 }); |
|
135 } |
|
136 |
|
137 /** |
|
138 * For a given URL, loads the corresponding thumbnail |
|
139 * to a canvas and passes its image data to the callback. |
|
140 * @param aURL The url associated with the thumbnail. |
|
141 * @param aCallback The function to pass the image data to. |
|
142 */ |
|
143 function retrieveImageDataForURL(aURL, aCallback) { |
|
144 let width = 100, height = 100; |
|
145 let thumb = PageThumbs.getThumbnailURL(aURL, width, height); |
|
146 // create a tab with a chrome:// URL so it can host the thumbnail image. |
|
147 // Note that we tried creating the element directly in the top-level chrome |
|
148 // document, but this caused a strange problem: |
|
149 // * call this with the url of an image. |
|
150 // * immediately change the image content. |
|
151 // * call this again with the same url (now holding different content) |
|
152 // The original image data would be used. Maybe the img hadn't been |
|
153 // collected yet and the platform noticed the same URL, so reused the |
|
154 // content? Not sure - but this solves the problem. |
|
155 addTab("chrome://global/content/mozilla.xhtml", () => { |
|
156 let doc = gBrowser.selectedBrowser.contentDocument; |
|
157 let htmlns = "http://www.w3.org/1999/xhtml"; |
|
158 let img = doc.createElementNS(htmlns, "img"); |
|
159 img.setAttribute("src", thumb); |
|
160 |
|
161 whenLoaded(img, function () { |
|
162 let canvas = document.createElementNS(htmlns, "canvas"); |
|
163 canvas.setAttribute("width", width); |
|
164 canvas.setAttribute("height", height); |
|
165 |
|
166 // Draw the image to a canvas and compare the pixel color values. |
|
167 let ctx = canvas.getContext("2d"); |
|
168 ctx.drawImage(img, 0, 0, width, height); |
|
169 let result = ctx.getImageData(0, 0, 100, 100).data; |
|
170 gBrowser.removeTab(gBrowser.selectedTab); |
|
171 aCallback(result); |
|
172 }); |
|
173 }); |
|
174 } |
|
175 |
|
176 /** |
|
177 * Returns the file of the thumbnail with the given URL. |
|
178 * @param aURL The URL of the thumbnail. |
|
179 */ |
|
180 function thumbnailFile(aURL) { |
|
181 return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL)); |
|
182 } |
|
183 |
|
184 /** |
|
185 * Checks if a thumbnail for the given URL exists. |
|
186 * @param aURL The url associated to the thumbnail. |
|
187 */ |
|
188 function thumbnailExists(aURL) { |
|
189 let file = thumbnailFile(aURL); |
|
190 return file.exists() && file.fileSize; |
|
191 } |
|
192 |
|
193 /** |
|
194 * Removes the thumbnail for the given URL. |
|
195 * @param aURL The URL associated with the thumbnail. |
|
196 */ |
|
197 function removeThumbnail(aURL) { |
|
198 let file = thumbnailFile(aURL); |
|
199 file.remove(false); |
|
200 } |
|
201 |
|
202 /** |
|
203 * Asynchronously adds visits to a page, invoking a callback function when done. |
|
204 * |
|
205 * @param aPlaceInfo |
|
206 * One of the following: a string spec, an nsIURI, an object describing |
|
207 * the Place as described below, or an array of any such types. An |
|
208 * object describing a Place must look like this: |
|
209 * { uri: nsIURI of the page, |
|
210 * [optional] transition: one of the TRANSITION_* from |
|
211 * nsINavHistoryService, |
|
212 * [optional] title: title of the page, |
|
213 * [optional] visitDate: visit date in microseconds from the epoch |
|
214 * [optional] referrer: nsIURI of the referrer for this visit |
|
215 * } |
|
216 * @param [optional] aCallback |
|
217 * Function to be invoked on completion. |
|
218 */ |
|
219 function addVisits(aPlaceInfo, aCallback) { |
|
220 let places = []; |
|
221 if (aPlaceInfo instanceof Ci.nsIURI) { |
|
222 places.push({ uri: aPlaceInfo }); |
|
223 } |
|
224 else if (Array.isArray(aPlaceInfo)) { |
|
225 places = places.concat(aPlaceInfo); |
|
226 } else { |
|
227 places.push(aPlaceInfo) |
|
228 } |
|
229 |
|
230 // Create mozIVisitInfo for each entry. |
|
231 let now = Date.now(); |
|
232 for (let i = 0; i < places.length; i++) { |
|
233 if (typeof(places[i] == "string")) { |
|
234 places[i] = { uri: Services.io.newURI(places[i], "", null) }; |
|
235 } |
|
236 if (!places[i].title) { |
|
237 places[i].title = "test visit for " + places[i].uri.spec; |
|
238 } |
|
239 places[i].visits = [{ |
|
240 transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK |
|
241 : places[i].transition, |
|
242 visitDate: places[i].visitDate || (now++) * 1000, |
|
243 referrerURI: places[i].referrer |
|
244 }]; |
|
245 } |
|
246 |
|
247 PlacesUtils.asyncHistory.updatePlaces( |
|
248 places, |
|
249 { |
|
250 handleError: function AAV_handleError() { |
|
251 throw("Unexpected error in adding visit."); |
|
252 }, |
|
253 handleResult: function () {}, |
|
254 handleCompletion: function UP_handleCompletion() { |
|
255 if (aCallback) |
|
256 aCallback(); |
|
257 } |
|
258 } |
|
259 ); |
|
260 } |
|
261 |
|
262 /** |
|
263 * Calls addVisits, and then forces the newtab module to repopulate its links. |
|
264 * See addVisits for parameter descriptions. |
|
265 */ |
|
266 function addVisitsAndRepopulateNewTabLinks(aPlaceInfo, aCallback) { |
|
267 addVisits(aPlaceInfo, () => NewTabUtils.links.populateCache(aCallback, true)); |
|
268 } |
|
269 |
|
270 /** |
|
271 * Calls a given callback when the thumbnail for a given URL has been found |
|
272 * on disk. Keeps trying until the thumbnail has been created. |
|
273 * |
|
274 * @param aURL The URL of the thumbnail's page. |
|
275 * @param [optional] aCallback |
|
276 * Function to be invoked on completion. |
|
277 */ |
|
278 function whenFileExists(aURL, aCallback = next) { |
|
279 let callback = aCallback; |
|
280 if (!thumbnailExists(aURL)) { |
|
281 callback = function () whenFileExists(aURL, aCallback); |
|
282 } |
|
283 |
|
284 executeSoon(callback); |
|
285 } |
|
286 |
|
287 /** |
|
288 * Calls a given callback when the given file has been removed. |
|
289 * Keeps trying until the file is removed. |
|
290 * |
|
291 * @param aFile The file that is being removed |
|
292 * @param [optional] aCallback |
|
293 * Function to be invoked on completion. |
|
294 */ |
|
295 function whenFileRemoved(aFile, aCallback) { |
|
296 let callback = aCallback; |
|
297 if (aFile.exists()) { |
|
298 callback = function () whenFileRemoved(aFile, aCallback); |
|
299 } |
|
300 |
|
301 executeSoon(callback || next); |
|
302 } |
|
303 |
|
304 function wait(aMillis) { |
|
305 setTimeout(next, aMillis); |
|
306 } |
|
307 |
|
308 /** |
|
309 * Makes sure that a given list of URLs is not implicitly expired. |
|
310 * |
|
311 * @param aURLs The list of URLs that should not be expired. |
|
312 */ |
|
313 function dontExpireThumbnailURLs(aURLs) { |
|
314 let dontExpireURLs = (cb) => cb(aURLs); |
|
315 PageThumbs.addExpirationFilter(dontExpireURLs); |
|
316 |
|
317 registerCleanupFunction(function () { |
|
318 PageThumbs.removeExpirationFilter(dontExpireURLs); |
|
319 }); |
|
320 } |
|
321 |
|
322 function bgCapture(aURL, aOptions) { |
|
323 bgCaptureWithMethod("capture", aURL, aOptions); |
|
324 } |
|
325 |
|
326 function bgCaptureIfMissing(aURL, aOptions) { |
|
327 bgCaptureWithMethod("captureIfMissing", aURL, aOptions); |
|
328 } |
|
329 |
|
330 function bgCaptureWithMethod(aMethodName, aURL, aOptions = {}) { |
|
331 // We'll get oranges if the expiration filter removes the file during the |
|
332 // test. |
|
333 dontExpireThumbnailURLs([aURL]); |
|
334 if (!aOptions.onDone) |
|
335 aOptions.onDone = next; |
|
336 BackgroundPageThumbs[aMethodName](aURL, aOptions); |
|
337 } |
|
338 |
|
339 function bgTestPageURL(aOpts = {}) { |
|
340 let TEST_PAGE_URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_background.sjs"; |
|
341 return TEST_PAGE_URL + "?" + encodeURIComponent(JSON.stringify(aOpts)); |
|
342 } |
|
343 |
|
344 function bgAddCrashObserver() { |
|
345 let crashed = false; |
|
346 Services.obs.addObserver(function crashObserver(subject, topic, data) { |
|
347 is(topic, 'ipc:content-shutdown', 'Received correct observer topic.'); |
|
348 ok(subject instanceof Components.interfaces.nsIPropertyBag2, |
|
349 'Subject implements nsIPropertyBag2.'); |
|
350 // we might see this called as the process terminates due to previous tests. |
|
351 // We are only looking for "abnormal" exits... |
|
352 if (!subject.hasKey("abnormal")) { |
|
353 info("This is a normal termination and isn't the one we are looking for..."); |
|
354 return; |
|
355 } |
|
356 Services.obs.removeObserver(crashObserver, 'ipc:content-shutdown'); |
|
357 crashed = true; |
|
358 |
|
359 var dumpID; |
|
360 if ('nsICrashReporter' in Components.interfaces) { |
|
361 dumpID = subject.getPropertyAsAString('dumpID'); |
|
362 ok(dumpID, "dumpID is present and not an empty string"); |
|
363 } |
|
364 |
|
365 if (dumpID) { |
|
366 var minidumpDirectory = getMinidumpDirectory(); |
|
367 removeFile(minidumpDirectory, dumpID + '.dmp'); |
|
368 removeFile(minidumpDirectory, dumpID + '.extra'); |
|
369 } |
|
370 }, 'ipc:content-shutdown', false); |
|
371 return { |
|
372 get crashed() crashed |
|
373 }; |
|
374 } |
|
375 |
|
376 function bgInjectCrashContentScript() { |
|
377 const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/toolkit/components/thumbnails/test/thumbnails_crash_content_helper.js"; |
|
378 let thumbnailBrowser = BackgroundPageThumbs._thumbBrowser; |
|
379 let mm = thumbnailBrowser.messageManager; |
|
380 mm.loadFrameScript(TEST_CONTENT_HELPER, false); |
|
381 return mm; |
|
382 } |
|
383 |
|
384 function getMinidumpDirectory() { |
|
385 var dir = Services.dirsvc.get('ProfD', Components.interfaces.nsIFile); |
|
386 dir.append("minidumps"); |
|
387 return dir; |
|
388 } |
|
389 |
|
390 function removeFile(directory, filename) { |
|
391 var file = directory.clone(); |
|
392 file.append(filename); |
|
393 if (file.exists()) { |
|
394 file.remove(false); |
|
395 } |
|
396 } |