Merge fx-team to m-c
authorWes Kocher <wkocher@mozilla.com>
Tue, 06 May 2014 18:04:03 -0700
changeset 181883 5bbc85136202211a6f479bdd4cb965d474119607
parent 181872 d50dd0f48ae8d1879e6525477f272fbfc9e011b7 (current diff)
parent 181882 dfba6113cb029b923db32ccf3b54aa1d5756bdd3 (diff)
child 181884 8fcfb1107b03eed0953fff37cce9d3625338c231
child 181925 0b7fbc19c190b47393d520d32c6cf9b9754a061f
child 181934 c23476640df167a7d6aab4ab12d9e6a055d549f5
child 181946 8f4c8f01f554c338bbcb6f3df054c7be90790102
push id43162
push userkwierso@gmail.com
push dateWed, 07 May 2014 01:07:29 +0000
treeherdermozilla-inbound@8fcfb1107b03 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone32.0a1
first release with
nightly linux32
5bbc85136202 / 32.0a1 / 20140507030202 / files
nightly linux64
5bbc85136202 / 32.0a1 / 20140507030202 / files
nightly mac
5bbc85136202 / 32.0a1 / 20140507030202 / files
nightly win32
5bbc85136202 / 32.0a1 / 20140507030202 / files
nightly win64
5bbc85136202 / 32.0a1 / 20140507030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c
--- a/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-set-param.js
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-set-param.js
@@ -27,20 +27,16 @@ function spawnTest () {
   ise(freq, 220, "AudioNode:setParam correctly sets a `number` AudioParam");
   is(resSuccess, undefined, "AudioNode:setParam returns undefined for correctly set AudioParam");
 
   resSuccess = yield oscNode.setParam("type", "square");
   let type = yield oscNode.getParam("type");
   ise(type, "square", "AudioNode:setParam correctly sets a `string` non-AudioParam");
   is(resSuccess, undefined, "AudioNode:setParam returns undefined for correctly set AudioParam");
 
-  resSuccess = yield oscNode.setParam("type", "\"triangle\"");
-  type = yield oscNode.getParam("type");
-  ise(type, "triangle", "AudioNode:setParam correctly removes quotes in `string` non-AudioParam");
-
   try {
     yield oscNode.setParam("frequency", "hello");
     ok(false, "setParam with invalid types should throw");
   } catch (e) {
     ok(/is not a finite floating-point/.test(e.message), "AudioNode:setParam returns error with correct message when attempting an invalid assignment");
     is(e.type, "TypeError", "AudioNode:setParam returns error with correct type when attempting an invalid assignment");
     freq = yield oscNode.getParam("frequency");
     ise(freq, 220, "AudioNode:setParam does not modify value when an error occurs");
--- a/browser/devtools/webaudioeditor/test/browser_wa_params_view_edit.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_params_view_edit.js
@@ -14,29 +14,37 @@ function spawnTest() {
 
   reload(target);
 
   let [[dest, osc, gain], [[_, destID], [_, oscID], [_, gainID]]] = yield Promise.all([
     get3(gFront, "create-node"),
     get3Spread(panel.panelWin, EVENTS.UI_ADD_NODE_LIST)
   ]);
 
+  let setAndCheck = setAndCheckVariable(panel.panelWin, gVars);
 
   checkVariableView(gVars, 1, {
     "type": "\"sine\"",
     "frequency": 440,
     "detune": 0
-  });
+  }, "default loaded string");
 
   checkVariableView(gVars, 2, {
     "gain": 0
-  });
-
-  yield modifyVariableView(panel.panelWin, gVars, 1, "type", "square");
+  }, "default loaded number");
 
-  checkVariableView(gVars, 1, {
-    "type": "\"square\""
-  });
+  yield setAndCheck(1, "type", "\"square\"", "\"square\"", "sets string as string");
+
+  yield setAndCheck(2, "gain", 0.005, 0.005, "sets number as number");
 
   yield teardown(panel);
   finish();
 }
 
+function setAndCheckVariable (panelWin, gVars) {
+  return Task.async(function (varNum, prop, value, expected, desc) {
+    yield modifyVariableView(panelWin, gVars, varNum, prop, value);
+    var props = {};
+    props[prop] = expected;
+    checkVariableView(gVars, varNum, props, desc);
+  });
+}
+
--- a/browser/devtools/webaudioeditor/test/head.js
+++ b/browser/devtools/webaudioeditor/test/head.js
@@ -206,25 +206,25 @@ function waitForGraphRendered (front, no
     if (nodes === nodeCount && edges === edgeCount) {
       front.off(eventName, onGraphRendered);
       deferred.resolve();
     }
   });
   return deferred.promise;
 }
 
-function checkVariableView (view, index, hash) {
+function checkVariableView (view, index, hash, description = "") {
   let scope = view.getScopeAtIndex(index);
   let variables = Object.keys(hash);
   variables.forEach(variable => {
     let aVar = scope.get(variable);
     is(aVar.target.querySelector(".name").getAttribute("value"), variable,
       "Correct property name for " + variable);
     is(aVar.target.querySelector(".value").getAttribute("value"), hash[variable],
-      "Correct property value of " + hash[variable] + " for " + variable);
+      "Correct property value of " + hash[variable] + " for " + variable + " " + description);
   });
 }
 
 function modifyVariableView (win, view, index, prop, value) {
   let deferred = Promise.defer();
   let scope = view.getScopeAtIndex(index);
   let aVar = scope.get(prop);
   scope.expand();
--- a/browser/devtools/webaudioeditor/webaudioeditor-view.js
+++ b/browser/devtools/webaudioeditor/webaudioeditor-view.js
@@ -264,22 +264,31 @@ let WebAudioParamView = {
 
   /**
    * Executed when an audio param is changed in the UI.
    */
   _onEval: Task.async(function* (variable, value) {
     let ownerScope = variable.ownerView;
     let node = getViewNodeById(ownerScope.actorID);
     let propName = variable.name;
-    let errorMessage = yield node.actor.setParam(propName, value);
+    let error;
+
+    // Cast value to proper type
+    try {
+      value = JSON.parse(value);
+      error = yield node.actor.setParam(propName, value);
+    }
+    catch (e) {
+      error = e;
+    }
 
     // TODO figure out how to handle and display set param errors
     // and enable `test/brorwser_wa_params_view_edit_error.js`
     // Bug 994258
-    if (!errorMessage) {
+    if (!error) {
       ownerScope.get(propName).setGrip(value);
       window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
     } else {
       window.emit(EVENTS.UI_SET_PARAM_ERROR, node.id, propName, value);
     }
   }),
 
   /**
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -1160,17 +1160,21 @@ Experiments.Experiments.prototype = {
 
     gPrefs.set(PREF_ACTIVE_EXPERIMENT, activeExperiment != null);
 
     if (activeChanged) {
       Services.obs.notifyObservers(null, EXPERIMENTS_CHANGED_TOPIC, null);
     }
 
     if ("@mozilla.org/toolkit/crash-reporter;1" in Cc && activeExperiment) {
-      gCrashReporter.annotateCrashReport("ActiveExperiment", activeExperiment.id);
+      try {
+        gCrashReporter.annotateCrashReport("ActiveExperiment", activeExperiment.id);
+      } catch (e) {
+        // It's ok if crash reporting is disabled.
+      }
     }
   },
 
   /*
    * Schedule the soonest re-check of experiment applicability that is needed.
    */
   _scheduleNextRun: function () {
     this._checkForShutdown();
--- a/mobile/android/base/EventDispatcher.java
+++ b/mobile/android/base/EventDispatcher.java
@@ -204,16 +204,17 @@ public final class EventDispatcher {
             for (final GeckoEventListener listener : listeners) {
                 listener.handleMessage(type, message);
             }
         } catch (final JSONException e) {
             Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
         }
     }
 
+    @RobocopTarget
     @Deprecated
     public static void sendResponse(JSONObject message, Object response) {
         sendResponseHelper(STATUS_SUCCESS, message, response);
     }
 
     @Deprecated
     public static void sendError(JSONObject message, Object response) {
         sendResponseHelper(STATUS_ERROR, message, response);
--- a/mobile/android/base/tests/StringHelper.java
+++ b/mobile/android/base/tests/StringHelper.java
@@ -87,16 +87,17 @@ public class StringHelper {
     public static final String ROBOCOP_BOXES_TITLE = "Browser Box test";
     public static final String ROBOCOP_GEOLOCATION_TITLE = "Geolocation Test Page";
     public static final String ROBOCOP_LOGIN_TITLE = "Robocop Login";
     public static final String ROBOCOP_OFFLINE_STORAGE_TITLE = "Robocop offline storage";
     public static final String ROBOCOP_PICTURE_LINK_TITLE = "Picture Link";
     public static final String ROBOCOP_SEARCH_TITLE = "Robocop Search Engine";
     public static final String ROBOCOP_TEXT_PAGE_TITLE = "Robocop Text Page";
     public static final String ROBOCOP_INPUT_TITLE = "Robocop Input";
+    public static final String ROBOCOP_SELECTION_HANDLER_TITLE = "Automated Text Selection tests for Mobile";
 
     // Settings menu strings
     // Section labels - ordered as found in the settings menu
     public static final String CUSTOMIZE_SECTION_LABEL = "Customize";
     public static final String DISPLAY_SECTION_LABEL = "Display";
     public static final String PRIVACY_SECTION_LABEL = "Privacy";
     public static final String MOZILLA_SECTION_LABEL = "Mozilla";
     public static final String DEVELOPER_TOOLS_SECTION_LABEL = "Developer tools";
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -129,14 +129,19 @@ skip-if = android_version == "8"
 #[testCheck2]
 #[testBrowserProviderPerf]
 
 # Using UITest
 #[testAboutHomePageNavigation] # see bug 947550, bug 979038 and bug 977952
 [testAboutHomeVisibility]
 # disabled on Android 2.3; bug 979597
 skip-if = android_version == "10"
+[testEventDispatcher]
 [testInputConnection]
 # disabled on Android 2.3; bug 983440
 skip-if = android_version == "10"
 [testJavascriptBridge]
 [testNativeCrypto]
 [testSessionHistory]
+
+# testSelectionHandler disabled on Android 2.3 by trailing skip-if, due to bug 980074
+[testSelectionHandler]
+skip-if = android_version == "10"
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/roboextender/testSelectionHandler.html
@@ -0,0 +1,301 @@
+<html>
+  <head>
+    <title>Automated Text Selection tests for Mobile</title>
+    <meta name="viewport" content="initial-scale=1.0"/>
+    <script type="application/javascript"
+      src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+    <script type="application/javascript">
+
+const DIV_POINT_TEXT = "Under";
+const INPUT_TEXT = "Text for select all in an <input>";
+const TEXTAREA_TEXT = "Text for select all in a <textarea>";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/* =================================================================================
+ *
+ * Start of all text selection tests, check initialization state.
+ *
+ */
+function startTests() {
+  testSelectAllDivs().
+    then(testSelectDivAtPoint).
+    then(testSelectInput).
+    then(testSelectTextarea).
+    then(testCloseSelection).
+    then(finishTests, function(err) {
+      ok(false, "Error in selection test " + err);
+      finishTests();
+    });
+}
+
+/* =================================================================================
+ *
+ * "Select all" text selection tests, for <div> (non-editable) fields.
+ *
+ */
+function testSelectAllDivs() {
+  var sh = getSelectionHandler();
+  var selDiv = document.getElementById("selDiv");
+  var nonSelDiv = document.getElementById("nonSelDiv");
+
+  // Check the initial state of the selection handler, and selectable/non-selectable <div>s.
+  return Promise.all([
+    ok(!sh.isSelectionActive(), "Selection should not be active at start of testSelectAllDivs"),
+    ok(sh.canSelect(selDiv), "Can select selectable <div>"),
+    ok(!sh.canSelect(nonSelDiv), "Can't select non-selectable <div>"),
+
+  ]).then(function() {
+    // Select all on a non-editable text node selects all the text in the page.
+    sh.startSelection(selDiv);
+    var selection = sh._getSelection();
+
+    return Promise.all([
+      ok(sh.isSelectionActive(), "Selection should be active now"),
+      is(selection.anchorNode, document.documentElement, "Anchor Node should be start of document"),
+      is(selection.anchorOffset, 0, "Anchor offset should be 0"),
+      is(selection.focusNode, document.body.lastChild, "Focus node should be lastChild of document"),
+      is(selection.focusOffset, document.body.lastChild.textContent.length, "Focus offset should be it's length"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * "Select word-at-point" text selection test, for <div> (non-editable) field.
+ * "collapseToStart" test closes selection (Bug 864589).
+ *
+ */
+function testSelectDivAtPoint() {
+  var sh = getSelectionHandler();
+  var selDiv = document.getElementById("selDiv");
+
+  // Select word at point in <div>
+  var rect = selDiv.getBoundingClientRect();
+  sh.startSelection(selDiv, {
+    mode: sh.SELECT_AT_POINT,
+    x: rect.left + 1,
+    y: rect.top + 1
+  });
+  var selection = sh._getSelection();
+
+  // Check the state of the selection handler after selecting at a point.
+  return Promise.all([
+    ok(sh.isSelectionActive(), "Selection should be active at start of testSelectDivAtPoint"),
+    is(selection.toString(), DIV_POINT_TEXT, "The first word in the <div> was selected"),
+
+  ]).then(function() {
+    // Check the state of the selection handler after collapsing a selection.
+    selection.collapseToStart();
+
+    return Promise.all([
+      ok(selection.getRangeAt(0).collapsed, "Selection should be collapsed"),
+      ok(!sh.isSelectionActive(), "Selection should not be active"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * "Select all" text selection test, for <input> (editable) field.
+ *
+ */
+function testSelectInput() {
+  var sh = getSelectionHandler();
+  var inputNode = document.getElementById("inputNode");
+  inputNode.value = INPUT_TEXT;
+
+  // Test that calling startSelection with an input selects all the text in the input.
+  return Promise.all([
+    ok(!sh.isSelectionActive(), "Selection should not be active at start of testSelectInput"),
+    ok(sh.canSelect(inputNode), "Can select selectable <input>"),
+
+  ]).then(function() {
+    // Check the state of the selection handler after calling startSelection on it.
+    sh.startSelection(inputNode);
+    var selection = sh._getSelection();
+
+    return Promise.all([
+      ok(sh.isSelectionActive(), "Selection should be active"),
+      ok((sh._targetElement instanceof Ci.nsIDOMNSEditableElement), "Selected element is editable"),
+      is(selection.toString(), INPUT_TEXT, "All text in the <input> was selected"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * "Select all" text selection test, for <textarea> (editable) field.
+ *
+ */
+
+function testSelectTextarea() {
+  var sh = getSelectionHandler();
+  var textareaNode = document.getElementById("textareaNode");
+  textareaNode.value = TEXTAREA_TEXT;
+
+  // Change (still-active) selection from previous <input> field to <textarea>
+  sh.startSelection(textareaNode);
+  var selection = sh._getSelection();
+
+  return Promise.all([
+    ok(sh.isSelectionActive(), "Selection should be active at start of testSelectTextarea"),
+    ok((sh._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement), "Selected element is editable, and a <textarea>"),
+    is(selection.toString(), TEXTAREA_TEXT, "All text in the <textarea> was selected"),
+
+  ]).then(function() {
+    // Collpase the selection to close it again.
+    selection.collapseToStart();
+
+    return Promise.all([
+      ok(!sh.isSelectionActive(), "Selection should not be active"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * Various text selection tests to end active selections, including:
+ *   1.) Clicking outside the selection.
+ *   2.) SelectionEnd or Tab:Selected messages from java.
+ *
+ */
+function testCloseSelection() {
+  var sh = getSelectionHandler();
+  var inputNode = document.getElementById("inputNode");
+  inputNode.value = INPUT_TEXT;
+
+  // Check the initial state of the selection handler.
+  return Promise.all([
+    ok(!sh.isSelectionActive(), "Selection should not be active at start of testCloseSelection"),
+
+  ]).then(function() {
+    // Start by selecting all in an <input>.
+    sh.startSelection(inputNode);
+    return is(sh._activeType, sh.TYPE_SELECTION, "Selection should be active in <input> before Gesture:SingleTap");
+
+  }).then(function() {
+    // Tap outside <input> to close active selection.
+    sh.observe(null, "Gesture:SingleTap", JSON.stringify({
+      x: 1,
+      y: 1
+    }));
+    return ok(!sh.isSelectionActive(), "Gesture:SingleTap outside <input> should close active selection");
+
+  // Various other ways to close an active selection.
+  }).then(function() {
+    sh.startSelection(inputNode);
+    sh.observe(null, "TextSelection:End", {});
+    return ok(!sh.isSelectionActive(), "TextSelection:End should close active selection");
+
+  }).then(function() {
+    sh.startSelection(inputNode);
+    sh.observe(null, "Tab:Selected", {});
+    return ok(!sh.isSelectionActive(), "Tab:Selected should close active selection");
+
+  }).then(function() {
+    sh.startSelection(inputNode);
+    sh.handleEvent({ type: "pagehide" });
+    return ok(!sh.isSelectionActive(), "pagehide should close active selection");
+
+  }).then(function() {
+    sh.startSelection(inputNode);
+    sh.handleEvent({ type: "blur" });
+    return ok(!sh.isSelectionActive(), "blur should close active selection");
+  });
+}
+
+/* =================================================================================
+ *
+ * After finish of all selection tests, wrap up and go home.
+ *
+ */
+function finishTests() {
+  sendMessageToJava({
+    type: "Robocop:testSelectionHandler",
+    result: true,
+    msg: "Done!",
+    done: true
+  });
+}
+
+/* ============================== Utility functions ======================
+ *
+ * Common functions available to all tests.
+ *
+ */
+function getSelectionHandler() {
+  return (!this._selectionHandler) ?
+    this._selectionHandler = Services.wm.getMostRecentWindow("navigator:browser").SelectionHandler :
+    this._selectionHandler;
+}
+
+function ok(one, msg) {
+  return new Promise(function(resolve, reject) {
+    sendMessageToJava({
+      type: "Robocop:testSelectionHandler",
+      result: one,
+      msg: msg
+    },
+    function (res, err) {
+      (err) ? reject(err) : resolve(res);
+    });
+  });
+}
+
+function is(one, two, msg) {
+  return new Promise(function(resolve, reject) {
+    sendMessageToJava({
+      type: "Robocop:testSelectionHandler",
+      result: one === two,
+      msg: msg + " : " + one + " === " + two
+    },
+    function (res, err) {
+      (err) ? reject(err) : resolve(res);
+    });
+  });
+}
+
+/* =================================================================================
+ *
+ * Page definition for all tests.
+ *
+ */
+    </script>
+  </head>
+
+  <body onload="startTests();">
+
+    <div id="selDiv">Under sufficiently extreme conditions, quarks may become
+      deconfined and exist as free particles. In the course of asymptotic freedom,
+      the strong interaction becomes weaker at higher temperatures. Eventually,
+      color confinement would be lost and an extremely hot plasma of freely moving
+      quarks and gluons would be formed. This theoretical phase of matter is called
+      quark-gluon plasma.[81] The exact conditions needed to give rise to this state
+      are unknown and have been the subject of a great deal of speculation and
+      experimentation. A recent estimate puts the needed temperature at
+      (1.90±0.02)×1012 Kelvin. While a state of entirely free quarks and gluons has
+      never been achieved (despite numerous attempts by CERN in the 1980s and 1990s),
+      recent experiments at the Relativistic Heavy Ion Collider have yielded evidence
+      for liquid-like quark matter exhibiting "nearly perfect" fluid motion.</div><br>
+
+    <div id="nonSelDiv" style="-moz-user-select: none;">Lorem ipsum dolor sit amet,
+      consectetur adipiscing elit. Proin in blandit magna, non porttitor augue.
+      Nam in neque sagittis, varius augue at, ornare velit. Vestibulum eget nisl
+      congue odio molestie scelerisque. Pellentesque ut augue orci. In hac habitasse
+      platea dictumst. Sed placerat tellus quis lacus condimentum, quis luctus elit
+      pellentesque. Mauris cursus neque diam, sit amet gravida quam porta ac.
+      Aliquam aliquam feugiat vestibulum. Proin commodo nulla ligula, in bibendum
+      massa euismod a. Ut ac lobortis dui. Ut id augue id arcu ornare suscipit eu
+      ornare lorem. Pellentesque nec dictum ante. Nam quis ligula ultricies, auctor
+      nunc vel, fringilla turpis. Nulla lacinia, leo ut egestas hendrerit, risus
+      ligula interdum enim, vel varius libero sem ut ligula.</div><br>
+
+    <input id="inputNode" type="text"><br>
+
+    <textarea id="textareaNode"></textarea><br>
+
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testEventDispatcher.java
@@ -0,0 +1,159 @@
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import org.mozilla.gecko.tests.helpers.*;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Tests the proper operation of EventDispatcher,
+ * including associated NativeJSObject objects.
+ */
+public class testEventDispatcher extends UITest
+        implements GeckoEventListener, NativeEventListener {
+
+    private static final String TEST_JS = "testEventDispatcher.js";
+    private static final String GECKO_EVENT = "Robocop:TestGeckoEvent";
+    private static final String GECKO_RESPONSE_EVENT = "Robocop:TestGeckoResponse";
+    private static final String NATIVE_EVENT = "Robocop:TestNativeEvent";
+    private static final String NATIVE_RESPONSE_EVENT = "Robocop:TestNativeResponse";
+
+    private JavascriptBridge js;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        js = new JavascriptBridge(this);
+
+        EventDispatcher.getInstance().registerGeckoThreadListener(
+                (GeckoEventListener) this, GECKO_EVENT, GECKO_RESPONSE_EVENT);
+        EventDispatcher.getInstance().registerGeckoThreadListener(
+                (NativeEventListener) this, NATIVE_EVENT, NATIVE_RESPONSE_EVENT);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(
+                (GeckoEventListener) this, GECKO_EVENT, GECKO_RESPONSE_EVENT);
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(
+                (NativeEventListener) this, NATIVE_EVENT, NATIVE_RESPONSE_EVENT);
+
+        js.disconnect();
+        super.tearDown();
+    }
+
+    public void testEventDispatcher() {
+        GeckoHelper.blockForReady();
+        NavigationHelper.enterAndLoadUrl(StringHelper.ROBOCOP_JS_HARNESS_URL +
+                                         "?path=" + TEST_JS);
+
+        js.syncCall("send_test_message", GECKO_EVENT);
+        js.syncCall("send_message_for_response", GECKO_RESPONSE_EVENT, "success");
+        js.syncCall("send_message_for_response", GECKO_RESPONSE_EVENT, "error");
+        js.syncCall("send_test_message", NATIVE_EVENT);
+        js.syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "success");
+        js.syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "error");
+        js.syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "cancel");
+        js.syncCall("finish_test");
+    }
+
+    @Override
+    public void handleMessage(final String event, final JSONObject message) {
+        ThreadUtils.assertOnGeckoThread();
+
+        try {
+            if (GECKO_EVENT.equals(event)) {
+                checkJSONObject(message);
+                checkJSONObject(message.getJSONObject("object"));
+
+            } else if (GECKO_RESPONSE_EVENT.equals(event)) {
+                final String response = message.getString("response");
+                if ("success".equals(response)) {
+                    EventDispatcher.getInstance().sendResponse(message, response);
+                } else if ("error".equals(response)) {
+                    EventDispatcher.getInstance().sendError(message, response);
+                } else {
+                    fFail("Response type should be valid: " + response);
+                }
+
+            } else {
+                fFail("Event type should be valid: " + event);
+            }
+        } catch (final JSONException e) {
+            fFail(e.toString());
+        }
+    }
+
+    @Override
+    public void handleMessage(final String event, final NativeJSObject message,
+                              final EventCallback callback) {
+        ThreadUtils.assertOnGeckoThread();
+
+        if (NATIVE_EVENT.equals(event)) {
+            checkNativeJSObject(message);
+            checkNativeJSObject(message.getObject("object"));
+            fAssertNotSame("optObject returns existent value",
+                    null, message.optObject("object", null));
+            fAssertSame("optObject returns fallback value if nonexistent",
+                    null, message.optObject("nonexistent_object", null));
+
+        } else if (NATIVE_RESPONSE_EVENT.equals(event)) {
+            final String response = message.getString("response");
+            if ("success".equals(response)) {
+                callback.sendSuccess(response);
+            } else if ("error".equals(response)) {
+                callback.sendError(response);
+            } else if ("cancel".equals(response)) {
+                callback.sendCancel();
+            } else {
+                fFail("Response type should be valid: " + response);
+            }
+
+        } else {
+            fFail("Event type should be valid: " + event);
+        }
+    }
+
+    private void checkJSONObject(final JSONObject object) throws JSONException {
+        fAssertEquals("JSON boolean has correct value", true, object.getBoolean("boolean"));
+        fAssertEquals("JSON int has correct value", 1, object.getInt("int"));
+        fAssertEquals("JSON double has correct value", 0.5, object.getDouble("double"));
+        fAssertEquals("JSON string has correct value", "foo", object.getString("string"));
+    }
+
+    private void checkNativeJSObject(final NativeJSObject object) {
+        fAssertEquals("Native boolean has correct value",
+                true, object.getBoolean("boolean"));
+        fAssertEquals("optBoolean returns existent value",
+                true, object.optBoolean("boolean", false));
+        fAssertEquals("optBoolean returns fallback value if nonexistent",
+                false, object.optBoolean("nonexistent_boolean", false));
+        fAssertEquals("Native int has correct value",
+                1, object.getInt("int"));
+        fAssertEquals("optInt returns existent value",
+                1, object.optInt("int", 0));
+        fAssertEquals("optInt returns fallback value if nonexistent",
+                0, object.optInt("nonexistent_int", 0));
+        fAssertEquals("Native double has correct value",
+                0.5, object.getDouble("double"));
+        fAssertEquals("optDouble returns existent value",
+                0.5, object.optDouble("double", -0.5));
+        fAssertEquals("optDouble returns fallback value if nonexistent",
+                -0.5, object.optDouble("nonexistent_double", -0.5));
+        fAssertEquals("Native string has correct value",
+                "foo", object.getString("string"));
+        fAssertEquals("optDouble returns existent value",
+                "foo", object.optString("string", "bar"));
+        fAssertEquals("optDouble returns fallback value if nonexistent",
+                "bar", object.optString("nonexistent_string", "bar"));
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testEventDispatcher.js
@@ -0,0 +1,45 @@
+Components.utils.import("resource://gre/modules/Messaging.jsm");
+
+let java = new JavaBridge(this);
+
+do_register_cleanup(() => {
+  java.disconnect();
+});
+do_test_pending();
+
+function send_test_message(type) {
+  sendMessageToJava({
+    type: type,
+    boolean: true,
+    int: 1,
+    double: 0.5,
+    string: "foo",
+    object: {
+      boolean: true,
+      int: 1,
+      double: 0.5,
+      string: "foo",
+    },
+  });
+}
+
+function send_message_for_response(type, response) {
+  sendMessageToJava({
+    type: type,
+    response: response,
+  }, (success, error) => {
+    if (response === "success") {
+      do_check_eq(success, response);
+      do_check_eq(error, null);
+    } else if (response === "error") {
+      do_check_eq(success, null);
+      do_check_eq(error, response);
+    } else {
+      do_throw("Unexpected response: " + response);
+    }
+  });
+}
+
+function finish_test() {
+  do_test_finished();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testSelectionHandler.java
@@ -0,0 +1,46 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+import android.util.Log;
+
+import org.json.JSONObject;
+
+
+public class testSelectionHandler extends UITest {
+
+    public void testSelectionHandler() {
+        GeckoHelper.blockForReady();
+
+        Actions.EventExpecter robocopTestExpecter = getActions().expectGeckoEvent("Robocop:testSelectionHandler");
+        NavigationHelper.enterAndLoadUrl("chrome://roboextender/content/testSelectionHandler.html");
+        mToolbar.assertTitle(StringHelper.ROBOCOP_SELECTION_HANDLER_TITLE);
+
+        while (!test(robocopTestExpecter)) {
+            // do nothing
+        }
+
+        robocopTestExpecter.unregisterListener();
+    }
+
+    private boolean test(Actions.EventExpecter expecter) {
+        final JSONObject eventData;
+        try {
+            eventData = new JSONObject(expecter.blockForEventData());
+        } catch(Exception ex) {
+            // Log and ignore
+            getAsserter().ok(false, "JS Test", "Error decoding data " + ex);
+            return false;
+        }
+
+        if (eventData.has("result")) {
+            getAsserter().ok(eventData.optBoolean("result"), "JS Test", eventData.optString("msg"));
+        }
+
+        EventDispatcher.sendResponse(eventData, new JSONObject());
+        return eventData.optBoolean("done", false);
+    }
+}
--- a/mobile/android/base/util/EventCallback.java
+++ b/mobile/android/base/util/EventCallback.java
@@ -1,17 +1,20 @@
 package org.mozilla.gecko.util;
 
+import org.mozilla.gecko.mozglue.RobocopTarget;
+
 /**
  * Callback interface for Gecko requests.
  *
  * For each instance of EventCallback, exactly one of sendResponse, sendError, or sendCancel
  * must be called to prevent observer leaks. If more than one send* method is called, or if a
  * single send method is called multiple times, an {@link IllegalStateException} will be thrown.
  */
+@RobocopTarget
 public interface EventCallback {
     /**
      * Sends a success response with the given data.
      *
      * @param response The response data to send to Gecko. Can be any of the types accepted by
      *                 JSONObject#put(String, Object).
      */
     public void sendSuccess(Object response);
--- a/mobile/android/base/util/ThreadUtils.java
+++ b/mobile/android/base/util/ThreadUtils.java
@@ -1,15 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.util;
 
+import org.mozilla.gecko.mozglue.RobocopTarget;
+
 import java.util.Map;
 
 import android.os.Handler;
 import android.os.MessageQueue;
 import android.util.Log;
 
 public final class ThreadUtils {
     private static final String LOGTAG = "ThreadUtils";
@@ -113,16 +115,17 @@ public final class ThreadUtils {
     public static void assertOnUiThread(final AssertBehavior assertBehavior) {
         assertOnThread(getUiThread(), assertBehavior);
     }
 
     public static void assertOnUiThread() {
         assertOnThread(getUiThread(), AssertBehavior.THROW);
     }
 
+    @RobocopTarget
     public static void assertOnGeckoThread() {
         assertOnThread(sGeckoThread, AssertBehavior.THROW);
     }
 
     public static void assertOnBackgroundThread() {
         assertOnThread(getBackgroundThread(), AssertBehavior.THROW);
     }
 
--- a/toolkit/content/aboutSupport.js
+++ b/toolkit/content/aboutSupport.js
@@ -8,23 +8,27 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Troubleshoot.jsm");
 Cu.import("resource://gre/modules/ResetProfile.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
 window.addEventListener("load", function onload(event) {
+  try {
   window.removeEventListener("load", onload, false);
   Troubleshoot.snapshot(function (snapshot) {
     for (let prop in snapshotFormatters)
       snapshotFormatters[prop](snapshot[prop]);
   });
   populateResetBox();
   setupEventListeners();
+  } catch (e) {
+    Cu.reportError("stack of load error for about:support: " + e + ": " + e.stack);
+  }
 }, false);
 
 // Each property in this object corresponds to a property in Troubleshoot.jsm's
 // snapshot data.  Each function is passed its property's corresponding data,
 // and it's the function's job to update the page with it.
 let snapshotFormatters = {
 
   application: function application(data) {
--- a/toolkit/crashreporter/nsExceptionHandler.cpp
+++ b/toolkit/crashreporter/nsExceptionHandler.cpp
@@ -1347,17 +1347,17 @@ InitInstallTime(nsACString& aInstallTime
 
 // Ensure a directory exists and create it if missing.
 static nsresult
 EnsureDirectoryExists(nsIFile* dir)
 {
   nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700);
 
   if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS)) {
-	return rv;
+    return rv;
   }
 
   return NS_OK;
 }
 
 // Annotate the crash report with a Unique User ID and time
 // since install.  Also do some prep work for recording
 // time since last crash, which must be calculated at
@@ -2076,68 +2076,93 @@ nsresult SetSubmitReports(bool aSubmitRe
     if (NS_FAILED(rv)) {
       return rv;
     }
 
     obsServ->NotifyObservers(nullptr, "submit-reports-pref-changed", nullptr);
     return NS_OK;
 }
 
-void
-UpdateCrashEventsDir()
+static void
+SetCrashEventsDir(nsIFile* aDir)
 {
-  nsCOMPtr<nsIFile> eventsDir;
-
-  // We prefer the following locations in order:
-  //
-  // 1. If environment variable is present, use it. We don't expect
-  //    the environment variable except for tests and other atypical setups.
-  // 2. Inside the profile directory.
-  // 3. Inside the user application data directory (no profile available).
-  // 4. A temporary directory (setup likely is invalid / application is buggy).
+  nsCOMPtr<nsIFile> eventsDir = aDir;
+
   const char *env = PR_GetEnv("CRASHES_EVENTS_DIR");
-  if (env) {
-    eventsDir = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
-    if (!eventsDir) {
-      return;
-    }
-    eventsDir->InitWithNativePath(nsDependentCString(env));
+  if (env && *env) {
+    NS_NewNativeLocalFile(nsDependentCString(env),
+                          false, getter_AddRefs(eventsDir));
     EnsureDirectoryExists(eventsDir);
-  } else {
-    nsresult rv = NS_GetSpecialDirectory("ProfD", getter_AddRefs(eventsDir));
-    if (NS_SUCCEEDED(rv)) {
-      eventsDir->Append(NS_LITERAL_STRING("crashes"));
-      EnsureDirectoryExists(eventsDir);
-      eventsDir->Append(NS_LITERAL_STRING("events"));
-      EnsureDirectoryExists(eventsDir);
-    } else {
-      rv = NS_GetSpecialDirectory("UAppData", getter_AddRefs(eventsDir));
-      if (NS_SUCCEEDED(rv)) {
-        eventsDir->Append(NS_LITERAL_STRING("Crash Reports"));
-        EnsureDirectoryExists(eventsDir);
-        eventsDir->Append(NS_LITERAL_STRING("events"));
-        EnsureDirectoryExists(eventsDir);
-      } else {
-        NS_WARNING("Couldn't get the user appdata directory. Crash events may not be produced.");
-        return;
-      }
-    }
+  }
+
+  if (eventsDirectory) {
+    NS_Free(eventsDirectory);
   }
 
 #ifdef XP_WIN
   nsString path;
   eventsDir->GetPath(path);
   eventsDirectory = reinterpret_cast<wchar_t*>(ToNewUnicode(path));
 #else
   nsCString path;
   eventsDir->GetNativePath(path);
   eventsDirectory = ToNewCString(path);
 #endif
 }
 
+void
+SetProfileDirectory(nsIFile* aDir)
+{
+  nsCOMPtr<nsIFile> dir;
+  aDir->Clone(getter_AddRefs(dir));
+
+  dir->Append(NS_LITERAL_STRING("crashes"));
+  EnsureDirectoryExists(dir);
+  dir->Append(NS_LITERAL_STRING("events"));
+  EnsureDirectoryExists(dir);
+  SetCrashEventsDir(dir);
+}
+
+void
+SetUserAppDataDirectory(nsIFile* aDir)
+{
+  nsCOMPtr<nsIFile> dir;
+  aDir->Clone(getter_AddRefs(dir));
+
+  dir->Append(NS_LITERAL_STRING("Crash Reports"));
+  EnsureDirectoryExists(dir);
+  dir->Append(NS_LITERAL_STRING("events"));
+  EnsureDirectoryExists(dir);
+  SetCrashEventsDir(dir);
+}
+
+void
+UpdateCrashEventsDir()
+{
+  const char *env = PR_GetEnv("CRASHES_EVENTS_DIR");
+  if (env && *env) {
+    SetCrashEventsDir(nullptr);
+  }
+
+  nsCOMPtr<nsIFile> eventsDir;
+  nsresult rv = NS_GetSpecialDirectory("ProfD", getter_AddRefs(eventsDir));
+  if (NS_SUCCEEDED(rv)) {
+    SetProfileDirectory(eventsDir);
+    return;
+  }
+
+  rv = NS_GetSpecialDirectory("UAppData", getter_AddRefs(eventsDir));
+  if (NS_SUCCEEDED(rv)) {
+    SetUserAppDataDirectory(eventsDir);
+    return;
+  }
+
+  NS_WARNING("Couldn't get the user appdata directory. Crash events may not be produced.");
+}
+
 bool GetCrashEventsDir(nsAString& aPath)
 {
   if (!eventsDirectory) {
     return false;
   }
 
   aPath = CONVERT_XP_CHAR_TO_UTF16(eventsDirectory);
   return true;
--- a/toolkit/crashreporter/nsExceptionHandler.h
+++ b/toolkit/crashreporter/nsExceptionHandler.h
@@ -31,20 +31,29 @@ template<class KeyClass, class DataType>
 class nsCStringHashKey;
 
 namespace CrashReporter {
 nsresult SetExceptionHandler(nsIFile* aXREDirectory, bool force=false);
 nsresult UnsetExceptionHandler();
 
 /**
  * Tell the crash reporter to recalculate where crash events files should go.
+ * SetCrashEventsDir is used before XPCOM is initialized from the startup
+ * code.
  *
- * This should be called during crash reporter initialization and when a
- * profile is activated or deactivated.
+ * UpdateCrashEventsDir uses the directory service to re-set the
+ * crash event directory based on the current profile.
+ *
+ * 1. If environment variable is present, use it. We don't expect
+ *    the environment variable except for tests and other atypical setups.
+ * 2. <profile>/crashes/events
+ * 3. <UAppData>/Crash Reports/events
  */
+void SetUserAppDataDirectory(nsIFile* aDir);
+void SetProfileDirectory(nsIFile* aDir);
 void UpdateCrashEventsDir();
 
 /**
  * Get the path where crash event files should be written.
  */
 bool     GetCrashEventsDir(nsAString& aPath);
 
 bool     GetEnabled();
--- a/toolkit/devtools/server/actors/webaudio.js
+++ b/toolkit/devtools/server/actors/webaudio.js
@@ -150,30 +150,25 @@ let AudioNodeActor = exports.AudioNodeAc
    */
   isSource: method(function () {
     return !!~this.type.indexOf("Source") || this.type === "OscillatorNode";
   }, {
     response: { source: RetVal("boolean") }
   }),
 
   /**
-   * Changes a param on the audio node. Responds with a `string` that's either
-   * an empty string `""` on success, or a description of the error upon
-   * param set failure.
+   * Changes a param on the audio node. Responds with either `undefined`
+   * on success, or a description of the error upon param set failure.
    *
    * @param String param
    *        Name of the AudioParam to change.
    * @param String value
    *        Value to change AudioParam to.
    */
   setParam: method(function (param, value) {
-    // Strip quotes because sometimes UIs include that for strings
-    if (typeof value === "string") {
-      value = value.replace(/[\'\"]*/g, "");
-    }
     try {
       if (isAudioParam(this.node, param))
         this.node[param].value = value;
       else
         this.node[param] = value;
       return undefined;
     } catch (e) {
       return constructError(e);
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -778,18 +778,23 @@ MockProvider.prototype = {
   startup: function MP_startup() {
     this.started = true;
   },
 
   /**
    * Called when the provider should shutdown.
    */
   shutdown: function MP_shutdown() {
-    for (let timer of this.callbackTimers)
+    if (this.callbackTimers.length) {
+      info("MockProvider: pending callbacks at shutdown(): calling immediately");
+    }
+    for (let timer of this.callbackTimers) {
+      timer.callback();
       timer.cancel();
+    }
     this.callbackTimers = [];
 
     this.started = false;
   },
 
   /**
    * Called to get an Addon with a particular ID.
    *
@@ -963,27 +968,30 @@ MockProvider.prototype = {
    * The delay is specified by the apiDelay property, in milliseconds.
    * Parameters to send to the callback should be specified as arguments after
    * the aCallback argument.
    *
    * @param aCallback Callback to eventually call
    */
   _delayCallback: function MP_delayCallback(aCallback, ...aArgs) {
     if (!this.useAsyncCallbacks) {
-      aCallback.apply(null, params);
+      aCallback.apply(null, aArgs);
       return;
     }
 
     var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     // Need to keep a reference to the timer, so it doesn't get GC'ed
-    var pos = this.callbackTimers.length;
     this.callbackTimers.push(timer);
-    var self = this;
-    timer.initWithCallback(function() {
-      self.callbackTimers.splice(pos, 1);
+    timer.initWithCallback(() => {
+      let idx = this.callbackTimers.indexOf(timer);
+      if (idx == -1) {
+        info("MockProvider._delayCallback lost track of a timer.");
+      } else {
+        this.callbackTimers.splice(idx, 1);
+      }
       aCallback.apply(null, aArgs);
     }, this.apiDelay, timer.TYPE_ONE_SHOT);
   }
 };
 
 /***** Mock Addon object for the Mock Provider *****/
 
 function MockAddon(aId, aName, aType, aOperationsRequiringRestart) {
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -3016,17 +3016,21 @@ XREMain::XRE_mainInit(bool* aExitFlag)
 #ifdef MOZ_CRASHREPORTER
   if (EnvHasValue("MOZ_CRASHREPORTER")) {
     mAppData->flags |= NS_XRE_ENABLE_CRASH_REPORTER;
   }
 
   if ((mAppData->flags & NS_XRE_ENABLE_CRASH_REPORTER) &&
       NS_SUCCEEDED(
          CrashReporter::SetExceptionHandler(mAppData->xreDirectory))) {
-    CrashReporter::UpdateCrashEventsDir();
+    nsCOMPtr<nsIFile> file;
+    rv = mDirProvider.GetUserAppDataDirectory(getter_AddRefs(file));
+    if (NS_SUCCEEDED(rv)) {
+      CrashReporter::SetUserAppDataDirectory(file);
+    }
     if (mAppData->crashReporterURL)
       CrashReporter::SetServerURL(nsDependentCString(mAppData->crashReporterURL));
 
     // pass some basic info from the app data
     if (mAppData->vendor)
       CrashReporter::AnnotateCrashReport(NS_LITERAL_CSTRING("Vendor"),
                                          nsDependentCString(mAppData->vendor));
     if (mAppData->name)
@@ -3671,17 +3675,17 @@ XREMain::XRE_mainStartup(bool* aExitFlag
   //////////////////////// NOW WE HAVE A PROFILE ////////////////////////
 
   mozilla::Telemetry::SetProfileDir(mProfD);
 
 #ifdef MOZ_CRASHREPORTER
   if (mAppData->flags & NS_XRE_ENABLE_CRASH_REPORTER)
       MakeOrSetMinidumpPath(mProfD);
 
-  CrashReporter::UpdateCrashEventsDir();
+  CrashReporter::SetProfileDirectory(mProfD);
 #endif
 
   nsAutoCString version;
   BuildVersion(version);
 
 #ifdef TARGET_OS_ABI
   NS_NAMED_LITERAL_CSTRING(osABI, TARGET_OS_ABI);
 #else