Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const FRAME_SCRIPT = "chrome://marionette/content/marionette-listener.js";
9 // import logger
10 Cu.import("resource://gre/modules/Log.jsm");
11 let logger = Log.repository.getLogger("Marionette");
12 logger.info('marionette-server.js loaded');
14 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
15 .getService(Ci.mozIJSSubScriptLoader);
16 loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js");
17 loader.loadSubScript("chrome://marionette/content/marionette-common.js");
18 Cu.import("resource://gre/modules/Services.jsm");
19 loader.loadSubScript("chrome://marionette/content/marionette-frame-manager.js");
20 Cu.import("chrome://marionette/content/marionette-elements.js");
21 let utils = {};
22 loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils);
23 loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", utils);
24 loader.loadSubScript("chrome://marionette/content/atoms.js", utils);
26 let specialpowers = {};
27 loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js",
28 specialpowers);
29 specialpowers.specialPowersObserver = new specialpowers.SpecialPowersObserver();
30 specialpowers.specialPowersObserver.init();
32 Cu.import("resource://gre/modules/FileUtils.jsm");
33 Cu.import("resource://gre/modules/NetUtil.jsm");
35 Services.prefs.setBoolPref("marionette.contentListener", false);
36 let appName = Services.appinfo.name;
38 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
39 let DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
40 this.DevToolsUtils = DevToolsUtils;
41 loader.loadSubScript("resource://gre/modules/devtools/server/transport.js");
43 let bypassOffline = false;
44 let qemu = "0";
45 let device = null;
47 try {
48 XPCOMUtils.defineLazyGetter(this, "libcutils", function () {
49 Cu.import("resource://gre/modules/systemlibs.js");
50 return libcutils;
51 });
52 if (libcutils) {
53 qemu = libcutils.property_get("ro.kernel.qemu");
54 logger.info("B2G emulator: " + (qemu == "1" ? "yes" : "no"));
55 device = libcutils.property_get("ro.product.device");
56 logger.info("Device detected is " + device);
57 bypassOffline = (qemu == "1" || device == "panda");
58 }
59 }
60 catch(e) {}
62 if (bypassOffline) {
63 logger.info("Bypassing offline status.");
64 Services.prefs.setBoolPref("network.gonk.manage-offline-status", false);
65 Services.io.manageOfflineStatus = false;
66 Services.io.offline = false;
67 }
69 // This is used to prevent newSession from returning before the telephony
70 // API's are ready; see bug 792647. This assumes that marionette-server.js
71 // will be loaded before the 'system-message-listener-ready' message
72 // is fired. If this stops being true, this approach will have to change.
73 let systemMessageListenerReady = false;
74 Services.obs.addObserver(function() {
75 systemMessageListenerReady = true;
76 }, "system-message-listener-ready", false);
78 /*
79 * Custom exceptions
80 */
81 function FrameSendNotInitializedError(frame) {
82 this.code = 54;
83 this.frame = frame;
84 this.message = "Error sending message to frame (NS_ERROR_NOT_INITIALIZED)";
85 this.toString = function() {
86 return this.message + " " + this.frame + "; frame has closed.";
87 }
88 }
90 function FrameSendFailureError(frame) {
91 this.code = 55;
92 this.frame = frame;
93 this.message = "Error sending message to frame (NS_ERROR_FAILURE)";
94 this.toString = function() {
95 return this.message + " " + this.frame + "; frame not responding.";
96 }
97 }
99 /**
100 * The server connection is responsible for all marionette API calls. It gets created
101 * for each connection and manages all chrome and browser based calls. It
102 * mediates content calls by issuing appropriate messages to the content process.
103 */
104 function MarionetteServerConnection(aPrefix, aTransport, aServer)
105 {
106 this.uuidGen = Cc["@mozilla.org/uuid-generator;1"]
107 .getService(Ci.nsIUUIDGenerator);
109 this.prefix = aPrefix;
110 this.server = aServer;
111 this.conn = aTransport;
112 this.conn.hooks = this;
114 // marionette uses a protocol based on the debugger server, which requires
115 // passing back "actor ids" with responses. unlike the debugger server,
116 // we don't have multiple actors, so just use a dummy value of "0" here
117 this.actorID = "0";
119 this.globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
120 .getService(Ci.nsIMessageBroadcaster);
121 this.messageManager = this.globalMessageManager;
122 this.browsers = {}; //holds list of BrowserObjs
123 this.curBrowser = null; // points to current browser
124 this.context = "content";
125 this.scriptTimeout = null;
126 this.searchTimeout = null;
127 this.pageTimeout = null;
128 this.timer = null;
129 this.inactivityTimer = null;
130 this.heartbeatCallback = function () {}; // called by simpletest methods
131 this.marionetteLog = new MarionetteLogObj();
132 this.command_id = null;
133 this.mainFrame = null; //topmost chrome frame
134 this.curFrame = null; // chrome iframe that currently has focus
135 this.mainContentFrameId = null;
136 this.importedScripts = FileUtils.getFile('TmpD', ['marionetteChromeScripts']);
137 this.importedScriptHashes = {"chrome" : [], "content": []};
138 this.currentFrameElement = null;
139 this.testName = null;
140 this.mozBrowserClose = null;
141 }
143 MarionetteServerConnection.prototype = {
145 QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener,
146 Ci.nsIObserver,
147 Ci.nsISupportsWeakReference]),
149 /**
150 * Debugger transport callbacks:
151 */
152 onPacket: function MSC_onPacket(aPacket) {
153 // Dispatch the request
154 if (this.requestTypes && this.requestTypes[aPacket.name]) {
155 try {
156 this.requestTypes[aPacket.name].bind(this)(aPacket);
157 } catch(e) {
158 this.conn.send({ error: ("error occurred while processing '" +
159 aPacket.name),
160 message: e.message });
161 }
162 } else {
163 this.conn.send({ error: "unrecognizedPacketType",
164 message: ('Marionette does not ' +
165 'recognize the packet type "' +
166 aPacket.name + '"') });
167 }
168 },
170 onClosed: function MSC_onClosed(aStatus) {
171 this.server._connectionClosed(this);
172 this.sessionTearDown();
173 },
175 /**
176 * Helper methods:
177 */
179 /**
180 * Switches to the global ChromeMessageBroadcaster, potentially replacing a frame-specific
181 * ChromeMessageSender. Has no effect if the global ChromeMessageBroadcaster is already
182 * in use. If this replaces a frame-specific ChromeMessageSender, it removes the message
183 * listeners from that sender, and then puts the corresponding frame script "to sleep",
184 * which removes most of the message listeners from it as well.
185 */
186 switchToGlobalMessageManager: function MDA_switchToGlobalMM() {
187 if (this.curBrowser && this.curBrowser.frameManager.currentRemoteFrame !== null) {
188 this.curBrowser.frameManager.removeMessageManagerListeners(this.messageManager);
189 this.sendAsync("sleepSession", null, null, true);
190 this.curBrowser.frameManager.currentRemoteFrame = null;
191 }
192 this.messageManager = this.globalMessageManager;
193 },
195 /**
196 * Helper method to send async messages to the content listener
197 *
198 * @param string name
199 * Suffix of the targetted message listener (Marionette:<suffix>)
200 * @param object values
201 * Object to send to the listener
202 */
203 sendAsync: function MDA_sendAsync(name, values, commandId, ignoreFailure) {
204 let success = true;
205 if (values instanceof Object && commandId) {
206 values.command_id = commandId;
207 }
208 if (this.curBrowser.frameManager.currentRemoteFrame !== null) {
209 try {
210 this.messageManager.sendAsyncMessage(
211 "Marionette:" + name + this.curBrowser.frameManager.currentRemoteFrame.targetFrameId, values);
212 }
213 catch(e) {
214 if (!ignoreFailure) {
215 success = false;
216 let error = e;
217 switch(e.result) {
218 case Components.results.NS_ERROR_FAILURE:
219 error = new FrameSendFailureError(this.curBrowser.frameManager.currentRemoteFrame);
220 break;
221 case Components.results.NS_ERROR_NOT_INITIALIZED:
222 error = new FrameSendNotInitializedError(this.curBrowser.frameManager.currentRemoteFrame);
223 break;
224 default:
225 break;
226 }
227 let code = error.hasOwnProperty('code') ? e.code : 500;
228 this.sendError(error.toString(), code, error.stack, commandId);
229 }
230 }
231 }
232 else {
233 this.messageManager.broadcastAsyncMessage(
234 "Marionette:" + name + this.curBrowser.curFrameId, values);
235 }
236 return success;
237 },
239 logRequest: function MDA_logRequest(type, data) {
240 logger.debug("Got request: " + type + ", data: " + JSON.stringify(data) + ", id: " + this.command_id);
241 },
243 /**
244 * Generic method to pass a response to the client
245 *
246 * @param object msg
247 * Response to send back to client
248 * @param string command_id
249 * Unique identifier assigned to the client's request.
250 * Used to distinguish the asynchronous responses.
251 */
252 sendToClient: function MDA_sendToClient(msg, command_id) {
253 logger.info("sendToClient: " + JSON.stringify(msg) + ", " + command_id +
254 ", " + this.command_id);
255 if (!command_id) {
256 logger.warn("got a response with no command_id");
257 return;
258 }
259 else if (command_id != -1) {
260 // A command_id of -1 is used for emulator callbacks, and those
261 // don't use this.command_id.
262 if (!this.command_id) {
263 // A null value for this.command_id means we've already processed
264 // a message for the previous value, and so the current message is a
265 // duplicate.
266 logger.warn("ignoring duplicate response for command_id " + command_id);
267 return;
268 }
269 else if (this.command_id != command_id) {
270 logger.warn("ignoring out-of-sync response");
271 return;
272 }
273 }
274 this.conn.send(msg);
275 if (command_id != -1) {
276 // Don't unset this.command_id if this message is to process an
277 // emulator callback, since another response for this command_id is
278 // expected, after the containing call to execute_async_script finishes.
279 this.command_id = null;
280 }
281 },
283 /**
284 * Send a value to client
285 *
286 * @param object value
287 * Value to send back to client
288 * @param string command_id
289 * Unique identifier assigned to the client's request.
290 * Used to distinguish the asynchronous responses.
291 */
292 sendResponse: function MDA_sendResponse(value, command_id) {
293 if (typeof(value) == 'undefined')
294 value = null;
295 this.sendToClient({from:this.actorID, value: value}, command_id);
296 },
298 sayHello: function MDA_sayHello() {
299 this.conn.send({ from: "root",
300 applicationType: "gecko",
301 traits: [] });
302 },
304 getMarionetteID: function MDA_getMarionette() {
305 this.conn.send({ "from": "root", "id": this.actorID });
306 },
308 /**
309 * Send ack to client
310 *
311 * @param string command_id
312 * Unique identifier assigned to the client's request.
313 * Used to distinguish the asynchronous responses.
314 */
315 sendOk: function MDA_sendOk(command_id) {
316 this.sendToClient({from:this.actorID, ok: true}, command_id);
317 },
319 /**
320 * Send error message to client
321 *
322 * @param string message
323 * Error message
324 * @param number status
325 * Status number
326 * @param string trace
327 * Stack trace
328 * @param string command_id
329 * Unique identifier assigned to the client's request.
330 * Used to distinguish the asynchronous responses.
331 */
332 sendError: function MDA_sendError(message, status, trace, command_id) {
333 let error_msg = {message: message, status: status, stacktrace: trace};
334 this.sendToClient({from:this.actorID, error: error_msg}, command_id);
335 },
337 /**
338 * Gets the current active window
339 *
340 * @return nsIDOMWindow
341 */
342 getCurrentWindow: function MDA_getCurrentWindow() {
343 let type = null;
344 if (this.curFrame == null) {
345 if (this.curBrowser == null) {
346 if (this.context == "content") {
347 type = 'navigator:browser';
348 }
349 return Services.wm.getMostRecentWindow(type);
350 }
351 else {
352 return this.curBrowser.window;
353 }
354 }
355 else {
356 return this.curFrame;
357 }
358 },
360 /**
361 * Gets the the window enumerator
362 *
363 * @return nsISimpleEnumerator
364 */
365 getWinEnumerator: function MDA_getWinEnumerator() {
366 let type = null;
367 if (appName != "B2G" && this.context == "content") {
368 type = 'navigator:browser';
369 }
370 return Services.wm.getEnumerator(type);
371 },
373 /**
374 * Create a new BrowserObj for window and add to known browsers
375 *
376 * @param nsIDOMWindow win
377 * Window for which we will create a BrowserObj
378 *
379 * @return string
380 * Returns the unique server-assigned ID of the window
381 */
382 addBrowser: function MDA_addBrowser(win) {
383 let browser = new BrowserObj(win, this);
384 let winId = win.QueryInterface(Ci.nsIInterfaceRequestor).
385 getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
386 winId = winId + ((appName == "B2G") ? '-b2g' : '');
387 this.browsers[winId] = browser;
388 this.curBrowser = this.browsers[winId];
389 if (this.curBrowser.elementManager.seenItems[winId] == undefined) {
390 //add this to seenItems so we can guarantee the user will get winId as this window's id
391 this.curBrowser.elementManager.seenItems[winId] = Cu.getWeakReference(win);
392 }
393 },
395 /**
396 * Start a new session in a new browser.
397 *
398 * If newSession is true, we will switch focus to the start frame
399 * when it registers. Also, if it is in desktop, then a new tab
400 * with the start page uri (about:blank) will be opened.
401 *
402 * @param nsIDOMWindow win
403 * Window whose browser we need to access
404 * @param boolean newSession
405 * True if this is the first time we're talking to this browser
406 */
407 startBrowser: function MDA_startBrowser(win, newSession) {
408 this.mainFrame = win;
409 this.curFrame = null;
410 this.addBrowser(win);
411 this.curBrowser.newSession = newSession;
412 this.curBrowser.startSession(newSession, win, this.whenBrowserStarted.bind(this));
413 },
415 /**
416 * Callback invoked after a new session has been started in a browser.
417 * Loads the Marionette frame script into the browser if needed.
418 *
419 * @param nsIDOMWindow win
420 * Window whose browser we need to access
421 * @param boolean newSession
422 * True if this is the first time we're talking to this browser
423 */
424 whenBrowserStarted: function MDA_whenBrowserStarted(win, newSession) {
425 try {
426 if (!Services.prefs.getBoolPref("marionette.contentListener") || !newSession) {
427 this.curBrowser.loadFrameScript(FRAME_SCRIPT, win);
428 }
429 }
430 catch (e) {
431 //there may not always be a content process
432 logger.info("could not load listener into content for page: " + win.location.href);
433 }
434 utils.window = win;
435 },
437 /**
438 * Recursively get all labeled text
439 *
440 * @param nsIDOMElement el
441 * The parent element
442 * @param array lines
443 * Array that holds the text lines
444 */
445 getVisibleText: function MDA_getVisibleText(el, lines) {
446 let nodeName = el.nodeName;
447 try {
448 if (utils.isElementDisplayed(el)) {
449 if (el.value) {
450 lines.push(el.value);
451 }
452 for (var child in el.childNodes) {
453 this.getVisibleText(el.childNodes[child], lines);
454 };
455 }
456 }
457 catch (e) {
458 if (nodeName == "#text") {
459 lines.push(el.textContent);
460 }
461 }
462 },
464 getCommandId: function MDA_getCommandId() {
465 return this.uuidGen.generateUUID().toString();
466 },
468 /**
469 * Given a file name, this will delete the file from the temp directory if it exists
470 */
471 deleteFile: function(filename) {
472 let file = FileUtils.getFile('TmpD', [filename.toString()]);
473 if (file.exists()) {
474 file.remove(true);
475 }
476 },
478 /**
479 * Marionette API:
480 *
481 * All methods implementing a command from the client should create a
482 * command_id, and then use this command_id in all messages exchanged with
483 * the frame scripts and with responses sent to the client. This prevents
484 * commands and responses from getting out-of-sync, which can happen in
485 * the case of execute_async calls that timeout and then later send a
486 * response, and other situations. See bug 779011. See setScriptTimeout()
487 * for a basic example.
488 */
490 /**
491 * Create a new session. This creates a new BrowserObj.
492 *
493 * In a desktop environment, this opens a new browser with
494 * "about:blank" which subsequent commands will be sent to.
495 *
496 * This will send a hash map of supported capabilities to the client
497 * as part of the Marionette:register IPC command in the
498 * receiveMessage callback when a new browser is created.
499 */
500 newSession: function MDA_newSession() {
501 this.command_id = this.getCommandId();
502 this.newSessionCommandId = this.command_id;
504 this.scriptTimeout = 10000;
506 function waitForWindow() {
507 let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
508 let win = this.getCurrentWindow();
509 if (!win ||
510 (appName == "Firefox" && !win.gBrowser) ||
511 (appName == "Fennec" && !win.BrowserApp)) {
512 checkTimer.initWithCallback(waitForWindow.bind(this), 100,
513 Ci.nsITimer.TYPE_ONE_SHOT);
514 }
515 else {
516 this.startBrowser(win, true);
517 }
518 }
520 if (!Services.prefs.getBoolPref("marionette.contentListener")) {
521 waitForWindow.call(this);
522 }
523 else if ((appName != "Firefox") && (this.curBrowser == null)) {
524 // If there is a content listener, then we just wake it up
525 this.addBrowser(this.getCurrentWindow());
526 this.curBrowser.startSession(false, this.getCurrentWindow(),
527 this.whenBrowserStarted);
528 this.messageManager.broadcastAsyncMessage("Marionette:restart", {});
529 }
530 else {
531 this.sendError("Session already running", 500, null,
532 this.command_id);
533 }
534 this.switchToGlobalMessageManager();
535 },
537 /**
538 * Send the current session's capabilities to the client.
539 *
540 * Capabilities informs the client of which WebDriver features are
541 * supported by Firefox and Marionette. They are immutable for the
542 * length of the session.
543 *
544 * The return value is an immutable map of string keys
545 * ("capabilities") to values, which may be of types boolean,
546 * numerical or string.
547 */
548 getSessionCapabilities: function MDA_getSessionCapabilities() {
549 this.command_id = this.getCommandId();
551 let isB2G = appName == "B2G";
552 let platformName = Services.appinfo.OS.toUpperCase();
554 let caps = {
555 // Mandated capabilities
556 "browserName": appName,
557 "browserVersion": Services.appinfo.version,
558 "platformName": platformName,
559 "platformVersion": Services.appinfo.platformVersion,
561 // Supported features
562 "cssSelectorsEnabled": true,
563 "handlesAlerts": false,
564 "javascriptEnabled": true,
565 "nativeEvents": false,
566 "rotatable": isB2G,
567 "secureSsl": false,
568 "takesElementScreenshot": true,
569 "takesScreenshot": true,
571 // Selenium 2 compat
572 "platform": platformName,
574 // Proprietary extensions
575 "XULappId" : Services.appinfo.ID,
576 "appBuildId" : Services.appinfo.appBuildID,
577 "device": qemu == "1" ? "qemu" : (!device ? "desktop" : device),
578 "version": Services.appinfo.version
579 };
581 // eideticker (bug 965297) and mochitest (bug 965304)
582 // compatibility. They only check for the presence of this
583 // property and should so not be in caps if not on a B2G device.
584 if (isB2G)
585 caps.b2g = true;
587 this.sendResponse(caps, this.command_id);
588 },
590 /**
591 * Log message. Accepts user defined log-level.
592 *
593 * @param object aRequest
594 * 'value' member holds log message
595 * 'level' member hold log level
596 */
597 log: function MDA_log(aRequest) {
598 this.command_id = this.getCommandId();
599 this.marionetteLog.log(aRequest.parameters.value, aRequest.parameters.level);
600 this.sendOk(this.command_id);
601 },
603 /**
604 * Return all logged messages.
605 */
606 getLogs: function MDA_getLogs() {
607 this.command_id = this.getCommandId();
608 this.sendResponse(this.marionetteLog.getLogs(), this.command_id);
609 },
611 /**
612 * Sets the context of the subsequent commands to be either 'chrome' or 'content'
613 *
614 * @param object aRequest
615 * 'value' member holds the name of the context to be switched to
616 */
617 setContext: function MDA_setContext(aRequest) {
618 this.command_id = this.getCommandId();
619 this.logRequest("setContext", aRequest);
620 let context = aRequest.parameters.value;
621 if (context != "content" && context != "chrome") {
622 this.sendError("invalid context", 500, null, this.command_id);
623 }
624 else {
625 this.context = context;
626 this.sendOk(this.command_id);
627 }
628 },
630 /**
631 * Returns a chrome sandbox that can be used by the execute_foo functions.
632 *
633 * @param nsIDOMWindow aWindow
634 * Window in which we will execute code
635 * @param Marionette marionette
636 * Marionette test instance
637 * @param object args
638 * Client given args
639 * @return Sandbox
640 * Returns the sandbox
641 */
642 createExecuteSandbox: function MDA_createExecuteSandbox(aWindow, marionette, args, specialPowers, command_id) {
643 try {
644 args = this.curBrowser.elementManager.convertWrappedArguments(args, aWindow);
645 }
646 catch(e) {
647 this.sendError(e.message, e.code, e.stack, command_id);
648 return;
649 }
651 let _chromeSandbox = new Cu.Sandbox(aWindow,
652 { sandboxPrototype: aWindow, wantXrays: false, sandboxName: ''});
653 _chromeSandbox.__namedArgs = this.curBrowser.elementManager.applyNamedArgs(args);
654 _chromeSandbox.__marionetteParams = args;
655 _chromeSandbox.testUtils = utils;
657 marionette.exports.forEach(function(fn) {
658 try {
659 _chromeSandbox[fn] = marionette[fn].bind(marionette);
660 }
661 catch(e) {
662 _chromeSandbox[fn] = marionette[fn];
663 }
664 });
666 _chromeSandbox.isSystemMessageListenerReady =
667 function() { return systemMessageListenerReady; }
669 if (specialPowers == true) {
670 loader.loadSubScript("chrome://specialpowers/content/specialpowersAPI.js",
671 _chromeSandbox);
672 loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserverAPI.js",
673 _chromeSandbox);
674 loader.loadSubScript("chrome://specialpowers/content/ChromePowers.js",
675 _chromeSandbox);
676 }
678 return _chromeSandbox;
679 },
681 /**
682 * Executes a script in the given sandbox.
683 *
684 * @param Sandbox sandbox
685 * Sandbox in which the script will run
686 * @param string script
687 * The script to run
688 * @param boolean directInject
689 * If true, then the script will be run as is,
690 * and not as a function body (as you would
691 * do using the WebDriver spec)
692 * @param boolean async
693 * True if the script is asynchronous
694 */
695 executeScriptInSandbox: function MDA_executeScriptInSandbox(sandbox, script,
696 directInject, async, command_id, timeout) {
698 if (directInject && async &&
699 (timeout == null || timeout == 0)) {
700 this.sendError("Please set a timeout", 21, null, command_id);
701 return;
702 }
704 if (this.importedScripts.exists()) {
705 let stream = Cc["@mozilla.org/network/file-input-stream;1"].
706 createInstance(Ci.nsIFileInputStream);
707 stream.init(this.importedScripts, -1, 0, 0);
708 let data = NetUtil.readInputStreamToString(stream, stream.available());
709 script = data + script;
710 }
712 let res = Cu.evalInSandbox(script, sandbox, "1.8", "dummy file", 0);
714 if (directInject && !async &&
715 (res == undefined || res.passed == undefined)) {
716 this.sendError("finish() not called", 500, null, command_id);
717 return;
718 }
720 if (!async) {
721 this.sendResponse(this.curBrowser.elementManager.wrapValue(res),
722 command_id);
723 }
724 },
726 /**
727 * Execute the given script either as a function body (executeScript)
728 * or directly (for 'mochitest' like JS Marionette tests)
729 *
730 * @param object aRequest
731 * 'script' member is the script to run
732 * 'args' member holds the arguments to the script
733 * @param boolean directInject
734 * if true, it will be run directly and not as a
735 * function body
736 */
737 execute: function MDA_execute(aRequest, directInject) {
738 let inactivityTimeout = aRequest.parameters.inactivityTimeout;
739 let timeout = aRequest.parameters.scriptTimeout ? aRequest.parameters.scriptTimeout : this.scriptTimeout;
740 let command_id = this.command_id = this.getCommandId();
741 let script;
742 this.logRequest("execute", aRequest);
743 if (aRequest.parameters.newSandbox == undefined) {
744 //if client does not send a value in newSandbox,
745 //then they expect the same behaviour as webdriver
746 aRequest.parameters.newSandbox = true;
747 }
748 if (this.context == "content") {
749 this.sendAsync("executeScript",
750 {
751 script: aRequest.parameters.script,
752 args: aRequest.parameters.args,
753 newSandbox: aRequest.parameters.newSandbox,
754 timeout: timeout,
755 specialPowers: aRequest.parameters.specialPowers,
756 filename: aRequest.parameters.filename,
757 line: aRequest.parameters.line
758 },
759 command_id);
760 return;
761 }
763 // handle the inactivity timeout
764 let that = this;
765 if (inactivityTimeout) {
766 let inactivityTimeoutHandler = function(message, status) {
767 let error_msg = {message: value, status: status};
768 that.sendToClient({from: that.actorID, error: error_msg},
769 marionette.command_id);
770 };
771 let setTimer = function() {
772 that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
773 if (that.inactivityTimer != null) {
774 that.inactivityTimer.initWithCallback(function() {
775 inactivityTimeoutHandler("timed out due to inactivity", 28);
776 }, inactivityTimeout, Ci.nsITimer.TYPE_ONESHOT);
777 }
778 }
779 setTimer();
780 this.heartbeatCallback = function resetInactivityTimer() {
781 that.inactivityTimer.cancel();
782 setTimer();
783 }
784 }
786 let curWindow = this.getCurrentWindow();
787 let marionette = new Marionette(this, curWindow, "chrome",
788 this.marionetteLog,
789 timeout, this.heartbeatCallback, this.testName);
790 let _chromeSandbox = this.createExecuteSandbox(curWindow,
791 marionette,
792 aRequest.parameters.args,
793 aRequest.parameters.specialPowers,
794 command_id);
795 if (!_chromeSandbox)
796 return;
798 try {
799 _chromeSandbox.finish = function chromeSandbox_finish() {
800 if (that.inactivityTimer != null) {
801 that.inactivityTimer.cancel();
802 }
803 return marionette.generate_results();
804 };
806 if (directInject) {
807 script = aRequest.parameters.script;
808 }
809 else {
810 script = "let func = function() {" +
811 aRequest.parameters.script +
812 "};" +
813 "func.apply(null, __marionetteParams);";
814 }
815 this.executeScriptInSandbox(_chromeSandbox, script, directInject,
816 false, command_id, timeout);
817 }
818 catch (e) {
819 let error = createStackMessage(e,
820 "execute_script",
821 aRequest.parameters.filename,
822 aRequest.parameters.line,
823 script);
824 this.sendError(error[0], 17, error[1], command_id);
825 }
826 },
828 /**
829 * Set the timeout for asynchronous script execution
830 *
831 * @param object aRequest
832 * 'ms' member is time in milliseconds to set timeout
833 */
834 setScriptTimeout: function MDA_setScriptTimeout(aRequest) {
835 this.command_id = this.getCommandId();
836 let timeout = parseInt(aRequest.parameters.ms);
837 if(isNaN(timeout)){
838 this.sendError("Not a Number", 500, null, this.command_id);
839 }
840 else {
841 this.scriptTimeout = timeout;
842 this.sendOk(this.command_id);
843 }
844 },
846 /**
847 * execute pure JS script. Used to execute 'mochitest'-style Marionette tests.
848 *
849 * @param object aRequest
850 * 'script' member holds the script to execute
851 * 'args' member holds the arguments to the script
852 * 'timeout' member will be used as the script timeout if it is given
853 */
854 executeJSScript: function MDA_executeJSScript(aRequest) {
855 let timeout = aRequest.parameters.scriptTimeout ? aRequest.parameters.scriptTimeout : this.scriptTimeout;
856 let command_id = this.command_id = this.getCommandId();
858 //all pure JS scripts will need to call Marionette.finish() to complete the test.
859 if (aRequest.newSandbox == undefined) {
860 //if client does not send a value in newSandbox,
861 //then they expect the same behaviour as webdriver
862 aRequest.newSandbox = true;
863 }
864 if (this.context == "chrome") {
865 if (aRequest.parameters.async) {
866 this.executeWithCallback(aRequest, aRequest.parameters.async);
867 }
868 else {
869 this.execute(aRequest, true);
870 }
871 }
872 else {
873 this.sendAsync("executeJSScript",
874 {
875 script: aRequest.parameters.script,
876 args: aRequest.parameters.args,
877 newSandbox: aRequest.parameters.newSandbox,
878 async: aRequest.parameters.async,
879 timeout: timeout,
880 inactivityTimeout: aRequest.parameters.inactivityTimeout,
881 specialPowers: aRequest.parameters.specialPowers,
882 filename: aRequest.parameters.filename,
883 line: aRequest.parameters.line,
884 },
885 command_id);
886 }
887 },
889 /**
890 * This function is used by executeAsync and executeJSScript to execute a script
891 * in a sandbox.
892 *
893 * For executeJSScript, it will return a message only when the finish() method is called.
894 * For executeAsync, it will return a response when marionetteScriptFinished/arguments[arguments.length-1]
895 * method is called, or if it times out.
896 *
897 * @param object aRequest
898 * 'script' member holds the script to execute
899 * 'args' member holds the arguments for the script
900 * @param boolean directInject
901 * if true, it will be run directly and not as a
902 * function body
903 */
904 executeWithCallback: function MDA_executeWithCallback(aRequest, directInject) {
905 let inactivityTimeout = aRequest.parameters.inactivityTimeout;
906 let timeout = aRequest.parameters.scriptTimeout ? aRequest.parameters.scriptTimeout : this.scriptTimeout;
907 let command_id = this.command_id = this.getCommandId();
908 let script;
909 this.logRequest("executeWithCallback", aRequest);
910 if (aRequest.parameters.newSandbox == undefined) {
911 //if client does not send a value in newSandbox,
912 //then they expect the same behaviour as webdriver
913 aRequest.parameters.newSandbox = true;
914 }
916 if (this.context == "content") {
917 this.sendAsync("executeAsyncScript",
918 {
919 script: aRequest.parameters.script,
920 args: aRequest.parameters.args,
921 id: this.command_id,
922 newSandbox: aRequest.parameters.newSandbox,
923 timeout: timeout,
924 inactivityTimeout: inactivityTimeout,
925 specialPowers: aRequest.parameters.specialPowers,
926 filename: aRequest.parameters.filename,
927 line: aRequest.parameters.line
928 },
929 command_id);
930 return;
931 }
933 // handle the inactivity timeout
934 let that = this;
935 if (inactivityTimeout) {
936 this.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
937 if (this.inactivityTimer != null) {
938 this.inactivityTimer.initWithCallback(function() {
939 chromeAsyncReturnFunc("timed out due to inactivity", 28);
940 }, inactivityTimeout, Ci.nsITimer.TYPE_ONESHOT);
941 }
942 this.heartbeatCallback = function resetInactivityTimer() {
943 that.inactivityTimer.cancel();
944 that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
945 if (that.inactivityTimer != null) {
946 that.inactivityTimer.initWithCallback(function() {
947 chromeAsyncReturnFunc("timed out due to inactivity", 28);
948 }, inactivityTimeout, Ci.nsITimer.TYPE_ONESHOT);
949 }
950 }
951 }
953 let curWindow = this.getCurrentWindow();
954 let original_onerror = curWindow.onerror;
955 let that = this;
956 that.timeout = timeout;
957 let marionette = new Marionette(this, curWindow, "chrome",
958 this.marionetteLog,
959 timeout, this.heartbeatCallback, this.testName);
960 marionette.command_id = this.command_id;
962 function chromeAsyncReturnFunc(value, status, stacktrace) {
963 if (that._emu_cbs && Object.keys(that._emu_cbs).length) {
964 value = "Emulator callback still pending when finish() called";
965 status = 500;
966 that._emu_cbs = null;
967 }
969 if (value == undefined)
970 value = null;
971 if (that.command_id == marionette.command_id) {
972 if (that.timer != null) {
973 that.timer.cancel();
974 that.timer = null;
975 }
977 curWindow.onerror = original_onerror;
979 if (status == 0 || status == undefined) {
980 that.sendToClient({from: that.actorID, value: that.curBrowser.elementManager.wrapValue(value), status: status},
981 marionette.command_id);
982 }
983 else {
984 let error_msg = {message: value, status: status, stacktrace: stacktrace};
985 that.sendToClient({from: that.actorID, error: error_msg},
986 marionette.command_id);
987 }
988 }
989 if (that.inactivityTimer != null) {
990 that.inactivityTimer.cancel();
991 }
992 }
994 curWindow.onerror = function (errorMsg, url, lineNumber) {
995 chromeAsyncReturnFunc(errorMsg + " at: " + url + " line: " + lineNumber, 17);
996 return true;
997 };
999 function chromeAsyncFinish() {
1000 chromeAsyncReturnFunc(marionette.generate_results(), 0);
1001 }
1003 let _chromeSandbox = this.createExecuteSandbox(curWindow,
1004 marionette,
1005 aRequest.parameters.args,
1006 aRequest.parameters.specialPowers,
1007 command_id);
1008 if (!_chromeSandbox)
1009 return;
1011 try {
1013 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
1014 if (this.timer != null) {
1015 this.timer.initWithCallback(function() {
1016 chromeAsyncReturnFunc("timed out", 28);
1017 }, that.timeout, Ci.nsITimer.TYPE_ONESHOT);
1018 }
1020 _chromeSandbox.returnFunc = chromeAsyncReturnFunc;
1021 _chromeSandbox.finish = chromeAsyncFinish;
1023 if (directInject) {
1024 script = aRequest.parameters.script;
1025 }
1026 else {
1027 script = '__marionetteParams.push(returnFunc);'
1028 + 'let marionetteScriptFinished = returnFunc;'
1029 + 'let __marionetteFunc = function() {' + aRequest.parameters.script + '};'
1030 + '__marionetteFunc.apply(null, __marionetteParams);';
1031 }
1033 this.executeScriptInSandbox(_chromeSandbox, script, directInject,
1034 true, command_id, timeout);
1035 } catch (e) {
1036 let error = createStackMessage(e,
1037 "execute_async_script",
1038 aRequest.parameters.filename,
1039 aRequest.parameters.line,
1040 script);
1041 chromeAsyncReturnFunc(error[0], 17, error[1]);
1042 }
1043 },
1045 /**
1046 * Navigate to to given URL.
1047 *
1048 * This will follow redirects issued by the server. When the method
1049 * returns is based on the page load strategy that the user has
1050 * selected.
1051 *
1052 * Documents that contain a META tag with the "http-equiv" attribute
1053 * set to "refresh" will return if the timeout is greater than 1
1054 * second and the other criteria for determining whether a page is
1055 * loaded are met. When the refresh period is 1 second or less and
1056 * the page load strategy is "normal" or "conservative", it will
1057 * wait for the page to complete loading before returning.
1058 *
1059 * If any modal dialog box, such as those opened on
1060 * window.onbeforeunload or window.alert, is opened at any point in
1061 * the page load, it will return immediately.
1062 *
1063 * If a 401 response is seen by the browser, it will return
1064 * immediately. That is, if BASIC, DIGEST, NTLM or similar
1065 * authentication is required, the page load is assumed to be
1066 * complete. This does not include FORM-based authentication.
1067 *
1068 * @param object aRequest where <code>url</code> property holds the
1069 * URL to navigate to
1070 */
1071 get: function MDA_get(aRequest) {
1072 let command_id = this.command_id = this.getCommandId();
1073 if (this.context != "chrome") {
1074 aRequest.command_id = command_id;
1075 aRequest.parameters.pageTimeout = this.pageTimeout;
1076 this.sendAsync("get", aRequest.parameters, command_id);
1077 return;
1078 }
1080 this.getCurrentWindow().location.href = aRequest.parameters.url;
1081 let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
1082 let start = new Date().getTime();
1083 let end = null;
1085 function checkLoad() {
1086 end = new Date().getTime();
1087 let elapse = end - start;
1088 if (this.pageTimeout == null || elapse <= this.pageTimeout){
1089 if (curWindow.document.readyState == "complete") {
1090 sendOk(command_id);
1091 return;
1092 }
1093 else{
1094 checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
1095 }
1096 }
1097 else{
1098 sendError("Error loading page", 13, null, command_id);
1099 return;
1100 }
1101 }
1102 checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
1103 },
1105 /**
1106 * Get a string representing the current URL.
1107 *
1108 * On Desktop this returns a string representation of the URL of the
1109 * current top level browsing context. This is equivalent to
1110 * document.location.href.
1111 *
1112 * When in the context of the chrome, this returns the canonical URL
1113 * of the current resource.
1114 */
1115 getCurrentUrl: function MDA_getCurrentUrl() {
1116 this.command_id = this.getCommandId();
1117 if (this.context == "chrome") {
1118 this.sendResponse(this.getCurrentWindow().location.href, this.command_id);
1119 }
1120 else {
1121 this.sendAsync("getCurrentUrl", {}, this.command_id);
1122 }
1123 },
1125 /**
1126 * Gets the current title of the window
1127 */
1128 getTitle: function MDA_getTitle() {
1129 this.command_id = this.getCommandId();
1130 if (this.context == "chrome"){
1131 var curWindow = this.getCurrentWindow();
1132 var title = curWindow.document.documentElement.getAttribute('title');
1133 this.sendResponse(title, this.command_id);
1134 }
1135 else {
1136 this.sendAsync("getTitle", {}, this.command_id);
1137 }
1138 },
1140 /**
1141 * Gets the current type of the window
1142 */
1143 getWindowType: function MDA_getWindowType() {
1144 this.command_id = this.getCommandId();
1145 var curWindow = this.getCurrentWindow();
1146 var type = curWindow.document.documentElement.getAttribute('windowtype');
1147 this.sendResponse(type, this.command_id);
1148 },
1150 /**
1151 * Gets the page source of the content document
1152 */
1153 getPageSource: function MDA_getPageSource(){
1154 this.command_id = this.getCommandId();
1155 if (this.context == "chrome"){
1156 let curWindow = this.getCurrentWindow();
1157 let XMLSerializer = curWindow.XMLSerializer;
1158 let pageSource = new XMLSerializer().serializeToString(curWindow.document);
1159 this.sendResponse(pageSource, this.command_id);
1160 }
1161 else {
1162 this.sendAsync("getPageSource", {}, this.command_id);
1163 }
1164 },
1166 /**
1167 * Go back in history
1168 */
1169 goBack: function MDA_goBack() {
1170 this.command_id = this.getCommandId();
1171 this.sendAsync("goBack", {}, this.command_id);
1172 },
1174 /**
1175 * Go forward in history
1176 */
1177 goForward: function MDA_goForward() {
1178 this.command_id = this.getCommandId();
1179 this.sendAsync("goForward", {}, this.command_id);
1180 },
1182 /**
1183 * Refresh the page
1184 */
1185 refresh: function MDA_refresh() {
1186 this.command_id = this.getCommandId();
1187 this.sendAsync("refresh", {}, this.command_id);
1188 },
1190 /**
1191 * Get the current window's handle.
1192 *
1193 * Return an opaque server-assigned identifier to this window that
1194 * uniquely identifies it within this Marionette instance. This can
1195 * be used to switch to this window at a later point.
1196 *
1197 * @return unique window handle (string)
1198 */
1199 getWindowHandle: function MDA_getWindowHandle() {
1200 this.command_id = this.getCommandId();
1201 for (let i in this.browsers) {
1202 if (this.curBrowser == this.browsers[i]) {
1203 this.sendResponse(i, this.command_id);
1204 return;
1205 }
1206 }
1207 },
1209 /**
1210 * Get list of windows in the current context.
1211 *
1212 * If called in the content context it will return a list of
1213 * references to all available browser windows. Called in the
1214 * chrome context, it will list all available windows, not just
1215 * browser windows (e.g. not just navigator.browser).
1216 *
1217 * Each window handle is assigned by the server, and the array of
1218 * strings returned does not have a guaranteed ordering.
1219 *
1220 * @return unordered array of unique window handles as strings
1221 */
1222 getWindowHandles: function MDA_getWindowHandles() {
1223 this.command_id = this.getCommandId();
1224 let res = [];
1225 let winEn = this.getWinEnumerator();
1226 while (winEn.hasMoreElements()) {
1227 let foundWin = winEn.getNext();
1228 let winId = foundWin.QueryInterface(Ci.nsIInterfaceRequestor)
1229 .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
1230 winId = winId + ((appName == "B2G") ? "-b2g" : "");
1231 res.push(winId);
1232 }
1233 this.sendResponse(res, this.command_id);
1234 },
1236 /**
1237 * Switch to a window based on name or server-assigned id.
1238 * Searches based on name, then id.
1239 *
1240 * @param object aRequest
1241 * 'name' member holds the name or id of the window to switch to
1242 */
1243 switchToWindow: function MDA_switchToWindow(aRequest) {
1244 let command_id = this.command_id = this.getCommandId();
1245 let winEn = this.getWinEnumerator();
1246 while(winEn.hasMoreElements()) {
1247 let foundWin = winEn.getNext();
1248 let winId = foundWin.QueryInterface(Ci.nsIInterfaceRequestor)
1249 .getInterface(Ci.nsIDOMWindowUtils)
1250 .outerWindowID;
1251 winId = winId + ((appName == "B2G") ? '-b2g' : '');
1252 if (aRequest.parameters.name == foundWin.name || aRequest.parameters.name == winId) {
1253 if (this.browsers[winId] == undefined) {
1254 //enable Marionette in that browser window
1255 this.startBrowser(foundWin, false);
1256 }
1257 else {
1258 utils.window = foundWin;
1259 this.curBrowser = this.browsers[winId];
1260 }
1261 this.sendOk(command_id);
1262 return;
1263 }
1264 }
1265 this.sendError("Unable to locate window " + aRequest.parameters.name, 23, null,
1266 command_id);
1267 },
1269 getActiveFrame: function MDA_getActiveFrame() {
1270 this.command_id = this.getCommandId();
1272 if (this.context == "chrome") {
1273 if (this.curFrame) {
1274 let frameUid = this.curBrowser.elementManager.addToKnownElements(this.curFrame.frameElement);
1275 this.sendResponse(frameUid, this.command_id);
1276 } else {
1277 // no current frame, we're at toplevel
1278 this.sendResponse(null, this.command_id);
1279 }
1280 } else {
1281 // not chrome
1282 this.sendResponse(this.currentFrameElement, this.command_id);
1283 }
1284 },
1286 /**
1287 * Switch to a given frame within the current window
1288 *
1289 * @param object aRequest
1290 * 'element' is the element to switch to
1291 * 'id' if element is not set, then this
1292 * holds either the id, name or index
1293 * of the frame to switch to
1294 */
1295 switchToFrame: function MDA_switchToFrame(aRequest) {
1296 let command_id = this.command_id = this.getCommandId();
1297 this.logRequest("switchToFrame", aRequest);
1298 let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
1299 let curWindow = this.getCurrentWindow();
1300 let checkLoad = function() {
1301 let errorRegex = /about:.+(error)|(blocked)\?/;
1302 let curWindow = this.getCurrentWindow();
1303 if (curWindow.document.readyState == "complete") {
1304 this.sendOk(command_id);
1305 return;
1306 }
1307 else if (curWindow.document.readyState == "interactive" && errorRegex.exec(curWindow.document.baseURI)) {
1308 this.sendError("Error loading page", 13, null, command_id);
1309 return;
1310 }
1312 checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
1313 }
1314 if (this.context == "chrome") {
1315 let foundFrame = null;
1316 if ((aRequest.parameters.id == null) && (aRequest.parameters.element == null)) {
1317 this.curFrame = null;
1318 if (aRequest.parameters.focus) {
1319 this.mainFrame.focus();
1320 }
1321 checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
1322 return;
1323 }
1324 if (aRequest.parameters.element != undefined) {
1325 if (this.curBrowser.elementManager.seenItems[aRequest.parameters.element]) {
1326 let wantedFrame = this.curBrowser.elementManager.getKnownElement(aRequest.parameters.element, curWindow); //HTMLIFrameElement
1327 let frames = curWindow.document.getElementsByTagName("iframe");
1328 let numFrames = frames.length;
1329 for (let i = 0; i < numFrames; i++) {
1330 if (XPCNativeWrapper(frames[i]) == XPCNativeWrapper(wantedFrame)) {
1331 curWindow = frames[i].contentWindow;
1332 this.curFrame = curWindow;
1333 if (aRequest.parameters.focus) {
1334 this.curFrame.focus();
1335 }
1336 checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
1337 return;
1338 }
1339 }
1340 }
1341 }
1342 switch(typeof(aRequest.parameters.id)) {
1343 case "string" :
1344 let foundById = null;
1345 let frames = curWindow.document.getElementsByTagName("iframe");
1346 let numFrames = frames.length;
1347 for (let i = 0; i < numFrames; i++) {
1348 //give precedence to name
1349 let frame = frames[i];
1350 if (frame.getAttribute("name") == aRequest.parameters.id) {
1351 foundFrame = i;
1352 curWindow = frame.contentWindow;
1353 break;
1354 } else if ((foundById == null) && (frame.id == aRequest.parameters.id)) {
1355 foundById = i;
1356 }
1357 }
1358 if ((foundFrame == null) && (foundById != null)) {
1359 foundFrame = foundById;
1360 curWindow = frames[foundById].contentWindow;
1361 }
1362 break;
1363 case "number":
1364 if (curWindow.frames[aRequest.parameters.id] != undefined) {
1365 foundFrame = aRequest.parameters.id;
1366 curWindow = curWindow.frames[foundFrame].frameElement.contentWindow;
1367 }
1368 break;
1369 }
1370 if (foundFrame != null) {
1371 this.curFrame = curWindow;
1372 if (aRequest.parameters.focus) {
1373 this.curFrame.focus();
1374 }
1375 checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
1376 } else {
1377 this.sendError("Unable to locate frame: " + aRequest.parameters.id, 8, null,
1378 command_id);
1379 }
1380 }
1381 else {
1382 if ((!aRequest.parameters.id) && (!aRequest.parameters.element) &&
1383 (this.curBrowser.frameManager.currentRemoteFrame !== null)) {
1384 // We're currently using a ChromeMessageSender for a remote frame, so this
1385 // request indicates we need to switch back to the top-level (parent) frame.
1386 // We'll first switch to the parent's (global) ChromeMessageBroadcaster, so
1387 // we send the message to the right listener.
1388 this.switchToGlobalMessageManager();
1389 }
1390 aRequest.command_id = command_id;
1391 this.sendAsync("switchToFrame", aRequest.parameters, command_id);
1392 }
1393 },
1395 /**
1396 * Set timeout for searching for elements
1397 *
1398 * @param object aRequest
1399 * 'ms' holds the search timeout in milliseconds
1400 */
1401 setSearchTimeout: function MDA_setSearchTimeout(aRequest) {
1402 this.command_id = this.getCommandId();
1403 let timeout = parseInt(aRequest.parameters.ms);
1404 if (isNaN(timeout)) {
1405 this.sendError("Not a Number", 500, null, this.command_id);
1406 }
1407 else {
1408 this.searchTimeout = timeout;
1409 this.sendOk(this.command_id);
1410 }
1411 },
1413 /**
1414 * Set timeout for page loading, searching and scripts
1415 *
1416 * @param object aRequest
1417 * 'type' hold the type of timeout
1418 * 'ms' holds the timeout in milliseconds
1419 */
1420 timeouts: function MDA_timeouts(aRequest){
1421 /*setTimeout*/
1422 this.command_id = this.getCommandId();
1423 let timeout_type = aRequest.parameters.type;
1424 let timeout = parseInt(aRequest.parameters.ms);
1425 if (isNaN(timeout)) {
1426 this.sendError("Not a Number", 500, null, this.command_id);
1427 }
1428 else {
1429 if (timeout_type == "implicit") {
1430 this.setSearchTimeout(aRequest);
1431 }
1432 else if (timeout_type == "script") {
1433 this.setScriptTimeout(aRequest);
1434 }
1435 else {
1436 this.pageTimeout = timeout;
1437 this.sendOk(this.command_id);
1438 }
1439 }
1440 },
1442 /**
1443 * Single Tap
1444 *
1445 * @param object aRequest
1446 'element' represents the ID of the element to single tap on
1447 */
1448 singleTap: function MDA_singleTap(aRequest) {
1449 this.command_id = this.getCommandId();
1450 let serId = aRequest.parameters.id;
1451 let x = aRequest.parameters.x;
1452 let y = aRequest.parameters.y;
1453 if (this.context == "chrome") {
1454 this.sendError("Command 'singleTap' is not available in chrome context", 500, null, this.command_id);
1455 }
1456 else {
1457 this.sendAsync("singleTap",
1458 {
1459 id: serId,
1460 corx: x,
1461 cory: y
1462 },
1463 this.command_id);
1464 }
1465 },
1467 /**
1468 * actionChain
1469 *
1470 * @param object aRequest
1471 * 'value' represents a nested array: inner array represents each event; outer array represents collection of events
1472 */
1473 actionChain: function MDA_actionChain(aRequest) {
1474 this.command_id = this.getCommandId();
1475 if (this.context == "chrome") {
1476 this.sendError("Command 'actionChain' is not available in chrome context", 500, null, this.command_id);
1477 }
1478 else {
1479 this.sendAsync("actionChain",
1480 {
1481 chain: aRequest.parameters.chain,
1482 nextId: aRequest.parameters.nextId
1483 },
1484 this.command_id);
1485 }
1486 },
1488 /**
1489 * multiAction
1490 *
1491 * @param object aRequest
1492 * 'value' represents a nested array: inner array represents each event;
1493 * middle array represents collection of events for each finger
1494 * outer array represents all the fingers
1495 */
1497 multiAction: function MDA_multiAction(aRequest) {
1498 this.command_id = this.getCommandId();
1499 if (this.context == "chrome") {
1500 this.sendError("Command 'multiAction' is not available in chrome context", 500, null, this.command_id);
1501 }
1502 else {
1503 this.sendAsync("multiAction",
1504 {
1505 value: aRequest.parameters.value,
1506 maxlen: aRequest.parameters.max_length
1507 },
1508 this.command_id);
1509 }
1510 },
1512 /**
1513 * Find an element using the indicated search strategy.
1514 *
1515 * @param object aRequest
1516 * 'using' member indicates which search method to use
1517 * 'value' member is the value the client is looking for
1518 */
1519 findElement: function MDA_findElement(aRequest) {
1520 let command_id = this.command_id = this.getCommandId();
1521 if (this.context == "chrome") {
1522 let id;
1523 try {
1524 let on_success = this.sendResponse.bind(this);
1525 let on_error = this.sendError.bind(this);
1526 id = this.curBrowser.elementManager.find(
1527 this.getCurrentWindow(),
1528 aRequest.parameters,
1529 this.searchTimeout,
1530 on_success,
1531 on_error,
1532 false,
1533 command_id);
1534 }
1535 catch (e) {
1536 this.sendError(e.message, e.code, e.stack, command_id);
1537 return;
1538 }
1539 }
1540 else {
1541 this.sendAsync("findElementContent",
1542 {
1543 value: aRequest.parameters.value,
1544 using: aRequest.parameters.using,
1545 element: aRequest.parameters.element,
1546 searchTimeout: this.searchTimeout
1547 },
1548 command_id);
1549 }
1550 },
1552 /**
1553 * Find elements using the indicated search strategy.
1554 *
1555 * @param object aRequest
1556 * 'using' member indicates which search method to use
1557 * 'value' member is the value the client is looking for
1558 */
1559 findElements: function MDA_findElements(aRequest) {
1560 let command_id = this.command_id = this.getCommandId();
1561 if (this.context == "chrome") {
1562 let id;
1563 try {
1564 let on_success = this.sendResponse.bind(this);
1565 let on_error = this.sendError.bind(this);
1566 id = this.curBrowser.elementManager.find(this.getCurrentWindow(),
1567 aRequest.parameters,
1568 this.searchTimeout,
1569 on_success,
1570 on_error,
1571 true,
1572 command_id);
1573 }
1574 catch (e) {
1575 this.sendError(e.message, e.code, e.stack, command_id);
1576 return;
1577 }
1578 }
1579 else {
1580 this.sendAsync("findElementsContent",
1581 {
1582 value: aRequest.parameters.value,
1583 using: aRequest.parameters.using,
1584 element: aRequest.parameters.element,
1585 searchTimeout: this.searchTimeout
1586 },
1587 command_id);
1588 }
1589 },
1591 /**
1592 * Return the active element on the page
1593 */
1594 getActiveElement: function MDA_getActiveElement(){
1595 let command_id = this.command_id = this.getCommandId();
1596 this.sendAsync("getActiveElement", {}, command_id);
1597 },
1599 /**
1600 * Send click event to element
1601 *
1602 * @param object aRequest
1603 * 'id' member holds the reference id to
1604 * the element that will be clicked
1605 */
1606 clickElement: function MDA_clickElementent(aRequest) {
1607 let command_id = this.command_id = this.getCommandId();
1608 if (this.context == "chrome") {
1609 try {
1610 //NOTE: click atom fails, fall back to click() action
1611 let el = this.curBrowser.elementManager.getKnownElement(
1612 aRequest.parameters.id, this.getCurrentWindow());
1613 el.click();
1614 this.sendOk(command_id);
1615 }
1616 catch (e) {
1617 this.sendError(e.message, e.code, e.stack, command_id);
1618 }
1619 }
1620 else {
1621 // We need to protect against the click causing an OOP frame to close.
1622 // This fires the mozbrowserclose event when it closes so we need to
1623 // listen for it and then just send an error back. The person making the
1624 // call should be aware something isnt right and handle accordingly
1625 let curWindow = this.getCurrentWindow();
1626 let self = this;
1627 this.mozBrowserClose = function() {
1628 curWindow.removeEventListener('mozbrowserclose', self.mozBrowserClose, true);
1629 self.switchToGlobalMessageManager();
1630 self.sendError("The frame closed during the click, recovering to allow further communications", 500, null, command_id);
1631 };
1632 curWindow.addEventListener('mozbrowserclose', this.mozBrowserClose, true);
1633 this.sendAsync("clickElement",
1634 { id: aRequest.parameters.id },
1635 command_id);
1636 }
1637 },
1639 /**
1640 * Get a given attribute of an element
1641 *
1642 * @param object aRequest
1643 * 'id' member holds the reference id to
1644 * the element that will be inspected
1645 * 'name' member holds the name of the attribute to retrieve
1646 */
1647 getElementAttribute: function MDA_getElementAttribute(aRequest) {
1648 let command_id = this.command_id = this.getCommandId();
1649 if (this.context == "chrome") {
1650 try {
1651 let el = this.curBrowser.elementManager.getKnownElement(
1652 aRequest.parameters.id, this.getCurrentWindow());
1653 this.sendResponse(utils.getElementAttribute(el, aRequest.parameters.name),
1654 command_id);
1655 }
1656 catch (e) {
1657 this.sendError(e.message, e.code, e.stack, command_id);
1658 }
1659 }
1660 else {
1661 this.sendAsync("getElementAttribute",
1662 {
1663 id: aRequest.parameters.id,
1664 name: aRequest.parameters.name
1665 },
1666 command_id);
1667 }
1668 },
1670 /**
1671 * Get the text of an element, if any. Includes the text of all child elements.
1672 *
1673 * @param object aRequest
1674 * 'id' member holds the reference id to
1675 * the element that will be inspected
1676 */
1677 getElementText: function MDA_getElementText(aRequest) {
1678 let command_id = this.command_id = this.getCommandId();
1679 if (this.context == "chrome") {
1680 //Note: for chrome, we look at text nodes, and any node with a "label" field
1681 try {
1682 let el = this.curBrowser.elementManager.getKnownElement(
1683 aRequest.parameters.id, this.getCurrentWindow());
1684 let lines = [];
1685 this.getVisibleText(el, lines);
1686 lines = lines.join("\n");
1687 this.sendResponse(lines, command_id);
1688 }
1689 catch (e) {
1690 this.sendError(e.message, e.code, e.stack, command_id);
1691 }
1692 }
1693 else {
1694 this.sendAsync("getElementText",
1695 { id: aRequest.parameters.id },
1696 command_id);
1697 }
1698 },
1700 /**
1701 * Get the tag name of the element.
1702 *
1703 * @param object aRequest
1704 * 'id' member holds the reference id to
1705 * the element that will be inspected
1706 */
1707 getElementTagName: function MDA_getElementTagName(aRequest) {
1708 let command_id = this.command_id = this.getCommandId();
1709 if (this.context == "chrome") {
1710 try {
1711 let el = this.curBrowser.elementManager.getKnownElement(
1712 aRequest.parameters.id, this.getCurrentWindow());
1713 this.sendResponse(el.tagName.toLowerCase(), command_id);
1714 }
1715 catch (e) {
1716 this.sendError(e.message, e.code, e.stack, command_id);
1717 }
1718 }
1719 else {
1720 this.sendAsync("getElementTagName",
1721 { id: aRequest.parameters.id },
1722 command_id);
1723 }
1724 },
1726 /**
1727 * Check if element is displayed
1728 *
1729 * @param object aRequest
1730 * 'id' member holds the reference id to
1731 * the element that will be checked
1732 */
1733 isElementDisplayed: function MDA_isElementDisplayed(aRequest) {
1734 let command_id = this.command_id = this.getCommandId();
1735 if (this.context == "chrome") {
1736 try {
1737 let el = this.curBrowser.elementManager.getKnownElement(
1738 aRequest.parameters.id, this.getCurrentWindow());
1739 this.sendResponse(utils.isElementDisplayed(el), command_id);
1740 }
1741 catch (e) {
1742 this.sendError(e.message, e.code, e.stack, command_id);
1743 }
1744 }
1745 else {
1746 this.sendAsync("isElementDisplayed",
1747 { id:aRequest.parameters.id },
1748 command_id);
1749 }
1750 },
1752 /**
1753 * Return the property of the computed style of an element
1754 *
1755 * @param object aRequest
1756 * 'id' member holds the reference id to
1757 * the element that will be checked
1758 * 'propertyName' is the CSS rule that is being requested
1759 */
1760 getElementValueOfCssProperty: function MDA_getElementValueOfCssProperty(aRequest){
1761 let command_id = this.command_id = this.getCommandId();
1762 this.sendAsync("getElementValueOfCssProperty",
1763 {id: aRequest.parameters.id, propertyName: aRequest.parameters.propertyName},
1764 command_id);
1765 },
1767 /**
1768 * Submit a form on a content page by either using form or element in a form
1769 * @param object aRequest
1770 * 'id' member holds the reference id to
1771 * the element that will be checked
1772 */
1773 submitElement: function MDA_submitElement(aRequest) {
1774 let command_id = this.command_id = this.getCommandId();
1775 if (this.context == "chrome") {
1776 this.sendError("Command 'submitElement' is not available in chrome context", 500, null, this.command_id);
1777 }
1778 else {
1779 this.sendAsync("submitElement", {id: aRequest.parameters.id}, command_id);
1780 }
1781 },
1783 /**
1784 * Check if element is enabled
1785 *
1786 * @param object aRequest
1787 * 'id' member holds the reference id to
1788 * the element that will be checked
1789 */
1790 isElementEnabled: function MDA_isElementEnabled(aRequest) {
1791 let command_id = this.command_id = this.getCommandId();
1792 if (this.context == "chrome") {
1793 try {
1794 //Selenium atom doesn't quite work here
1795 let el = this.curBrowser.elementManager.getKnownElement(
1796 aRequest.parameters.id, this.getCurrentWindow());
1797 if (el.disabled != undefined) {
1798 this.sendResponse(!!!el.disabled, command_id);
1799 }
1800 else {
1801 this.sendResponse(true, command_id);
1802 }
1803 }
1804 catch (e) {
1805 this.sendError(e.message, e.code, e.stack, command_id);
1806 }
1807 }
1808 else {
1809 this.sendAsync("isElementEnabled",
1810 { id:aRequest.parameters.id },
1811 command_id);
1812 }
1813 },
1815 /**
1816 * Check if element is selected
1817 *
1818 * @param object aRequest
1819 * 'id' member holds the reference id to
1820 * the element that will be checked
1821 */
1822 isElementSelected: function MDA_isElementSelected(aRequest) {
1823 let command_id = this.command_id = this.getCommandId();
1824 if (this.context == "chrome") {
1825 try {
1826 //Selenium atom doesn't quite work here
1827 let el = this.curBrowser.elementManager.getKnownElement(
1828 aRequest.parameters.id, this.getCurrentWindow());
1829 if (el.checked != undefined) {
1830 this.sendResponse(!!el.checked, command_id);
1831 }
1832 else if (el.selected != undefined) {
1833 this.sendResponse(!!el.selected, command_id);
1834 }
1835 else {
1836 this.sendResponse(true, command_id);
1837 }
1838 }
1839 catch (e) {
1840 this.sendError(e.message, e.code, e.stack, command_id);
1841 }
1842 }
1843 else {
1844 this.sendAsync("isElementSelected",
1845 { id:aRequest.parameters.id },
1846 command_id);
1847 }
1848 },
1850 getElementSize: function MDA_getElementSize(aRequest) {
1851 let command_id = this.command_id = this.getCommandId();
1852 if (this.context == "chrome") {
1853 try {
1854 let el = this.curBrowser.elementManager.getKnownElement(
1855 aRequest.parameters.id, this.getCurrentWindow());
1856 let clientRect = el.getBoundingClientRect();
1857 this.sendResponse({width: clientRect.width, height: clientRect.height},
1858 command_id);
1859 }
1860 catch (e) {
1861 this.sendError(e.message, e.code, e.stack, command_id);
1862 }
1863 }
1864 else {
1865 this.sendAsync("getElementSize",
1866 { id:aRequest.parameters.id },
1867 command_id);
1868 }
1869 },
1871 /**
1872 * Send key presses to element after focusing on it
1873 *
1874 * @param object aRequest
1875 * 'id' member holds the reference id to
1876 * the element that will be checked
1877 * 'value' member holds the value to send to the element
1878 */
1879 sendKeysToElement: function MDA_sendKeysToElement(aRequest) {
1880 let command_id = this.command_id = this.getCommandId();
1881 if (this.context == "chrome") {
1882 try {
1883 let el = this.curBrowser.elementManager.getKnownElement(
1884 aRequest.parameters.id, this.getCurrentWindow());
1885 el.focus();
1886 utils.sendString(aRequest.parameters.value.join(""), utils.window);
1887 this.sendOk(command_id);
1888 }
1889 catch (e) {
1890 this.sendError(e.message, e.code, e.stack, command_id);
1891 }
1892 }
1893 else {
1894 this.sendAsync("sendKeysToElement",
1895 {
1896 id:aRequest.parameters.id,
1897 value: aRequest.parameters.value
1898 },
1899 command_id);
1900 }
1901 },
1903 /**
1904 * Sets the test name
1905 *
1906 * The test name is used in logging messages.
1907 */
1908 setTestName: function MDA_setTestName(aRequest) {
1909 this.command_id = this.getCommandId();
1910 this.logRequest("setTestName", aRequest);
1911 this.testName = aRequest.parameters.value;
1912 this.sendAsync("setTestName",
1913 { value: aRequest.parameters.value },
1914 this.command_id);
1915 },
1917 /**
1918 * Clear the text of an element
1919 *
1920 * @param object aRequest
1921 * 'id' member holds the reference id to
1922 * the element that will be cleared
1923 */
1924 clearElement: function MDA_clearElement(aRequest) {
1925 let command_id = this.command_id = this.getCommandId();
1926 if (this.context == "chrome") {
1927 //the selenium atom doesn't work here
1928 try {
1929 let el = this.curBrowser.elementManager.getKnownElement(
1930 aRequest.parameters.id, this.getCurrentWindow());
1931 if (el.nodeName == "textbox") {
1932 el.value = "";
1933 }
1934 else if (el.nodeName == "checkbox") {
1935 el.checked = false;
1936 }
1937 this.sendOk(command_id);
1938 }
1939 catch (e) {
1940 this.sendError(e.message, e.code, e.stack, command_id);
1941 }
1942 }
1943 else {
1944 this.sendAsync("clearElement",
1945 { id:aRequest.parameters.id },
1946 command_id);
1947 }
1948 },
1950 /**
1951 * Get an element's location on the page.
1952 *
1953 * The returned point will contain the x and y coordinates of the
1954 * top left-hand corner of the given element. The point (0,0)
1955 * refers to the upper-left corner of the document.
1956 *
1957 * @return a point containing x and y coordinates as properties
1958 */
1959 getElementLocation: function MDA_getElementLocation(aRequest) {
1960 this.command_id = this.getCommandId();
1961 this.sendAsync("getElementLocation", {id: aRequest.parameters.id},
1962 this.command_id);
1963 },
1965 /**
1966 * Add a cookie to the document.
1967 */
1968 addCookie: function MDA_addCookie(aRequest) {
1969 this.command_id = this.getCommandId();
1970 this.sendAsync("addCookie",
1971 { cookie:aRequest.parameters.cookie },
1972 this.command_id);
1973 },
1975 /**
1976 * Get all the cookies for the current domain.
1977 *
1978 * This is the equivalent of calling "document.cookie" and parsing
1979 * the result.
1980 */
1981 getCookies: function MDA_getCookies() {
1982 this.command_id = this.getCommandId();
1983 this.sendAsync("getCookies", {}, this.command_id);
1984 },
1986 /**
1987 * Delete all cookies that are visible to a document
1988 */
1989 deleteAllCookies: function MDA_deleteAllCookies() {
1990 this.command_id = this.getCommandId();
1991 this.sendAsync("deleteAllCookies", {}, this.command_id);
1992 },
1994 /**
1995 * Delete a cookie by name
1996 */
1997 deleteCookie: function MDA_deleteCookie(aRequest) {
1998 this.command_id = this.getCommandId();
1999 this.sendAsync("deleteCookie",
2000 { name:aRequest.parameters.name },
2001 this.command_id);
2002 },
2004 /**
2005 * Close the current window, ending the session if it's the last
2006 * window currently open.
2007 *
2008 * On B2G this method is a noop and will return immediately.
2009 */
2010 close: function MDA_close() {
2011 let command_id = this.command_id = this.getCommandId();
2012 if (appName == "B2G") {
2013 // We can't close windows so just return
2014 this.sendOk(command_id);
2015 }
2016 else {
2017 // Get the total number of windows
2018 let numOpenWindows = 0;
2019 let winEnum = this.getWinEnumerator();
2020 while (winEnum.hasMoreElements()) {
2021 numOpenWindows += 1;
2022 winEnum.getNext();
2023 }
2025 // if there is only 1 window left, delete the session
2026 if (numOpenWindows === 1) {
2027 try {
2028 this.sessionTearDown();
2029 }
2030 catch (e) {
2031 this.sendError("Could not clear session", 500,
2032 e.name + ": " + e.message, command_id);
2033 return;
2034 }
2035 this.sendOk(command_id);
2036 return;
2037 }
2039 try {
2040 this.messageManager.removeDelayedFrameScript(FRAME_SCRIPT);
2041 this.getCurrentWindow().close();
2042 this.sendOk(command_id);
2043 }
2044 catch (e) {
2045 this.sendError("Could not close window: " + e.message, 13, e.stack,
2046 command_id);
2047 }
2048 }
2049 },
2051 /**
2052 * Deletes the session.
2053 *
2054 * If it is a desktop environment, it will close the session's tab and close all listeners
2055 *
2056 * If it is a B2G environment, it will make the main content listener sleep, and close
2057 * all other listeners. The main content listener persists after disconnect (it's the homescreen),
2058 * and can safely be reused.
2059 */
2060 sessionTearDown: function MDA_sessionTearDown() {
2061 if (this.curBrowser != null) {
2062 if (appName == "B2G") {
2063 this.globalMessageManager.broadcastAsyncMessage(
2064 "Marionette:sleepSession" + this.curBrowser.mainContentId, {});
2065 this.curBrowser.knownFrames.splice(
2066 this.curBrowser.knownFrames.indexOf(this.curBrowser.mainContentId), 1);
2067 }
2068 else {
2069 //don't set this pref for B2G since the framescript can be safely reused
2070 Services.prefs.setBoolPref("marionette.contentListener", false);
2071 }
2072 this.curBrowser.closeTab();
2073 //delete session in each frame in each browser
2074 for (let win in this.browsers) {
2075 for (let i in this.browsers[win].knownFrames) {
2076 this.globalMessageManager.broadcastAsyncMessage("Marionette:deleteSession" + this.browsers[win].knownFrames[i], {});
2077 }
2078 }
2079 let winEnum = this.getWinEnumerator();
2080 while (winEnum.hasMoreElements()) {
2081 winEnum.getNext().messageManager.removeDelayedFrameScript(FRAME_SCRIPT);
2082 }
2083 this.curBrowser.frameManager.removeMessageManagerListeners(this.globalMessageManager);
2084 }
2085 this.switchToGlobalMessageManager();
2086 // reset frame to the top-most frame
2087 this.curFrame = null;
2088 if (this.mainFrame) {
2089 this.mainFrame.focus();
2090 }
2091 this.deleteFile('marionetteChromeScripts');
2092 this.deleteFile('marionetteContentScripts');
2093 },
2095 /**
2096 * Processes the 'deleteSession' request from the client by tearing down
2097 * the session and responding 'ok'.
2098 */
2099 deleteSession: function MDA_deleteSession() {
2100 let command_id = this.command_id = this.getCommandId();
2101 try {
2102 this.sessionTearDown();
2103 }
2104 catch (e) {
2105 this.sendError("Could not delete session", 500, e.name + ": " + e.message, command_id);
2106 return;
2107 }
2108 this.sendOk(command_id);
2109 },
2111 /**
2112 * Returns the current status of the Application Cache
2113 */
2114 getAppCacheStatus: function MDA_getAppCacheStatus(aRequest) {
2115 this.command_id = this.getCommandId();
2116 this.sendAsync("getAppCacheStatus", {}, this.command_id);
2117 },
2119 _emu_cb_id: 0,
2120 _emu_cbs: null,
2121 runEmulatorCmd: function runEmulatorCmd(cmd, callback) {
2122 if (callback) {
2123 if (!this._emu_cbs) {
2124 this._emu_cbs = {};
2125 }
2126 this._emu_cbs[this._emu_cb_id] = callback;
2127 }
2128 this.sendToClient({emulator_cmd: cmd, id: this._emu_cb_id}, -1);
2129 this._emu_cb_id += 1;
2130 },
2132 runEmulatorShell: function runEmulatorShell(args, callback) {
2133 if (callback) {
2134 if (!this._emu_cbs) {
2135 this._emu_cbs = {};
2136 }
2137 this._emu_cbs[this._emu_cb_id] = callback;
2138 }
2139 this.sendToClient({emulator_shell: args, id: this._emu_cb_id}, -1);
2140 this._emu_cb_id += 1;
2141 },
2143 emulatorCmdResult: function emulatorCmdResult(message) {
2144 if (this.context != "chrome") {
2145 this.sendAsync("emulatorCmdResult", message, -1);
2146 return;
2147 }
2149 if (!this._emu_cbs) {
2150 return;
2151 }
2153 let cb = this._emu_cbs[message.id];
2154 delete this._emu_cbs[message.id];
2155 if (!cb) {
2156 return;
2157 }
2158 try {
2159 cb(message.result);
2160 }
2161 catch(e) {
2162 this.sendError(e.message, e.code, e.stack, -1);
2163 return;
2164 }
2165 },
2167 importScript: function MDA_importScript(aRequest) {
2168 let command_id = this.command_id = this.getCommandId();
2169 let converter =
2170 Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
2171 createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
2172 converter.charset = "UTF-8";
2173 let result = {};
2174 let data = converter.convertToByteArray(aRequest.parameters.script, result);
2175 let ch = Components.classes["@mozilla.org/security/hash;1"]
2176 .createInstance(Components.interfaces.nsICryptoHash);
2177 ch.init(ch.MD5);
2178 ch.update(data, data.length);
2179 let hash = ch.finish(true);
2180 if (this.importedScriptHashes[this.context].indexOf(hash) > -1) {
2181 //we have already imported this script
2182 this.sendOk(command_id);
2183 return;
2184 }
2185 this.importedScriptHashes[this.context].push(hash);
2186 if (this.context == "chrome") {
2187 let file;
2188 if (this.importedScripts.exists()) {
2189 file = FileUtils.openFileOutputStream(this.importedScripts,
2190 FileUtils.MODE_APPEND | FileUtils.MODE_WRONLY);
2191 }
2192 else {
2193 //Note: The permission bits here don't actually get set (bug 804563)
2194 this.importedScripts.createUnique(
2195 Components.interfaces.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
2196 file = FileUtils.openFileOutputStream(this.importedScripts,
2197 FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
2198 this.importedScripts.permissions = parseInt("0666", 8); //actually set permissions
2199 }
2200 file.write(aRequest.parameters.script, aRequest.parameters.script.length);
2201 file.close();
2202 this.sendOk(command_id);
2203 }
2204 else {
2205 this.sendAsync("importScript",
2206 { script: aRequest.parameters.script },
2207 command_id);
2208 }
2209 },
2211 clearImportedScripts: function MDA_clearImportedScripts(aRequest) {
2212 let command_id = this.command_id = this.getCommandId();
2213 try {
2214 if (this.context == "chrome") {
2215 this.deleteFile('marionetteChromeScripts');
2216 }
2217 else {
2218 this.deleteFile('marionetteContentScripts');
2219 }
2220 }
2221 catch (e) {
2222 this.sendError("Could not clear imported scripts", 500, e.name + ": " + e.message, command_id);
2223 return;
2224 }
2225 this.sendOk(command_id);
2226 },
2228 /**
2229 * Takes a screenshot of a web element or the current frame.
2230 *
2231 * The screen capture is returned as a lossless PNG image encoded as
2232 * a base 64 string. If the <code>id</code> argument is not null
2233 * and refers to a present and visible web element's ID, the capture
2234 * area will be limited to the bounding box of that element.
2235 * Otherwise, the capture area will be the bounding box of the
2236 * current frame.
2237 *
2238 * @param id an optional reference to a web element
2239 * @param highlights an optional list of web elements to draw a red
2240 * box around in the returned capture
2241 * @return PNG image encoded as base 64 string
2242 */
2243 takeScreenshot: function MDA_takeScreenshot(aRequest) {
2244 this.command_id = this.getCommandId();
2245 this.sendAsync("takeScreenshot",
2246 {id: aRequest.parameters.id,
2247 highlights: aRequest.parameters.highlights},
2248 this.command_id);
2249 },
2251 /**
2252 * Get the current browser orientation.
2253 *
2254 * Will return one of the valid primary orientation values
2255 * portrait-primary, landscape-primary, portrait-secondary, or
2256 * landscape-secondary.
2257 */
2258 getScreenOrientation: function MDA_getScreenOrientation(aRequest) {
2259 this.command_id = this.getCommandId();
2260 let curWindow = this.getCurrentWindow();
2261 let or = curWindow.screen.mozOrientation;
2262 this.sendResponse(or, this.command_id);
2263 },
2265 /**
2266 * Set the current browser orientation.
2267 *
2268 * The supplied orientation should be given as one of the valid
2269 * orientation values. If the orientation is unknown, an error will
2270 * be raised.
2271 *
2272 * Valid orientations are "portrait" and "landscape", which fall
2273 * back to "portrait-primary" and "landscape-primary" respectively,
2274 * and "portrait-secondary" as well as "landscape-secondary".
2275 */
2276 setScreenOrientation: function MDA_setScreenOrientation(aRequest) {
2277 const ors = ["portrait", "landscape",
2278 "portrait-primary", "landscape-primary",
2279 "portrait-secondary", "landscape-secondary"];
2281 this.command_id = this.getCommandId();
2282 let or = String(aRequest.parameters.orientation);
2284 let mozOr = or.toLowerCase();
2285 if (ors.indexOf(mozOr) < 0) {
2286 this.sendError("Unknown screen orientation: " + or, 500, null,
2287 this.command_id);
2288 return;
2289 }
2291 let curWindow = this.getCurrentWindow();
2292 if (!curWindow.screen.mozLockOrientation(mozOr)) {
2293 this.sendError("Unable to set screen orientation: " + or, 500,
2294 null, this.command_id);
2295 }
2296 this.sendOk(this.command_id);
2297 },
2299 /**
2300 * Helper function to convert an outerWindowID into a UID that Marionette
2301 * tracks.
2302 */
2303 generateFrameId: function MDA_generateFrameId(id) {
2304 let uid = id + (appName == "B2G" ? "-b2g" : "");
2305 return uid;
2306 },
2308 /**
2309 * Receives all messages from content messageManager
2310 */
2311 receiveMessage: function MDA_receiveMessage(message) {
2312 // We need to just check if we need to remove the mozbrowserclose listener
2313 if (this.mozBrowserClose !== null){
2314 let curWindow = this.getCurrentWindow();
2315 curWindow.removeEventListener('mozbrowserclose', this.mozBrowserClose, true);
2316 this.mozBrowserClose = null;
2317 }
2319 switch (message.name) {
2320 case "Marionette:done":
2321 this.sendResponse(message.json.value, message.json.command_id);
2322 break;
2323 case "Marionette:ok":
2324 this.sendOk(message.json.command_id);
2325 break;
2326 case "Marionette:error":
2327 this.sendError(message.json.message, message.json.status, message.json.stacktrace, message.json.command_id);
2328 break;
2329 case "Marionette:log":
2330 //log server-side messages
2331 logger.info(message.json.message);
2332 break;
2333 case "Marionette:shareData":
2334 //log messages from tests
2335 if (message.json.log) {
2336 this.marionetteLog.addLogs(message.json.log);
2337 }
2338 break;
2339 case "Marionette:runEmulatorCmd":
2340 case "Marionette:runEmulatorShell":
2341 this.sendToClient(message.json, -1);
2342 break;
2343 case "Marionette:switchToFrame":
2344 this.curBrowser.frameManager.switchToFrame(message);
2345 this.messageManager = this.curBrowser.frameManager.currentRemoteFrame.messageManager.get();
2346 break;
2347 case "Marionette:switchToModalOrigin":
2348 this.curBrowser.frameManager.switchToModalOrigin(message);
2349 this.messageManager = this.curBrowser.frameManager.currentRemoteFrame.messageManager.get();
2350 break;
2351 case "Marionette:switchedToFrame":
2352 logger.info("Switched to frame: " + JSON.stringify(message.json));
2353 if (message.json.restorePrevious) {
2354 this.currentFrameElement = this.previousFrameElement;
2355 }
2356 else {
2357 if (message.json.storePrevious) {
2358 // we don't arbitrarily save previousFrameElement, since
2359 // we allow frame switching after modals appear, which would
2360 // override this value and we'd lose our reference
2361 this.previousFrameElement = this.currentFrameElement;
2362 }
2363 this.currentFrameElement = message.json.frameValue;
2364 }
2365 break;
2366 case "Marionette:register":
2367 // This code processes the content listener's registration information
2368 // and either accepts the listener, or ignores it
2369 let nullPrevious = (this.curBrowser.curFrameId == null);
2370 let listenerWindow =
2371 Services.wm.getOuterWindowWithId(message.json.value);
2373 //go in here if we're already in a remote frame.
2374 if ((!listenerWindow || (listenerWindow.location &&
2375 listenerWindow.location.href != message.json.href)) &&
2376 (this.curBrowser.frameManager.currentRemoteFrame !== null)) {
2377 // The outerWindowID from an OOP frame will not be meaningful to
2378 // the parent process here, since each process maintains its own
2379 // independent window list. So, it will either be null (!listenerWindow)
2380 // if we're already in a remote frame,
2381 // or it will point to some random window, which will hopefully
2382 // cause an href mismatch. Currently this only happens
2383 // in B2G for OOP frames registered in Marionette:switchToFrame, so
2384 // we'll acknowledge the switchToFrame message here.
2385 // XXX: Should have a better way of determining that this message
2386 // is from a remote frame.
2387 this.curBrowser.frameManager.currentRemoteFrame.targetFrameId = this.generateFrameId(message.json.value);
2388 this.sendOk(this.command_id);
2389 }
2391 let browserType;
2392 try {
2393 browserType = message.target.getAttribute("type");
2394 } catch (ex) {
2395 // browserType remains undefined.
2396 }
2397 let reg = {};
2398 // this will be sent to tell the content process if it is the main content
2399 let mainContent = (this.curBrowser.mainContentId == null);
2400 if (!browserType || browserType != "content") {
2401 //curBrowser holds all the registered frames in knownFrames
2402 reg.id = this.curBrowser.register(this.generateFrameId(message.json.value),
2403 listenerWindow);
2404 }
2405 // set to true if we updated mainContentId
2406 mainContent = ((mainContent == true) && (this.curBrowser.mainContentId != null));
2407 if (mainContent) {
2408 this.mainContentFrameId = this.curBrowser.curFrameId;
2409 }
2410 this.curBrowser.elementManager.seenItems[reg.id] = Cu.getWeakReference(listenerWindow);
2411 if (nullPrevious && (this.curBrowser.curFrameId != null)) {
2412 if (!this.sendAsync("newSession",
2413 { B2G: (appName == "B2G") },
2414 this.newSessionCommandId)) {
2415 return;
2416 }
2417 if (this.curBrowser.newSession) {
2418 this.getSessionCapabilities();
2419 this.newSessionCommandId = null;
2420 }
2421 }
2422 return [reg, mainContent];
2423 case "Marionette:emitTouchEvent":
2424 let globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
2425 .getService(Ci.nsIMessageBroadcaster);
2426 globalMessageManager.broadcastAsyncMessage(
2427 "MarionetteMainListener:emitTouchEvent", message.json);
2428 return;
2429 }
2430 }
2431 };
2433 MarionetteServerConnection.prototype.requestTypes = {
2434 "getMarionetteID": MarionetteServerConnection.prototype.getMarionetteID,
2435 "sayHello": MarionetteServerConnection.prototype.sayHello,
2436 "newSession": MarionetteServerConnection.prototype.newSession,
2437 "getSessionCapabilities": MarionetteServerConnection.prototype.getSessionCapabilities,
2438 "log": MarionetteServerConnection.prototype.log,
2439 "getLogs": MarionetteServerConnection.prototype.getLogs,
2440 "setContext": MarionetteServerConnection.prototype.setContext,
2441 "executeScript": MarionetteServerConnection.prototype.execute,
2442 "setScriptTimeout": MarionetteServerConnection.prototype.setScriptTimeout,
2443 "timeouts": MarionetteServerConnection.prototype.timeouts,
2444 "singleTap": MarionetteServerConnection.prototype.singleTap,
2445 "actionChain": MarionetteServerConnection.prototype.actionChain,
2446 "multiAction": MarionetteServerConnection.prototype.multiAction,
2447 "executeAsyncScript": MarionetteServerConnection.prototype.executeWithCallback,
2448 "executeJSScript": MarionetteServerConnection.prototype.executeJSScript,
2449 "setSearchTimeout": MarionetteServerConnection.prototype.setSearchTimeout,
2450 "findElement": MarionetteServerConnection.prototype.findElement,
2451 "findElements": MarionetteServerConnection.prototype.findElements,
2452 "clickElement": MarionetteServerConnection.prototype.clickElement,
2453 "getElementAttribute": MarionetteServerConnection.prototype.getElementAttribute,
2454 "getElementText": MarionetteServerConnection.prototype.getElementText,
2455 "getElementTagName": MarionetteServerConnection.prototype.getElementTagName,
2456 "isElementDisplayed": MarionetteServerConnection.prototype.isElementDisplayed,
2457 "getElementValueOfCssProperty": MarionetteServerConnection.prototype.getElementValueOfCssProperty,
2458 "submitElement": MarionetteServerConnection.prototype.submitElement,
2459 "getElementSize": MarionetteServerConnection.prototype.getElementSize,
2460 "isElementEnabled": MarionetteServerConnection.prototype.isElementEnabled,
2461 "isElementSelected": MarionetteServerConnection.prototype.isElementSelected,
2462 "sendKeysToElement": MarionetteServerConnection.prototype.sendKeysToElement,
2463 "getElementLocation": MarionetteServerConnection.prototype.getElementLocation,
2464 "getElementPosition": MarionetteServerConnection.prototype.getElementLocation, // deprecated
2465 "clearElement": MarionetteServerConnection.prototype.clearElement,
2466 "getTitle": MarionetteServerConnection.prototype.getTitle,
2467 "getWindowType": MarionetteServerConnection.prototype.getWindowType,
2468 "getPageSource": MarionetteServerConnection.prototype.getPageSource,
2469 "get": MarionetteServerConnection.prototype.get,
2470 "goUrl": MarionetteServerConnection.prototype.get, // deprecated
2471 "getCurrentUrl": MarionetteServerConnection.prototype.getCurrentUrl,
2472 "getUrl": MarionetteServerConnection.prototype.getCurrentUrl, // deprecated
2473 "goBack": MarionetteServerConnection.prototype.goBack,
2474 "goForward": MarionetteServerConnection.prototype.goForward,
2475 "refresh": MarionetteServerConnection.prototype.refresh,
2476 "getWindowHandle": MarionetteServerConnection.prototype.getWindowHandle,
2477 "getCurrentWindowHandle": MarionetteServerConnection.prototype.getWindowHandle, // Selenium 2 compat
2478 "getWindow": MarionetteServerConnection.prototype.getWindowHandle, // deprecated
2479 "getWindowHandles": MarionetteServerConnection.prototype.getWindowHandles,
2480 "getCurrentWindowHandles": MarionetteServerConnection.prototype.getWindowHandles, // Selenium 2 compat
2481 "getWindows": MarionetteServerConnection.prototype.getWindowHandles, // deprecated
2482 "getActiveFrame": MarionetteServerConnection.prototype.getActiveFrame,
2483 "switchToFrame": MarionetteServerConnection.prototype.switchToFrame,
2484 "switchToWindow": MarionetteServerConnection.prototype.switchToWindow,
2485 "deleteSession": MarionetteServerConnection.prototype.deleteSession,
2486 "emulatorCmdResult": MarionetteServerConnection.prototype.emulatorCmdResult,
2487 "importScript": MarionetteServerConnection.prototype.importScript,
2488 "clearImportedScripts": MarionetteServerConnection.prototype.clearImportedScripts,
2489 "getAppCacheStatus": MarionetteServerConnection.prototype.getAppCacheStatus,
2490 "close": MarionetteServerConnection.prototype.close,
2491 "closeWindow": MarionetteServerConnection.prototype.close, // deprecated
2492 "setTestName": MarionetteServerConnection.prototype.setTestName,
2493 "takeScreenshot": MarionetteServerConnection.prototype.takeScreenshot,
2494 "screenShot": MarionetteServerConnection.prototype.takeScreenshot, // deprecated
2495 "screenshot": MarionetteServerConnection.prototype.takeScreenshot, // Selenium 2 compat
2496 "addCookie": MarionetteServerConnection.prototype.addCookie,
2497 "getCookies": MarionetteServerConnection.prototype.getCookies,
2498 "getAllCookies": MarionetteServerConnection.prototype.getCookies, // deprecated
2499 "deleteAllCookies": MarionetteServerConnection.prototype.deleteAllCookies,
2500 "deleteCookie": MarionetteServerConnection.prototype.deleteCookie,
2501 "getActiveElement": MarionetteServerConnection.prototype.getActiveElement,
2502 "getScreenOrientation": MarionetteServerConnection.prototype.getScreenOrientation,
2503 "setScreenOrientation": MarionetteServerConnection.prototype.setScreenOrientation
2504 };
2506 /**
2507 * Creates a BrowserObj. BrowserObjs handle interactions with the
2508 * browser, according to the current environment (desktop, b2g, etc.)
2509 *
2510 * @param nsIDOMWindow win
2511 * The window whose browser needs to be accessed
2512 */
2514 function BrowserObj(win, server) {
2515 this.DESKTOP = "desktop";
2516 this.B2G = "B2G";
2517 this.browser;
2518 this.tab = null; //Holds a reference to the created tab, if any
2519 this.window = win;
2520 this.knownFrames = [];
2521 this.curFrameId = null;
2522 this.startPage = "about:blank";
2523 this.mainContentId = null; // used in B2G to identify the homescreen content page
2524 this.newSession = true; //used to set curFrameId upon new session
2525 this.elementManager = new ElementManager([SELECTOR, NAME, LINK_TEXT, PARTIAL_LINK_TEXT]);
2526 this.setBrowser(win);
2527 this.frameManager = new FrameManager(server); //We should have one FM per BO so that we can handle modals in each Browser
2529 //register all message listeners
2530 this.frameManager.addMessageManagerListeners(server.messageManager);
2531 }
2533 BrowserObj.prototype = {
2534 /**
2535 * Set the browser if the application is not B2G
2536 *
2537 * @param nsIDOMWindow win
2538 * current window reference
2539 */
2540 setBrowser: function BO_setBrowser(win) {
2541 switch (appName) {
2542 case "Firefox":
2543 this.browser = win.gBrowser;
2544 break;
2545 case "Fennec":
2546 this.browser = win.BrowserApp;
2547 break;
2548 }
2549 },
2550 /**
2551 * Called when we start a session with this browser.
2552 *
2553 * In a desktop environment, if newTab is true, it will start
2554 * a new 'about:blank' tab and change focus to this tab.
2555 *
2556 * This will also set the active messagemanager for this object
2557 *
2558 * @param boolean newTab
2559 * If true, create new tab
2560 */
2561 startSession: function BO_startSession(newTab, win, callback) {
2562 if (appName != "Firefox") {
2563 callback(win, newTab);
2564 }
2565 else if (newTab) {
2566 this.tab = this.addTab(this.startPage);
2567 //if we have a new tab, make it the selected tab
2568 this.browser.selectedTab = this.tab;
2569 let newTabBrowser = this.browser.getBrowserForTab(this.tab);
2570 // wait for tab to be loaded
2571 newTabBrowser.addEventListener("load", function onLoad() {
2572 newTabBrowser.removeEventListener("load", onLoad, true);
2573 callback(win, newTab);
2574 }, true);
2575 }
2576 else {
2577 //set this.tab to the currently focused tab
2578 if (this.browser != undefined && this.browser.selectedTab != undefined) {
2579 this.tab = this.browser.selectedTab;
2580 }
2581 callback(win, newTab);
2582 }
2583 },
2585 /**
2586 * Closes current tab
2587 */
2588 closeTab: function BO_closeTab() {
2589 if (this.tab != null && (appName != "B2G")) {
2590 this.browser.removeTab(this.tab);
2591 this.tab = null;
2592 }
2593 },
2595 /**
2596 * Opens a tab with given uri
2597 *
2598 * @param string uri
2599 * URI to open
2600 */
2601 addTab: function BO_addTab(uri) {
2602 return this.browser.addTab(uri, true);
2603 },
2605 /**
2606 * Loads content listeners if we don't already have them
2607 *
2608 * @param string script
2609 * path of script to load
2610 * @param nsIDOMWindow frame
2611 * frame to load the script in
2612 */
2613 loadFrameScript: function BO_loadFrameScript(script, frame) {
2614 frame.window.messageManager.loadFrameScript(script, true, true);
2615 Services.prefs.setBoolPref("marionette.contentListener", true);
2616 },
2618 /**
2619 * Registers a new frame, and sets its current frame id to this frame
2620 * if it is not already assigned, and if a) we already have a session
2621 * or b) we're starting a new session and it is the right start frame.
2622 *
2623 * @param string uid
2624 * frame uid
2625 * @param object frameWindow
2626 * the DOMWindow object of the frame that's being registered
2627 */
2628 register: function BO_register(uid, frameWindow) {
2629 if (this.curFrameId == null) {
2630 // If we're setting up a new session on Firefox, we only process the
2631 // registration for this frame if it belongs to the tab we've just
2632 // created.
2633 if ((!this.newSession) ||
2634 (this.newSession &&
2635 ((appName != "Firefox") ||
2636 frameWindow == this.browser.getBrowserForTab(this.tab).contentWindow))) {
2637 this.curFrameId = uid;
2638 this.mainContentId = uid;
2639 }
2640 }
2641 this.knownFrames.push(uid); //used to delete sessions
2642 return uid;
2643 },
2644 }
2646 /**
2647 * Marionette server -- this class holds a reference to a socket and creates
2648 * MarionetteServerConnection objects as needed.
2649 */
2650 this.MarionetteServer = function MarionetteServer(port, forceLocal) {
2651 let flags = Ci.nsIServerSocket.KeepWhenOffline;
2652 if (forceLocal) {
2653 flags |= Ci.nsIServerSocket.LoopbackOnly;
2654 }
2655 let socket = new ServerSocket(port, flags, 0);
2656 logger.info("Listening on port " + socket.port + "\n");
2657 socket.asyncListen(this);
2658 this.listener = socket;
2659 this.nextConnId = 0;
2660 this.connections = {};
2661 };
2663 MarionetteServer.prototype = {
2664 onSocketAccepted: function(serverSocket, clientSocket)
2665 {
2666 logger.debug("accepted connection on " + clientSocket.host + ":" + clientSocket.port);
2668 let input = clientSocket.openInputStream(0, 0, 0);
2669 let output = clientSocket.openOutputStream(0, 0, 0);
2670 let aTransport = new DebuggerTransport(input, output);
2671 let connID = "conn" + this.nextConnID++ + '.';
2672 let conn = new MarionetteServerConnection(connID, aTransport, this);
2673 this.connections[connID] = conn;
2675 // Create a root actor for the connection and send the hello packet.
2676 conn.sayHello();
2677 aTransport.ready();
2678 },
2680 closeListener: function() {
2681 this.listener.close();
2682 this.listener = null;
2683 },
2685 _connectionClosed: function DS_connectionClosed(aConnection) {
2686 delete this.connections[aConnection.prefix];
2687 }
2688 };