|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 var EXPORTED_SYMBOLS = ["applicationName", "assert", "Copy", "getBrowserObject", |
|
6 "getChromeWindow", "getWindows", "getWindowByTitle", |
|
7 "getWindowByType", "getWindowId", "getMethodInWindows", |
|
8 "getPreference", "saveDataURL", "setPreference", |
|
9 "sleep", "startTimer", "stopTimer", "takeScreenshot", |
|
10 "unwrapNode", "waitFor" |
|
11 ]; |
|
12 |
|
13 const Cc = Components.classes; |
|
14 const Ci = Components.interfaces; |
|
15 const Cu = Components.utils; |
|
16 |
|
17 |
|
18 Cu.import("resource://gre/modules/NetUtil.jsm"); |
|
19 Cu.import("resource://gre/modules/Services.jsm"); |
|
20 |
|
21 const applicationIdMap = { |
|
22 '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'Firefox', |
|
23 '{99bceaaa-e3c6-48c1-b981-ef9b46b67d60}': 'MetroFirefox' |
|
24 } |
|
25 const applicationName = applicationIdMap[Services.appinfo.ID] || Services.appinfo.name; |
|
26 |
|
27 var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions); |
|
28 var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker); |
|
29 var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors); |
|
30 |
|
31 var assert = new assertions.Assert(); |
|
32 |
|
33 var hwindow = Services.appShell.hiddenDOMWindow; |
|
34 |
|
35 var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); |
|
36 |
|
37 function Copy (obj) { |
|
38 for (var n in obj) { |
|
39 this[n] = obj[n]; |
|
40 } |
|
41 } |
|
42 |
|
43 /** |
|
44 * Returns the browser object of the specified window |
|
45 * |
|
46 * @param {Window} aWindow |
|
47 * Window to get the browser element from. |
|
48 * |
|
49 * @returns {Object} The browser element |
|
50 */ |
|
51 function getBrowserObject(aWindow) { |
|
52 switch(applicationName) { |
|
53 case "MetroFirefox": |
|
54 return aWindow.Browser; |
|
55 case "Firefox": |
|
56 default: |
|
57 return aWindow.gBrowser; |
|
58 } |
|
59 } |
|
60 |
|
61 function getChromeWindow(aWindow) { |
|
62 var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
63 .getInterface(Ci.nsIWebNavigation) |
|
64 .QueryInterface(Ci.nsIDocShellTreeItem) |
|
65 .rootTreeItem |
|
66 .QueryInterface(Ci.nsIInterfaceRequestor) |
|
67 .getInterface(Ci.nsIDOMWindow) |
|
68 .QueryInterface(Ci.nsIDOMChromeWindow); |
|
69 |
|
70 return chromeWin; |
|
71 } |
|
72 |
|
73 function getWindows(type) { |
|
74 if (type == undefined) { |
|
75 type = ""; |
|
76 } |
|
77 |
|
78 var windows = []; |
|
79 var enumerator = Services.wm.getEnumerator(type); |
|
80 |
|
81 while (enumerator.hasMoreElements()) { |
|
82 windows.push(enumerator.getNext()); |
|
83 } |
|
84 |
|
85 if (type == "") { |
|
86 windows.push(hwindow); |
|
87 } |
|
88 |
|
89 return windows; |
|
90 } |
|
91 |
|
92 function getMethodInWindows(methodName) { |
|
93 for each (var w in getWindows()) { |
|
94 if (w[methodName] != undefined) { |
|
95 return w[methodName]; |
|
96 } |
|
97 } |
|
98 |
|
99 throw new Error("Method with name: '" + methodName + "' is not in any open window."); |
|
100 } |
|
101 |
|
102 function getWindowByTitle(title) { |
|
103 for each (var w in getWindows()) { |
|
104 if (w.document.title && w.document.title == title) { |
|
105 return w; |
|
106 } |
|
107 } |
|
108 |
|
109 throw new Error("Window with title: '" + title + "' not found."); |
|
110 } |
|
111 |
|
112 function getWindowByType(type) { |
|
113 return Services.wm.getMostRecentWindow(type); |
|
114 } |
|
115 |
|
116 /** |
|
117 * Retrieve the outer window id for the given window. |
|
118 * |
|
119 * @param {Number} aWindow |
|
120 * Window to retrieve the id from. |
|
121 * @returns {Boolean} The outer window id |
|
122 **/ |
|
123 function getWindowId(aWindow) { |
|
124 try { |
|
125 // Normally we can retrieve the id via window utils |
|
126 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor). |
|
127 getInterface(Ci.nsIDOMWindowUtils). |
|
128 outerWindowID; |
|
129 } catch (e) { |
|
130 // ... but for observer notifications we need another interface |
|
131 return aWindow.QueryInterface(Ci.nsISupportsPRUint64).data; |
|
132 } |
|
133 } |
|
134 |
|
135 var checkChrome = function () { |
|
136 var loc = window.document.location.href; |
|
137 try { |
|
138 loc = window.top.document.location.href; |
|
139 } catch (e) { |
|
140 } |
|
141 |
|
142 return /^chrome:\/\//.test(loc); |
|
143 } |
|
144 |
|
145 /** |
|
146 * Called to get the state of an individual preference. |
|
147 * |
|
148 * @param aPrefName string The preference to get the state of. |
|
149 * @param aDefaultValue any The default value if preference was not found. |
|
150 * |
|
151 * @returns any The value of the requested preference |
|
152 * |
|
153 * @see setPref |
|
154 * Code by Henrik Skupin: <hskupin@gmail.com> |
|
155 */ |
|
156 function getPreference(aPrefName, aDefaultValue) { |
|
157 try { |
|
158 var branch = Services.prefs; |
|
159 |
|
160 switch (typeof aDefaultValue) { |
|
161 case ('boolean'): |
|
162 return branch.getBoolPref(aPrefName); |
|
163 case ('string'): |
|
164 return branch.getCharPref(aPrefName); |
|
165 case ('number'): |
|
166 return branch.getIntPref(aPrefName); |
|
167 default: |
|
168 return branch.getComplexValue(aPrefName); |
|
169 } |
|
170 } catch (e) { |
|
171 return aDefaultValue; |
|
172 } |
|
173 } |
|
174 |
|
175 /** |
|
176 * Called to set the state of an individual preference. |
|
177 * |
|
178 * @param aPrefName string The preference to set the state of. |
|
179 * @param aValue any The value to set the preference to. |
|
180 * |
|
181 * @returns boolean Returns true if value was successfully set. |
|
182 * |
|
183 * @see getPref |
|
184 * Code by Henrik Skupin: <hskupin@gmail.com> |
|
185 */ |
|
186 function setPreference(aName, aValue) { |
|
187 try { |
|
188 var branch = Services.prefs; |
|
189 |
|
190 switch (typeof aValue) { |
|
191 case ('boolean'): |
|
192 branch.setBoolPref(aName, aValue); |
|
193 break; |
|
194 case ('string'): |
|
195 branch.setCharPref(aName, aValue); |
|
196 break; |
|
197 case ('number'): |
|
198 branch.setIntPref(aName, aValue); |
|
199 break; |
|
200 default: |
|
201 branch.setComplexValue(aName, aValue); |
|
202 } |
|
203 } catch (e) { |
|
204 return false; |
|
205 } |
|
206 |
|
207 return true; |
|
208 } |
|
209 |
|
210 /** |
|
211 * Sleep for the given amount of milliseconds |
|
212 * |
|
213 * @param {number} milliseconds |
|
214 * Sleeps the given number of milliseconds |
|
215 */ |
|
216 function sleep(milliseconds) { |
|
217 var timeup = false; |
|
218 |
|
219 hwindow.setTimeout(function () { timeup = true; }, milliseconds); |
|
220 var thread = Services.tm.currentThread; |
|
221 |
|
222 while (!timeup) { |
|
223 thread.processNextEvent(true); |
|
224 } |
|
225 |
|
226 broker.pass({'function':'utils.sleep()'}); |
|
227 } |
|
228 |
|
229 /** |
|
230 * Check if the callback function evaluates to true |
|
231 */ |
|
232 function assert(callback, message, thisObject) { |
|
233 var result = callback.call(thisObject); |
|
234 |
|
235 if (!result) { |
|
236 throw new Error(message || arguments.callee.name + ": Failed for '" + callback + "'"); |
|
237 } |
|
238 |
|
239 return true; |
|
240 } |
|
241 |
|
242 /** |
|
243 * Unwraps a node which is wrapped into a XPCNativeWrapper or XrayWrapper |
|
244 * |
|
245 * @param {DOMnode} Wrapped DOM node |
|
246 * @returns {DOMNode} Unwrapped DOM node |
|
247 */ |
|
248 function unwrapNode(aNode) { |
|
249 var node = aNode; |
|
250 if (node) { |
|
251 // unwrap is not available on older branches (3.5 and 3.6) - Bug 533596 |
|
252 if ("unwrap" in XPCNativeWrapper) { |
|
253 node = XPCNativeWrapper.unwrap(node); |
|
254 } |
|
255 else if (node.wrappedJSObject != null) { |
|
256 node = node.wrappedJSObject; |
|
257 } |
|
258 } |
|
259 |
|
260 return node; |
|
261 } |
|
262 |
|
263 /** |
|
264 * Waits for the callback evaluates to true |
|
265 */ |
|
266 function waitFor(callback, message, timeout, interval, thisObject) { |
|
267 broker.log({'function': 'utils.waitFor() - DEPRECATED', |
|
268 'message': 'utils.waitFor() is deprecated. Use assert.waitFor() instead'}); |
|
269 assert.waitFor(callback, message, timeout, interval, thisObject); |
|
270 } |
|
271 |
|
272 /** |
|
273 * Calculates the x and y chrome offset for an element |
|
274 * See https://developer.mozilla.org/en/DOM/window.innerHeight |
|
275 * |
|
276 * Note this function will not work if the user has custom toolbars (via extension) at the bottom or left/right of the screen |
|
277 */ |
|
278 function getChromeOffset(elem) { |
|
279 var win = elem.ownerDocument.defaultView; |
|
280 // Calculate x offset |
|
281 var chromeWidth = 0; |
|
282 |
|
283 if (win["name"] != "sidebar") { |
|
284 chromeWidth = win.outerWidth - win.innerWidth; |
|
285 } |
|
286 |
|
287 // Calculate y offset |
|
288 var chromeHeight = win.outerHeight - win.innerHeight; |
|
289 // chromeHeight == 0 means elem is already in the chrome and doesn't need the addonbar offset |
|
290 if (chromeHeight > 0) { |
|
291 // window.innerHeight doesn't include the addon or find bar, so account for these if present |
|
292 var addonbar = win.document.getElementById("addon-bar"); |
|
293 if (addonbar) { |
|
294 chromeHeight -= addonbar.scrollHeight; |
|
295 } |
|
296 |
|
297 var findbar = win.document.getElementById("FindToolbar"); |
|
298 if (findbar) { |
|
299 chromeHeight -= findbar.scrollHeight; |
|
300 } |
|
301 } |
|
302 |
|
303 return {'x':chromeWidth, 'y':chromeHeight}; |
|
304 } |
|
305 |
|
306 /** |
|
307 * Takes a screenshot of the specified DOM node |
|
308 */ |
|
309 function takeScreenshot(node, highlights) { |
|
310 var rect, win, width, height, left, top, needsOffset; |
|
311 // node can be either a window or an arbitrary DOM node |
|
312 try { |
|
313 // node is an arbitrary DOM node |
|
314 win = node.ownerDocument.defaultView; |
|
315 rect = node.getBoundingClientRect(); |
|
316 width = rect.width; |
|
317 height = rect.height; |
|
318 top = rect.top; |
|
319 left = rect.left; |
|
320 // offset for highlights not needed as they will be relative to this node |
|
321 needsOffset = false; |
|
322 } catch (e) { |
|
323 // node is a window |
|
324 win = node; |
|
325 width = win.innerWidth; |
|
326 height = win.innerHeight; |
|
327 top = 0; |
|
328 left = 0; |
|
329 // offset needed for highlights to take 'outerHeight' of window into account |
|
330 needsOffset = true; |
|
331 } |
|
332 |
|
333 var canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); |
|
334 canvas.width = width; |
|
335 canvas.height = height; |
|
336 |
|
337 var ctx = canvas.getContext("2d"); |
|
338 // Draws the DOM contents of the window to the canvas |
|
339 ctx.drawWindow(win, left, top, width, height, "rgb(255,255,255)"); |
|
340 |
|
341 // This section is for drawing a red rectangle around each element passed in via the highlights array |
|
342 if (highlights) { |
|
343 ctx.lineWidth = "2"; |
|
344 ctx.strokeStyle = "red"; |
|
345 ctx.save(); |
|
346 |
|
347 for (var i = 0; i < highlights.length; ++i) { |
|
348 var elem = highlights[i]; |
|
349 rect = elem.getBoundingClientRect(); |
|
350 |
|
351 var offsetY = 0, offsetX = 0; |
|
352 if (needsOffset) { |
|
353 var offset = getChromeOffset(elem); |
|
354 offsetX = offset.x; |
|
355 offsetY = offset.y; |
|
356 } else { |
|
357 // Don't need to offset the window chrome, just make relative to containing node |
|
358 offsetY = -top; |
|
359 offsetX = -left; |
|
360 } |
|
361 |
|
362 // Draw the rectangle |
|
363 ctx.strokeRect(rect.left + offsetX, rect.top + offsetY, rect.width, rect.height); |
|
364 } |
|
365 } |
|
366 |
|
367 return canvas.toDataURL("image/jpeg", 0.5); |
|
368 } |
|
369 |
|
370 /** |
|
371 * Save the dataURL content to the specified file. It will be stored in either the persisted screenshot or temporary folder. |
|
372 * |
|
373 * @param {String} aDataURL |
|
374 * The dataURL to save |
|
375 * @param {String} aFilename |
|
376 * Target file name without extension |
|
377 * |
|
378 * @returns {Object} The hash containing the path of saved file, and the failure bit |
|
379 */ |
|
380 function saveDataURL(aDataURL, aFilename) { |
|
381 var frame = {}; Cu.import('resource://mozmill/modules/frame.js', frame); |
|
382 const FILE_PERMISSIONS = parseInt("0644", 8); |
|
383 |
|
384 var file; |
|
385 file = Cc['@mozilla.org/file/local;1'] |
|
386 .createInstance(Ci.nsILocalFile); |
|
387 file.initWithPath(frame.persisted['screenshots']['path']); |
|
388 file.append(aFilename + ".jpg"); |
|
389 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FILE_PERMISSIONS); |
|
390 |
|
391 // Create an output stream to write to file |
|
392 let foStream = Cc["@mozilla.org/network/file-output-stream;1"] |
|
393 .createInstance(Ci.nsIFileOutputStream); |
|
394 foStream.init(file, 0x02 | 0x08 | 0x10, FILE_PERMISSIONS, foStream.DEFER_OPEN); |
|
395 |
|
396 let dataURI = NetUtil.newURI(aDataURL, "UTF8", null); |
|
397 if (!dataURI.schemeIs("data")) { |
|
398 throw TypeError("aDataURL parameter has to have 'data'" + |
|
399 " scheme instead of '" + dataURI.scheme + "'"); |
|
400 } |
|
401 |
|
402 // Write asynchronously to buffer; |
|
403 // Input and output streams are closed after write |
|
404 |
|
405 let ready = false; |
|
406 let failure = false; |
|
407 |
|
408 function sync(aStatus) { |
|
409 if (!Components.isSuccessCode(aStatus)) { |
|
410 failure = true; |
|
411 } |
|
412 ready = true; |
|
413 } |
|
414 |
|
415 NetUtil.asyncFetch(dataURI, function (aInputStream, aAsyncFetchResult) { |
|
416 if (!Components.isSuccessCode(aAsyncFetchResult)) { |
|
417 // An error occurred! |
|
418 sync(aAsyncFetchResult); |
|
419 } else { |
|
420 // Consume the input stream. |
|
421 NetUtil.asyncCopy(aInputStream, foStream, function (aAsyncCopyResult) { |
|
422 sync(aAsyncCopyResult); |
|
423 }); |
|
424 } |
|
425 }); |
|
426 |
|
427 assert.waitFor(function () { |
|
428 return ready; |
|
429 }, "DataURL has been saved to '" + file.path + "'"); |
|
430 |
|
431 return {filename: file.path, failure: failure}; |
|
432 } |
|
433 |
|
434 /** |
|
435 * Some very brain-dead timer functions useful for performance optimizations |
|
436 * This is only enabled in debug mode |
|
437 * |
|
438 **/ |
|
439 var gutility_mzmltimer = 0; |
|
440 /** |
|
441 * Starts timer initializing with current EPOC time in milliseconds |
|
442 * |
|
443 * @returns none |
|
444 **/ |
|
445 function startTimer(){ |
|
446 dump("TIMERCHECK:: starting now: " + Date.now() + "\n"); |
|
447 gutility_mzmltimer = Date.now(); |
|
448 } |
|
449 |
|
450 /** |
|
451 * Checks the timer and outputs current elapsed time since start of timer. It |
|
452 * will print out a message you provide with its "time check" so you can |
|
453 * correlate in the log file and figure out elapsed time of specific functions. |
|
454 * |
|
455 * @param aMsg string The debug message to print with the timer check |
|
456 * |
|
457 * @returns none |
|
458 **/ |
|
459 function checkTimer(aMsg){ |
|
460 var end = Date.now(); |
|
461 dump("TIMERCHECK:: at " + aMsg + " is: " + (end - gutility_mzmltimer) + "\n"); |
|
462 } |