Bug 1123506 - Evaluate scripts in content with lasting side-effects; r=automatedtester a=test-only
authorAndreas Tolfsen <ato@mozilla.com>
Fri, 26 Feb 2016 14:36:39 +0000
changeset 333234 ba30a69fed983e083be59ac9c273b2dc6ec3f59e
parent 333233 58cc44dac6d5a6e4d39a494401c18ac7cdc8a40a
child 333235 ed39408d402720c6915be8335ec73bbbd7176929
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester, test-only
bugs1123506
milestone48.0a2
Bug 1123506 - Evaluate scripts in content with lasting side-effects; r=automatedtester a=test-only In order to achieve WebDriver parity, Marionette needs the ability to evaluate scripts in content space with lasting side-effects. This means that state modifications should affect behaviour and state of the browsing context, and such transgress the boundaries of the sandbox. This patch brings a new script evaluation module that is shared between code in chrome- and content space. This brings the number of unique script evaluation implementations in Marionette down from six to one. evaluate.sandbox provides the main entry-point for execution. It is compatible with existing Marionette uses of Execute Script and Execute Async Script commands in Mozilla clients, but also provides a new stateful sandbox for evaluation that should have lasting side-effects. It is not expected that Mozilla clients, such as testing/marionette/client and the Node.js client in Gaia, should have to change as a consequence of this change. A substantial change to the script's runtime environment is that many globals that previously existed are now only exposed whenever needed. This means for example that Simple Test harness functionality (waitFor, ok, isnot, is, &c.) is only available when using a sandbox augmented with a Simple Test harness adapter. Conversely, this patch does not expose marionetteScriptFinished as a callback to asynchronous scripts for sandboxes which sandboxName parameter is undefined, because this is what determines if the script should be evaluated under WebDriver conformance constraints. In all other cases where sandboxName _is_ defined, the traditional marionetteScriptFinished et al. runtime environment is preserved. MozReview-Commit-ID: 8FZ6rNVImuC
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/common.js
testing/marionette/dispatcher.js
testing/marionette/driver.js
testing/marionette/emulator.js
testing/marionette/error.js
testing/marionette/evaluate.js
testing/marionette/harness/marionette/marionette_test.py
testing/marionette/harness/marionette/tests/unit/test_click_chrome.py
testing/marionette/harness/marionette/tests/unit/test_execute_sandboxes.py
testing/marionette/harness/marionette/tests/unit/test_execute_script.py
testing/marionette/harness/marionette/tests/unit/test_log.py
testing/marionette/harness/marionette/tests/unit/test_simpletest_sanity.py
testing/marionette/jar.mn
testing/marionette/listener.js
testing/marionette/logging.js
testing/marionette/proxy.js
testing/marionette/simpletest.js
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -939,22 +939,28 @@ class Marionette(object):
                 let secMan = Services.scriptSecurityManager;
                 let attrs = {appId: perm.appId, inIsolatedMozBrowser: perm.isInIsolatedMozBrowserElement};
                 let principal = secMan.createCodebasePrincipal(Services.io.newURI(perm.url, null, null),
                                                                attrs);
                 Services.perms.addFromPrincipal(principal, perm.type, perm.action);
                 return true;
                 """, script_args=[perm])
 
-        with self.using_context('content'):
+        with self.using_context("content"):
             self.execute_async_script("""
-                waitFor(marionetteScriptFinished, function() {
-                  return window.wrappedJSObject.permChanged;
-                });
-                """, sandbox='system')
+                let start = new Date();
+                let end = new Date(start.valueOf() + 5000);
+                let wait = function() {
+                  let now = new Date();
+                  if (window.wrappedJSObject.permChanged || end >= now) {
+                    marionetteScriptFinished();
+                  }
+                };
+                window.setTimeout(wait, 100);
+                """, sandbox="system")
 
     @contextmanager
     def using_permissions(self, perms):
         '''
         Sets permissions for code being executed in a `with` block,
         and restores them on exit.
 
         :param perms: A dict containing one or more perms and their
@@ -1773,23 +1779,23 @@ class Marionette(object):
         return self._send_message(
             "findElements", body, key="value" if self.protocol == 1 else None)
 
 
     def get_active_element(self):
         el = self._send_message("getActiveElement", key="value")
         return HTMLElement(self, el)
 
-    def log(self, msg, level=None):
+    def log(self, msg, level="INFO"):
         """Stores a timestamped log message in the Marionette server
         for later retrieval.
 
         :param msg: String with message to log.
-        :param level: String with log level (e.g. "INFO" or "DEBUG"). If None,
-            defaults to "INFO".
+        :param level: String with log level (e.g. "INFO" or "DEBUG").
+            Defaults to "INFO".
         """
         body = {"value": msg, "level": level}
         self._send_message("log", body)
 
     def get_logs(self):
         """Returns the list of logged messages.
 
         Each log message is an array with three string elements: the level,
deleted file mode 100644
--- a/testing/marionette/common.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// This file contains common code that is shared between
-// driver.jj and listener.js.
-
-/**
- * Creates an error message for a JavaScript exception thrown during
- * execute_(async_)script.
- *
- * This will generate a [msg, trace] pair like:
- *
- * ['ReferenceError: foo is not defined',
- *  'execute_script @test_foo.py, line 10
- *   inline javascript, line 2
- *   src: "return foo;"']
- *
- * @param error An Error object passed to a catch() clause.
-          fnName The name of the function to use in the stack trace message
-                 (e.g., 'execute_script').
-          pythonFile The filename of the test file containing the Marionette
-                  command that caused this exception to occur.
-          pythonLine The line number of the above test file.
-          script The JS script being executed in text form.
- */
-this.createStackMessage = function createStackMessage(error, fnName, pythonFile,
-  pythonLine, script) {
-  let python_stack = fnName + " @" + pythonFile;
-  if (pythonLine !== null) {
-    python_stack += ", line " + pythonLine;
-  }
-  let trace, msg;
-  if (typeof(error) == "object" && 'name' in error && 'stack' in error) {
-    let stack = error.stack.split("\n");
-    let match = stack[0].match(/:(\d+):\d+$/);
-    let line = match ? parseInt(match[1]) : 0;
-    msg = error.name + ('message' in error ? ": " + error.message : "");
-    trace = python_stack +
-                "\ninline javascript, line " + line +
-                "\nsrc: \"" + script.split("\n")[line] + "\"";
-  }
-  else {
-    trace = python_stack;
-    msg = error + "";
-  }
-  return [msg, trace];
-}
-
-this.MarionetteLogObj = function MarionetteLogObj() {
-  this.logs = [];
-}
-MarionetteLogObj.prototype = {
-  /**
-   * Log message. Accepts user defined log-level.
-   * @param msg String
-   *        The message to be logged
-   * @param level String
-   *        The logging level to be used
-   */
-  log: function ML_log(msg, level) {
-    let lev = level ? level : "INFO";
-    this.logs.push( [lev, msg, (new Date()).toString()]);
-  },
-
-  /**
-   * Add a list of logs to its list
-   * @param msgs Object
-   *        Takes a list of strings
-   */
-  addLogs: function ML_addLogs(msgs) {
-    for (let i = 0; i < msgs.length; i++) {
-      this.logs.push(msgs[i]);
-    }
-  },
-
-  /**
-   * Return all logged messages.
-   */
-  getLogs: function ML_getLogs() {
-    let logs = this.logs;
-    this.clearLogs();
-    return logs;
-  },
-
-  /**
-   * Clears the logs
-   */
-  clearLogs: function ML_clearLogs() {
-    this.logs = [];
-  },
-}
--- a/testing/marionette/dispatcher.js
+++ b/testing/marionette/dispatcher.js
@@ -24,35 +24,35 @@ const logger = Log.repository.getLogger(
 /**
  * Manages a Marionette connection, and dispatches packets received to
  * their correct destinations.
  *
  * @param {number} connId
  *     Unique identifier of the connection this dispatcher should handle.
  * @param {DebuggerTransport} transport
  *     Debugger transport connection to the client.
- * @param {function(Emulator): GeckoDriver} driverFactory
- *     A factory function that takes an Emulator as argument and produces
+ * @param {function(EmulatorService): GeckoDriver} driverFactory
+ *     A factory function that takes an EmulatorService and produces
  *     a GeckoDriver.
  */
 this.Dispatcher = function(connId, transport, driverFactory) {
   this.connId = connId;
   this.conn = transport;
 
   // transport hooks are Dispatcher#onPacket
   // and Dispatcher#onClosed
   this.conn.hooks = this;
 
   // callback for when connection is closed
   this.onclose = null;
 
   // last received/sent message ID
   this.lastId = 0;
 
-  this.emulator = new Emulator(this.sendEmulator.bind(this));
+  this.emulator = new emulator.EmulatorService(this.sendEmulator.bind(this));
   this.driver = driverFactory(this.emulator);
 
   // lookup of commands sent by server to client by message ID
   this.commands_ = new Map();
 };
 
 /**
  * Debugger transport callback that cleans up
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -7,39 +7,35 @@
 var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
     .getService(Ci.mozIJSSubScriptLoader);
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-var {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-this.DevToolsUtils = devtools.require("devtools/shared/DevToolsUtils");
-
 XPCOMUtils.defineLazyServiceGetter(
     this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2");
 
 Cu.import("chrome://marionette/content/action.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/element.js");
+Cu.import("chrome://marionette/content/emulator.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/frame.js");
 Cu.import("chrome://marionette/content/interaction.js");
+Cu.import("chrome://marionette/content/logging.js");
 Cu.import("chrome://marionette/content/modal.js");
 Cu.import("chrome://marionette/content/proxy.js");
 Cu.import("chrome://marionette/content/simpletest.js");
 
-loader.loadSubScript("chrome://marionette/content/common.js");
-
 this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
 
 var FRAME_SCRIPT = "chrome://marionette/content/listener.js";
 const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
 const CONTENT_LISTENER_PREF = "marionette.contentListener";
 
@@ -86,18 +82,19 @@ this.Context.fromString = function(s) {
  * object.
  *
  * @param {string} appName
  *     Description of the product, for example "B2G" or "Firefox".
  * @param {string} device
  *     Device this driver should assume.
  * @param {function()} stopSignal
  *     Signal to stop the Marionette server.
- * @param {Emulator=} emulator
- *     Reference to the emulator connection, if running on an emulator.
+ * @param {EmulatorService=} emulator
+ *     Interface that allows instructing the emulator connected to the
+ *     client to run commands and perform shell invocations.
  */
 this.GeckoDriver = function(appName, device, stopSignal, emulator) {
   this.appName = appName;
   this.stopSignal_ = stopSignal;
   this.emulator = emulator;
   // TODO(ato): hack
   this.emulator.sendToListener = this.sendAsync.bind(this);
 
@@ -107,29 +104,27 @@ this.GeckoDriver = function(appName, dev
   // points to current browser
   this.curBrowser = null;
   this.context = Context.CONTENT;
   this.scriptTimeout = null;
   this.searchTimeout = null;
   this.pageTimeout = null;
   this.timer = null;
   this.inactivityTimer = null;
-  // called by simpletest methods
-  this.heartbeatCallback = function() {};
-  this.marionetteLog = new MarionetteLogObj();
+  this.marionetteLog = new logging.ContentLogger();
   // topmost chrome frame
   this.mainFrame = null;
   // chrome iframe that currently has focus
   this.curFrame = null;
   this.mainContentFrameId = null;
   this.importedScripts = new evaluate.ScriptStorageService([Context.CHROME, Context.CONTENT]);
   this.currentFrameElement = null;
   this.testName = null;
   this.mozBrowserClose = null;
-  this.sandboxes = {};
+  this.sandboxes = new Sandboxes(() => this.getCurrentWindow());
   // frame ID of the current remote frame, used for mozbrowserclose events
   this.oopFrameId = null;
   this.observing = null;
   this._browserIds = new WeakMap();
   this.actions = new action.Chain();
 
   this.sessionCapabilities = {
     // mandated capabilities
@@ -217,20 +212,20 @@ GeckoDriver.prototype.sendAsync = functi
   // this can be removed.
   if (cmdId) {
     msg.command_id = cmdId;
   }
 
   if (curRemoteFrame === null) {
     this.curBrowser.executeWhenReady(() => {
       if (this.curBrowser.curFrameId) {
-          this.mm.broadcastAsyncMessage(name + this.curBrowser.curFrameId, msg);
-      }
-      else {
-          throw new WebDriverError("Can not send call to listener as it doesnt exist");
+        this.mm.broadcastAsyncMessage(name + this.curBrowser.curFrameId, msg);
+      } else {
+        throw new NoSuchFrameError(
+            "No such content frame; perhaps the listener was not registered?");
       }
     });
   } else {
     let remoteFrameId = curRemoteFrame.targetFrameId;
     try {
       this.mm.sendAsyncMessage(name + remoteFrameId, msg);
     } catch (e) {
       switch(e.result) {
@@ -249,17 +244,17 @@ GeckoDriver.prototype.sendAsync = functi
  *
  * @return {nsIDOMWindow}
  */
 GeckoDriver.prototype.getCurrentWindow = function() {
   let typ = null;
   if (this.curFrame === null) {
     if (this.curBrowser === null) {
       if (this.context == Context.CONTENT) {
-        typ = 'navigator:browser';
+        typ = "navigator:browser";
       }
       return Services.wm.getMostRecentWindow(typ);
     } else {
       return this.curBrowser.window;
     }
   } else {
     return this.curFrame;
   }
@@ -685,22 +680,25 @@ GeckoDriver.prototype.setUpProxy = funct
  * Log message.  Accepts user defined log-level.
  *
  * @param {string} value
  *     Log message.
  * @param {string} level
  *     Arbitrary log level.
  */
 GeckoDriver.prototype.log = function(cmd, resp) {
-  this.marionetteLog.log(cmd.parameters.value, cmd.parameters.level);
+  // if level is null, we want to use ContentLogger#send's default
+  this.marionetteLog.log(
+      cmd.parameters.value,
+      cmd.parameters.level || undefined);
 };
 
 /** Return all logged messages. */
 GeckoDriver.prototype.getLogs = function(cmd, resp) {
-  resp.body = this.marionetteLog.getLogs();
+  resp.body = this.marionetteLog.get();
 };
 
 /**
  * Sets the context of the subsequent commands to be either "chrome" or
  * "content".
  *
  * @param {string} value
  *     Name of the context to be switched to.  Must be one of "chrome" or
@@ -716,209 +714,221 @@ GeckoDriver.prototype.setContext = funct
 };
 
 /** Gets the context of the server, either "chrome" or "content". */
 GeckoDriver.prototype.getContext = function(cmd, resp) {
   resp.body.value = this.context.toString();
 };
 
 /**
- * Returns a chrome sandbox that can be used by the execute and
- * executeWithCallback functions.
+ * Executes a JavaScript function in the context of the current browsing
+ * context, if in content space, or in chrome space otherwise, and returns
+ * the return value of the function.
+ *
+ * It is important to note that if the {@code sandboxName} parameter
+ * is left undefined, the script will be evaluated in a mutable sandbox,
+ * causing any change it makes on the global state of the document to have
+ * lasting side-effects.
  *
- * @param {nsIDOMWindow} win
- *     Window in which we will execute code.
- * @param {Marionette} mn
- *     Marionette test instance.
- * @param {string} sandboxName
- *     The name for the sandbox.  If 'system', create the sandbox
- *     with elevated privileges.
+ * @param {string} script
+ *     Script to evaluate as a function body.
+ * @param {Array.<(string|boolean|number|object|WebElement)>} args
+ *     Arguments exposed to the script in {@code arguments}.  The array
+ *     items must be serialisable to the WebDriver protocol.
+ * @param {number} scriptTimeout
+ *     Duration in milliseconds of when to interrupt and abort the
+ *     script evaluation.
+ * @param {string=} sandbox
+ *     Name of the sandbox to evaluate the script in.  The sandbox is
+ *     cached for later re-use on the same Window object if
+ *     {@code newSandbox} is false.  If he parameter is undefined,
+ *     the script is evaluated in a mutable sandbox.  If the parameter
+ *     is "system", it will be evaluted in a sandbox with elevated system
+ *     privileges, equivalent to chrome space.
+ * @param {boolean=} newSandbox
+ *     Forces the script to be evaluated in a fresh sandbox.  Note that if
+ *     it is undefined, the script will normally be evaluted in a fresh
+ *     sandbox.
+ * @param {string=} filename
+ *     Filename of the client's program where this script is evaluated.
+ * @param {number=} line
+ *     Line in the client's program where this script is evaluated.
+ * @param {boolean=} debug_script
+ *     Attach an {@code onerror} event handler on the Window object.
+ *     It does not differentiate content errors from chrome errors.
+ * @param {boolean=} directInject
+ *     Evaluate the script without wrapping it in a function.
  *
- * @return {nsIXPCComponents_utils_Sandbox}
- *     Returns the sandbox.
+ * @return {(string|boolean|number|object|WebElement)}
+ *     Return value from the script, or null which signifies either the
+ *     JavaScript notion of null or undefined.
+ *
+ * @throws ScriptTimeoutError
+ *     If the script was interrupted due to reaching the {@code
+ *     scriptTimeout} or default timeout.
+ * @throws JavaScriptError
+ *     If an Error was thrown whilst evaluating the script.
  */
-GeckoDriver.prototype.createExecuteSandbox = function(win, mn, sandboxName) {
-  let principal = win;
-  if (sandboxName == 'system') {
-    principal = Cc["@mozilla.org/systemprincipal;1"].
-                createInstance(Ci.nsIPrincipal);
-  }
-  let sb = new Cu.Sandbox(principal,
-      {sandboxPrototype: win, wantXrays: false, sandboxName: ""});
-  sb.global = sb;
-  sb.proto = win;
+GeckoDriver.prototype.executeScript = function*(cmd, resp) {
+  let {script, args, scriptTimeout} = cmd.parameters;
+  scriptTimeout = scriptTimeout || this.scriptTimeout;
 
-  mn.exports.forEach(function(fn) {
-    if (typeof mn[fn] === 'function') {
-      sb[fn] = mn[fn].bind(mn);
-    } else {
-      sb[fn] = mn[fn];
-    }
-  });
+  let opts = {
+    sandboxName: cmd.parameters.sandbox,
+    newSandbox: !!(typeof cmd.parameters.newSandbox == "undefined") ||
+        cmd.parameters.newSandbox,
+    filename: cmd.parameters.filename,
+    line: cmd.parameters.line,
+    debug: cmd.parameters.debug_script,
+  };
 
-  sb.isSystemMessageListenerReady = () => systemMessageListenerReady;
-
-  this.sandboxes[sandboxName] = sb;
+  resp.body.value = yield this.execute_(script, args, scriptTimeout, opts);
 };
 
 /**
- * Apply arguments sent from the client to the current (possibly reused)
- * execution sandbox.
+ * Executes a JavaScript function in the context of the current browsing
+ * context, if in content space, or in chrome space otherwise, and returns
+ * the object passed to the callback.
+ *
+ * The callback is always the last argument to the {@code arguments}
+ * list passed to the function scope of the script.  It can be retrieved
+ * as such:
+ *
+ *     let callback = arguments[arguments.length - 1];
+ *     callback("foo");
+ *     // "foo" is returned
+ *
+ * It is important to note that if the {@code sandboxName} parameter
+ * is left undefined, the script will be evaluated in a mutable sandbox,
+ * causing any change it makes on the global state of the document to have
+ * lasting side-effects.
+ *
+ * @param {string} script
+ *     Script to evaluate as a function body.
+ * @param {Array.<(string|boolean|number|object|WebElement)>} args
+ *     Arguments exposed to the script in {@code arguments}.  The array
+ *     items must be serialisable to the WebDriver protocol.
+ * @param {number} scriptTimeout
+ *     Duration in milliseconds of when to interrupt and abort the
+ *     script evaluation.
+ * @param {string=} sandbox
+ *     Name of the sandbox to evaluate the script in.  The sandbox is
+ *     cached for later re-use on the same Window object if
+ *     {@code newSandbox} is false.  If he parameter is undefined,
+ *     the script is evaluated in a mutable sandbox.  If the parameter
+ *     is "system", it will be evaluted in a sandbox with elevated system
+ *     privileges, equivalent to chrome space.
+ * @param {boolean=} newSandbox
+ *     Forces the script to be evaluated in a fresh sandbox.  Note that if
+ *     it is undefined, the script will normally be evaluted in a fresh
+ *     sandbox.
+ * @param {string=} filename
+ *     Filename of the client's program where this script is evaluated.
+ * @param {number=} line
+ *     Line in the client's program where this script is evaluated.
+ * @param {boolean=} debug_script
+ *     Attach an {@code onerror} event handler on the Window object.
+ *     It does not differentiate content errors from chrome errors.
+ * @param {boolean=} directInject
+ *     Evaluate the script without wrapping it in a function.
+ *
+ * @return {(string|boolean|number|object|WebElement)}
+ *     Return value from the script, or null which signifies either the
+ *     JavaScript notion of null or undefined.
+ *
+ * @throws ScriptTimeoutError
+ *     If the script was interrupted due to reaching the {@code
+ *     scriptTimeout} or default timeout.
+ * @throws JavaScriptError
+ *     If an Error was thrown whilst evaluating the script.
  */
-GeckoDriver.prototype.applyArgumentsToSandbox = function(win, sb, args) {
-  sb.__marionetteParams = this.curBrowser.elementManager.convertWrappedArguments(args,
-    { frame: win });
-  sb.__namedArgs = this.curBrowser.elementManager.applyNamedArgs(args);
+GeckoDriver.prototype.executeAsyncScript = function(cmd, resp) {
+  let {script, args, scriptTimeout} = cmd.parameters;
+  scriptTimeout = scriptTimeout || this.scriptTimeout;
+
+  let opts = {
+    sandboxName: cmd.parameters.sandbox,
+    newSandbox: !!(typeof cmd.parameters.newSandbox == "undefined") ||
+        cmd.parameters.newSandbox,
+    filename: cmd.parameters.filename,
+    line: cmd.parameters.line,
+    debug: cmd.parameters.debug_script,
+    async: true,
+  };
+
+  resp.body.value = yield this.execute_(script, args, scriptTimeout, opts);
 };
 
-/**
- * Executes a script in the given sandbox.
- *
- * @param {Response} resp
- *     Response object given to the command calling this routine.
- * @param {nsIXPCComponents_utils_Sandbox} sandbox
- *     Sandbox in which the script will run.
- * @param {string} script
- *     Script to run.
- * @param {boolean} directInject
- *     If true, then the script will be run as is, and not as a function
- *     body (as you would do using the WebDriver spec).
- * @param {boolean} async
- *     True if the script is asynchronous.
- * @param {number} timeout
- *     When to interrupt script in milliseconds.
- * @param {string} filename
- *     Optional. URI or name of the file we are executing.
- *     (Used to improve stack trace readability)
- */
-GeckoDriver.prototype.executeScriptInSandbox = function(
-    resp,
-    sandbox,
-    script,
-    directInject,
-    async,
-    timeout,
-    filename) {
-  if (directInject && async && (timeout === null || timeout === 0)) {
-    throw new TimeoutError("Please set a timeout");
-  }
+GeckoDriver.prototype.execute_ = function(script, args, timeout, opts = {}) {
+  switch (this.context) {
+    case Context.CONTENT:
+      // evaluate in content with lasting side-effects
+      if (!opts.sandboxName) {
+        return this.listener.execute(script, args, timeout, opts);
+
+      // evaluate in content with sandbox
+      } else {
+        return this.listener.executeInSandbox(script, args, timeout, opts);
+      }
 
-  script = this.importedScripts.for(Context.CHROME).concat(script);
-  let res = Cu.evalInSandbox(script, sandbox, "1.8", filename ? filename : "dummy file", 0);
+    case Context.CHROME:
+      let sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox);
+      if (opts.sandboxName) {
+        sb = sandbox.augment(sb, new logging.Adapter(this.marionetteLog));
+        sb = sandbox.augment(sb, {global: sb});
+        sb = sandbox.augment(sb, new emulator.Adapter(this.emulator));
+      }
 
-  if (directInject && !async &&
-      (typeof res == "undefined" || typeof res.passed == "undefined")) {
-    throw new WebDriverError("finish() not called");
-  }
-
-  if (!async) {
-    // It's fine to pass on and modify resp here because
-    // executeScriptInSandbox is the last function to be called
-    // in execute and executeWithCallback respectively.
-    resp.body.value = this.curBrowser.elementManager.wrapValue(res);
+      opts.timeout = timeout;
+      script = this.importedScripts.for(Context.CHROME).concat(script);
+      let wargs = this.curBrowser.elementManager.convertWrappedArguments(args, {frame: sb.window});
+      let evaluatePromise = evaluate.sandbox(sb, script, wargs, opts);
+      return evaluatePromise.then(res => this.curBrowser.elementManager.wrapValue(res));
   }
 };
 
 /**
- * Execute the given script either as a function body or directly (for
- * mochitest-like JS Marionette tests).
+ * Execute pure JavaScript.  Used to execute simpletest harness tests,
+ * which are like mochitests only injected using Marionette.
  *
- * If directInject is ture, it will run directly and not as a function
- * body.
+ * Scripts are expected to call the {@code finish} global when done.
  */
-GeckoDriver.prototype.execute = function*(cmd, resp, directInject) {
-  let {inactivityTimeout,
-       scriptTimeout,
-       script,
-       newSandbox,
-       args,
-       filename,
-       line} = cmd.parameters;
-  let sandboxName = cmd.parameters.sandbox || 'default';
+GeckoDriver.prototype.executeJSScript = function(cmd, resp) {
+  let {script, args, scriptTimeout} = cmd.parameters;
+  scriptTimeout = scriptTimeout || this.scriptTimeout;
 
-  if (!scriptTimeout) {
-    scriptTimeout = this.scriptTimeout;
-  }
-  if (typeof newSandbox == "undefined") {
-    newSandbox = true;
-  }
-
-  if (this.context == Context.CONTENT) {
-    resp.body.value = yield this.listener.executeScript({
-      script: script,
-      args: args,
-      newSandbox: newSandbox,
-      timeout: scriptTimeout,
-      filename: filename,
-      line: line,
-      sandboxName: sandboxName
-    });
-    return;
-  }
+  let opts = {
+    filename: cmd.parameters.filename,
+    line: cmd.parameters.line,
+    async: cmd.parameters.async,
+  };
 
-  // handle the inactivity timeout
-  let that = this;
-  if (inactivityTimeout) {
-    let setTimer = function() {
-      that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-      if (that.inactivityTimer !== null) {
-        that.inactivityTimer.initWithCallback(function() {
-          throw new ScriptTimeoutError("timed out due to inactivity");
-        }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
-      }
-    };
-    setTimer();
-    this.heartbeatCallback = function() {
-      that.inactivityTimer.cancel();
-      setTimer();
-    };
-  }
+  switch (this.context) {
+    case Context.CHROME:
+      let win = this.getCurrentWindow();
+      let wargs = this.curBrowser.elementManager.convertWrappedArguments(args, {frame: win});
+      let harness = new simpletest.Harness(
+          win,
+          Context.CHROME,
+          this.marionetteLog,
+          scriptTimeout,
+          function() {},
+          this.testName);
 
-  let win = this.getCurrentWindow();
-  if (newSandbox ||
-      !(sandboxName in this.sandboxes) ||
-      (this.sandboxes[sandboxName].proto != win)) {
-    let marionette = new Marionette(
-        win,
-        "chrome",
-        this.marionetteLog,
-        scriptTimeout,
-        this.heartbeatCallback,
-        this.testName);
-    this.createExecuteSandbox(
-        win,
-        marionette,
-        sandboxName);
-    if (!this.sandboxes[sandboxName]) {
-      return;
-    }
-  }
-  this.applyArgumentsToSandbox(win, this.sandboxes[sandboxName], args);
+      let sb = sandbox.createSimpleTest(win, harness);
+      // TODO(ato): Not sure this is needed:
+      sb = sandbox.augment(sb, new logging.Adapter(this.marionetteLog));
 
-  try {
-    this.sandboxes[sandboxName].finish = () => {
-      if (this.inactivityTimer !== null) {
-        this.inactivityTimer.cancel();
-      }
-      return this.sandboxes[sandboxName].generate_results();
-    };
+      let res = yield evaluate.sandbox(sb, script, wargs, opts);
+      resp.body.value = this.curBrowser.elementManager.wrapValue(res);
+      break;
 
-    if (!directInject) {
-      script = "var func = function() { " + script + " }; func.apply(null, __marionetteParams);";
-    }
-    this.executeScriptInSandbox(
-        resp,
-        this.sandboxes[sandboxName],
-        script,
-        directInject,
-        false /* async */,
-        scriptTimeout,
-        filename);
-  } catch (e) {
-    throw new JavaScriptError(e, "execute_script", filename, line, script);
+    case Context.CONTENT:
+      resp.body.value = yield this.listener.executeSimpleTest(script, args, scriptTimeout, opts);
+      break;
   }
 };
 
 /**
  * Set the timeout for asynchronous script execution.
  *
  * @param {number} ms
  *     Time in milliseconds.
@@ -927,227 +937,16 @@ GeckoDriver.prototype.setScriptTimeout =
   let ms = parseInt(cmd.parameters.ms);
   if (isNaN(ms)) {
     throw new WebDriverError("Not a Number");
   }
   this.scriptTimeout = ms;
 };
 
 /**
- * Execute pure JavaScript.  Used to execute mochitest-like Marionette
- * tests.
- */
-GeckoDriver.prototype.executeJSScript = function*(cmd, resp) {
-  // TODO(ato): cmd.newSandbox doesn't ever exist?
-  // All pure JS scripts will need to call
-  // Marionette.finish() to complete the test
-  if (typeof cmd.newSandbox == "undefined") {
-    // If client does not send a value in newSandbox,
-    // then they expect the same behaviour as WebDriver.
-    cmd.newSandbox = true;
-  }
-
-  switch (this.context) {
-    case Context.CHROME:
-      if (cmd.parameters.async) {
-        yield this.executeWithCallback(cmd, resp, cmd.parameters.async);
-      } else {
-        this.execute(cmd, resp, true /* async */);
-      }
-      break;
-
-    case Context.CONTENT:
-      resp.body.value = yield this.listener.executeJSScript({
-        script: cmd.parameters.script,
-        args: cmd.parameters.args,
-        newSandbox: cmd.parameters.newSandbox,
-        async: cmd.parameters.async,
-        timeout: cmd.parameters.scriptTimeout ?
-            cmd.parameters.scriptTimeout : this.scriptTimeout,
-        inactivityTimeout: cmd.parameters.inactivityTimeout,
-        filename: cmd.parameters.filename,
-        line: cmd.parameters.line,
-        sandboxName: cmd.parameters.sandbox || 'default',
-      });
-      break;
- }
-};
-
-/**
- * This function is used by executeAsync and executeJSScript to execute
- * a script in a sandbox.
- *
- * For executeJSScript, it will return a message only when the finish()
- * method is called.
- *
- * For executeAsync, it will return a response when
- * {@code marionetteScriptFinished} (equivalent to
- * {@code arguments[arguments.length-1]}) function is called,
- * or if it times out.
- *
- * If directInject is true, it will be run directly and not as a
- * function body.
- */
-GeckoDriver.prototype.executeWithCallback = function*(cmd, resp, directInject) {
-  let {script,
-      args,
-      newSandbox,
-      inactivityTimeout,
-      scriptTimeout,
-      filename,
-      line} = cmd.parameters;
-  let sandboxName = cmd.parameters.sandbox || "default";
-
-  if (!scriptTimeout) {
-    scriptTimeout = this.scriptTimeout;
-  }
-  if (typeof newSandbox == "undefined") {
-    newSandbox = true;
-  }
-
-  if (this.context == Context.CONTENT) {
-    resp.body.value = yield this.listener.executeAsyncScript({
-      script: script,
-      args: args,
-      id: cmd.id,
-      newSandbox: newSandbox,
-      timeout: scriptTimeout,
-      inactivityTimeout: inactivityTimeout,
-      filename: filename,
-      line: line,
-      sandboxName: sandboxName,
-    });
-    return;
-  }
-
-  // handle the inactivity timeout
-  let that = this;
-  if (inactivityTimeout) {
-    this.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-    if (this.inactivityTimer !== null) {
-      this.inactivityTimer.initWithCallback(function() {
-       chromeAsyncReturnFunc(new ScriptTimeoutError("timed out due to inactivity"));
-      }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
-    }
-    this.heartbeatCallback = function resetInactivityTimer() {
-      that.inactivityTimer.cancel();
-      that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-      if (that.inactivityTimer !== null) {
-        that.inactivityTimer.initWithCallback(function() {
-          chromeAsyncReturnFunc(new ScriptTimeoutError("timed out due to inactivity"));
-        }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
-      }
-    };
-  }
-
-  let win = this.getCurrentWindow();
-  let origOnError = win.onerror;
-  that.timeout = scriptTimeout;
-
-  let res = yield new Promise(function(resolve, reject) {
-    let chromeAsyncReturnFunc = function(val) {
-      if (cmd.id == that.sandboxes[sandboxName].command_id) {
-        if (that.timer !== null) {
-          that.timer.cancel();
-          that.timer = null;
-        }
-
-        win.onerror = origOnError;
-
-        if (error.isError(val)) {
-          reject(val);
-        } else {
-          resolve(val);
-        }
-      }
-
-      if (that.inactivityTimer !== null) {
-        that.inactivityTimer.cancel();
-      }
-    };
-
-    let chromeAsyncFinish = function() {
-      let res = that.sandboxes[sandboxName].generate_results();
-      chromeAsyncReturnFunc(res);
-    };
-
-    let chromeAsyncError = function(e, func, file, line, script) {
-      let err = new JavaScriptError(e, func, file, line, script);
-      chromeAsyncReturnFunc(err);
-    };
-
-    if (newSandbox || !(sandboxName in this.sandboxes)) {
-      let marionette = new Marionette(
-          win,
-          "chrome",
-          this.marionetteLog,
-          scriptTimeout,
-          this.heartbeatCallback,
-          this.testName);
-      this.createExecuteSandbox(win, marionette, sandboxName);
-    }
-    if (!this.sandboxes[sandboxName]) {
-      return;
-    }
-
-    this.sandboxes[sandboxName].command_id = cmd.id;
-    this.sandboxes[sandboxName].runEmulatorCmd =
-        (cmd, cb) => this.emulator.command(cmd, cb, chromeAsyncError);
-    this.sandboxes[sandboxName].runEmulatorShell =
-        (args, cb) => this.emulator.shell(args, cb, chromeAsyncError);
-
-    this.applyArgumentsToSandbox(win, this.sandboxes[sandboxName], args);
-
-    // NB: win.onerror is not hooked by default due to the inability to
-    // differentiate content exceptions from chrome exceptions. See bug
-    // 1128760 for more details. A debug_script flag can be set to
-    // reenable onerror hooking to help debug test scripts.
-    if (cmd.parameters.debug_script) {
-      win.onerror = function(msg, url, line) {
-        let err = new JavaScriptError(`${msg} at: ${url} line: ${line}`);
-        chromeAsyncReturnFunc(err);
-        return true;
-      };
-    }
-
-    try {
-      this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-      if (this.timer !== null) {
-        this.timer.initWithCallback(function() {
-          chromeAsyncReturnFunc(new ScriptTimeoutError("timed out"));
-        }, that.timeout, Ci.nsITimer.TYPE_ONE_SHOT);
-      }
-
-      this.sandboxes[sandboxName].returnFunc = chromeAsyncReturnFunc;
-      this.sandboxes[sandboxName].finish = chromeAsyncFinish;
-
-      if (!directInject) {
-        script =  "__marionetteParams.push(returnFunc);" +
-            "var marionetteScriptFinished = returnFunc;" +
-            "var __marionetteFunc = function() {" + script + "};" +
-            "__marionetteFunc.apply(null, __marionetteParams);";
-      }
-
-      this.executeScriptInSandbox(
-          resp,
-          this.sandboxes[sandboxName],
-          script,
-          directInject,
-          true /* async */,
-          scriptTimeout,
-          filename);
-    } catch (e) {
-      chromeAsyncError(e, "execute_async_script", filename, line, script);
-    }
-  }.bind(this));
-
-  resp.body.value = that.curBrowser.elementManager.wrapValue(res) || null;
-};
-
-/**
  * Navigate to given URL.
  *
  * Navigates the current browsing context to the given URL and waits for
  * the document to load or the session's page timeout duration to elapse
  * before returning.
  *
  * The command will return with a failure if there is an error loading
  * the document or the URL is blocked.  This can occur if it fails to
@@ -2442,17 +2241,17 @@ GeckoDriver.prototype.sessionTearDown = 
   this.sessionId = null;
 
   if (this.observing !== null) {
     for (let topic in this.observing) {
       Services.obs.removeObserver(this.observing[topic], topic);
     }
     this.observing = null;
   }
-  this.sandboxes = {};
+  this.sandboxes.clear();
 };
 
 /**
  * Processes the "deleteSession" request from the client by tearing down
  * the session and responding "ok".
  */
 GeckoDriver.prototype.deleteSession = function(cmd, resp) {
   this.sessionTearDown();
@@ -2789,17 +2588,17 @@ GeckoDriver.prototype.receiveMessage = f
     case "Marionette:log":
       // log server-side messages
       logger.info(message.json.message);
       break;
 
     case "Marionette:shareData":
       // log messages from tests
       if (message.json.log) {
-        this.marionetteLog.addLogs(message.json.log);
+        this.marionetteLog.addAll(message.json.log);
       }
       break;
 
     case "Marionette:switchToModalOrigin":
       this.curBrowser.frameManager.switchToModalOrigin(message);
       this.mm = this.curBrowser.frameManager
           .currentRemoteFrame.messageManager.get();
       break;
@@ -2898,23 +2697,23 @@ GeckoDriver.prototype.commands = {
   "getMarionetteID": GeckoDriver.prototype.getMarionetteID,
   "sayHello": GeckoDriver.prototype.sayHello,
   "newSession": GeckoDriver.prototype.newSession,
   "getSessionCapabilities": GeckoDriver.prototype.getSessionCapabilities,
   "log": GeckoDriver.prototype.log,
   "getLogs": GeckoDriver.prototype.getLogs,
   "setContext": GeckoDriver.prototype.setContext,
   "getContext": GeckoDriver.prototype.getContext,
-  "executeScript": GeckoDriver.prototype.execute,
+  "executeScript": GeckoDriver.prototype.executeScript,
   "setScriptTimeout": GeckoDriver.prototype.setScriptTimeout,
   "timeouts": GeckoDriver.prototype.timeouts,
   "singleTap": GeckoDriver.prototype.singleTap,
   "actionChain": GeckoDriver.prototype.actionChain,
   "multiAction": GeckoDriver.prototype.multiAction,
-  "executeAsyncScript": GeckoDriver.prototype.executeWithCallback,
+  "executeAsyncScript": GeckoDriver.prototype.executeAsyncScript,
   "executeJSScript": GeckoDriver.prototype.executeJSScript,
   "setSearchTimeout": GeckoDriver.prototype.setSearchTimeout,
   "findElement": GeckoDriver.prototype.findElement,
   "findElements": GeckoDriver.prototype.findElements,
   "clickElement": GeckoDriver.prototype.clickElement,
   "getElementAttribute": GeckoDriver.prototype.getElementAttribute,
   "getElementText": GeckoDriver.prototype.getElementText,
   "getElementTagName": GeckoDriver.prototype.getElementTagName,
--- a/testing/marionette/emulator.js
+++ b/testing/marionette/emulator.js
@@ -1,111 +1,166 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("chrome://marionette/content/error.js");
+
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 const logger = Log.repository.getLogger("Marionette");
 
-this.EXPORTED_SYMBOLS = ["Emulator"];
+this.EXPORTED_SYMBOLS = ["emulator"];
+
+this.emulator = {};
 
 /**
- * Represents the connection between Marionette and the emulator it's
- * running on.
- *
- * When injected scripts call the JS routines {@code runEmulatorCmd} or
- * {@code runEmulatorShell}, the second argument to those is a callback
- * which is stored in cbs.  They are later retreived by their unique ID
- * using popCallback.
- *
- * @param {function(Object)} sendToEmulatorFn
- *     Callback function that sends a message to the emulator.
- * @param {function(Object)} sendToEmulatorFn
- *     Callback function that sends a message asynchronously to the
- *     current listener.
+ * Provides a service for instructing the emulator attached to the
+ * currently connected client to perform shell- and command instructions.
  */
-this.Emulator = function(sendToEmulatorFn) {
-  this.sendToEmulator = sendToEmulatorFn;
+emulator.EmulatorService = class {
+  /*
+   * @param {function(Object)} sendToEmulatorFn
+   *     Callback function that sends a message to the emulator.
+   */
+  constructor(sendToEmulatorFn) {
+    this.sendToEmulator = sendToEmulatorFn;
+  }
+
+  /**
+   * Instruct the client to run an Android emulator command.
+   *
+   * @param {string} cmd
+   *     The command to run.
+   * @param {function(?)} resCb
+   *     Callback on a result response from the emulator.
+   * @param {function(?)} errCb
+   *     Callback on an error in running the command.
+   */
+  command(cmd, resCb, errCb) {
+    if (arguments.length < 1) {
+      throw new ValueError("Not enough arguments");
+    }
+    this.sendToEmulator(
+        "runEmulatorCmd", {emulator_cmd: cmd}, resCb, errCb);
+  }
+
+  /**
+   * Instruct the client to execute Android emulator shell arguments.
+   *
+   * @param {Array.<string>} args
+   *     The shell instruction for the emulator to execute.
+   * @param {function(?)} resCb
+   *     Callback on a result response from the emulator.
+   * @param {function(?)} errCb
+   *     Callback on an error in executing the shell arguments.
+   */
+  shell(args, resCb, errCb) {
+    if (arguments.length < 1) {
+      throw new ValueError("Not enough arguments");
+    }
+    this.sendToEmulator(
+        "runEmulatorShell", {emulator_shell: args}, resCb, errCb);
+  }
+
+  processMessage(msg) {
+    let resCb = this.resultCallback(msg.json.id);
+    let errCb = this.errorCallback(msg.json.id);
+
+    switch (msg.name) {
+      case "Marionette:runEmulatorCmd":
+        this.command(msg.json.arguments, resCb, errCb);
+        break;
+
+      case "Marionette:runEmulatorShell":
+        this.shell(msg.json.arguments, resCb, errCb);
+        break;
+    }
+  }
+
+  resultCallback(uuid) {
+    return res => this.sendResult({value: res, id: uuid});
+  }
+
+  errorCallback(uuid) {
+    return err => this.sendResult({error: err, id: uuid});
+  }
+
+  sendResult(msg) {
+    // sendToListener set explicitly in GeckoDriver's ctor
+    this.sendToListener("listenerResponse", msg);
+  }
+
+  /** Receives IPC messages from the listener. */
+  // TODO(ato): The idea of services in chrome space
+  // can be generalised at some later time.
+  receiveMessage(msg) {
+    let uuid = msg.json.id;
+    try {
+      this.processMessage(msg);
+    } catch (e) {
+      this.sendResult({error: `${e.name}: ${e.message}`, id: uuid});
+    }
+  }
 };
 
-/**
- * Instruct the client to run an Android emulator command.
- *
- * @param {string} cmd
- *     The command to run.
- * @param {function(?)} resCb
- *     Callback on a result response from the emulator.
- * @param {function(?)} errCb
- *     Callback on an error in running the command.
- */
-Emulator.prototype.command = function(cmd, resCb, errCb) {
-  assertDefined(cmd, "runEmulatorCmd");
-  this.sendToEmulator(
-      "runEmulatorCmd", {emulator_cmd: cmd}, resCb, errCb);
+emulator.EmulatorService.prototype.QueryInterface =
+    XPCOMUtils.generateQI([
+      Ci.nsIMessageListener,
+      Ci.nsISupportsWeakReference
+    ]);
+
+emulator.EmulatorServiceClient = class {
+  constructor(chromeProxy) {
+    this.chrome = chromeProxy;
+  }
+
+  *command(cmd, cb) {
+    let res = yield this.chrome.runEmulatorCmd(cmd);
+    if (cb) {
+      cb(res);
+    }
+  }
+
+  *shell(args, cb) {
+    let res = yield this.chrome.runEmulatorShell(args);
+    if (cb) {
+      cb(res);
+    }
+  }
 };
 
 /**
- * Instruct the client to execute Android emulator shell arguments.
- *
- * @param {Array.<string>} args
- *     The shell instruction for the emulator to execute.
- * @param {function(?)} resCb
- *     Callback on a result response from the emulator.
- * @param {function(?)} errCb
- *     Callback on an error in executing the shell arguments.
+ * Adapts EmulatorService for use with sandboxes that scripts are
+ * evaluated in.  Is consumed by sandbox.augment.
  */
-Emulator.prototype.shell = function(args, resCb, errCb) {
-  assertDefined(args, "runEmulatorShell");
-  this.sendToEmulator(
-      "runEmulatorShell", {emulator_shell: args}, resCb, errCb);
-};
+emulator.Adapter = class {
+  constructor(emulator) {
+    this.emulator = emulator;
+  }
+
+  get exports() {
+    return new Map([
+      ["runEmulatorCmd", this.runEmulatorCmd.bind(this)],
+      ["runEmulatorShell", this.runEmulatorShell.bind(this)],
+    ]);
+  }
 
-Emulator.prototype.processMessage = function(msg) {
-  let resCb = this.resultCallback(msg.json.id);
-  let errCb = this.errorCallback(msg.json.id);
+  runEmulatorCmd(cmd, cb) {
+    this.yield(this.emulator.command(cmd, cb));
+  }
 
-  switch (msg.name) {
-    case "Marionette:runEmulatorCmd":
-      this.command(msg.json.command, resCb, errCb);
-      break;
+  runEmulatorShell(args, cb) {
+    this.yield(this.emulator.shell(args, cb));
+  }
 
-    case "Marionette:runEmulatorShell":
-      this.shell(msg.json.arguments, resCb, errCb);
-      break;
+  yield(promise) {
+    Task.spawn(function() {
+      yield promise;
+    });
   }
 };
-
-Emulator.prototype.resultCallback = function(msgId) {
-  return res => this.sendResult({result: res, id: msgId});
-};
-
-Emulator.prototype.errorCallback = function(msgId) {
-  return err => this.sendResult({error: err, id: msgId});
-};
-
-Emulator.prototype.sendResult = function(msg) {
-  // sendToListener set explicitly in GeckoDriver's ctor
-  this.sendToListener("emulatorCmdResult", msg);
-};
-
-/** Receives IPC messages from the listener. */
-Emulator.prototype.receiveMessage = function(msg) {
-  try {
-    this.processMessage(msg);
-  } catch (e) {
-    this.sendResult({error: `${e.name}: ${e.message}`, id: msg.json.id});
-  }
-};
-
-Emulator.prototype.QueryInterface = XPCOMUtils.generateQI(
-    [Ci.nsIMessageListener, Ci.nsISupportsWeakReference]);
-
-function assertDefined(arg, action) {
-  if (typeof arg == "undefined") {
-    throw new TypeError("Not enough arguments to " + action);
-  }
-}
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -80,16 +80,27 @@ error.isWebDriverError = function(obj) {
 error.wrap = function(err) {
   if (error.isWebDriverError(err)) {
     return err;
   }
   return new WebDriverError(`${err.name}: ${err.message}`, err.stack);
 };
 
 /**
+ * Wraps an Error as a WebDriverError type.  If the given error is already
+ * in the WebDriverError prototype chain, this function acts as a no-op.
+ */
+error.wrap = function(err) {
+  if (error.isWebDriverError(err)) {
+    return err;
+  }
+  return new WebDriverError(err.message, err.stacktrace);
+};
+
+/**
  * Unhandled error reporter.  Dumps the error and its stacktrace to console,
  * and reports error to the Browser Console.
  */
 error.report = function(err) {
   let msg = `Marionette threw an error: ${error.stringify(err)}`;
   dump(msg + "\n");
   if (Cu.reportError) {
     Cu.reportError(msg);
@@ -161,16 +172,17 @@ error.fromJson = function(json) {
  * WebDriverError is the prototypal parent of all WebDriver errors.
  * It should not be used directly, as it does not correspond to a real
  * error in the specification.
  */
 this.WebDriverError = function(msg, stack = undefined) {
   Error.call(this, msg);
   this.name = "WebDriverError";
   this.message = msg;
+  this.stack = stack;
   this.status = "webdriver error";
   this.stack = stack;
 };
 WebDriverError.prototype = Object.create(Error.prototype);
 
 this.ElementNotAccessibleError = function(msg) {
   WebDriverError.call(this, msg);
   this.name = "ElementNotAccessibleError";
--- a/testing/marionette/evaluate.js
+++ b/testing/marionette/evaluate.js
@@ -2,25 +2,319 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+Cu.import("chrome://marionette/content/error.js");
+
 const logger = Log.repository.getLogger("Marionette");
 
-this.EXPORTED_SYMBOLS = ["evaluate"];
+this.EXPORTED_SYMBOLS = ["evaluate", "sandbox", "Sandboxes"];
+
+const ARGUMENTS = "__webDriverArguments";
+const CALLBACK = "__webDriverCallback";
+const COMPLETE = "__webDriverComplete";
+const DEFAULT_TIMEOUT = 10000; // ms
+const FINISH = "finish";
+const MARIONETTE_SCRIPT_FINISHED = "marionetteScriptFinished";
+const ELEMENT_KEY = "element";
+const W3C_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
 
 this.evaluate = {};
 
 /**
+ * Evaluate a script in given sandbox.
+ *
+ * If the option {@code directInject} is not specified, the script will
+ * be executed as a function with the {@code args} argument applied.
+ *
+ * The arguments provided by the {@code args} argument are exposed through
+ * the {@code arguments} object available in the script context, and if
+ * the script is executed asynchronously with the {@code async}
+ * option, an additional last argument that is synonymous to the
+ * {@code marionetteScriptFinished} global is appended, and can be
+ * accessed through {@code arguments[arguments.length - 1]}.
+ *
+ * The {@code timeout} option specifies the duration for how long the
+ * script should be allowed to run before it is interrupted and aborted.
+ * An interrupted script will cause a ScriptTimeoutError to occur.
+ *
+ * The {@code async} option indicates that the script will not return
+ * until the {@code marionetteScriptFinished} global callback is invoked,
+ * which is analogous to the last argument of the {@code arguments}
+ * object.
+ *
+ * The option {@code directInject} causes the script to be evaluated
+ * without being wrapped in a function and the provided arguments will
+ * be disregarded.  This will cause such things as root scope return
+ * statements to throw errors because they are not used inside a function.
+ *
+ * The {@code filename} option is used in error messages to provide
+ * information on the origin script file in the local end.
+ *
+ * The {@code line} option is used in error messages, along with
+ * {@code filename}, to provide the line number in the origin script
+ * file on the local end.
+ *
+ * @param {nsISandbox) sb
+ *     The sandbox the script will be evaluted in.
+ * @param {string} script
+ *     The script to evaluate.
+ * @param {Array.<?>=} args
+ *     A sequence of arguments to call the script with.
+ * @param {Object.<string, ?>=} opts
+ *     Dictionary of options:
+ *
+ *       async (boolean)
+ *         Indicates if the script should return immediately or wait
+ *         for the callback be invoked before returning.
+ *       debug (boolean)
+ *         Attaches an {@code onerror} event listener.
+ *       directInject (boolean)
+ *         Evaluates the script without wrapping it in a function.
+ *       filename (string)
+ *         File location of the program in the client.
+ *       line (number)
+ *         Line number of the program in the client.
+ *       sandboxName (string)
+ *         Name of the sandbox.  Elevated system privileges, equivalent
+ *         to chrome space, will be given if it is "system".
+ *       timeout (boolean)
+ *         Duration in milliseconds before interrupting the script.
+ *
+ * @return {Promise}
+ *     A promise that when resolved will give you the return value from
+ *     the script.  Note that the return value requires serialisation before
+ *     it can be sent to the client.
+ *
+ * @throws JavaScriptError
+ *   If an Error was thrown whilst evaluating the script.
+ * @throws ScriptTimeoutError
+ *   If the script was interrupted due to script timeout.
+ */
+evaluate.sandbox = function(sb, script, args = [], opts = {}) {
+  let timeoutId, timeoutHandler, unloadHandler;
+
+  let promise = new Promise((resolve, reject) => {
+    sb[COMPLETE] = resolve;
+    timeoutHandler = () => reject(new ScriptTimeoutError("Timed out"));
+    unloadHandler = () => reject(
+        new JavaScriptError("Document was unloaded during execution"));
+
+    // wrap in function
+    if (!opts.directInject) {
+      sb[CALLBACK] = sb[COMPLETE];
+      sb[ARGUMENTS] = Cu.cloneInto(args, sb, {wrapReflectors: true});
+
+      script = `${ARGUMENTS}.push(${CALLBACK});` +
+          `(function() { ${script} }).apply(null, ${ARGUMENTS})`;
+
+      // marionetteScriptFinished is not WebDriver conformant,
+      // hence it is only exposed to immutable sandboxes
+      if (opts.sandboxName) {
+        sb[MARIONETTE_SCRIPT_FINISHED] = sb[CALLBACK];
+      }
+    }
+
+    // onerror is not hooked on by default because of the inability to
+    // differentiate content errors from chrome errors.
+    //
+    // see bug 1128760 for more details
+    if (opts.debug) {
+      sb.window.onerror = (msg, url, line) => {
+        let err = new JavaScriptError(`${msg} at: ${url} line: ${line}`);
+        reject(err);
+      };
+    }
+
+    // timeout and unload handlers
+    timeoutId = sb.window.setTimeout(
+        timeoutHandler, opts.timeout || DEFAULT_TIMEOUT);
+    sb.window.addEventListener("unload", unloadHandler);
+
+    let res;
+    try {
+      res = Cu.evalInSandbox(
+          script, sb, "1.8", opts.filename || "dummy file", 0);
+    } catch (e) {
+      let err = new JavaScriptError(
+          e,
+          "execute_script",
+          opts.filename,
+          opts.line,
+          script);
+      reject(err);
+    }
+
+    if (!opts.async) {
+      resolve(res);
+    }
+  });
+
+  return promise.then(res => {
+    sb.window.clearTimeout(timeoutId);
+    sb.window.removeEventListener("unload", unloadHandler);
+    return res;
+  });
+};
+
+this.sandbox = {};
+
+/**
+ * Augment given sandbox by an adapter that has an {@code exports}
+ * map property, or a normal map, of function names and function
+ * references.
+ *
+ * @param {Sandbox} sb
+ *     The sandbox to augment.
+ * @param {Object} adapter
+ *     Object that holds an {@code exports} property, or a map, of
+ *     function names and function references.
+ *
+ * @return {Sandbox}
+ *     The augmented sandbox.
+ */
+sandbox.augment = function(sb, adapter) {
+  function* entries(obj) {
+     for (let key of Object.keys(obj)) {
+       yield [key, obj[key]];
+     }
+  }
+
+  let funcs = adapter.exports || entries(adapter);
+  for (let [name, func] of funcs) {
+    sb[name] = func;
+  }
+
+  return sb;
+};
+
+/**
+ * Creates a sandbox.
+ *
+ * @param {Window} window
+ *     The DOM Window object.
+ * @param {nsIPrincipal=} principal
+ *     An optional, custom principal to prefer over the Window.  Useful if
+ *     you need elevated security permissions.
+ *
+ * @return {Sandbox}
+ *     The created sandbox.
+ */
+sandbox.create = function(window, principal = null, opts = {}) {
+  let p = principal || window;
+  opts = Object.assign({
+    sameZoneAs: window,
+    sandboxPrototype: window,
+    wantComponents: true,
+    wantXrays: true,
+  }, opts);
+  return new Cu.Sandbox(p, opts);
+};
+
+/**
+ * Creates a mutable sandbox, where changes to the global scope
+ * will have lasting side-effects.
+ *
+ * @param {Window} window
+ *     The DOM Window object.
+ *
+ * @return {Sandbox}
+ *     The created sandbox.
+ */
+sandbox.createMutable = function(window) {
+  let opts = {
+    wantComponents: false,
+    wantXrays: false,
+  };
+  return sandbox.create(window, null, opts);
+};
+
+sandbox.createSystemPrincipal = function(window) {
+  let principal = Cc["@mozilla.org/systemprincipal;1"]
+      .createInstance(Ci.nsIPrincipal);
+  return sandbox.create(window, principal);
+};
+
+sandbox.createSimpleTest = function(window, harness) {
+  let sb = sandbox.create(window);
+  sb = sandbox.augment(sb, harness);
+  sb[FINISH] = () => sb[COMPLETE](harness.generate_results());
+  return sb;
+};
+
+/**
+ * Sandbox storage.  When the user requests a sandbox by a specific name,
+ * if one exists in the storage this will be used as long as its window
+ * reference is still valid.
+ */
+this.Sandboxes = class {
+  /**
+   * @param {function(): Window} windowFn
+   *     A function that returns the references to the current Window
+   *     object.
+   */
+  constructor(windowFn) {
+    this.windowFn_ = windowFn;
+    this.boxes_ = new Map();
+  }
+
+  get window_() {
+    return this.windowFn_();
+  }
+
+  /**
+   * Factory function for getting a sandbox by name, or failing that,
+   * creating a new one.
+   *
+   * If the sandbox' window does not match the provided window, a new one
+   * will be created.
+   *
+   * @param {string} name
+   *     The name of the sandbox to get or create.
+   * @param {boolean} fresh
+   *     Remove old sandbox by name first, if it exists.
+   *
+   * @return {Sandbox}
+   *     A used or fresh sandbox.
+   */
+  get(name = "default", fresh = false) {
+    let sb = this.boxes_.get(name);
+    if (sb) {
+      if (fresh || sb.window != this.window_) {
+        this.boxes_.delete(name);
+        return this.get(name, false);
+      }
+    } else {
+      if (name == "system") {
+        sb = sandbox.createSystemPrincipal(this.window_);
+      } else {
+        sb = sandbox.create(this.window_);
+      }
+      this.boxes_.set(name, sb);
+    }
+    return sb;
+  }
+
+  /**
+   * Clears cache of sandboxes.
+   */
+  clear() {
+    this.boxes_.clear();
+  }
+};
+
+/**
  * Stores scripts imported from the local end through the
  * {@code GeckoDriver#importScript} command.
  *
  * Imported scripts are prepended to the script that is evaluated
  * on each call to {@code GeckoDriver#executeScript},
  * {@code GeckoDriver#executeAsyncScript}, and
  * {@code GeckoDriver#executeJSScript}.
  *
--- a/testing/marionette/harness/marionette/marionette_test.py
+++ b/testing/marionette/harness/marionette/marionette_test.py
@@ -736,25 +736,27 @@ class MarionetteTestCase(CommonTestCase)
                                   filepath=filepath,
                                   testvars=testvars,
                                   **kwargs))
 
     def setUp(self):
         CommonTestCase.setUp(self)
         self.marionette.test_name = self.test_name
         self.marionette.execute_script("log('TEST-START: %s:%s')" %
-                                       (self.filepath.replace('\\', '\\\\'), self.methodName))
+                                       (self.filepath.replace('\\', '\\\\'), self.methodName),
+                                       sandbox="simpletest")
 
     def tearDown(self):
         if not self.marionette.check_for_crash():
             try:
                 self.marionette.clear_imported_scripts()
                 self.marionette.execute_script("log('TEST-END: %s:%s')" %
                                                (self.filepath.replace('\\', '\\\\'),
-                                                self.methodName))
+                                                self.methodName),
+                                               sandbox="simpletest")
                 self.marionette.test_name = None
             except (MarionetteException, IOError):
                 # We have tried to log the test end when there is no listener
                 # object that we can access
                 pass
 
         CommonTestCase.tearDown(self)
 
@@ -797,21 +799,25 @@ class MarionetteJSTestCase(CommonTestCas
 
     @classmethod
     def add_tests_to_suite(cls, mod_name, filepath, suite, testloader, marionette, testvars, **kwargs):
         suite.addTest(cls(weakref.ref(marionette), jsFile=filepath, **kwargs))
 
     def runTest(self):
         if self.marionette.session is None:
             self.marionette.start_session()
-        self.marionette.execute_script("log('TEST-START: %s');" % self.jsFile.replace('\\', '\\\\'))
+        self.marionette.execute_script(
+            "log('TEST-START: %s');" % self.jsFile.replace('\\', '\\\\'),
+            sandbox="simpletest")
 
         self.run_js_test(self.jsFile)
 
-        self.marionette.execute_script("log('TEST-END: %s');" % self.jsFile.replace('\\', '\\\\'))
+        self.marionette.execute_script(
+            "log('TEST-END: %s');" % self.jsFile.replace('\\', '\\\\'),
+            sandbox="simpletest")
         self.marionette.test_name = None
 
     def get_test_class_name(self):
         # returns a dot separated folders as class name
         dirname = os.path.dirname(self.jsFile).replace('\\', '/')
         if dirname.startswith('/'):
             dirname = dirname[1:]
         return '.'.join(dirname.split('/'))
--- a/testing/marionette/harness/marionette/tests/unit/test_click_chrome.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_click_chrome.py
@@ -4,29 +4,31 @@
 
 from marionette import MarionetteTestCase
 from marionette_driver.by import By
 
 
 class TestClickChrome(MarionetteTestCase):
     def setUp(self):
         MarionetteTestCase.setUp(self)
+        self.root_window = self.marionette.current_window_handle
         self.marionette.set_context("chrome")
-        self.win = self.marionette.current_window_handle
-        self.marionette.execute_script("window.open('chrome://marionette/content/test.xul', 'foo', 'chrome,centerscreen');")
-        self.marionette.switch_to_window('foo')
-        self.assertNotEqual(self.win, self.marionette.current_window_handle)
+        self.marionette.execute_script(
+            "window.open('chrome://marionette/content/test.xul', 'foo', 'chrome,centerscreen')")
+        self.marionette.switch_to_window("foo")
+        self.assertNotEqual(self.root_window, self.marionette.current_window_handle)
 
     def tearDown(self):
-        self.assertNotEqual(self.win, self.marionette.current_window_handle)
-        self.marionette.execute_script("window.close();")
-        self.marionette.switch_to_window(self.win)
+        self.assertNotEqual(self.root_window, self.marionette.current_window_handle)
+        self.marionette.execute_script("window.close()")
+        self.marionette.switch_to_window(self.root_window)
         MarionetteTestCase.tearDown(self)
 
     def test_click(self):
-        wins = self.marionette.window_handles
-        wins.remove(self.win)
-        newWin = wins.pop()
-        self.marionette.switch_to_window(newWin)
+        def checked():
+            return self.marionette.execute_script(
+                "return arguments[0].checked",
+                script_args=[box])
+
         box = self.marionette.find_element(By.ID, "testBox")
-        self.assertFalse(self.marionette.execute_script("return arguments[0].checked;", [box]))
+        self.assertFalse(checked())
         box.click()
-        self.assertTrue(self.marionette.execute_script("return arguments[0].checked;", [box]))
+        self.assertTrue(checked())
--- a/testing/marionette/harness/marionette/tests/unit/test_execute_sandboxes.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_execute_sandboxes.py
@@ -6,67 +6,73 @@ from marionette import MarionetteTestCas
 from marionette_driver.errors import JavascriptException
 
 
 class TestExecuteSandboxes(MarionetteTestCase):
     def setUp(self):
         super(TestExecuteSandboxes, self).setUp()
 
     def test_execute_system_sandbox(self):
-        # Test that 'system' sandbox has elevated privileges in execute_script
-        result = self.marionette.execute_script("""
-            return Components.interfaces.nsIPermissionManager.ALLOW_ACTION;
-            """, sandbox='system')
+        # Test that "system" sandbox has elevated privileges in execute_script
+        result = self.marionette.execute_script(
+            "return Components.interfaces.nsIPermissionManager.ALLOW_ACTION",
+            sandbox="system")
         self.assertEqual(result, 1)
 
     def test_execute_async_system_sandbox(self):
-        # Test that 'system' sandbox has elevated privileges in
+        # Test that "system" sandbox has elevated privileges in
         # execute_async_script.
         result = self.marionette.execute_async_script("""
-            let result = Components.interfaces.nsIPermissionManager.ALLOW_ACTION;
-            marionetteScriptFinished(result);
-            """, sandbox='system')
+            const Ci = Components.interfaces;
+            let result = Ci.nsIPermissionManager.ALLOW_ACTION;
+            marionetteScriptFinished(result);""",
+            sandbox="system")
         self.assertEqual(result, 1)
 
     def test_execute_switch_sandboxes(self):
         # Test that sandboxes are retained when switching between them
         # for execute_script.
-        self.marionette.execute_script("foo = 1;", sandbox='1')
-        self.marionette.execute_script("foo = 2;", sandbox='2')
-        foo = self.marionette.execute_script("return foo;", sandbox='1',
-                                             new_sandbox=False)
+        self.marionette.execute_script("foo = 1", sandbox="1")
+        self.marionette.execute_script("foo = 2", sandbox="2")
+        foo = self.marionette.execute_script(
+            "return foo", sandbox="1", new_sandbox=False)
         self.assertEqual(foo, 1)
-        foo = self.marionette.execute_script("return foo;", sandbox='2',
-                                             new_sandbox=False)
+        foo = self.marionette.execute_script(
+            "return foo", sandbox="2", new_sandbox=False)
         self.assertEqual(foo, 2)
 
     def test_execute_new_sandbox(self):
-        # Test that clearing a sandbox does not affect other sandboxes
-        self.marionette.execute_script("foo = 1;", sandbox='1')
-        self.marionette.execute_script("foo = 2;", sandbox='2')
-        self.assertRaises(JavascriptException,
-                          self.marionette.execute_script,
-                          "return foo;", sandbox='1', new_sandbox=True)
-        foo = self.marionette.execute_script("return foo;", sandbox='2',
-                                             new_sandbox=False)
+        # test that clearing a sandbox does not affect other sandboxes
+        self.marionette.execute_script("foo = 1", sandbox="1")
+        self.marionette.execute_script("foo = 2", sandbox="2")
+
+        # deprecate sandbox 1 by asking explicitly for a fresh one
+        with self.assertRaises(JavascriptException):
+            self.marionette.execute_script("return foo",
+                sandbox="1", new_sandbox=True)
+
+        foo = self.marionette.execute_script(
+            "return foo", sandbox="2", new_sandbox=False)
         self.assertEqual(foo, 2)
 
     def test_execute_async_switch_sandboxes(self):
         # Test that sandboxes are retained when switching between them
         # for execute_async_script.
-        self.marionette.execute_async_script("foo = 1; marionetteScriptFinished()",
-                                             sandbox='1')
-        self.marionette.execute_async_script("foo = 2; marionetteScriptFinished()",
-                                             sandbox='2')
-        foo = self.marionette.execute_async_script("marionetteScriptFinished(foo);",
-                                                   sandbox='1',
-                                                   new_sandbox=False)
+        self.marionette.execute_async_script(
+            "foo = 1; marionetteScriptFinished()", sandbox="1")
+        self.marionette.execute_async_script(
+            "foo = 2; marionetteScriptFinished()", sandbox='2')
+        foo = self.marionette.execute_async_script(
+            "marionetteScriptFinished(foo)",
+            sandbox="1",
+            new_sandbox=False)
         self.assertEqual(foo, 1)
-        foo = self.marionette.execute_async_script("marionetteScriptFinished(foo);",
-                                                   sandbox='2',
-                                                   new_sandbox=False)
+        foo = self.marionette.execute_async_script(
+            "marionetteScriptFinished(foo)",
+            sandbox="2",
+            new_sandbox=False)
         self.assertEqual(foo, 2)
 
 
 class TestExecuteSandboxesChrome(TestExecuteSandboxes):
     def setUp(self):
         super(TestExecuteSandboxesChrome, self).setUp()
         self.marionette.set_context("chrome")
--- a/testing/marionette/harness/marionette/tests/unit/test_execute_script.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_execute_script.py
@@ -10,113 +10,232 @@ from marionette import MarionetteTestCas
 
 
 def inline(doc):
     return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
 
 
 elements = inline("<p>foo</p> <p>bar</p>")
 
+globals = set([
+              "atob",
+              "Audio",
+              "btoa",
+              "document",
+              "navigator",
+              "URL",
+              "window",
+              ])
 
-class TestExecuteContent(MarionetteTestCase):
-    def test_execute_js_script_stack_trace(self):
+
+class TestExecuteSimpleTestContent(MarionetteTestCase):
+    def test_stack_trace(self):
         try:
             self.marionette.execute_js_script("""
                 let a = 1;
                 throwHere();
                 """, filename="file.js")
             self.assertFalse(True)
-        except errors.JavascriptException as inst:
-            self.assertIn('throwHere is not defined', inst.msg)
-            self.assertIn('@file.js:2', inst.stacktrace)
+        except errors.JavascriptException as e:
+            self.assertIn("throwHere is not defined", e.msg)
+            self.assertIn("@file.js:2", e.stacktrace)
+
+
+class TestExecuteContent(MarionetteTestCase):
+    def test_return_number(self):
+        self.assertEqual(1, self.marionette.execute_script("return 1"))
+        self.assertEqual(1.5, self.marionette.execute_script("return 1.5"))
+
+    def test_return_boolean(self):
+        self.assertEqual(True, self.marionette.execute_script("return true"))
+
+    def test_return_string(self):
+        self.assertEqual("foo", self.marionette.execute_script("return 'foo'"))
+
+    def test_return_array(self):
+        self.assertEqual(
+            [1, 2], self.marionette.execute_script("return [1, 2]"))
+        self.assertEqual(
+            [1.25, 1.75], self.marionette.execute_script("return [1.25, 1.75]"))
+        self.assertEqual(
+            [True, False], self.marionette.execute_script("return [true, false]"))
+        self.assertEqual(
+            ["foo", "bar"], self.marionette.execute_script("return ['foo', 'bar']"))
+        self.assertEqual(
+            [1, 1.5, True, "foo"], self.marionette.execute_script("return [1, 1.5, true, 'foo']"))
+        self.assertEqual(
+            [1, [2]], self.marionette.execute_script("return [1, [2]]"))
 
-    def test_execute_script_stack_trace(self):
-        try:
-            self.marionette.execute_script("""
-                let a = 1;
-                return b;
-                """)
-            self.assertFalse(True)
-        except errors.JavascriptException as inst:
-            # By default execute_script pass the name of the python file
-            self.assertIn(os.path.basename(__file__.replace(".pyc", ".py")), inst.stacktrace)
-            self.assertIn('b is not defined', inst.msg)
-            self.assertIn('return b', inst.stacktrace)
+    def test_return_object(self):
+        self.assertEqual(
+            {"foo": 1}, self.marionette.execute_script("return {foo: 1}"))
+        self.assertEqual(
+            {"foo": 1.5}, self.marionette.execute_script("return {foo: 1.5}"))
+        self.assertEqual(
+            {"foo": True}, self.marionette.execute_script("return {foo: true}"))
+        self.assertEqual(
+            {"foo": "bar"}, self.marionette.execute_script("return {foo: 'bar'}"))
+        self.assertEqual(
+            {"foo": [1, 2]}, self.marionette.execute_script("return {foo: [1, 2]}"))
+        self.assertEqual(
+            {"foo": {"bar": [1, 2]}}, self.marionette.execute_script("return {foo: {bar: [1, 2]}}"))
 
-    def test_execute_simple(self):
-        self.assertEqual(1, self.marionette.execute_script("return 1;"))
+    def test_no_return_value(self):
+        self.assertIsNone(self.marionette.execute_script("true"))
+
+    def test_argument_null(self):
+        self.assertEqual(
+            None, self.marionette.execute_script("return arguments[0]", [None]))
 
-    def test_check_window(self):
-        self.assertTrue(self.marionette.execute_script("return (window !=null && window != undefined);"))
+    def test_argument_number(self):
+        self.assertEqual(
+            1, self.marionette.execute_script("return arguments[0]", [1]))
+        self.assertEqual(
+            1.5, self.marionette.execute_script("return arguments[0]", [1.5]))
+
+    def test_argument_boolean(self):
+        self.assertEqual(
+            True, self.marionette.execute_script("return arguments[0]", [True]))
 
-    def test_execute_no_return(self):
-        self.assertEqual(self.marionette.execute_script("1;"), None)
+    def test_argument_string(self):
+        self.assertEqual(
+            "foo", self.marionette.execute_script("return arguments[0]", ["foo"]))
+
+    def test_argument_array(self):
+        self.assertEqual(
+            [1, 2], self.marionette.execute_script("return arguments[0]", [[1, 2]]))
+
+    def test_argument_object(self):
+        self.assertEqual({"foo": 1}, self.marionette.execute_script(
+            "return arguments[0]", [{"foo": 1}]))
 
-    def test_execute_js_exception(self):
-        self.assertRaises(errors.JavascriptException,
-            self.marionette.execute_script, "return foo(bar);")
+    def assert_is_defined(self, property, sandbox="default"):
+        self.assertTrue(self.marionette.execute_script(
+            "return typeof %s != 'undefined'" % property,
+            sandbox=sandbox),
+            "property %s is undefined" % property)
 
-    def test_execute_permission(self):
+    def test_globals(self):
+        for property in globals:
+            self.assert_is_defined(property)
+        self.assert_is_defined("Components")
+        self.assert_is_defined("window.wrappedJSObject")
+
+    def test_system_globals(self):
+        for property in globals:
+            self.assert_is_defined(property, sandbox="system")
+        self.assert_is_defined("Components", sandbox="system")
+        self.assert_is_defined("window.wrappedJSObject")
+
+    def test_exception(self):
         self.assertRaises(errors.JavascriptException,
-                          self.marionette.execute_script,
-                          """
-let prefs = Components.classes["@mozilla.org/preferences-service;1"]
-                              .getService(Components.interfaces.nsIPrefBranch);
-""")
+                          self.marionette.execute_script, "return foo")
+
+    def test_stacktrace(self):
+        try:
+            self.marionette.execute_script("return b")
+            self.assertFalse(True)
+        except errors.JavascriptException as e:
+            # by default execute_script pass the name of the python file
+            self.assertIn(
+                os.path.basename(__file__.replace(".pyc", ".py")), e.stacktrace)
+            self.assertIn("b is not defined", e.msg)
+            self.assertIn("return b", e.stacktrace)
 
-    def test_complex_return_values(self):
-        self.assertEqual(self.marionette.execute_script("return [1, 2];"), [1, 2])
-        self.assertEqual(self.marionette.execute_script("return {'foo': 'bar', 'fizz': 'fazz'};"),
-                         {'foo': 'bar', 'fizz': 'fazz'})
-        self.assertEqual(self.marionette.execute_script("return [1, {'foo': 'bar'}, 2];"),
-                         [1, {'foo': 'bar'}, 2])
-        self.assertEqual(self.marionette.execute_script("return {'foo': [1, 'a', 2]};"),
-                         {'foo': [1, 'a', 2]})
+    def test_permission(self):
+        with self.assertRaises(errors.JavascriptException):
+            self.marionette.execute_script("""
+                let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+                    .getService(Components.interfaces.nsIPrefBranch)""")
+
+    def test_return_web_element(self):
+        self.marionette.navigate(elements)
+        expected = self.marionette.find_element(By.TAG_NAME, "p")
+        actual = self.marionette.execute_script(
+            "return document.querySelector('p')")
+        self.assertEqual(expected, actual)
+
+    def test_return_web_element_array(self):
+        self.marionette.navigate(elements)
+        expected = self.marionette.find_elements(By.TAG_NAME, "p")
+        actual = self.marionette.execute_script("""
+            let els = document.querySelectorAll('p')
+            return [els[0], els[1]]""")
+        self.assertEqual(expected, actual)
 
     # Bug 938228 identifies a problem with unmarshaling NodeList
     # objects from the DOM.  document.querySelectorAll returns this
     # construct.
-    def test_unmarshal_element_collection(self):
+    def test_return_web_element_nodelist(self):
         self.marionette.navigate(elements)
         expected = self.marionette.find_elements(By.TAG_NAME, "p")
         actual = self.marionette.execute_script(
             "return document.querySelectorAll('p')")
         self.assertEqual(expected, actual)
 
     def test_sandbox_reuse(self):
         # Sandboxes between `execute_script()` invocations are shared.
         self.marionette.execute_script("this.foobar = [23, 42];")
-        self.assertEqual(self.marionette.execute_script("return this.foobar;", new_sandbox=False), [23, 42])
+        self.assertEqual(self.marionette.execute_script(
+            "return this.foobar;", new_sandbox=False), [23, 42])
 
         self.marionette.execute_script("global.barfoo = [42, 23];")
-        self.assertEqual(self.marionette.execute_script("return global.barfoo;", new_sandbox=False), [42, 23])
+        self.assertEqual(self.marionette.execute_script(
+            "return global.barfoo;", new_sandbox=False), [42, 23])
 
     def test_sandbox_refresh_arguments(self):
-        self.marionette.execute_script("this.foobar = [arguments[0], arguments[1]];",
-                                       script_args=[23, 42])
-        self.assertEqual(self.marionette.execute_script("return this.foobar;", new_sandbox=False),
-                         [23, 42])
+        self.marionette.execute_script(
+            "this.foobar = [arguments[0], arguments[1]]", [23, 42])
+        self.assertEqual(self.marionette.execute_script(
+            "return this.foobar", new_sandbox=False), [23, 42])
+
+    def test_wrappedjsobject(self):
+        self.marionette.execute_script("window.wrappedJSObject.foo = 3")
+        self.assertEqual(
+            3, self.marionette.execute_script("return window.wrappedJSObject.foo"))
 
-        self.marionette.execute_script("global.barfoo = [arguments[0], arguments[1]];",
-                                       script_args=[42, 23],
-                                       new_sandbox=False)
-        self.assertEqual(self.marionette.execute_script("return global.barfoo;", new_sandbox=False),
-                         [42, 23])
+    def test_system_sandbox_wrappedjsobject(self):
+        self.marionette.execute_script(
+            "window.wrappedJSObject.foo = 4", sandbox="system")
+        self.assertEqual(4, self.marionette.execute_script(
+            "return window.wrappedJSObject.foo", sandbox="system"))
+
+    def test_system_dead_object(self):
+        self.marionette.execute_script(
+            "window.wrappedJSObject.foo = function() { return 'yo' }",
+            sandbox="system")
+        self.marionette.execute_script(
+            "dump(window.wrappedJSObject.foo)", sandbox="system")
 
-    def test_that_we_can_pass_in_floats(self):
-        expected_result = 1.2
-        result = self.marionette.execute_script("return arguments[0]",
-                                                [expected_result])
-        self.assertTrue(isinstance(result, float))
-        self.assertEqual(result, expected_result)
+        self.marionette.execute_script(
+            "window.wrappedJSObject.foo = function() { return 'yolo' }",
+            sandbox="system")
+        typ = self.marionette.execute_script(
+            "return typeof window.wrappedJSObject.foo", sandbox="system")
+        self.assertEqual("function", typ)
+        obj = self.marionette.execute_script(
+            "return window.wrappedJSObject.foo.toString()", sandbox="system")
+        self.assertIn("yolo", obj)
 
-    def test_null_argument(self):
-        result = self.marionette.execute_script("return arguments[0]",
-                                                [None])
-        self.assertIs(result, None)
+    def test_lasting_side_effects(self):
+        def send(script):
+            return self.marionette._send_message(
+                "executeScript", {"script": script}, key="value")
+
+        send("window.foo = 1")
+        foo = send("return window.foo")
+        self.assertEqual(1, foo)
+
+        for property in globals:
+            exists = send("return typeof %s != 'undefined'" % property)
+            self.assertTrue(exists, "property %s is undefined" % property)
+        # TODO(ato): For some reason this fails, probably Sandbox bug?
+        # self.assertTrue(send("return typeof Components == 'undefined'"))
+        self.assertTrue(
+            send("return typeof window.wrappedJSObject == 'undefined'"))
 
 
 @skip_if_b2g
 class TestExecuteChrome(TestExecuteContent):
     def setUp(self):
         super(TestExecuteChrome, self).setUp()
         self.win = self.marionette.current_window_handle
         self.marionette.set_context("chrome")
--- a/testing/marionette/harness/marionette/tests/unit/test_log.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_log.py
@@ -1,47 +1,64 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette import MarionetteTestCase
 
 
 class TestLog(MarionetteTestCase):
-    def test_log_basic(self):
-        # clear any previous data
-        self.marionette.get_logs()
-
-        self.marionette.log("I am info")
-        self.assertTrue("I am info" in self.marionette.get_logs()[0])
-        self.marionette.log("I AM ERROR", "ERROR")
-        self.assertTrue("I AM ERROR" in self.marionette.get_logs()[0])
-
-    def test_that_we_can_clear_the_logs(self):
-        # clear any previous data
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        # clears log cache
         self.marionette.get_logs()
 
-        self.marionette.log("I am info")
-        self.assertTrue("I am info" in self.marionette.get_logs()[0])
-        self.marionette.log("I AM ERROR", "ERROR")
-        self.assertTrue("I AM ERROR" in self.marionette.get_logs()[0])
+    def test_log(self):
+        self.marionette.log("foo")
+        logs = self.marionette.get_logs()
+        self.assertEqual("INFO", logs[0][0])
+        self.assertEqual("foo", logs[0][1])
 
-        # Check that is empty if we call it again
+    def test_log_level(self):
+        self.marionette.log("foo", "ERROR")
+        logs = self.marionette.get_logs()
+        self.assertEqual("ERROR", logs[0][0])
+        self.assertEqual("foo", logs[0][1])
+
+    def test_clear(self):
+        self.marionette.log("foo")
+        self.assertEqual(1, len(self.marionette.get_logs()))
         self.assertEqual(0, len(self.marionette.get_logs()))
 
-    def test_log_script(self):
-        # clear any previous data
-        self.marionette.get_logs()
+    def test_multiple_entries(self):
+        self.marionette.log("foo")
+        self.marionette.log("bar")
+        self.assertEqual(2, len(self.marionette.get_logs()))
+
+    def test_log_from_sync_script(self):
+        self.marionette.execute_script("log('foo')")
+        logs = self.marionette.get_logs()
+        self.assertEqual("INFO", logs[0][0])
+        self.assertEqual("foo", logs[0][1])
 
-        self.marionette.execute_script("log('some log');")
-        self.assertTrue("some log" in self.marionette.get_logs()[0])
-        self.marionette.execute_script("log('some error', 'ERROR');")
-        self.assertTrue("some error" in self.marionette.get_logs()[0])
-        self.marionette.set_script_timeout(2000)
-        self.marionette.execute_async_script("log('some more logs'); finish();")
-        self.assertTrue("some more logs" in self.marionette.get_logs()[0])
-        self.marionette.execute_async_script("log('some more errors', 'ERROR'); finish();")
-        self.assertTrue("some more errors" in self.marionette.get_logs()[0])
+    def test_log_from_sync_script_level(self):
+        self.marionette.execute_script("log('foo', 'ERROR')")
+        logs = self.marionette.get_logs()
+        self.assertEqual("ERROR", logs[0][0])
+        self.assertEqual("foo", logs[0][1])
+
+    def test_log_from_async_script(self):
+        self.marionette.execute_async_script("log('foo'); arguments[0]();")
+        logs = self.marionette.get_logs()
+        self.assertEqual("INFO", logs[0][0])
+        self.assertEqual("foo", logs[0][1])
+
+    def test_log_from_async_script_variable_arguments(self):
+        self.marionette.execute_async_script("log('foo', 'ERROR'); arguments[0]();")
+        logs = self.marionette.get_logs()
+        self.assertEqual("ERROR", logs[0][0])
+        self.assertEqual("foo", logs[0][1])
+
 
 class TestLogChrome(TestLog):
     def setUp(self):
-        MarionetteTestCase.setUp(self)
+        TestLog.setUp(self)
         self.marionette.set_context("chrome")
--- a/testing/marionette/harness/marionette/tests/unit/test_simpletest_sanity.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_simpletest_sanity.py
@@ -1,101 +1,106 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette import MarionetteTestCase
 
 class SimpletestSanityTest(MarionetteTestCase):
+    callFinish = "return finish();"
 
-    callFinish = "return finish();"
+    def run_sync(self, test):
+        return self.marionette.execute_js_script(test, async=False)
+
+    def run_async(self, test):
+        return self.marionette.execute_js_script(test)
 
     def test_is(self):
         def runtests():
             sentFail1 = "is(true, false, 'isTest1', TEST_UNEXPECTED_FAIL, TEST_PASS);" + self.callFinish
             sentFail2 = "is(true, false, 'isTest2', TEST_UNEXPECTED_FAIL, TEST_PASS);" + self.callFinish
             sentPass1 = "is(true, true, 'isTest3');" + self.callFinish
             sentPass2 = "is(true, true, 'isTest4');" + self.callFinish
 
-            self.assertEqual(1, len(self.marionette.execute_script(sentFail1)["failures"]))
-            self.assertEqual(0, self.marionette.execute_script(sentFail2)["passed"])
-            self.assertEqual(1, self.marionette.execute_script(sentPass1)["passed"])
-            self.assertEqual(0, len(self.marionette.execute_script(sentPass2)["failures"]))
+            self.assertEqual(1, len(self.run_sync(sentFail1)["failures"]))
+            self.assertEqual(0, self.run_sync(sentFail2)["passed"])
+            self.assertEqual(1, self.run_sync(sentPass1)["passed"])
+            self.assertEqual(0, len(self.run_sync(sentPass2)["failures"]))
 
             self.marionette.set_script_timeout(1000)
-            self.assertEqual(1, len(self.marionette.execute_async_script(sentFail1)["failures"]))
-            self.assertEqual(0, self.marionette.execute_async_script(sentFail2)["passed"])
-            self.assertEqual(1, self.marionette.execute_async_script(sentPass1)["passed"])
-            self.assertEqual(0, len(self.marionette.execute_async_script(sentPass2)["failures"]))
+            self.assertEqual(1, len(self.run_async(sentFail1)["failures"]))
+            self.assertEqual(0, self.run_async(sentFail2)["passed"])
+            self.assertEqual(1, self.run_async(sentPass1)["passed"])
+            self.assertEqual(0, len(self.run_async(sentPass2)["failures"]))
 
         self.marionette.set_context("content")
         runtests()
         self.marionette.set_context("chrome")
         runtests()
 
     def test_isnot(self):
         def runtests():
            sentFail1 = "isnot(true, true, 'isnotTest3', TEST_UNEXPECTED_FAIL, TEST_PASS);" + self.callFinish
            sentFail2 = "isnot(true, true, 'isnotTest4', TEST_UNEXPECTED_FAIL, TEST_PASS);" + self.callFinish
            sentPass1 = "isnot(true, false, 'isnotTest1');" + self.callFinish
            sentPass2 = "isnot(true, false, 'isnotTest2');" + self.callFinish
 
-           self.assertEqual(1, len(self.marionette.execute_script(sentFail1)["failures"]));
-           self.assertEqual(0, self.marionette.execute_script(sentFail2)["passed"]);
-           self.assertEqual(0, len(self.marionette.execute_script(sentPass1)["failures"]));
-           self.assertEqual(1, self.marionette.execute_script(sentPass2)["passed"]);
+           self.assertEqual(1, len(self.run_sync(sentFail1)["failures"]));
+           self.assertEqual(0, self.run_sync(sentFail2)["passed"]);
+           self.assertEqual(0, len(self.run_sync(sentPass1)["failures"]));
+           self.assertEqual(1, self.run_sync(sentPass2)["passed"]);
 
            self.marionette.set_script_timeout(1000)
-           self.assertEqual(1, len(self.marionette.execute_async_script(sentFail1)["failures"]));
-           self.assertEqual(0, self.marionette.execute_async_script(sentFail2)["passed"]);
-           self.assertEqual(0, len(self.marionette.execute_async_script(sentPass1)["failures"]));
-           self.assertEqual(1, self.marionette.execute_async_script(sentPass2)["passed"]);
+           self.assertEqual(1, len(self.run_async(sentFail1)["failures"]));
+           self.assertEqual(0, self.run_async(sentFail2)["passed"]);
+           self.assertEqual(0, len(self.run_async(sentPass1)["failures"]));
+           self.assertEqual(1, self.run_async(sentPass2)["passed"]);
 
         self.marionette.set_context("content")
         runtests()
         self.marionette.set_context("chrome")
         runtests()
 
     def test_ok(self):
         def runtests():
             sentFail1 = "ok(1==2, 'testOk1', TEST_UNEXPECTED_FAIL, TEST_PASS);" + self.callFinish
             sentFail2 = "ok(1==2, 'testOk2', TEST_UNEXPECTED_FAIL, TEST_PASS);" + self.callFinish
             sentPass1 = "ok(1==1, 'testOk3');" + self.callFinish
             sentPass2 = "ok(1==1, 'testOk4');" + self.callFinish
 
-            self.assertEqual(1, len(self.marionette.execute_script(sentFail1)["failures"]));
-            self.assertEqual(0, self.marionette.execute_script(sentFail2)["passed"]);
-            self.assertEqual(0, len(self.marionette.execute_script(sentPass1)["failures"]));
-            self.assertEqual(1, self.marionette.execute_script(sentPass2)["passed"]);
+            self.assertEqual(1, len(self.run_sync(sentFail1)["failures"]));
+            self.assertEqual(0, self.run_sync(sentFail2)["passed"]);
+            self.assertEqual(0, len(self.run_sync(sentPass1)["failures"]));
+            self.assertEqual(1, self.run_sync(sentPass2)["passed"]);
 
             self.marionette.set_script_timeout(1000)
-            self.assertEqual(1, len(self.marionette.execute_async_script(sentFail1)["failures"]));
-            self.assertEqual(0, self.marionette.execute_async_script(sentFail2)["passed"]);
-            self.assertEqual(0, len(self.marionette.execute_async_script(sentPass1)["failures"]));
-            self.assertEqual(1, self.marionette.execute_async_script(sentPass2)["passed"]);
+            self.assertEqual(1, len(self.run_async(sentFail1)["failures"]));
+            self.assertEqual(0, self.run_async(sentFail2)["passed"]);
+            self.assertEqual(0, len(self.run_async(sentPass1)["failures"]));
+            self.assertEqual(1, self.run_async(sentPass2)["passed"]);
 
         self.marionette.set_context("content")
         runtests()
         self.marionette.set_context("chrome")
         runtests()
 
     def test_todo(self):
         def runtests():
             sentFail1 = "todo(1==1, 'testTodo1', TEST_UNEXPECTED_PASS, TEST_KNOWN_FAIL);" + self.callFinish
             sentFail2 = "todo(1==1, 'testTodo2', TEST_UNEXPECTED_PASS, TEST_KNOWN_FAIL);" + self.callFinish
             sentPass1 = "todo(1==2, 'testTodo3');" + self.callFinish
             sentPass2 = "todo(1==2, 'testTodo4');" + self.callFinish
 
-            self.assertEqual(1, len(self.marionette.execute_script(sentFail1)["unexpectedSuccesses"]));
-            self.assertEqual(0, len(self.marionette.execute_script(sentFail2)["expectedFailures"]));
-            self.assertEqual(0, len(self.marionette.execute_script(sentPass1)["unexpectedSuccesses"]));
-            self.assertEqual(1, len(self.marionette.execute_script(sentPass2)["expectedFailures"]));
+            self.assertEqual(1, len(self.run_sync(sentFail1)["unexpectedSuccesses"]));
+            self.assertEqual(0, len(self.run_sync(sentFail2)["expectedFailures"]));
+            self.assertEqual(0, len(self.run_sync(sentPass1)["unexpectedSuccesses"]));
+            self.assertEqual(1, len(self.run_sync(sentPass2)["expectedFailures"]));
 
             self.marionette.set_script_timeout(1000)
-            self.assertEqual(1, len(self.marionette.execute_async_script(sentFail1)["unexpectedSuccesses"]));
-            self.assertEqual(0, len(self.marionette.execute_async_script(sentFail2)["expectedFailures"]));
-            self.assertEqual(0, len(self.marionette.execute_async_script(sentPass1)["unexpectedSuccesses"]));
-            self.assertEqual(1, len(self.marionette.execute_async_script(sentPass2)["expectedFailures"]));
+            self.assertEqual(1, len(self.run_async(sentFail1)["unexpectedSuccesses"]));
+            self.assertEqual(0, len(self.run_async(sentFail2)["expectedFailures"]));
+            self.assertEqual(0, len(self.run_async(sentPass1)["unexpectedSuccesses"]));
+            self.assertEqual(1, len(self.run_async(sentPass2)["expectedFailures"]));
 
         self.marionette.set_context("content")
         runtests()
         self.marionette.set_context("chrome")
         runtests()
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -6,30 +6,30 @@ marionette.jar:
 % content marionette %content/
   content/server.js (server.js)
   content/driver.js (driver.js)
   content/action.js (action.js)
   content/interaction.js (interaction.js)
   content/accessibility.js (accessibility.js)
   content/listener.js (listener.js)
   content/element.js (element.js)
-  content/common.js (common.js)
   content/simpletest.js (simpletest.js)
   content/frame.js (frame.js)
   content/event.js  (event.js)
   content/error.js (error.js)
   content/message.js (message.js)
   content/dispatcher.js (dispatcher.js)
   content/emulator.js (emulator.js)
   content/modal.js (modal.js)
   content/proxy.js (proxy.js)
   content/capture.js (capture.js)
   content/cookies.js (cookies.js)
   content/atom.js (atom.js)
   content/evaluate.js (evaluate.js)
+  content/logging.js (logging.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (harness/marionette/chrome/test.xul)
   content/test2.xul  (harness/marionette/chrome/test2.xul)
   content/test_nested_iframe.xul  (harness/marionette/chrome/test_nested_iframe.xul)
   content/test_anonymous_content.xul  (harness/marionette/chrome/test_anonymous_content.xul)
 #endif
 
 % content specialpowers %content/
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -1,104 +1,110 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+"use strict";
+
 var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 var uuidGen = Cc["@mozilla.org/uuid-generator;1"]
     .getService(Ci.nsIUUIDGenerator);
 
 var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
     .getService(Ci.mozIJSSubScriptLoader);
 
-loader.loadSubScript("chrome://marionette/content/simpletest.js");
-loader.loadSubScript("chrome://marionette/content/common.js");
-
-Cu.import("resource://gre/modules/Task.jsm");
-
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/action.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/capture.js");
 Cu.import("chrome://marionette/content/cookies.js");
 Cu.import("chrome://marionette/content/element.js");
+Cu.import("chrome://marionette/content/emulator.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
+Cu.import("chrome://marionette/content/logging.js");
 Cu.import("chrome://marionette/content/proxy.js");
+Cu.import("chrome://marionette/content/simpletest.js");
 
-var marionetteLogObj = new MarionetteLogObj();
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var contentLog = new logging.ContentLogger();
 
 var isB2G = false;
 
 var marionetteTestName;
 var winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIDOMWindowUtils);
 var listenerId = null; // unique ID of this listener
 var curContainer = { frame: content, shadowRoot: null };
 var isRemoteBrowser = () => curContainer.frame.contentWindow !== null;
 var previousContainer = null;
 var elementManager = new ElementManager();
-
 var capabilities = {};
 
 var actions = new action.Chain(checkForInterrupted);
 
 // Contains the last file input element that was the target of
 // sendKeysToElement.
 var fileInputElement;
 
-// A dict of sandboxes used this session
-var sandboxes = {};
-// The name of the current sandbox
-var sandboxName = 'default';
-
 // the unload handler
 var onunload;
 
 // Flag to indicate whether an async script is currently running or not.
 var asyncTestRunning = false;
 var asyncTestCommandId;
 var asyncTestTimeoutId;
 
 var inactivityTimeoutId = null;
-var heartbeatCallback = function () {}; // Called by the simpletest methods.
 
 var originalOnError;
 //timer for doc changes
 var checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 //timer for readystate
 var readyStateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 // timer for navigation commands.
 var navTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 var onDOMContentLoaded;
 // Send move events about this often
 var EVENT_INTERVAL = 30; // milliseconds
 // last touch for each fingerId
 var multiLast = {};
-
-var chrome = proxy.toChrome(sendSyncMessage.bind(this));
-var cookies = new Cookies(() => curContainer.frame.document, chrome);
-var importedScripts = new evaluate.ScriptStorageServiceClient(chrome);
+var asyncChrome = proxy.toChromeAsync({
+  addMessageListener: addMessageListenerId.bind(this),
+  removeMessageListener: removeMessageListenerId.bind(this),
+  sendAsyncMessage: sendAsyncMessage.bind(this),
+});
+var syncChrome = proxy.toChrome(sendSyncMessage.bind(this));
+var cookies = new Cookies(() => curContainer.frame.document, syncChrome);
+var importedScripts = new evaluate.ScriptStorageServiceClient(syncChrome);
 
 Cu.import("resource://gre/modules/Log.jsm");
 var logger = Log.repository.getLogger("Marionette");
 logger.debug("loaded listener.js");
+
 var modalHandler = function() {
   // This gets called on the system app only since it receives the mozbrowserprompt event
-  sendSyncMessage("Marionette:switchedToFrame", { frameValue: null, storePrevious: true });
+  sendSyncMessage("Marionette:switchedToFrame", {frameValue: null, storePrevious: true});
   let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value;
   if (isLocal) {
     previousContainer = curContainer;
   }
-  curContainer = { frame: content, shadowRoot: null };
+  curContainer = {frame: content, shadowRoot: null};
 };
 
+// sandbox storage and name of the current sandbox
+var sandboxes = new Sandboxes(() => curContainer.frame);
+var sandboxName = "default";
+
 /**
  * Called when listener is first started up.
  * The listener sends its unique window ID and its current URI to the actor.
  * If the actor returns an ID, we start the listeners. Otherwise, nothing happens.
  */
 function registerSelf() {
   let msg = {value: winUtil.outerWindowID};
   // register will have the ID and a boolean describing if this is the main process or not
@@ -223,26 +229,29 @@ var getCookiesFn = dispatch(getCookies);
 var singleTapFn = dispatch(singleTap);
 var takeScreenshotFn = dispatch(takeScreenshot);
 var getScreenshotHashFn = dispatch(getScreenshotHash);
 var actionChainFn = dispatch(actionChain);
 var multiActionFn = dispatch(multiAction);
 var addCookieFn = dispatch(addCookie);
 var deleteCookieFn = dispatch(deleteCookie);
 var deleteAllCookiesFn = dispatch(deleteAllCookies);
+var executeFn = dispatch(execute);
+var executeInSandboxFn = dispatch(executeInSandbox);
+var executeSimpleTestFn = dispatch(executeSimpleTest);
 
 /**
  * Start all message listeners
  */
 function startListeners() {
   addMessageListenerId("Marionette:receiveFiles", receiveFiles);
   addMessageListenerId("Marionette:newSession", newSession);
-  addMessageListenerId("Marionette:executeScript", executeScript);
-  addMessageListenerId("Marionette:executeAsyncScript", executeAsyncScript);
-  addMessageListenerId("Marionette:executeJSScript", executeJSScript);
+  addMessageListenerId("Marionette:execute", executeFn);
+  addMessageListenerId("Marionette:executeInSandbox", executeInSandboxFn);
+  addMessageListenerId("Marionette:executeSimpleTest", executeSimpleTestFn);
   addMessageListenerId("Marionette:singleTap", singleTapFn);
   addMessageListenerId("Marionette:actionChain", actionChainFn);
   addMessageListenerId("Marionette:multiAction", multiActionFn);
   addMessageListenerId("Marionette:get", get);
   addMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
   addMessageListenerId("Marionette:cancelRequest", cancelRequest);
   addMessageListenerId("Marionette:getCurrentUrl", getCurrentUrlFn);
   addMessageListenerId("Marionette:getTitle", getTitleFn);
@@ -335,19 +344,19 @@ function restart(msg) {
 }
 
 /**
  * Removes all listeners
  */
 function deleteSession(msg) {
   removeMessageListenerId("Marionette:receiveFiles", receiveFiles);
   removeMessageListenerId("Marionette:newSession", newSession);
-  removeMessageListenerId("Marionette:executeScript", executeScript);
-  removeMessageListenerId("Marionette:executeAsyncScript", executeAsyncScript);
-  removeMessageListenerId("Marionette:executeJSScript", executeJSScript);
+  removeMessageListenerId("Marionette:execute", executeFn);
+  removeMessageListenerId("Marionette:executeInSandbox", executeInSandboxFn);
+  removeMessageListenerId("Marionette:executeSimpleTest", executeSimpleTestFn);
   removeMessageListenerId("Marionette:singleTap", singleTapFn);
   removeMessageListenerId("Marionette:actionChain", actionChainFn);
   removeMessageListenerId("Marionette:multiAction", multiActionFn);
   removeMessageListenerId("Marionette:get", get);
   removeMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
   removeMessageListenerId("Marionette:cancelRequest", cancelRequest);
   removeMessageListenerId("Marionette:getTitle", getTitleFn);
   removeMessageListenerId("Marionette:getPageSource", getPageSourceFn);
@@ -451,18 +460,18 @@ function sendError(err, uuid) {
 function sendLog(msg) {
   sendToServer("Marionette:log", {message: msg});
 }
 
 /**
  * Clear test values after completion of test
  */
 function resetValues() {
-  sandboxes = {};
-  curContainer = { frame: content, shadowRoot: null };
+  sandboxes.clear();
+  curContainer = {frame: content, shadowRoot: null};
   actions.mouseEventsOnly = false;
 }
 
 /**
  * Dump a logline to stdout. Prepends logline with a timestamp.
  */
 function dumpLog(logline) {
   dump(Date.now() + " Marionette: " + logline);
@@ -494,199 +503,79 @@ function checkForInterrupted() {
       else {
         //else we're in OOP environment, so we'll switch to the original OOP frame
         sendSyncMessage("Marionette:switchToModalOrigin");
       }
       sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true });
     }
 }
 
-/*
- * Marionette Methods
- */
-
-/**
- * Returns a content sandbox that can be used by the execute_foo functions.
- */
-function createExecuteContentSandbox(win, timeout) {
-  let mn = new Marionette(
-      win,
-      "content",
-      marionetteLogObj,
-      timeout,
-      heartbeatCallback,
-      marionetteTestName);
-
-  let principal = win;
-  if (sandboxName == "system") {
-    principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
-  }
-  let sandbox = new Cu.Sandbox(principal, {sandboxPrototype: win});
-  sandbox.global = sandbox;
-  sandbox.window = win;
-  sandbox.document = sandbox.window.document;
-  sandbox.navigator = sandbox.window.navigator;
-  sandbox.asyncTestCommandId = asyncTestCommandId;
-  sandbox.marionette = mn;
-
-  mn.exports.forEach(fn => {
-    if (typeof mn[fn] == "function") {
-      sandbox[fn] = mn[fn].bind(mn);
-    } else {
-      sandbox[fn] = mn[fn];
-    }
-  });
-  sandbox.runEmulatorCmd = (cmd, cb) => this.runEmulatorCmd(cmd, cb);
-  sandbox.runEmulatorShell = (args, cb) => this.runEmulatorShell(args, cb);
+function* execute(script, args, timeout, opts) {
+  opts.timeout = timeout;
+  script = importedScripts.for("content").concat(script);
 
-  sandbox.asyncComplete = (obj, id) => {
-    if (id == asyncTestCommandId) {
-      curContainer.frame.removeEventListener("unload", onunload, false);
-      curContainer.frame.clearTimeout(asyncTestTimeoutId);
-
-      if (inactivityTimeoutId != null) {
-        curContainer.frame.clearTimeout(inactivityTimeoutId);
-      }
-
-      sendSyncMessage("Marionette:shareData",
-          {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
-      marionetteLogObj.clearLogs();
+  let sb = sandbox.createMutable(curContainer.frame);
+  let wargs = elementManager.convertWrappedArguments(args, curContainer);
+  let res = yield evaluate.sandbox(sb, script, wargs, opts);
 
-      if (error.isError(obj)) {
-        sendError(obj, id);
-      } else {
-        if (Object.keys(_emu_cbs).length) {
-          _emu_cbs = {};
-          sendError(new WebDriverError("Emulator callback still pending when finish() called"), id);
-        } else {
-          sendResponse(elementManager.wrapValue(obj), id);
-        }
-      }
-
-      asyncTestRunning = false;
-      asyncTestTimeoutId = undefined;
-      asyncTestCommandId = undefined;
-      inactivityTimeoutId = null;
-    }
-  };
-
-  sandbox.finish = function() {
-    if (asyncTestRunning) {
-      sandbox.asyncComplete(mn.generate_results(), sandbox.asyncTestCommandId);
-    } else {
-      return mn.generate_results();
-    }
-  };
-  sandbox.marionetteScriptFinished = val =>
-      sandbox.asyncComplete(val, sandbox.asyncTestCommandId);
-
-  sandboxes[sandboxName] = sandbox;
+  return elementManager.wrapValue(res);
 }
 
-/**
- * Execute the given script either as a function body (executeScript)
- * or directly (for mochitest like JS Marionette tests).
- */
-function executeScript(msg, directInject) {
-  // Set up inactivity timeout.
-  if (msg.json.inactivityTimeout) {
-    let setTimer = function() {
-      inactivityTimeoutId = curContainer.frame.setTimeout(function() {
-        sendError(new ScriptTimeoutError("timed out due to inactivity"), asyncTestCommandId);
-      }, msg.json.inactivityTimeout);
-   };
+function* executeInSandbox(script, args, timeout, opts) {
+  opts.timeout = timeout;
+  script = importedScripts.for("content").concat(script);
 
-    setTimer();
-    heartbeatCallback = function() {
-      curContainer.frame.clearTimeout(inactivityTimeoutId);
-      setTimer();
-    };
-  }
-
-  asyncTestCommandId = msg.json.command_id;
-  let script = msg.json.script;
-  let filename = msg.json.filename;
-  sandboxName = msg.json.sandboxName;
-
-  if (msg.json.newSandbox ||
-      !(sandboxName in sandboxes) ||
-      (sandboxes[sandboxName].window != curContainer.frame)) {
-    createExecuteContentSandbox(curContainer.frame, msg.json.timeout);
-    if (!sandboxes[sandboxName]) {
-      sendError(new WebDriverError("Could not create sandbox!"), asyncTestCommandId);
-      return;
-    }
-  } else {
-    sandboxes[sandboxName].asyncTestCommandId = asyncTestCommandId;
+  let sb = sandboxes.get(opts.sandboxName, opts.newSandbox);
+  if (opts.sandboxName) {
+    sb = sandbox.augment(sb, {global: sb});
+    sb = sandbox.augment(sb, new logging.Adapter(contentLog));
+    let emulatorClient = new emulator.EmulatorServiceClient(asyncChrome);
+    sb = sandbox.augment(sb, new emulator.Adapter(emulatorClient));
   }
 
-  let sandbox = sandboxes[sandboxName];
+  let wargs = elementManager.convertWrappedArguments(args, curContainer);
+  let evaluatePromise = evaluate.sandbox(sb, script, wargs, opts);
 
-  try {
-    if (directInject) {
-      script = importedScripts.for("content").concat(script);
-      let res = Cu.evalInSandbox(script, sandbox, "1.8", filename ? filename : "dummy file" ,0);
+  let res = yield evaluatePromise;
+  sendSyncMessage("Marionette:shareData", {log: elementManager.wrapValue(contentLog.get())});
+  return elementManager.wrapValue(res);
+}
 
-      sendSyncMessage("Marionette:shareData",
-                      {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
-      marionetteLogObj.clearLogs();
+function* executeSimpleTest(script, args, timeout, opts) {
+  opts.timeout = timeout;
+  let win = curContainer.frame;
+  script = importedScripts.for("content").concat(script);
 
-      if (res == undefined || res.passed == undefined) {
-        sendError(new JavaScriptError("Marionette.finish() not called"), asyncTestCommandId);
-      }
-      else {
-        sendResponse(elementManager.wrapValue(res), asyncTestCommandId);
-      }
-    }
-    else {
-      try {
-        sandbox.__marionetteParams = Cu.cloneInto(elementManager.convertWrappedArguments(
-          msg.json.args, curContainer), sandbox, { wrapReflectors: true });
-      } catch (e) {
-        sendError(e, asyncTestCommandId);
-        return;
-      }
+  let harness = new simpletest.Harness(
+      win,
+      "content",
+      contentLog,
+      timeout,
+      marionetteTestName);
+  let sb = sandbox.createSimpleTest(curContainer.frame, harness);
+  // TODO(ato): Not sure this is needed:
+  sb = sandbox.augment(sb, new logging.Adapter(contentLog));
 
-      script = "var __marionetteFunc = function(){" + script + "};" +
-                   "__marionetteFunc.apply(null, __marionetteParams);";
-      script = importedScripts.for("content").concat(script);
+  let wargs = elementManager.convertWrappedArguments(args, curContainer);
+  let evaluatePromise = evaluate.sandbox(sb, script, wargs, opts);
 
-      let res = Cu.evalInSandbox(script, sandbox, "1.8", filename ? filename : "dummy file", 0);
-      sendSyncMessage("Marionette:shareData",
-                      {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
-      marionetteLogObj.clearLogs();
-      sendResponse(elementManager.wrapValue(res), asyncTestCommandId);
-    }
-  } catch (e) {
-    let err = new JavaScriptError(
-        e,
-        "execute_script",
-        msg.json.filename,
-        msg.json.line,
-        script);
-    sendError(err, asyncTestCommandId);
-  }
+  let res = yield evaluatePromise;
+  sendSyncMessage("Marionette:shareData", {log: elementManager.wrapValue(contentLog.get())});
+  return elementManager.wrapValue(res);
 }
 
 /**
  * Sets the test name, used in logging messages.
  */
 function setTestName(msg) {
   marionetteTestName = msg.json.value;
   sendOk(msg.json.command_id);
 }
 
 /**
- * Execute async script
- */
-function executeAsyncScript(msg) {
-  executeWithCallback(msg);
-}
-
-/**
  * Receive file objects from chrome in order to complete a
  * sendKeysToElement action on a file input element.
  */
 function receiveFiles(msg) {
   if ('error' in msg.json) {
     let err = new InvalidArgumentError(msg.json.error);
     sendError(err, msg.json.command_id);
     return;
@@ -699,123 +588,16 @@ function receiveFiles(msg) {
   let fs = Array.prototype.slice.call(fileInputElement.files);
   fs.push(msg.json.file);
   fileInputElement.mozSetFileArray(fs);
   fileInputElement = null;
   sendOk(msg.json.command_id);
 }
 
 /**
- * Execute pure JS test. Handles both async and sync cases.
- */
-function executeJSScript(msg) {
-  if (msg.json.async) {
-    executeWithCallback(msg, msg.json.async);
-  }
-  else {
-    executeScript(msg, true);
-  }
-}
-
-/**
- * This function is used by executeAsync and executeJSScript to execute a script
- * in a sandbox.
- *
- * For executeJSScript, it will return a message only when the finish() method is called.
- * For executeAsync, it will return a response when marionetteScriptFinished/arguments[arguments.length-1]
- * method is called, or if it times out.
- */
-function executeWithCallback(msg, useFinish) {
-  // Set up inactivity timeout.
-  if (msg.json.inactivityTimeout) {
-    let setTimer = function() {
-      inactivityTimeoutId = curContainer.frame.setTimeout(function() {
-        sandbox.asyncComplete(new ScriptTimeoutError("timed out due to inactivity"), asyncTestCommandId);
-      }, msg.json.inactivityTimeout);
-    };
-
-    setTimer();
-    heartbeatCallback = function() {
-      curContainer.frame.clearTimeout(inactivityTimeoutId);
-      setTimer();
-    };
-  }
-
-  let script = msg.json.script;
-  let filename = msg.json.filename;
-  asyncTestCommandId = msg.json.command_id;
-  sandboxName = msg.json.sandboxName;
-
-  onunload = function() {
-    sendError(new JavaScriptError("unload was called"), asyncTestCommandId);
-  };
-  curContainer.frame.addEventListener("unload", onunload, false);
-
-  if (msg.json.newSandbox ||
-      !(sandboxName in sandboxes) ||
-      (sandboxes[sandboxName].window != curContainer.frame)) {
-    createExecuteContentSandbox(curContainer.frame, msg.json.timeout);
-    if (!sandboxes[sandboxName]) {
-      sendError(new JavaScriptError("Could not create sandbox!"), asyncTestCommandId);
-      return;
-    }
-  }
-  else {
-    sandboxes[sandboxName].asyncTestCommandId = asyncTestCommandId;
-  }
-  let sandbox = sandboxes[sandboxName];
-  sandbox.tag = script;
-
-  asyncTestTimeoutId = curContainer.frame.setTimeout(function() {
-    sandbox.asyncComplete(new ScriptTimeoutError("timed out"), asyncTestCommandId);
-  }, msg.json.timeout);
-
-  originalOnError = curContainer.frame.onerror;
-  curContainer.frame.onerror = function errHandler(msg, url, line) {
-    sandbox.asyncComplete(new JavaScriptError(msg + "@" + url + ", line " + line), asyncTestCommandId);
-    curContainer.frame.onerror = originalOnError;
-  };
-
-  let scriptSrc;
-  if (useFinish) {
-    if (msg.json.timeout == null || msg.json.timeout == 0) {
-      sendError(new TimeoutError("Please set a timeout"), asyncTestCommandId);
-    }
-    scriptSrc = script;
-  }
-  else {
-    try {
-      sandbox.__marionetteParams = Cu.cloneInto(elementManager.convertWrappedArguments(
-        msg.json.args, curContainer), sandbox, { wrapReflectors: true });
-    } catch (e) {
-      sendError(e, asyncTestCommandId);
-      return;
-    }
-
-    scriptSrc = "__marionetteParams.push(marionetteScriptFinished);" +
-                "var __marionetteFunc = function() { " + script + "};" +
-                "__marionetteFunc.apply(null, __marionetteParams); ";
-  }
-
-  try {
-    asyncTestRunning = true;
-    scriptSrc = importedScripts.for("content").concat(scriptSrc);
-    Cu.evalInSandbox(scriptSrc, sandbox, "1.8", filename ? filename : "dummy file", 0);
-  } catch (e) {
-    let err = new JavaScriptError(
-        e,
-        "execute_async_script",
-        msg.json.filename,
-        msg.json.line,
-        scriptSrc);
-    sandbox.asyncComplete(err, asyncTestCommandId);
-  }
-}
-
-/**
  * This function creates a touch event given a touch type and a touch
  */
 function emitTouchEvent(type, touch) {
   if (!wasInterrupted()) {
     let loggingInfo = "emitting Touch event of type " + type + " to element with id: " + touch.target.id + " and tag name: " + touch.target.tagName + " at coordinates (" + touch.clientX + ", " + touch.clientY + ") relative to the viewport";
     dumpLog(loggingInfo);
     var docShell = curContainer.frame.document.defaultView.
                    QueryInterface(Components.interfaces.nsIInterfaceRequestor).
@@ -833,20 +615,20 @@ function emitTouchEvent(type, touch) {
             radiusX: touch.radiusX, radiusY: touch.radiusY,
             rotation: touch.rotationAngle, force: touch.force });
         return;
       }
     }
     // we get here if we're not in asyncPacZoomEnabled land, or if we're the main process
     /*
     Disabled per bug 888303
-    marionetteLogObj.log(loggingInfo, "TRACE");
+    contentLog.log(loggingInfo, "TRACE");
     sendSyncMessage("Marionette:shareData",
-                    {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
-    marionetteLogObj.clearLogs();
+                    {log: elementManager.wrapValue(contentLog.get())});
+    contentLog.clear();
     */
     let domWindowUtils = curContainer.frame.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils);
     domWindowUtils.sendTouchEvent(type, [touch.identifier], [touch.clientX], [touch.clientY], [touch.radiusX], [touch.radiusY], [touch.rotationAngle], [touch.force], 1, 0);
   }
 }
 
 /**
  * Function that perform a single tap
@@ -1130,17 +912,16 @@ function get(msg) {
   // Prevent DOMContentLoaded events from frames from invoking this
   // code, unless the event is coming from the frame associated with
   // the current window (i.e. someone has used switch_to_frame).
   onDOMContentLoaded = function onDOMContentLoaded(event) {
     if (!event.originalTarget.defaultView.frameElement ||
         event.originalTarget.defaultView.frameElement == curContainer.frame.frameElement) {
       pollForReadyState(msg, start, () => {
         removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
-        onDOMContentLoaded = null;
       });
     }
   };
 
   function timerFunc() {
     removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
     sendError(new TimeoutError("Error loading page, timed out (onDOMContentLoaded)"), msg.json.command_id);
   }
@@ -1149,17 +930,16 @@ function get(msg) {
   }
   addEventListener("DOMContentLoaded", onDOMContentLoaded, false);
   if (isB2G) {
     curContainer.frame.location = msg.json.url;
   } else {
     // We need to move to the top frame before navigating
     sendSyncMessage("Marionette:switchedToFrame", { frameValue: null });
     curContainer.frame = content;
-
     curContainer.frame.location = msg.json.url;
   }
 }
 
  /**
  * Cancel the polling and remove the event listener associated with a current
  * navigation request in case we're interupted by an onbeforeunload handler
  * and navigation doesn't complete.
@@ -1216,17 +996,17 @@ function goForward(msg) {
 
 /**
  * Refresh the page
  */
 function refresh(msg) {
   let command_id = msg.json.command_id;
   curContainer.frame.location.reload(true);
   let listen = function() {
-    removeEventListener("DOMContentLoaded", arguments.callee, false);
+    removeEventListener("DOMContentLoaded", listen, false);
     sendOk(command_id);
   };
   addEventListener("DOMContentLoaded", listen, false);
 }
 
 /**
  * Find an element in the current browsing context's document using the
  * given search strategy.
@@ -1611,29 +1391,30 @@ function switchToFrame(msg) {
 
   if (foundFrame === null) {
     sendError(new NoSuchFrameError("Unable to locate frame: " + (msg.json.id || msg.json.element)), command_id);
     return true;
   }
 
   // send a synchronous message to let the server update the currently active
   // frame element (for getActiveFrame)
-  let frameValue = elementManager.wrapValue(curContainer.frame.wrappedJSObject)['ELEMENT'];
-  sendSyncMessage("Marionette:switchedToFrame", { frameValue: frameValue });
+  let frameValue = elementManager.wrapValue(curContainer.frame.wrappedJSObject).ELEMENT;
+  sendSyncMessage("Marionette:switchedToFrame", {frameValue: frameValue});
 
   let rv = null;
   if (curContainer.frame.contentWindow === null) {
     // The frame we want to switch to is a remote/OOP frame;
     // notify our parent to handle the switch
     curContainer.frame = content;
     rv = {win: parWindow, frame: foundFrame};
   } else {
     curContainer.frame = curContainer.frame.contentWindow;
-    if (msg.json.focus)
+    if (msg.json.focus) {
       curContainer.frame.focus();
+    }
     checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
   }
 
   sendResponse(rv, command_id);
 }
 
 function addCookie(cookie) {
   cookies.add(cookie.name, cookie.value, cookie);
new file mode 100644
--- /dev/null
+++ b/testing/marionette/logging.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["logging"];
+
+this.logging = {};
+
+/** Simple logger that is used in Simple Test harness tests. */
+logging.ContentLogger = class {
+  constructor() {
+    this.logs_ = [];
+  }
+
+  /**
+   * Append a log entry.
+   *
+   * @param {string} message
+   *     Log entry message.
+   * @param {string=} level
+   *     Severity of entry.  Defaults to "INFO".
+   */
+  log(message, level = "INFO") {
+    let now = (new Date()).toString();
+    this.logs_.push([level, message, now]);
+  }
+
+  /**
+   * Append array of log entries.
+   *
+   * @param {Array.<Array<string, string, string>>} messages
+   *     List of log entries, that are of the form severity, message,
+   *     and date.
+   */
+  addAll(messages) {
+    for (let message of messages) {
+      this.logs_.push(message);
+    }
+  }
+
+  /**
+   * Gets current log entries and clears the cache.
+   *
+   * @return {Array.<Array<string, string, string>>}
+   *     Log entries of the form severity, message, and date.
+   */
+  get() {
+    let logs = this.logs_;
+    this.logs_ = [];
+    return logs;
+  }
+};
+
+/**
+ * Adapts an instance of ContentLogger for use in a sandbox.  Is consumed
+ * by sandbox.augment.
+ */
+logging.Adapter = class {
+  constructor(logger = null) {
+    this.logger = logger;
+  }
+
+  get exports() {
+    return new Map([["log", this.log.bind(this)]]);
+  }
+
+  log(message, level = "INFO") {
+    dump(`MARIONETTE LOG: ${level}: ${message}\n`);
+    if (this.logger) {
+      this.logger.log(message, level);
+    }
+  }
+};
--- a/testing/marionette/proxy.js
+++ b/testing/marionette/proxy.js
@@ -219,16 +219,20 @@ proxy.AsyncMessageChannel = class {
       callback(msg);
     };
 
     this.mm.addMessageListener(path, autoRemover);
     this.listeners_.set(path, autoRemover);
   }
 
   removeListener_(path) {
+    if (!this.listeners_.has(path)) {
+      return true;
+    }
+
     let l = this.listeners_.get(path);
     this.mm.removeMessageListener(path, l[1]);
     return this.listeners_.delete(path);
   }
 
   removeAllListeners_() {
     let ok = true;
     for (let [p, cb] of this.listeners_) {
@@ -239,16 +243,88 @@ proxy.AsyncMessageChannel = class {
 };
 proxy.AsyncMessageChannel.ReplyType = {
   Ok: 0,
   Value: 1,
   Error: 2,
 };
 
 /**
+ * A transparent content-to-chrome RPC interface where responses are
+ * presented as promises.
+ *
+ * @param {nsIFrameMessageManager} frameMessageManager
+ *     The content frame's message manager, which itself is usually an
+ *     implementor of.
+ */
+proxy.toChromeAsync = function(frameMessageManager) {
+  let sender = new AsyncChromeSender(frameMessageManager);
+  return new Proxy(sender, ownPriorityGetterTrap);
+};
+
+/**
+ * Sends asynchronous RPC messages to chrome space using a frame's
+ * sendAsyncMessage (nsIAsyncMessageSender) function.
+ *
+ * Example on how to use from a frame content script:
+ *
+ *     let sender = new AsyncChromeSender(messageManager);
+ *     let promise = sender.send("runEmulatorCmd", "my command");
+ *     let rv = yield promise;
+ */
+this.AsyncChromeSender = class {
+  constructor(frameMessageManager) {
+    this.mm = frameMessageManager;
+  }
+
+  /**
+   * Call registered function in chrome context.
+   *
+   * @param {string} name
+   *     Function to call in the chrome, e.g. for "Marionette:foo", use
+   *     "foo".
+   * @param {?} args
+   *     Argument list to pass the function.  Must be JSON serialisable.
+   *
+   * @return {Promise}
+   *     A promise that resolves to the value sent back.
+   */
+  send(name, args) {
+    let uuid = uuidgen.generateUUID().toString();
+
+    let proxy = new Promise((resolve, reject) => {
+      let responseListener = msg => {
+        if (msg.json.id != uuid) {
+          return;
+        }
+
+        this.mm.removeMessageListener(
+            "Marionette:listenerResponse", responseListener);
+
+        if ("value" in msg.json) {
+          resolve(msg.json.value);
+        } else if ("error" in msg.json) {
+          reject(msg.json.error);
+        } else {
+          throw new TypeError(
+              `Unexpected response: ${msg.name} ${JSON.stringify(msg.json)}`);
+        }
+      };
+
+      let msg = {arguments: marshal(args), id: uuid};
+      this.mm.addMessageListener(
+          "Marionette:listenerResponse", responseListener);
+      this.mm.sendAsyncMessage("Marionette:" + name, msg);
+    });
+
+    return proxy;
+  }
+};
+
+/**
  * Creates a transparent interface from the content- to the chrome context.
  *
  * Calls to this object will be proxied via the frame's sendSyncMessage
  * (nsISyncMessageSender) function.  Since the message is synchronous,
  * the return value is presented as a return value.
  *
  * Example on how to use from a frame content script:
  *
@@ -260,30 +336,31 @@ proxy.AsyncMessageChannel.ReplyType = {
  */
 proxy.toChrome = function(sendSyncMessageFn) {
   let sender = new proxy.SyncChromeSender(sendSyncMessageFn);
   return new Proxy(sender, ownPriorityGetterTrap);
 };
 
 /**
  * The SyncChromeSender sends synchronous RPC messages to the chrome
- * context, using a frame's sendSyncMessage (nsISyncMessageSender) function.
+ * context, using a frame's sendSyncMessage (nsISyncMessageSender)
+ * function.
  *
  * Example on how to use from a frame content script:
  *
  *     let sender = new SyncChromeSender(sendSyncMessage.bind(this));
  *     let res = sender.send("addCookie", cookie);
  */
 proxy.SyncChromeSender = class {
   constructor(sendSyncMessage) {
     this.sendSyncMessage_ = sendSyncMessage;
   }
 
   send(func, args) {
-    let name = "Marionette:" + func;
+    let name = "Marionette:" + func.toString();
     return this.sendSyncMessage_(name, marshal(args));
   }
 };
 
 var marshal = function(args) {
   if (args.length == 1 && typeof args[0] == "object") {
     return args[0];
   }
--- a/testing/marionette/simpletest.js
+++ b/testing/marionette/simpletest.js
@@ -1,110 +1,114 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-var {utils: Cu} = Components;
+"use strict";
+
+const {utils: Cu} = Components;
 
 Cu.import("chrome://marionette/content/error.js");
 
-this.EXPORTED_SYMBOLS = ["Marionette"];
+this.EXPORTED_SYMBOLS = ["simpletest"];
 
-/*
- * The Marionette object, passed to the script context.
+this.simpletest = {};
+
+/**
+ * The simpletest harness, exposed in the script evaluation sandbox.
  */
-this.Marionette = function(window, context, logObj, timeout,
-    heartbeatCallback, testName) {
-  this.window = window;
-  this.tests = [];
-  this.logObj = logObj;
-  this.context = context;
-  this.timeout = timeout;
-  this.heartbeatCallback = heartbeatCallback;
-  this.testName = testName;
-  this.TEST_UNEXPECTED_FAIL = "TEST-UNEXPECTED-FAIL";
-  this.TEST_UNEXPECTED_PASS = "TEST-UNEXPECTED-PASS";
-  this.TEST_PASS = "TEST-PASS";
-  this.TEST_KNOWN_FAIL = "TEST-KNOWN-FAIL";
-};
+simpletest.Harness = class {
+  constructor(window, context, contentLogger, timeout, testName) {
+    this.window = window;
+    this.tests = [];
+    this.logger = contentLogger;
+    this.context = context;
+    this.timeout = timeout;
+    this.testName = testName;
+    this.TEST_UNEXPECTED_FAIL = "TEST-UNEXPECTED-FAIL";
+    this.TEST_UNEXPECTED_PASS = "TEST-UNEXPECTED-PASS";
+    this.TEST_PASS = "TEST-PASS";
+    this.TEST_KNOWN_FAIL = "TEST-KNOWN-FAIL";
+  }
 
-Marionette.prototype = {
-  exports: [
-    "ok",
-    "is",
-    "isnot",
-    "todo",
-    "log",
-    "getLogs",
-    "generate_results",
-    "waitFor",
-    "TEST_PASS",
-    "TEST_KNOWN_FAIL",
-    "TEST_UNEXPECTED_FAIL",
-    "TEST_UNEXPECTED_PASS"
-  ],
+  get exports() {
+    return new Map([
+      ["ok", this.ok.bind(this)],
+      ["is", this.is.bind(this)],
+      ["isnot", this.isnot.bind(this)],
+      ["todo", this.todo.bind(this)],
+      ["log", this.log.bind(this)],
+      ["getLogs", this.getLogs.bind(this)],
+      ["generate_results", this.generate_results.bind(this)],
+      ["waitFor", this.waitFor.bind(this)],
+      ["TEST_PASS", this.TEST_PASS],
+      ["TEST_KNOWN_FAIL", this.TEST_KNOWN_FAIL],
+      ["TEST_UNEXPECTED_FAIL", this.TEST_UNEXPECTED_FAIL],
+      ["TEST_UNEXPECTED_PASS", this.TEST_UNEXPECTED_PASS],
+    ]);
+  }
 
-  addTest: function Marionette__addTest(condition, name, passString, failString, diag, state) {
-    let test = {'result': !!condition, 'name': name, 'diag': diag, 'state': state};
-    this.logResult(test,
-                   typeof(passString) == "undefined" ? this.TEST_PASS : passString,
-                   typeof(failString) == "undefined" ? this.TEST_UNEXPECTED_FAIL : failString);
+  addTest(condition, name, passString, failString, diag, state) {
+    let test = {
+      result: !!condition,
+      name: name,
+      diag: diag,
+      state: state
+    };
+    this.logResult(
+        test,
+        typeof passString == "undefined" ? this.TEST_PASS : passString,
+        typeof failString == "undefined" ? this.TEST_UNEXPECTED_FAIL : failString);
     this.tests.push(test);
-  },
+  }
 
-  ok: function Marionette__ok(condition, name, passString, failString) {
-    this.heartbeatCallback();
-    let diag = this.repr(condition) + " was " + !!condition + ", expected true";
+  ok(condition, name, passString, failString) {
+    let diag = `${this.repr(condition)} was ${!!condition}, expected true`;
     this.addTest(condition, name, passString, failString, diag);
-  },
+  }
 
-  is: function Marionette__is(a, b, name, passString, failString) {
-    this.heartbeatCallback();
+  is(a, b, name, passString, failString) {
     let pass = (a == b);
     let diag = pass ? this.repr(a) + " should equal " + this.repr(b)
                     : "got " + this.repr(a) + ", expected " + this.repr(b);
     this.addTest(pass, name, passString, failString, diag);
-  },
+  }
 
-  isnot: function Marionette__isnot (a, b, name, passString, failString) {
-    this.heartbeatCallback();
+  isnot(a, b, name, passString, failString) {
     let pass = (a != b);
     let diag = pass ? this.repr(a) + " should not equal " + this.repr(b)
                     : "didn't expect " + this.repr(a) + ", but got it";
     this.addTest(pass, name, passString, failString, diag);
-  },
+  }
 
-  todo: function Marionette__todo(condition, name, passString, failString) {
-    this.heartbeatCallback();
+  todo(condition, name, passString, failString) {
     let diag = this.repr(condition) + " was expected false";
     this.addTest(!condition,
                  name,
                  typeof(passString) == "undefined" ? this.TEST_KNOWN_FAIL : passString,
                  typeof(failString) == "undefined" ? this.TEST_UNEXPECTED_FAIL : failString,
                  diag,
                  "todo");
-  },
+  }
 
-  log: function Marionette__log(msg, level) {
-    this.heartbeatCallback();
+  log(msg, level) {
     dump("MARIONETTE LOG: " + (level ? level : "INFO") + ": " + msg + "\n");
-    if (this.logObj != null) {
-      this.logObj.log(msg, level);
+    if (this.logger) {
+      this.logger.log(msg, level);
     }
-  },
+  }
 
-  getLogs: function Marionette__getLogs() {
-    this.heartbeatCallback();
-    if (this.logObj != null) {
-      this.logObj.getLogs();
+  // TODO(ato): Suspect this isn't used anywhere
+  getLogs() {
+    if (this.logger) {
+      return this.logger.get();
     }
-  },
+  }
 
-  generate_results: function Marionette__generate_results() {
-    this.heartbeatCallback();
+  generate_results() {
     let passed = 0;
     let failures = [];
     let expectedFailures = [];
     let unexpectedSuccesses = [];
     for (let i in this.tests) {
       let isTodo = (this.tests[i].state == "todo");
       if(this.tests[i].result) {
         if (isTodo) {
@@ -120,83 +124,85 @@ Marionette.prototype = {
         }
         else {
           failures.push({'name': this.tests[i].name, 'diag': this.tests[i].diag});
         }
       }
     }
     // Reset state in case this object is reused for more tests.
     this.tests = [];
-    return {"passed": passed, "failures": failures, "expectedFailures": expectedFailures,
-            "unexpectedSuccesses": unexpectedSuccesses};
-  },
+    return {
+      passed: passed,
+      failures: failures,
+      expectedFailures: expectedFailures,
+      unexpectedSuccesses: unexpectedSuccesses,
+    };
+  }
 
-  logToFile: function Marionette__logToFile(file) {
-    this.heartbeatCallback();
+  logToFile(file) {
     //TODO
-  },
+  }
 
-  logResult: function Marionette__logResult(test, passString, failString) {
-    this.heartbeatCallback();
+  logResult(test, passString, failString) {
     //TODO: dump to file
     let resultString = test.result ? passString : failString;
     let diagnostic = test.name + (test.diag ? " - " + test.diag : "");
     let msg = resultString + " | " + this.testName + " | " + diagnostic;
     dump("MARIONETTE TEST RESULT:" + msg + "\n");
-  },
+  }
+
+  repr(o) {
+    if (typeof o == "undefined") {
+      return "undefined";
+    } else if (o === null) {
+      return "null";
+    }
 
-  repr: function Marionette__repr(o) {
-      if (typeof(o) == "undefined") {
-          return "undefined";
-      } else if (o === null) {
-          return "null";
-      }
-      try {
-          if (typeof(o.__repr__) == 'function') {
-              return o.__repr__();
-          } else if (typeof(o.repr) == 'function' && o.repr != arguments.callee) {
-              return o.repr();
-          }
-     } catch (e) {
-     }
-     try {
-          if (typeof(o.NAME) == 'string' && (
-                  o.toString == Function.prototype.toString ||
-                  o.toString == Object.prototype.toString
-              )) {
-              return o.NAME;
-          }
-      } catch (e) {
+    try {
+        if (typeof o.__repr__ == "function") {
+          return o.__repr__();
+        } else if (typeof o.repr == "function" && o.repr !== arguments.callee) {
+          return o.repr();
+        }
+   } catch (e) {}
+
+   try {
+      if (typeof o.NAME === "string" &&
+          (o.toString === Function.prototype.toString || o.toString === Object.prototype.toString)) {
+        return o.NAME;
       }
-      let ostring;
-      try {
-          ostring = (o + "");
-      } catch (e) {
-          return "[" + typeof(o) + "]";
+    } catch (e) {}
+
+    let ostring;
+    try {
+      ostring = (o + "");
+    } catch (e) {
+      return "[" + typeof(o) + "]";
+    }
+
+    if (typeof o == "function") {
+      o = ostring.replace(/^\s+/, "");
+      let idx = o.indexOf("{");
+      if (idx != -1) {
+        o = o.substr(0, idx) + "{...}";
       }
-      if (typeof(o) == "function") {
-          o = ostring.replace(/^\s+/, "");
-          let idx = o.indexOf("{");
-          if (idx != -1) {
-              o = o.substr(0, idx) + "{...}";
-          }
-      }
-      return ostring;
-  },
+    }
+    return ostring;
+  }
 
-  waitFor: function test_waitFor(callback, test, timeout) {
-      this.heartbeatCallback();
-      if (test()) {
-        callback();
-        return;
-      }
-      var now = new Date();
-      var deadline = (timeout instanceof Date) ? timeout :
-                     new Date(now.valueOf() + (typeof(timeout) == "undefined" ? this.timeout : timeout))
-      if (deadline <= now) {
-        dump("waitFor timeout: " + test.toString() + "\n");
-        // the script will timeout here, so no need to raise a separate
-        // timeout exception
-        return;
-      }
-      this.window.setTimeout(this.waitFor.bind(this), 100, callback, test, deadline);
-  },
+  waitFor(callback, test, timeout) {
+    if (test()) {
+      callback();
+      return;
+    }
+
+    let now = new Date();
+    let deadline = (timeout instanceof Date) ? timeout :
+        new Date(now.valueOf() + (typeof timeout == "undefined" ? this.timeout : timeout));
+    if (deadline <= now) {
+      dump("waitFor timeout: " + test.toString() + "\n");
+      // the script will timeout here, so no need to raise a separate
+      // timeout exception
+      return;
+    }
+    this.window.setTimeout(this.waitFor.bind(this), 100, callback, test, deadline);
+  }
 };