Merge inbound to mozilla-central. a=merge
authorMargareta Eliza Balazs <ebalazs@mozilla.com>
Wed, 09 May 2018 12:33:51 +0300
changeset 417536 9294f67b3f3bd4a3dd898961148cecd8bfc1ce9c
parent 417516 a2eccfbeb0ae9c9d71b76a9e6fcea48addaa84ca (current diff)
parent 417535 882a5a8235be63dea073aa736ea57cd8f8eaaa71 (diff)
child 417543 508d0fad4bb0bdad3c4225f7269b895a478e68c3
child 417561 a6a898e6b75a013cb8ac6781893aa9aaed7a3320
push id33969
push userebalazs@mozilla.com
push dateWed, 09 May 2018 09:34:26 +0000
treeherdermozilla-central@9294f67b3f3b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone62.0a1
first release with
nightly linux32
9294f67b3f3b / 62.0a1 / 20180509100510 / files
nightly linux64
9294f67b3f3b / 62.0a1 / 20180509100510 / files
nightly mac
9294f67b3f3b / 62.0a1 / 20180509100510 / files
nightly win32
9294f67b3f3b / 62.0a1 / 20180509100510 / files
nightly win64
9294f67b3f3b / 62.0a1 / 20180509100510 / 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 inbound to mozilla-central. a=merge
accessible/tests/mochitest/jsat/test_content_text.html
browser/docs/UITelemetry.rst
toolkit/components/telemetry/docs/fhr/architecture.rst
toolkit/components/telemetry/docs/fhr/dataformat.rst
toolkit/components/telemetry/docs/fhr/identifiers.rst
toolkit/components/telemetry/docs/fhr/index.rst
toolkit/mozapps/extensions/test/xpcshell/test_default_providers_pref.js
toolkit/mozapps/extensions/test/xpcshell/test_proxies.js
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -161,26 +161,30 @@ var AccessFu = {
         break;
       case "AccessFu:DoScroll":
         this.Input.doScroll(aMessage.json);
         break;
     }
   },
 
   _output: function _output(aPresentationData, aBrowser) {
-    if (!aPresentationData || typeof aPresentationData == "string") {
+    if (!aPresentationData) {
       // Either no android events to send or a string used for testing only.
       return;
     }
 
     if (!Utils.isAliveAndVisible(Utils.AccService.getAccessibleFor(aBrowser))) {
       return;
     }
 
     for (let evt of aPresentationData) {
+      if (typeof evt == "string") {
+        continue;
+      }
+
       Utils.win.WindowEventDispatcher.sendRequest({
         ...evt,
         type: "GeckoView:AccessibilityEvent"
       });
     }
 
     if (this._notifyOutputPref.value) {
       Services.obs.notifyObservers(null, "accessibility-output",
--- a/accessible/jsat/Presentation.jsm
+++ b/accessible/jsat/Presentation.jsm
@@ -167,35 +167,35 @@ class AndroidPresentor {
   }
 
   /**
    * Selection has changed.
    * XXX: Implement android event?
    * @param {nsIAccessible} aObject the object that has been selected.
    */
   selectionChanged(aObject) {
-    return "todo.selection-changed";
+    return ["todo.selection-changed"];
   }
 
   /**
    * Name has changed.
    * XXX: Implement android event?
    * @param {nsIAccessible} aAccessible the object whose value has changed.
    */
   nameChanged(aAccessible) {
-    return "todo.name-changed";
+    return ["todo.name-changed"];
   }
 
   /**
    * Value has changed.
    * XXX: Implement android event?
    * @param {nsIAccessible} aAccessible the object whose value has changed.
    */
   valueChanged(aAccessible) {
-    return "todo.value-changed";
+    return ["todo.value-changed"];
   }
 
   /**
    * The tab, or the tab's document state has changed.
    * @param {nsIAccessible} aDocObj the tab document accessible that has had its
    *    state changed, or null if the tab has no associated document yet.
    * @param {string} aPageState the state name for the tab, valid states are:
    *    'newtab', 'loading', 'newdoc', 'loaded', 'stopped', and 'reload'.
--- a/accessible/tests/mochitest/jsat/a11y.ini
+++ b/accessible/tests/mochitest/jsat/a11y.ini
@@ -6,20 +6,21 @@ support-files =
   doc_content_integration.html
   doc_content_text.html
   !/accessible/tests/mochitest/*.js
   !/accessible/tests/mochitest/moz.png
 skip-if = (os == 'win' && (os_version == '5.1' || os_version == '5.2'))
 
 [test_alive.html]
 [test_content_integration.html]
-skip-if = buildapp == 'mulet'
-[test_content_text.html]
-skip-if = buildapp == 'mulet'
 [test_explicit_names.html]
 [test_hints.html]
 [test_landmarks.html]
 [test_live_regions.html]
 [test_output_mathml.html]
 [test_output.html]
 [test_tables.html]
+[test_text_editable_navigation.html]
+[test_text_editing.html]
+[test_text_navigation_focus.html]
+[test_text_navigation.html]
 [test_traversal.html]
 [test_traversal_helper.html]
--- a/accessible/tests/mochitest/jsat/doc_content_integration.html
+++ b/accessible/tests/mochitest/jsat/doc_content_integration.html
@@ -11,55 +11,20 @@
       "<ul>" +
       '<li><label><input type="checkbox">many option</label></li>' +
       "</ul>" +
       '<label for="r">much range</label>' +
       '<input min="0" max="10" value="5" type="range" id="r">' +
       "</body>" +
       "</html>";
 
-    function showAlert() {
-      document.getElementById("alert").hidden = false;
-    }
-
     function hideAlert() {
       document.getElementById("alert").hidden = true;
     }
 
-    function ariaShowBack() {
-      document.getElementById("back").setAttribute("aria-hidden", false);
-    }
-
-    function ariaHideBack() {
-      document.getElementById("back").setAttribute("aria-hidden", true);
-    }
-
-    function ariaShowIframe() {
-      document.getElementById("iframe").setAttribute("aria-hidden", false);
-    }
-
-    function ariaHideIframe() {
-      document.getElementById("iframe").setAttribute("aria-hidden", true);
-    }
-
-    function renameFruit() {
-      document.getElementById("fruit").setAttribute("aria-label", "banana");
-    }
-
-    function renameSlider() {
-      document.getElementById("slider").setAttribute(
-        "aria-label", "mover");
-    }
-
-    function changeSliderValue() {
-      document.getElementById("slider").setAttribute("aria-valuenow", "5");
-      document.getElementById("slider").setAttribute(
-        "aria-valuetext", "medium");
-    }
-
     function toggleLight() {
       var lightSwitch = document.getElementById("light");
       lightSwitch.setAttribute("aria-checked",
         lightSwitch.getAttribute("aria-checked") === "true" ? "false" : "true");
     }
 
   </script>
   <style>
--- a/accessible/tests/mochitest/jsat/jsatcommon.js
+++ b/accessible/tests/mochitest/jsat/jsatcommon.js
@@ -14,16 +14,23 @@ var gTestFuncs = [];
   */
 var gIterator;
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");
 ChromeUtils.import("resource://gre/modules/accessibility/EventManager.jsm");
 ChromeUtils.import("resource://gre/modules/accessibility/Constants.jsm");
 
+const MovementGranularity = {
+  CHARACTER: 1,
+  WORD: 2,
+  LINE: 4,
+  PARAGRAPH: 8
+};
+
 var AccessFuTest = {
 
   addFunc: function AccessFuTest_addFunc(aFunc) {
     if (aFunc) {
       gTestFuncs.push(aFunc);
     }
   },
 
@@ -159,511 +166,268 @@ var AccessFuTest = {
         // Run all test functions synchronously.
         gTestFuncs.forEach(testFunc => testFunc());
         AccessFuTest.finish();
       }
     });
   }
 };
 
-function AccessFuContentTest(aFuncResultPairs) {
-  this.queue = aFuncResultPairs;
-}
-
-AccessFuContentTest.prototype = {
-  expected: [],
-  currentAction: null,
-  actionNum: -1,
+class AccessFuContentTestRunner {
+  constructor() {
+    this.listenersMap = new Map();
+    let frames = Array.from(currentTabDocument().querySelectorAll("iframe"));
+    this.mms = [Utils.getMessageManager(currentBrowser()),
+      ...frames.map(f => Utils.getMessageManager(f)).filter(mm => !!mm)];
+  }
 
   start(aFinishedCallback) {
     Logger.logLevel = Logger.DEBUG;
     this.finishedCallback = aFinishedCallback;
-    var self = this;
 
-    // Get top content message manager, and set it up.
-    this.mms = [Utils.getMessageManager(currentBrowser())];
-    this.setupMessageManager(this.mms[0], function() {
-      // Get child message managers and set them up
-      var frames = currentTabDocument().querySelectorAll("iframe");
-      if (frames.length === 0) {
-        self.pump();
-        return;
-      }
-
-      var toSetup = 0;
-      for (var i = 0; i < frames.length; i++ ) {
-        var mm = Utils.getMessageManager(frames[i]);
-        if (mm) {
-          toSetup++;
-          self.mms.push(mm);
-          self.setupMessageManager(mm, function() {
-            if (--toSetup === 0) {
-              // All message managers are loaded and ready to go.
-              self.pump();
-            }
-          });
-        }
-      }
-    });
-  },
+    return Promise.all(this.mms.map(mm => this.setupMessageManager(mm)));
+  }
 
   finish() {
     Logger.logLevel = Logger.INFO;
+    this.listenersMap.clear();
     for (var mm of this.mms) {
-        mm.sendAsyncMessage("AccessFu:Stop");
-        mm.removeMessageListener("AccessFu:Present", this);
-        mm.removeMessageListener("AccessFu:Input", this);
-        mm.removeMessageListener("AccessFu:CursorCleared", this);
-        mm.removeMessageListener("AccessFu:Focused", this);
-        mm.removeMessageListener("AccessFu:AriaHidden", this);
-        mm.removeMessageListener("AccessFu:Ready", this);
-        mm.removeMessageListener("AccessFu:ContentStarted", this);
-      }
-    if (this.finishedCallback) {
-      this.finishedCallback();
+      mm.sendAsyncMessage("AccessFu:Stop");
+      mm.removeMessageListener("AccessFu:Present", this);
     }
-  },
+  }
+
+  sendMessage(message) {
+    // First message manager is the top-level one.
+    this.mms[0].sendAsyncMessage(message.name, message.data);
+  }
 
-  setupMessageManager(aMessageManager, aCallback) {
+  androidEvent(eventType) {
+    return new Promise(resolve => {
+      if (!this.listenersMap.has(eventType)) {
+        this.listenersMap.set(eventType, [resolve]);
+      } else {
+        this.listenersMap.get(eventType).push(resolve);
+      }
+    }).then(evt => {
+      if (this.debug) {
+        info("Resolving event: " + evt.eventType);
+      }
+      return evt;
+    });
+  }
+
+  isFocused(aExpected) {
+    var doc = currentTabDocument();
+    SimpleTest.is(doc.activeElement, doc.querySelector(aExpected),
+      "Correct element is focused: " + aExpected);
+  }
+
+  async setupMessageManager(aMessageManager) {
     function contentScript() {
-      addMessageListener("AccessFuTest:Focus", function(aMessage) {
-        var elem = content.document.querySelector(aMessage.json.selector);
+      addMessageListener("AccessFuTest:Focus", aMessage => {
+        var elem = content.document.querySelector(aMessage.data.selector);
         if (elem) {
-          if (aMessage.json.blur) {
-            elem.blur();
-          } else {
-            elem.focus();
-          }
+          elem.focus();
         }
       });
+
+      addMessageListener("AccessFuTest:Blur", () => {
+        content.document.activeElement.blur();
+      });
     }
 
-    aMessageManager.addMessageListener("AccessFu:Present", this);
-    aMessageManager.addMessageListener("AccessFu:Input", this);
-    aMessageManager.addMessageListener("AccessFu:CursorCleared", this);
-    aMessageManager.addMessageListener("AccessFu:Focused", this);
-    aMessageManager.addMessageListener("AccessFu:AriaHidden", this);
-    aMessageManager.addMessageListener("AccessFu:Ready", function() {
-      aMessageManager.addMessageListener("AccessFu:ContentStarted", aCallback);
-      aMessageManager.sendAsyncMessage("AccessFu:Start",
-        { buildApp: "browser",
-          androidSdkVersion: Utils.AndroidSdkVersion,
-          logLevel: "DEBUG",
-          inTest: true });
-    });
+    aMessageManager.loadFrameScript(
+      "data:,(" + contentScript.toString() + ")();", false);
+
+    let readyPromise = new Promise(resolve =>
+      aMessageManager.addMessageListener("AccessFu:Ready", resolve));
 
     aMessageManager.loadFrameScript(
       "chrome://global/content/accessibility/content-script.js", false);
-    aMessageManager.loadFrameScript(
-      "data:,(" + contentScript.toString() + ")();", false);
-  },
+
+    await readyPromise;
 
-  pump() {
-    this.expected.shift();
-    if (this.expected.length) {
-      return;
-    }
-
-    var currentPair = this.queue.shift();
+    let startedPromise = new Promise(resolve =>
+      aMessageManager.addMessageListener("AccessFu:ContentStarted", resolve));
 
-    if (currentPair) {
-      this.actionNum++;
-      this.currentAction = currentPair[0];
-      if (typeof this.currentAction === "function") {
-        this.currentAction(this.mms[0]);
-      } else if (this.currentAction) {
-        this.mms[0].sendAsyncMessage(this.currentAction.name,
-         this.currentAction.json);
-      }
+    aMessageManager.sendAsyncMessage("AccessFu:Start",
+      { buildApp: "browser",
+        androidSdkVersion: Utils.AndroidSdkVersion,
+        logLevel: "DEBUG",
+        inTest: true });
 
-      this.expected = currentPair.slice(1, currentPair.length);
+    await startedPromise;
 
-      if (!this.expected[0]) {
-       this.pump();
-     }
-    } else {
-      this.finish();
-    }
-  },
+    aMessageManager.addMessageListener("AccessFu:Present", this);
+  }
 
   receiveMessage(aMessage) {
-    var expected = this.expected[0];
-
-    if (!expected) {
+    if (aMessage.name != "AccessFu:Present" || !aMessage.data) {
       return;
     }
 
-    var actionsString = typeof this.currentAction === "function" ?
-      this.currentAction.toString() : JSON.stringify(this.currentAction);
-
-    if (typeof expected === "string") {
-      ok(true, "Got " + expected + " after " + actionsString);
-      this.pump();
-    } else if (expected.ignore && !expected.ignore(aMessage)) {
-      expected.is(aMessage.json, "after " + actionsString +
-        " (" + this.actionNum + ")");
-      expected.is_correct_focus();
-      this.pump();
-    }
-  }
-};
-
-// Common content messages
-
-var ContentMessages = {
-  simpleMoveFirst: {
-    name: "AccessFu:MoveCursor",
-    json: {
-      action: "moveFirst",
-      rule: "Simple",
-      inputType: "gesture",
-      origin: "top"
-    }
-  },
-
-  simpleMoveLast: {
-    name: "AccessFu:MoveCursor",
-    json: {
-      action: "moveLast",
-      rule: "Simple",
-      inputType: "gesture",
-      origin: "top"
-    }
-  },
-
-  simpleMoveNext: {
-    name: "AccessFu:MoveCursor",
-    json: {
-      action: "moveNext",
-      rule: "Simple",
-      inputType: "gesture",
-      origin: "top"
-    }
-  },
-
-  simpleMovePrevious: {
-    name: "AccessFu:MoveCursor",
-    json: {
-      action: "movePrevious",
-      rule: "Simple",
-      inputType: "gesture",
-      origin: "top"
-    }
-  },
-
-  clearCursor: {
-    name: "AccessFu:ClearCursor",
-    json: {
-      origin: "top"
-    }
-  },
-
-  moveOrAdjustUp: function moveOrAdjustUp(aRule) {
-    return {
-      name: "AccessFu:MoveCursor",
-      json: {
-        origin: "top",
-        action: "movePrevious",
-        inputType: "gesture",
-        rule: (aRule || "Simple"),
-        adjustRange: true
-      }
-    };
-  },
-
-  moveOrAdjustDown: function moveOrAdjustUp(aRule) {
-    return {
-      name: "AccessFu:MoveCursor",
-      json: {
-        origin: "top",
-        action: "moveNext",
-        inputType: "gesture",
-        rule: (aRule || "Simple"),
-        adjustRange: true
+    for (let evt of aMessage.data) {
+      if (this.debug) {
+        info("Android event: " + JSON.stringify(evt));
       }
-    };
-  },
-
-  androidScrollForward: function adjustUp() {
-    return {
-      name: "AccessFu:AndroidScroll",
-      json: { origin: "top", direction: "forward" }
-    };
-  },
-
-  androidScrollBackward: function adjustDown() {
-    return {
-      name: "AccessFu:AndroidScroll",
-      json: { origin: "top", direction: "backward" }
-    };
-  },
-
-  focusSelector: function focusSelector(aSelector, aBlur) {
-    return {
-      name: "AccessFuTest:Focus",
-      json: {
-        selector: aSelector,
-        blur: aBlur
-      }
-    };
-  },
-
-  activateCurrent: function activateCurrent(aOffset) {
-    return {
-      name: "AccessFu:Activate",
-      json: {
-        origin: "top",
-        offset: aOffset
-      }
-    };
-  },
-
-  moveNextBy: function moveNextBy(aGranularity) {
-    return {
-      name: "AccessFu:MoveByGranularity",
-      json: {
-        direction: "Next",
-        granularity: this._granularityMap[aGranularity]
-      }
-    };
-  },
-
-  movePreviousBy: function movePreviousBy(aGranularity) {
-    return {
-      name: "AccessFu:MoveByGranularity",
-      json: {
-        direction: "Previous",
-        granularity: this._granularityMap[aGranularity]
+      let listener = (this.listenersMap.get(evt.eventType || evt) || []).shift();
+      if (listener) {
+        listener(evt);
       }
-    };
-  },
-
-  moveCaretNextBy: function moveCaretNextBy(aGranularity) {
-    return {
-      name: "AccessFu:MoveCaret",
-      json: {
-        direction: "Next",
-        granularity: this._granularityMap[aGranularity]
-      }
-    };
-  },
-
-  moveCaretPreviousBy: function moveCaretPreviousBy(aGranularity) {
-    return {
-      name: "AccessFu:MoveCaret",
-      json: {
-        direction: "Previous",
-        granularity: this._granularityMap[aGranularity]
-      }
-    };
-  },
-
-  _granularityMap: {
-    "character": 1, // MOVEMENT_GRANULARITY_CHARACTER
-    "word": 2, // MOVEMENT_GRANULARITY_WORD
-    "paragraph": 8 // MOVEMENT_GRANULARITY_PARAGRAPH
-  }
-};
-
-function ExpectedMessage(aName, aOptions) {
-  this.name = aName;
-  this.options = aOptions || {};
-  this.json = {};
-}
-
-ExpectedMessage.prototype.lazyCompare = function(aReceived, aExpected, aInfo) {
-  if (aExpected && !aReceived) {
-    return [false, "Expected something but got nothing -- " + aInfo];
-  }
-
-  if (typeof aReceived === "string" || typeof aExpected === "string") {
-    return [aReceived == aExpected, `String comparison: Got '${aReceived}.', expected: ${aExpected} -- ${aInfo}`];
-  }
-
-  var matches = true;
-  var delta = [];
-  for (var attr in aExpected) {
-    var expected = aExpected[attr];
-    var received = aReceived[attr];
-    if (typeof expected === "object") {
-      var [childMatches, childDelta] = this.lazyCompare(received, expected);
-      if (!childMatches) {
-        delta.push(attr + " [ " + childDelta + " ]");
-        matches = false;
-      }
-    } else if (received !== expected) {
-      delta.push(
-        attr + " [ expected " + JSON.stringify(expected) +
-        " got " + JSON.stringify(received) + " ]");
-      matches = false;
     }
   }
 
-  var msg = delta.length ? delta.join(" ") : "Structures lazily match";
-  return [matches, msg + " -- " + aInfo];
-};
+  async expectAndroidEvents(aFunc, ...aExpectedEvents) {
+    let events = Promise.all(aExpectedEvents.map(e => this.androidEvent(e)));
+    aFunc();
+    let gotEvents = await events;
+    return gotEvents.length == 1 ? gotEvents[0] : gotEvents;
+  }
 
-ExpectedMessage.prototype.is = function(aReceived, aInfo) {
-  var checkFunc = this.options.todo ? "todo" : "ok";
-  SimpleTest[checkFunc].apply(
-    SimpleTest, this.lazyCompare(aReceived, this.json, aInfo));
-};
+  moveCursor(aArgs, ...aExpectedEvents) {
+    return this.expectAndroidEvents(() => {
+      this.sendMessage({
+        name: "AccessFu:MoveCursor",
+        data: {
+          inputType: "gesture",
+          origin: "top",
+          ...aArgs
+        }
+      });
+    }, ...aExpectedEvents);
+  }
 
-ExpectedMessage.prototype.is_correct_focus = function(aInfo) {
-  if (!this.options.focused) {
-    return;
+  moveNext(aRule, ...aExpectedEvents) {
+    return this.moveCursor({ action: "moveNext", rule: aRule },
+      ...aExpectedEvents);
+  }
+
+  movePrevious(aRule, ...aExpectedEvents) {
+    return this.moveCursor({ action: "movePrevious", rule: aRule },
+      ...aExpectedEvents);
+  }
+
+  moveFirst(aRule, ...aExpectedEvents) {
+    return this.moveCursor({ action: "moveFirst", rule: aRule },
+      ...aExpectedEvents);
   }
 
-  var checkFunc = this.options.focused_todo ? "todo_is" : "is";
-  var doc = currentTabDocument();
-  SimpleTest[checkFunc].apply(SimpleTest,
-    [ doc.activeElement, doc.querySelector(this.options.focused),
-      "Correct element is focused: " + this.options.focused + " -- " + aInfo ]);
-};
-
-ExpectedMessage.prototype.ignore = function(aMessage) {
-  return aMessage.name !== this.name;
-};
-
-function ExpectedPresent(aAndroidEvents, aOptions) {
-  ExpectedMessage.call(this, "AccessFu:Present", aOptions);
-  this.expectedEvents = aAndroidEvents;
-}
-
-ExpectedPresent.prototype = Object.create(ExpectedMessage.prototype);
-
-ExpectedPresent.prototype.is = function(aReceived, aInfo) {
-  if (typeof this.expectedEvents == "string") {
-    // This is an event we have yet to implement, do a simple string comparison.
-    if (this.expectedEvents == aReceived) {
-      SimpleTest.todo(false, `${aInfo} (${aReceived})`);
-      return;
-    }
+  moveLast(aRule, ...aExpectedEvents) {
+    return this.moveCursor({ action: "moveLast", rule: aRule },
+      ...aExpectedEvents);
   }
 
-  SimpleTest[this.options.todo ? "todo" : "ok"].apply(SimpleTest,
-    this.lazyCompare(aReceived, this.expectedEvents, aInfo + " aReceived: " +
-      JSON.stringify(aReceived) + " evt: " + JSON.stringify(this.expectedEvents)));
-};
+  async clearCursor() {
+    return new Promise(resolve => {
+      let _listener = (msg) => {
+        this.mms.forEach(
+          mm => mm.removeMessageListener("AccessFu:CursorCleared", _listener));
+        resolve();
+      };
+      this.mms.forEach(
+        mm => mm.addMessageListener("AccessFu:CursorCleared", _listener));
+      this.sendMessage({
+        name: "AccessFu:ClearCursor",
+        data: {
+          origin: "top"
+        }
+      });
+    });
+  }
 
-ExpectedPresent.prototype.ignore = function(aMessage) {
-  if (!aMessage.json || ExpectedMessage.prototype.ignore.call(this, aMessage)) {
-    return true;
+  focusSelector(aSelector, ...aExpectedEvents) {
+    return this.expectAndroidEvents(() => {
+      this.sendMessage({
+        name: "AccessFuTest:Focus",
+        data: {
+          selector: aSelector
+        }
+      });
+    }, ...aExpectedEvents);
+  }
+
+  blur(...aExpectedEvents) {
+    return this.expectAndroidEvents(() => {
+      this.sendMessage({ name: "AccessFuTest:Blur" });
+    }, ...aExpectedEvents);
   }
 
-  let firstEvent = (aMessage.json || [])[0];
-
-  return firstEvent && firstEvent.eventType === AndroidEvents.VIEW_SCROLLED;
-};
-
-function ExpectedCursorChange(aSpeech, aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: 0x8000, // VIEW_ACCESSIBILITY_FOCUSED
-  }], aOptions);
-}
-
-ExpectedCursorChange.prototype = Object.create(ExpectedPresent.prototype);
+  activateCurrent(aOffset, ...aExpectedEvents) {
+    return this.expectAndroidEvents(() => {
+      this.sendMessage({
+        name: "AccessFu:Activate",
+        data: {
+          origin: "top",
+          offset: aOffset
+        }
+      });
+    }, ...aExpectedEvents);
+  }
 
-function ExpectedCursorTextChange(aSpeech, aStartOffset, aEndOffset, aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-    fromIndex: aStartOffset,
-    toIndex: aEndOffset
-  }], aOptions);
-
-  // bug 980509
-  this.options.b2g_todo = true;
-}
-
-ExpectedCursorTextChange.prototype =
-  Object.create(ExpectedCursorChange.prototype);
+  typeKey(aKey, ...aExpectedEvents) {
+    return this.expectAndroidEvents(() => {
+      synthesizeKey(aKey, {}, currentTabWindow());
+    }, ...aExpectedEvents);
+  }
 
-function ExpectedClickAction(aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: AndroidEvents.VIEW_CLICKED
-  }], aOptions);
-}
-
-ExpectedClickAction.prototype = Object.create(ExpectedPresent.prototype);
-
-function ExpectedCheckAction(aChecked, aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: AndroidEvents.VIEW_CLICKED,
-    checked: aChecked
-  }], aOptions);
-}
+  eventTextMatches(aEvent, aExpected) {
+    isDeeply(aEvent.text, aExpected,
+      "Event text matches. " +
+      `Got ${JSON.stringify(aEvent.text)}, expected ${JSON.stringify(aExpected)}.`);
+  }
 
-ExpectedCheckAction.prototype = Object.create(ExpectedPresent.prototype);
+  androidScrollForward() {
+    this.sendMessage({
+      name: "AccessFu:AndroidScroll",
+      data: { origin: "top", direction: "forward" }
+    });
+  }
 
-function ExpectedSwitchAction(aSwitched, aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: AndroidEvents.VIEW_CLICKED,
-    checked: aSwitched
-  }], aOptions);
-}
-
-ExpectedSwitchAction.prototype = Object.create(ExpectedPresent.prototype);
-
-// XXX: Implement Android event?
-function ExpectedNameChange(aName, aOptions) {
-  ExpectedPresent.call(this, "todo.name-changed", aOptions);
-}
+  androidScrollBackward() {
+    this.sendMessage({
+      name: "AccessFu:AndroidScroll",
+      data: { origin: "top", direction: "backward" }
+    });
+  }
 
-ExpectedNameChange.prototype = Object.create(ExpectedPresent.prototype);
+  moveByGranularity(aDirection, aGranularity, ...aExpectedEvents) {
+    return this.expectAndroidEvents(() => {
+      this.sendMessage({
+        name: "AccessFu:MoveByGranularity",
+        data: {
+          direction: aDirection,
+          granularity: aGranularity
+        }
+      });
+    }, ...aExpectedEvents);
+  }
 
-// XXX: Implement Android event?
-function ExpectedValueChange(aValue, aOptions) {
-  ExpectedPresent.call(this, "todo.value-changed", aOptions);
-}
+  moveNextByGranularity(aGranularity, ...aExpectedEvents) {
+    return this.moveByGranularity("Next", aGranularity, ...aExpectedEvents);
+  }
 
-ExpectedValueChange.prototype = Object.create(ExpectedPresent.prototype);
+  movePreviousByGranularity(aGranularity, ...aExpectedEvents) {
+    return this.moveByGranularity("Previous", aGranularity, ...aExpectedEvents);
+  }
 
-// XXX: Implement Android event?
-function ExpectedTextChanged(aValue, aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: AndroidEvents.VIEW_TEXT_CHANGED
-  }], aOptions);
-}
-
-ExpectedTextChanged.prototype = Object.create(ExpectedPresent.prototype);
+  // XXX: moveCaret* methods will go away soon as we fold it into the
+  // granularity messages above.
+  moveCaret(aDirection, aGranularity, ...aExpectedEvents) {
+    return this.expectAndroidEvents(() => {
+      this.sendMessage({
+        name: "AccessFu:MoveCaret",
+        data: {
+          direction: aDirection,
+          granularity: aGranularity
+        }
+      });
+    }, ...aExpectedEvents);
+  }
 
-function ExpectedEditState(aEditState, aOptions) {
-  ExpectedMessage.call(this, "AccessFu:Input", aOptions);
-  this.json = aEditState;
-}
+  moveCaretNext(aGranularity, ...aExpectedEvents) {
+    return this.moveCaret("Next", aGranularity, ...aExpectedEvents);
+  }
 
-ExpectedEditState.prototype = Object.create(ExpectedMessage.prototype);
-
-function ExpectedTextSelectionChanged(aStart, aEnd, aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
-  }], aOptions);
+  moveCaretPrevious(aGranularity, ...aExpectedEvents) {
+    return this.moveCaret("Previous", aGranularity, ...aExpectedEvents);
+  }
 }
-
-ExpectedTextSelectionChanged.prototype =
-  Object.create(ExpectedPresent.prototype);
-
-function ExpectedTextCaretChanged(aFrom, aTo, aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-    fromIndex: aFrom,
-    toIndex: aTo
-  }], aOptions);
-}
-
-ExpectedTextCaretChanged.prototype = Object.create(ExpectedPresent.prototype);
-
-function ExpectedAnnouncement(aAnnouncement, aOptions) {
-  ExpectedPresent.call(this, [{
-    eventType: AndroidEvents.ANNOUNCEMENT,
-    text: [ aAnnouncement],
-    addedCount: aAnnouncement.length
-  }], aOptions);
-}
-
-ExpectedAnnouncement.prototype = Object.create(ExpectedPresent.prototype);
-
-// XXX: Implement Android event?
-function ExpectedNoMove(aOptions) {
-  ExpectedPresent.call(this, null, aOptions);
-}
-
-ExpectedNoMove.prototype = Object.create(ExpectedPresent.prototype);
--- a/accessible/tests/mochitest/jsat/test_content_integration.html
+++ b/accessible/tests/mochitest/jsat/test_content_integration.html
@@ -16,299 +16,422 @@
   <script type="application/javascript" src="../browser.js"></script>
   <script type="application/javascript" src="../events.js"></script>
   <script type="application/javascript" src="../role.js"></script>
   <script type="application/javascript" src="../states.js"></script>
   <script type="application/javascript" src="../layout.js"></script>
   <script type="application/javascript" src="jsatcommon.js"></script>
 
   <script type="application/javascript">
+    async function testSimpleNavigation(doc, runner) {
+      let evt;
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+      runner.isFocused("body");
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      is(evt.exitView, "movePrevious");
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "wow", "heading level 1"]);
+      runner.isFocused("iframe");
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["many option", "not checked", "check button", "First item", "list", "1 item"]);
+
+      evt = await runner.activateCurrent(0,
+        AndroidEvents.VIEW_CLICKED,
+        AndroidEvents.VIEW_CLICKED);
+      is(evt[1].checked, true, "checkbox is checked");
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["much range", "label"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["much range", "5", "slider"]);
+
+      expectedMsg = runner.androidEvent("todo.value-changed");
+      runner.androidScrollBackward();
+      evt = await expectedMsg;
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Home", "button"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["apple", "button"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Light", "off", "switch"]);
+
+      evt = await runner.activateCurrent(0,
+        AndroidEvents.VIEW_CLICKED,
+        AndroidEvents.VIEW_CLICKED);
+      is(evt[1].checked, true, "checkbox is checked");
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["slider", "0", "slider", "live"]);
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Light", "on", "switch"]);
+
+      evt = await runner.activateCurrent(0,
+        AndroidEvents.VIEW_CLICKED,
+        AndroidEvents.VIEW_CLICKED);
+      is(evt[1].checked, false, "checkbox is checked");
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["apple", "button"]);
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Home", "button"]);
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "much range", "4", "slider"]);
+
+      expectedMsg = runner.androidEvent("todo.value-changed");
+      runner.androidScrollForward();
+      evt = await expectedMsg;
+
+      expectedMsg = runner.androidEvent("todo.value-changed");
+      runner.androidScrollBackward();
+      evt = await expectedMsg;
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["much range", "label"]);
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["many option", "checked", "check button", "First item", "list", "1 item"]);
+
+      evt = await runner.activateCurrent(0,
+        AndroidEvents.VIEW_CLICKED,
+        AndroidEvents.VIEW_CLICKED);
+      is(evt[1].checked, false, "checkbox is checked");
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["wow", "heading level 1"]);
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Phone status bar"]);
+
+      // Reached top
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      is(evt.exitView, "movePrevious");
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+
+      // Moving to the absolute last item from an embedded document
+      // fails. Bug 972035.
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "wow", "heading level 1"]);
+
+      // Move from an inner frame to the last element in the parent doc
+
+      evt = await runner.moveLast("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["much range", "4", "slider"]);
+
+      await runner.clearCursor();
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+
+      evt = await runner.moveNext("FormElement",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "many option", "not checked", "check button", "First item", "list", "1 item"]);
+
+      evt = await runner.moveNext("FormElement",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["much range", "4", "slider"]);
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["much range", "label"]);
+
+      evt = await runner.movePrevious("FormElement",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["many option", "not checked", "check button", "First item", "list", "1 item"]);
+
+      evt = await runner.movePrevious("FormElement",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+
+      await runner.clearCursor();
+
+
+      // Moving to the absolute first item from an embedded document
+      // fails. Bug 972035.
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "wow", "heading level 1"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["many option", "not checked", "check button", "First item", "list", "1 item"]);
+
+      evt = await runner.moveFirst("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      todo_is(evt.text[0], "Phone status bar");
+
+      await runner.clearCursor();
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+
+      evt = await runner.focusSelector("button#fruit",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["apple", "button"]);
+
+      // Current virtual cursor's position's name changes
+      expectedMsg = runner.androidEvent("todo.name-changed");
+      doc.getElementById("fruit").setAttribute("aria-label", "banana");
+      evt = await expectedMsg;
+
+      // Name and value changes inside a live-region (no cursor present)
+      expectedMsg = runner.androidEvent("todo.name-changed");
+      doc.getElementById("slider").setAttribute("aria-label", "mover");
+      evt = await expectedMsg;
+
+      expectedMsg = runner.androidEvent("todo.value-changed");
+      doc.getElementById("slider").setAttribute("aria-valuenow", "5");
+      doc.getElementById("slider").setAttribute("aria-valuetext", "medium");
+      evt = await expectedMsg;
+
+      // Blur button and reset cursor
+      runner.blur();
+      await runner.clearCursor();
+
+      // Move cursor with focus in outside document
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+
+      evt = await runner.focusSelector("button#home",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Home", "button"]);
+
+      // Blur button and reset cursor
+      runner.blur();
+      await runner.clearCursor();
+
+      // Set focus on element outside of embedded frame while cursor is in frame
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "wow", "heading level 1"]);
+
+      evt = await runner.focusSelector("button#home",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Home", "button"]);
+
+      // Blur button and reset cursor
+      runner.blur();
+      await runner.clearCursor();
+
+      // XXX: Set focus on iframe itself.
+      // XXX: Set focus on element in iframe when cursor is outside of it.
+      // XXX: Set focus on element in iframe when cursor is in iframe.
+
+      // aria-hidden element that the virtual cursor is positioned on
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+
+      evt = await runner.expectAndroidEvents(() => {
+        doc.getElementById("back").setAttribute("aria-hidden", true);
+      }, AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "wow", "heading level 1"]);
+
+      // Changing aria-hidden attribute twice and making sure that the event
+      // is fired only once when the actual change happens.
+      doc.getElementById("back").setAttribute("aria-hidden", true);
+      doc.getElementById("back").setAttribute("aria-hidden", false);
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+      await runner.clearCursor();
+
+      // aria-hidden on the iframe that has the vc.
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "wow", "heading level 1"]);
+
+
+      evt = await runner.expectAndroidEvents(() => {
+        doc.getElementById("iframe").setAttribute("aria-hidden", true);
+      }, AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Home", "button"]);
+
+      doc.getElementById("iframe").setAttribute("aria-hidden", false);
+      await runner.clearCursor();
+
+      // aria-hidden element and auto Move
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+
+      doc.getElementById("back").setAttribute("aria-hidden", true);
+      evt = await runner.focusSelector("button#back",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "wow", "heading level 1"]);
+
+      doc.getElementById("back").setAttribute("aria-hidden", false);
+      runner.blur();
+      await runner.clearCursor();
+
+      // Open dialog in outer doc, while cursor is also in outer doc
+      evt = await runner.moveLast("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "mover", "medium", "slider", "live"]);
+
+      evt = await runner.expectAndroidEvents(() => {
+        doc.getElementById("alert").hidden = false;
+      }, AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["This is an alert!", "heading level 1", "dialog"]);
+
+      evt = await runner.expectAndroidEvents(() => {
+        doc.getElementById("alert").hidden = true;
+      }, AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "mover", "medium", "slider", "live"]);
+
+      await runner.clearCursor();
+
+      // Open dialog in outer doc, while cursor is in inner frame
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Back", "button"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["such app", "wow", "heading level 1"]);
+      evt = await runner.expectAndroidEvents(() => {
+        doc.getElementById("alert").hidden = false;
+      }, AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["This is an alert!", "heading level 1", "dialog"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Do you agree?"]);
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Yes", "button"]);
+
+      evt = await runner.activateCurrent(0,
+        AndroidEvents.VIEW_CLICKED,
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt[1], ["such app", "wow", "heading level 1"]);
+
+      await runner.clearCursor();
+
+      // Open dialog, then focus on something when closing
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Phone status bar"]);
+      evt = await runner.expectAndroidEvents(() => {
+        doc.getElementById("alert").hidden = false;
+      }, AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["This is an alert!", "heading level 1", "dialog"]);
+
+      evt = await runner.expectAndroidEvents(() => {
+        doc.getElementById("alert").hidden = true;
+        doc.querySelector("button#home").focus();
+      }, AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Traversal Rule test document", "Home", "button"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["banana", "button"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Light", "off", "switch"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["mover", "medium", "slider", "live"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      is(evt.exitView, "moveNext", "Reached end of content");
+    }
+
     function doTest() {
       var doc = currentTabDocument();
       var iframe = doc.createElement("iframe");
       iframe.id = "iframe";
       iframe.mozbrowser = true;
       iframe.addEventListener("mozbrowserloadend", function() {
-      var contentTest = new AccessFuContentTest(
-        [
-          // Simple traversal forward
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(
-            ["Traversal Rule test document", "Phone status bar"],
-            { focused: "body" })],
-          [ContentMessages.simpleMovePrevious, new ExpectedNoMove()],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMoveNext, new ExpectedCursorChange(
-            ["such app", "wow", {"string": "headingLevel", "args": [1]}],
-            { focused: "iframe" })],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["many option", {"string": "stateNotChecked"},
-            {"string": "checkbutton"}, {"string": "listStart"},
-            {"string": "list"}, {"string": "listItemsCount", "count": 1}])],
-
-          // check checkbox
-          [ContentMessages.activateCurrent(),
-           new ExpectedClickAction(),
-           new ExpectedCheckAction(true)],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["much range", {"string": "label"}])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["much range", "5", {"string": "slider"}])],
-          [ContentMessages.moveOrAdjustUp(), new ExpectedValueChange("6")],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Home", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["apple", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Light", {"string": "stateOff"}, {"string": "switch"}])],
-          // switch on
-          [ContentMessages.activateCurrent(),
-           new ExpectedClickAction(),
-           new ExpectedSwitchAction(true)],
-           [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["slider", "0", {"string": "slider"}])],
-
-          // Simple traversal backward
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["Light", {"string": "stateOn"}, {"string": "switch"}])],
-          // switch off
-          [ContentMessages.activateCurrent(),
-           new ExpectedClickAction(),
-           new ExpectedSwitchAction(false)],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["apple", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["Home", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["such app", "much range", "6", {"string": "slider"}])],
-          [ContentMessages.moveOrAdjustDown(), new ExpectedValueChange("5")],
-          [ContentMessages.androidScrollForward(), new ExpectedValueChange("6")],
-          [ContentMessages.androidScrollBackward(), new ExpectedValueChange("5")],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["much range", {"string": "label"}])],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["many option", {"string": "stateChecked"},
-            {"string": "checkbutton"}, {"string": "listStart"},
-            {"string": "list"}, {"string": "listItemsCount", "count": 1}])],
-          // uncheck checkbox
-          [ContentMessages.activateCurrent(),
-           new ExpectedClickAction(),
-           new ExpectedCheckAction(false)],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["wow", {"string": "headingLevel", "args": [1]}])],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["Phone status bar"])],
-
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          // Moving to the absolute last item from an embedded document
-          // fails. Bug 972035.
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(
-            ["such app", "wow", {"string": "headingLevel", "args": [1]}])],
-          // Move from an inner frame to the last element in the parent doc
-          [ContentMessages.simpleMoveLast,
-            new ExpectedCursorChange(
-              ["slider", "0", {"string": "slider"}])],
-
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [ContentMessages.moveOrAdjustDown("FormElement"),
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [ContentMessages.moveOrAdjustDown("FormElement"),
-           new ExpectedCursorChange(["such app", "many option", {"string": "stateNotChecked"},
-            {"string": "checkbutton"}, {"string": "listStart"},
-            {"string": "list"}, {"string": "listItemsCount", "count": 1}])],
-          [ContentMessages.moveOrAdjustDown("FormElement"),
-           new ExpectedCursorChange(["much range", "5", {"string": "slider"}])],
-          // Calling AdjustOrMove should adjust the range.
-          [ContentMessages.moveOrAdjustDown("FormElement"),
-           new ExpectedValueChange("4")],
-          [ContentMessages.moveOrAdjustUp("FormElement"),
-           new ExpectedValueChange("5")],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["much range", {"string": "label"}])],
-          [ContentMessages.moveOrAdjustUp("FormElement"),
-           new ExpectedCursorChange(["many option", {"string": "stateNotChecked"},
-            {"string": "checkbutton"}, {"string": "listStart"},
-            {"string": "list"}, {"string": "listItemsCount", "count": 1}])],
-          [ContentMessages.moveOrAdjustUp("FormElement"),
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // Moving to the absolute first item from an embedded document
-          // fails. Bug 972035.
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["such app", "wow", {"string": "headingLevel", "args": [1]}])],
-          [ContentMessages.simpleMoveNext, new ExpectedCursorChange(
-            ["many option", {"string": "stateNotChecked"},
-             {"string": "checkbutton"}, {"string": "listStart"},
-             {"string": "list"}, {"string": "listItemsCount", "count": 1}])],
-          [ContentMessages.simpleMoveFirst,
-            new ExpectedCursorChange(["Phone status bar"])],
-
-          // Reset cursors
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // Current virtual cursor's position's name changes
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [ContentMessages.focusSelector("button#fruit", false),
-           new ExpectedCursorChange(["apple", {"string": "pushbutton"}])],
-          [doc.defaultView.renameFruit, new ExpectedNameChange("banana")],
+        addA11yLoadEvent(async function() {
+          let runner = new AccessFuContentTestRunner();
+          await runner.start();
 
-          // Name and value changes inside a live-region (no cursor present)
-          [doc.defaultView.renameSlider,
-            new ExpectedNameChange("mover")],
-          [doc.defaultView.changeSliderValue,
-            new ExpectedValueChange("medium")],
-
-          // Blur button and reset cursor
-          [ContentMessages.focusSelector("button#fruit", true), null],
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // Move cursor with focus in outside document
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [ContentMessages.focusSelector("button#home", false),
-           new ExpectedCursorChange(["Home", {"string": "pushbutton"}])],
-
-          // Blur button and reset cursor
-          [ContentMessages.focusSelector("button#home", true), null],
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // Set focus on element outside of embedded frame while
-          // cursor is in frame
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["such app", "wow", {"string": "headingLevel", "args": [1]}])],
-          [ContentMessages.focusSelector("button#home", false),
-           new ExpectedCursorChange(["Home", {"string": "pushbutton"}])],
-
-          // Blur button and reset cursor
-          [ContentMessages.focusSelector("button#home", true), null],
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // XXX: Set focus on iframe itself.
-          // XXX: Set focus on element in iframe when cursor is outside of it.
-          // XXX: Set focus on element in iframe when cursor is in iframe.
-
-          // aria-hidden element that the virtual cursor is positioned on
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [doc.defaultView.ariaHideBack,
-           new ExpectedCursorChange(
-            ["such app", "wow", {"string": "headingLevel", "args": [1]}])],
-          // Changing aria-hidden attribute twice and making sure that the event
-          // is fired only once when the actual change happens.
-          [doc.defaultView.ariaHideBack],
-          [doc.defaultView.ariaShowBack],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // aria-hidden on the iframe that has the vc.
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["such app", "wow", {"string": "headingLevel", "args": [1]}])],
-          [doc.defaultView.ariaHideIframe,
-           new ExpectedCursorChange(["Home", {"string": "pushbutton"}])],
-          [doc.defaultView.ariaShowIframe],
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
+          await testSimpleNavigation(doc, runner);
 
-          // aria-hidden element and auto Move
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [doc.defaultView.ariaHideBack],
-          [ContentMessages.focusSelector("button#back", false),
-            // Must not speak Back button as it is aria-hidden
-           new ExpectedCursorChange(
-             ["such app", "wow", {"string": "headingLevel", "args": [1]}])],
-          [doc.defaultView.ariaShowBack],
-          [ContentMessages.focusSelector("button#back", true), null],
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // Open dialog in outer doc, while cursor is also in outer doc
-          [ContentMessages.simpleMoveLast,
-           new ExpectedCursorChange(["Traversal Rule test document", "mover",
-             "medium", {"string": "slider"}])],
-          [doc.defaultView.showAlert,
-            new ExpectedCursorChange(["This is an alert!",
-              {"string": "headingLevel", "args": [1]},
-              {"string": "dialog"}])],
-
-          [doc.defaultView.hideAlert,
-           new ExpectedCursorChange(["Traversal Rule test document", "mover",
-             "medium", {"string": "slider"}])],
-
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // Open dialog in outer doc, while cursor is in inner frame
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(
-            ["such app", "wow", {"string": "headingLevel", "args": [1]}])],
-          [doc.defaultView.showAlert, new ExpectedCursorChange(["This is an alert!",
-                    {"string": "headingLevel", "args": [1]},
-                    {"string": "dialog"}])],
-
-          [ContentMessages.simpleMoveNext,
-            new ExpectedCursorChange(["Do you agree?"])],
-          [ContentMessages.simpleMoveNext,
-            new ExpectedCursorChange(["Yes", {"string": "pushbutton"}])],
-          [ContentMessages.activateCurrent(),
-           new ExpectedClickAction(),
-           new ExpectedCursorChange(
-            ["such app", "wow", {"string": "headingLevel", "args": [1]}])],
-
-          [ContentMessages.clearCursor, "AccessFu:CursorCleared"],
-
-          // Open dialog, then focus on something when closing
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["Traversal Rule test document", "Phone status bar"])],
-          [doc.defaultView.showAlert,
-           new ExpectedCursorChange(["This is an alert!",
-            {"string": "headingLevel", "args": [1]}, {"string": "dialog"}])],
-
-          [function hideAlertAndFocusHomeButton() {
-            doc.defaultView.hideAlert();
-            doc.querySelector("button#home").focus();
-          }, new ExpectedCursorChange(["Traversal Rule test document",
-            "Home", {"string": "pushbutton"}])],
-          [ContentMessages.simpleMoveNext,
-            new ExpectedCursorChange(["banana", {"string": "pushbutton"}])]
-          [ContentMessages.simpleMoveNext, new ExpectedNoMove()]
-        ]);
-
-        addA11yLoadEvent(function() {
-          contentTest.start(function() {
-            closeBrowserWindow();
-            SimpleTest.finish();
-          });
+          runner.finish();
+          closeBrowserWindow();
+          SimpleTest.finish();
         }, doc.defaultView);
       });
       iframe.src = "data:text/html;charset=utf-8," + doc.defaultView.frameContents;
       doc.getElementById("appframe").appendChild(iframe);
     }
 
     SimpleTest.waitForExplicitFinish();
     addLoadEvent(
@@ -330,15 +453,15 @@
           getRootDirectory(window.location.href) + "doc_content_integration.html");
         });
   </script>
 </head>
 <body id="body">
 
   <a target="_blank"
      title="Add tests for OOP message handling and general integration"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=972047">Mozilla Bug 933808</a>
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=1459677">Mozilla Bug 1459677</a>
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 </body>
 </html>
deleted file mode 100644
--- a/accessible/tests/mochitest/jsat/test_content_text.html
+++ /dev/null
@@ -1,311 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <title>Tests AccessFu content integration</title>
-  <meta charset="utf-8" />
-  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
-
-  <script type="application/javascript"
-          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
-  </script>
-  <script type="application/javascript"
-          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
-  </script>
-  <script type="application/javascript"
-          src="chrome://mochikit/content/chrome-harness.js">
-  </script>
-
-  <script type="application/javascript" src="../common.js"></script>
-  <script type="application/javascript" src="../browser.js"></script>
-  <script type="application/javascript" src="../events.js"></script>
-  <script type="application/javascript" src="../role.js"></script>
-  <script type="application/javascript" src="../states.js"></script>
-  <script type="application/javascript" src="../layout.js"></script>
-  <script type="application/javascript" src="jsatcommon.js"></script>
-
-  <script type="application/javascript">
-    function doTest() {
-      var doc = currentTabDocument();
-      var textTest = new AccessFuContentTest(
-        [
-          // Read-only text tests
-          [ContentMessages.simpleMoveFirst,
-           new ExpectedCursorChange(
-            ["Text content test document", "These are my awards, Mother. " +
-             "From Army. The seal is for marksmanship, and the gorilla is " +
-             "for sand racing."])],
-          [ContentMessages.moveNextBy("word"),
-           new ExpectedCursorTextChange("These", 0, 5)],
-          [ContentMessages.moveNextBy("word"),
-           new ExpectedCursorTextChange("are", 6, 9)],
-          [ContentMessages.moveNextBy("word"),
-           new ExpectedCursorTextChange("my", 10, 12)],
-          [ContentMessages.moveNextBy("word"),
-           new ExpectedCursorTextChange("awards,", 13, 20)],
-          [ContentMessages.moveNextBy("word"),
-           new ExpectedCursorTextChange("Mother.", 21, 28)],
-          [ContentMessages.movePreviousBy("word"),
-           new ExpectedCursorTextChange("awards,", 13, 20)],
-          [ContentMessages.movePreviousBy("word"),
-           new ExpectedCursorTextChange("my", 10, 12)],
-          [ContentMessages.movePreviousBy("word"),
-           new ExpectedCursorTextChange("are", 6, 9)],
-          [ContentMessages.movePreviousBy("word"),
-           new ExpectedCursorTextChange("These", 0, 5)],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(["You're a good guy, mon frere. " +
-              "That means brother in French. " +
-              "I don't know how I know that. " +
-              "I took four years of Spanish."])],
-          // XXX: Word boundary should be past the apostraphe.
-          [ContentMessages.moveNextBy("word"),
-           new ExpectedCursorTextChange("You're", 0, 6,
-             { todo: true /* Bug 980512 */})],
-
-          // Editable text tests.
-          [ContentMessages.focusSelector("textarea"),
-           new ExpectedAnnouncement("editing"),
-           new ExpectedEditState({
-            editing: true,
-            multiline: true,
-            atStart: true,
-            atEnd: false
-           }),
-           new ExpectedCursorChange(
-            ["Please refrain from Mayoneggs during this salmonella scare.",
-             {string: "textarea"}]),
-           new ExpectedTextSelectionChanged(0, 0)
-          ],
-          [ContentMessages.activateCurrent(10),
-           new ExpectedTextCaretChanged(0, 10),
-           new ExpectedEditState({ editing: true,
-             multiline: true,
-             atStart: false,
-             atEnd: false }),
-           new ExpectedTextSelectionChanged(10, 10)],
-          [ContentMessages.activateCurrent(20),
-           new ExpectedTextCaretChanged(10, 20),
-           new ExpectedTextSelectionChanged(20, 20)
-          ],
-          [ContentMessages.moveCaretNextBy("word"),
-           new ExpectedTextCaretChanged(20, 29),
-           new ExpectedTextSelectionChanged(29, 29)
-          ],
-          [ContentMessages.moveCaretNextBy("word"),
-           new ExpectedTextCaretChanged(29, 36),
-           new ExpectedTextSelectionChanged(36, 36)
-          ],
-          [ContentMessages.moveCaretNextBy("character"),
-           new ExpectedTextCaretChanged(36, 37),
-           new ExpectedTextSelectionChanged(37, 37)
-          ],
-          [ContentMessages.moveCaretNextBy("character"),
-           new ExpectedTextCaretChanged(37, 38),
-           new ExpectedTextSelectionChanged(38, 38)
-          ],
-          [ContentMessages.moveCaretNextBy("paragraph"),
-           new ExpectedTextCaretChanged(38, 59),
-           new ExpectedTextSelectionChanged(59, 59)
-          ],
-          [ContentMessages.moveCaretPreviousBy("word"),
-           new ExpectedTextCaretChanged(53, 59),
-           new ExpectedTextSelectionChanged(53, 53)
-          ],
-
-          // bug xxx
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(
-            ["So we don't get dessert?", {string: "label"}],
-            { focused: "html"}),
-           new ExpectedAnnouncement("navigating"),
-           new ExpectedEditState({
-            editing: false,
-            multiline: false,
-            atStart: true,
-            atEnd: false })],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(
-            [{ string: "entry" }],
-            { focused: "html"})],
-          [ContentMessages.activateCurrent(0),
-           new ExpectedClickAction(),
-           new ExpectedAnnouncement("editing"),
-           new ExpectedEditState({
-            editing: true,
-            multiline: false,
-            atStart: true,
-            atEnd: true
-           }, { focused: "input[type=text]" }),
-           new ExpectedTextSelectionChanged(0, 0)
-           ],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(
-            ["So we don't get dessert?", {string: "label"}]),
-           new ExpectedAnnouncement("navigating"),
-           new ExpectedEditState({
-            editing: false,
-            multiline: false,
-            atStart: true,
-            atEnd: false
-           }, { focused: "html" })
-         ],
-          [ContentMessages.simpleMoveNext,
-           new ExpectedCursorChange(
-            [{ string: "entry" }],
-            { focused: "html"})],
-          [ContentMessages.activateCurrent(0),
-           new ExpectedClickAction(),
-           new ExpectedAnnouncement("editing"),
-           new ExpectedEditState({
-            editing: true,
-            multiline: false,
-            atStart: true,
-            atEnd: true
-           },
-           { focused: "input[type=text]" }),
-           new ExpectedTextSelectionChanged(0, 0,
-             // Bug 1455749: Fix granularity control in text entries.
-             { todo: true }
-           )],
-          [ContentMessages.simpleMovePrevious,
-           new ExpectedCursorChange(
-            [ "So we don't get dessert?", {string: "label"} ]),
-           new ExpectedAnnouncement("navigating"),
-           new ExpectedEditState({
-            editing: false,
-            multiline: false,
-            atStart: true,
-            atEnd: false
-           }, { focused: "html" })],
-
-          [ContentMessages.focusSelector("input"),
-           new ExpectedAnnouncement("editing"),
-           new ExpectedEditState({
-            editing: true,
-            multiline: false,
-            atStart: true,
-            atEnd: true
-           }),
-           new ExpectedCursorChange([{string: "entry"}]),
-           new ExpectedTextSelectionChanged(0, 0)
-          ],
-          [function() {
-             SpecialPowers.pushPrefEnv({"set": [[KEYBOARD_ECHO_SETTING, 3]]}, typeKey("a")());
-           },
-           new ExpectedTextChanged("a"),
-           new ExpectedTextSelectionChanged(1, 1),
-           new ExpectedValueChange(),
-          ],
-          [typeKey("b"),
-           new ExpectedTextChanged("b"),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(2, 2),
-          ],
-          [typeKey("c"),
-           new ExpectedTextChanged("c"),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(3, 3),
-          ],
-          [typeKey("d"),
-           new ExpectedTextChanged("d"),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(4, 4),
-          ],
-          [typeKey(" "),
-           new ExpectedTextChanged(" abcd"),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(5, 5),
-          ],
-          [typeKey("e"),
-           new ExpectedTextChanged("e"),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(6, 6),
-          ],
-          [function() {
-             SpecialPowers.pushPrefEnv({"set": [[KEYBOARD_ECHO_SETTING, 2]]}, typeKey("a")());
-           },
-           new ExpectedTextChanged(""),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(7, 7),
-          ],
-          [typeKey("d"),
-           new ExpectedTextChanged(""),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(8, 8),
-          ],
-          [typeKey(" "),
-           new ExpectedTextChanged(" ead"),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(9, 9),
-          ],
-          [function() {
-             SpecialPowers.pushPrefEnv({"set": [[KEYBOARD_ECHO_SETTING, 1]]}, typeKey("f")());
-           },
-           new ExpectedTextChanged("f"),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(10, 10),
-          ],
-          [typeKey("g"),
-           new ExpectedTextChanged("g"),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(11, 11),
-          ],
-          [typeKey(" "),
-           new ExpectedTextChanged(" "),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(12, 12),
-          ],
-          [function() {
-             SpecialPowers.pushPrefEnv({"set": [[KEYBOARD_ECHO_SETTING, 0]]}, typeKey("f")());
-           },
-           new ExpectedTextChanged(""),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(13, 13),
-          ],
-          [typeKey("g"),
-           new ExpectedTextChanged(""),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(14, 14),
-          ],
-          [typeKey(" "),
-           new ExpectedTextChanged(""),
-           new ExpectedValueChange(),
-           new ExpectedTextSelectionChanged(15, 15),
-          ],
-        ]);
-
-      const KEYBOARD_ECHO_SETTING = "accessibility.accessfu.keyboard_echo";
-      function typeKey(key) {
-        let func = function() { synthesizeKey(key, {}, currentTabWindow()); };
-        func.toString = () => `typeKey('${key}')`;
-        return func;
-      }
-
-      addA11yLoadEvent(function() {
-        textTest.start(function() {
-          closeBrowserWindow();
-          SimpleTest.finish();
-        });
-      }, doc.defaultView);
-    }
-
-    SimpleTest.waitForExplicitFinish();
-    addLoadEvent(
-      function() {
-        openBrowserWindow(
-          doTest,
-          getRootDirectory(window.location.href) + "doc_content_text.html");
-        });
-  </script>
-</head>
-<body id="body">
-
-  <a target="_blank"
-     title="Add tests for text editing and navigating"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=972047">Mozilla Bug 933808</a>
-  <p id="display"></p>
-  <div id="content" style="display: none"></div>
-  <pre id="test">
-  </pre>
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/jsat/test_text_editable_navigation.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Tests AccessFu navigation in focused editable</title>
+  <meta charset="utf-8" />
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/chrome-harness.js">
+  </script>
+
+  <script type="application/javascript" src="../common.js"></script>
+  <script type="application/javascript" src="../browser.js"></script>
+  <script type="application/javascript" src="../events.js"></script>
+  <script type="application/javascript" src="../role.js"></script>
+  <script type="application/javascript" src="../states.js"></script>
+  <script type="application/javascript" src="../layout.js"></script>
+  <script type="application/javascript" src="jsatcommon.js"></script>
+
+  <script type="application/javascript">
+    function checkMoveCaret(aMoveEvent, aSelectionEvent, aFrom, aTo) {
+      is(aMoveEvent.fromIndex, Math.min(aFrom, aTo), "Move to offset (fromIndex)");
+      is(aMoveEvent.toIndex, Math.max(aFrom, aTo), "Move to offset (toIndex)");
+      is(aSelectionEvent.fromIndex, aTo, "Caret offset (fromIndex)");
+      is(aSelectionEvent.toIndex, aTo, "Caret offset (toIndex)");
+    }
+
+    async function testEditableTextNavigation(doc, runner) {
+      // Editable text tests.
+      let evt;
+
+      evt = await runner.focusSelector("textarea",
+        AndroidEvents.ANNOUNCEMENT,
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      // XXX: Get rid of announcements, and send focus events instead
+      runner.eventTextMatches(evt[0], ["editing"]);
+      runner.eventTextMatches(evt[1],
+        ["Text content test document",
+         "Please refrain from Mayoneggs during this salmonella scare.",
+         "text area"]);
+      is(evt[2].fromIndex, 0, "Correct fromIndex");
+      is(evt[2].toIndex, 0, "Correct toIndex");
+
+      evt = await runner.activateCurrent(10,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      checkMoveCaret(...evt, 0, 10);
+
+      evt = await runner.activateCurrent(20,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      checkMoveCaret(...evt, 10, 20);
+
+      evt = await runner.moveCaretNext(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      checkMoveCaret(...evt, 20, 29);
+
+      evt = await runner.moveCaretNext(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      checkMoveCaret(...evt, 29, 36);
+
+      evt = await runner.moveCaretNext(MovementGranularity.CHARACTER,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      checkMoveCaret(...evt, 36, 37);
+
+      evt = await runner.moveCaretNext(MovementGranularity.CHARACTER,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      checkMoveCaret(...evt, 37, 38);
+
+      evt = await runner.moveCaretNext(MovementGranularity.PARAGRAPH,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      checkMoveCaret(...evt, 38, 59);
+
+      evt = await runner.moveCaretPrevious(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      checkMoveCaret(...evt, 59, 53);
+
+      evt = await runner.blur(AndroidEvents.ANNOUNCEMENT);
+      runner.eventTextMatches(evt, ["navigating"]);
+    }
+
+    function doTest() {
+      var doc = currentTabDocument();
+
+      addA11yLoadEvent(async function() {
+        let runner = new AccessFuContentTestRunner();
+        await runner.start();
+
+        await testEditableTextNavigation(doc, runner);
+
+        runner.finish();
+        closeBrowserWindow();
+        SimpleTest.finish();
+      }, doc.defaultView);
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addLoadEvent(
+      function() {
+        openBrowserWindow(
+          doTest,
+          getRootDirectory(window.location.href) + "doc_content_text.html");
+        });
+  </script>
+</head>
+<body id="body">
+
+  <a target="_blank"
+     title="Add tests for text editing and navigating"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=1459677">Mozilla Bug 1459677</a>
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test">
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/jsat/test_text_editing.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Tests AccessFu text editing</title>
+  <meta charset="utf-8" />
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/chrome-harness.js">
+  </script>
+
+  <script type="application/javascript" src="../common.js"></script>
+  <script type="application/javascript" src="../browser.js"></script>
+  <script type="application/javascript" src="../events.js"></script>
+  <script type="application/javascript" src="../role.js"></script>
+  <script type="application/javascript" src="../states.js"></script>
+  <script type="application/javascript" src="../layout.js"></script>
+  <script type="application/javascript" src="jsatcommon.js"></script>
+
+  <script type="application/javascript">
+    async function testTextEditing(doc, runner) {
+      function checkInsert(textChangeEvent, testSelEvent, text, insertIndex, addedCount) {
+        runner.eventTextMatches(textChangeEvent, text);
+        is(textChangeEvent.addedCount, addedCount);
+        is(textChangeEvent.fromIndex, insertIndex);
+        runner.eventTextMatches(testSelEvent, text);
+        is(testSelEvent.toIndex, insertIndex + addedCount);
+        is(testSelEvent.fromIndex, insertIndex + addedCount);
+      }
+
+      let evt;
+
+      evt = await runner.focusSelector("input",
+        AndroidEvents.ANNOUNCEMENT,
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      // XXX: Get rid of announcements, and send focus events instead
+      runner.eventTextMatches(evt[0], ["editing"]);
+      runner.eventTextMatches(evt[1], ["Text content test document", "entry"]);
+      is(evt[2].fromIndex, 0, "Caret at start (fromIndex)");
+      is(evt[2].toIndex, 0, "Caret at start (toIndex)");
+
+      evt = await runner.typeKey("B",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["B"], 0, 1);
+
+      evt = await runner.typeKey("o",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bo"], 1, 1);
+
+      evt = await runner.typeKey("b",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bob"], 2, 1);
+
+      evt = await runner.typeKey(" ",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bob "], 3, 1);
+
+      evt = await runner.typeKey("L",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bob L"], 4, 1);
+
+      evt = await runner.typeKey("o",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bob Lo"], 5, 1);
+
+      evt = await runner.typeKey("b",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bob Lob"], 6, 1);
+
+      evt = await runner.typeKey("l",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bob Lobl"], 7, 1);
+
+      evt = await runner.typeKey("a",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bob Lobla"], 8, 1);
+
+      evt = await runner.typeKey("w",
+        AndroidEvents.VIEW_TEXT_CHANGED,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        "todo.value-changed");
+      checkInsert(evt[0], evt[1], ["Bob Loblaw"], 9, 1);
+
+      evt = await runner.blur(AndroidEvents.ANNOUNCEMENT);
+      runner.eventTextMatches(evt, ["navigating"]);
+    }
+
+
+    function doTest() {
+      var doc = currentTabDocument();
+
+      addA11yLoadEvent(async function() {
+        let runner = new AccessFuContentTestRunner();
+        await runner.start();
+
+        await testTextEditing(doc, runner);
+
+        runner.finish();
+        closeBrowserWindow();
+        SimpleTest.finish();
+      }, doc.defaultView);
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addLoadEvent(
+      function() {
+        openBrowserWindow(
+          doTest,
+          getRootDirectory(window.location.href) + "doc_content_text.html");
+        });
+  </script>
+</head>
+<body id="body">
+
+  <a target="_blank"
+     title="Add tests for text editing and navigating"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=1459677">Mozilla Bug 1459677</a>
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test">
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/jsat/test_text_navigation.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Tests AccessFu text navigation</title>
+  <meta charset="utf-8" />
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/chrome-harness.js">
+  </script>
+
+  <script type="application/javascript" src="../common.js"></script>
+  <script type="application/javascript" src="../browser.js"></script>
+  <script type="application/javascript" src="../events.js"></script>
+  <script type="application/javascript" src="../role.js"></script>
+  <script type="application/javascript" src="../states.js"></script>
+  <script type="application/javascript" src="../layout.js"></script>
+  <script type="application/javascript" src="jsatcommon.js"></script>
+
+  <script type="application/javascript">
+    function checkFromToIndex(aEvent, aFrom, aTo) {
+      is(aEvent.fromIndex, aFrom, "Correct fromIndex");
+      is(aEvent.toIndex, aTo, "Correct toIndex");
+    }
+
+    async function testTextNavigation(doc, runner) {
+      let evt;
+
+      // Read-only text tests
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["Text content test document",
+        "These are my awards, Mother. From Army. " +
+        "The seal is for marksmanship, and the gorilla is for sand racing."]);
+
+      // "These"
+      evt = await runner.moveNextByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 0, 5);
+
+      // "are"
+      evt = await runner.moveNextByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 6, 9);
+
+      // "my"
+      evt = await runner.moveNextByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 10, 12);
+
+      // "awards,"
+      evt = await runner.moveNextByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 13, 20);
+
+      // "Mother."
+      evt = await runner.moveNextByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 21, 28);
+
+      // "awards,"
+      evt = await runner.movePreviousByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 13, 20);
+
+      // "my"
+      evt = await runner.movePreviousByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 10, 12);
+
+      // "are"
+      evt = await runner.movePreviousByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 6, 9);
+
+      // "These"
+      evt = await runner.movePreviousByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      checkFromToIndex(evt, 0, 5);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["You're a good guy, mon frere. " +
+        "That means brother in French. I don't know how I know that. " +
+        "I took four years of Spanish."]);
+
+      // "You're"
+      evt = await runner.moveNextByGranularity(MovementGranularity.WORD,
+        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+      is(evt.fromIndex, 0, "Correct fromIndex");
+      todo_is(evt.toIndex, 6, "Word boundary should be past the apostraphe");
+    }
+
+    function doTest() {
+      var doc = currentTabDocument();
+
+      addA11yLoadEvent(async function() {
+        let runner = new AccessFuContentTestRunner();
+        await runner.start();
+
+        await testTextNavigation(doc, runner);
+
+        runner.finish();
+        closeBrowserWindow();
+        SimpleTest.finish();
+      }, doc.defaultView);
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addLoadEvent(
+      function() {
+        openBrowserWindow(
+          doTest,
+          getRootDirectory(window.location.href) + "doc_content_text.html");
+        });
+  </script>
+</head>
+<body id="body">
+
+  <a target="_blank"
+     title="Add tests for text editing and navigating"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=1459677">Mozilla Bug 1459677</a>
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test">
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/jsat/test_text_navigation_focus.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Tests AccessFu text entry focus and a11y focus</title>
+  <meta charset="utf-8" />
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/chrome-harness.js">
+  </script>
+
+  <script type="application/javascript" src="../common.js"></script>
+  <script type="application/javascript" src="../browser.js"></script>
+  <script type="application/javascript" src="../events.js"></script>
+  <script type="application/javascript" src="../role.js"></script>
+  <script type="application/javascript" src="../states.js"></script>
+  <script type="application/javascript" src="../layout.js"></script>
+  <script type="application/javascript" src="jsatcommon.js"></script>
+
+  <script type="application/javascript">
+    async function testTextNavigationFocus(doc, runner) {
+      let evt;
+
+      evt = await runner.focusSelector("textarea",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
+        AndroidEvents.ANNOUNCEMENT);
+      // XXX: Get rid of announcements, and send focus events instead
+      runner.eventTextMatches(evt[0],
+        ["Text content test document",
+         "Please refrain from Mayoneggs during this salmonella scare.",
+         "text area"]);
+      runner.eventTextMatches(evt[1], ["editing"]);
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
+        AndroidEvents.ANNOUNCEMENT);
+      runner.eventTextMatches(evt[0], ["So we don't get dessert?", "label"]);
+      runner.eventTextMatches(evt[1], ["navigating"]);
+      runner.isFocused("html");
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["entry"]);
+      runner.isFocused("html");
+
+      evt = await runner.activateCurrent(0,
+        AndroidEvents.VIEW_CLICKED,
+        AndroidEvents.ANNOUNCEMENT,
+        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
+      runner.eventTextMatches(evt[1], ["editing"]);
+      is(evt[2].fromIndex, 0, "Cursor at start");
+      runner.isFocused("input[type=text]");
+
+      evt = await runner.movePrevious("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
+        AndroidEvents.ANNOUNCEMENT);
+      runner.eventTextMatches(evt[0], ["So we don't get dessert?", "label"]);
+      runner.eventTextMatches(evt[1], ["navigating"]);
+      runner.isFocused("html");
+
+      evt = await runner.moveNext("Simple",
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      runner.eventTextMatches(evt, ["entry"]);
+      runner.isFocused("html");
+
+      // XXX: TEXT_SELECTION_CHANGED should be fired here
+      evt = await runner.activateCurrent(0,
+        AndroidEvents.VIEW_CLICKED,
+        AndroidEvents.ANNOUNCEMENT);
+      runner.eventTextMatches(evt[1], ["editing"]);
+      runner.isFocused("input[type=text]");
+
+      evt = await runner.blur(AndroidEvents.ANNOUNCEMENT);
+      runner.eventTextMatches(evt, ["navigating"]);
+    }
+
+
+    function doTest() {
+      var doc = currentTabDocument();
+
+      addA11yLoadEvent(async function() {
+        let runner = new AccessFuContentTestRunner();
+        await runner.start();
+
+        await testTextNavigationFocus(doc, runner);
+
+        runner.finish();
+        closeBrowserWindow();
+        SimpleTest.finish();
+      }, doc.defaultView);
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addLoadEvent(
+      function() {
+        openBrowserWindow(
+          doTest,
+          getRootDirectory(window.location.href) + "doc_content_text.html");
+        });
+  </script>
+</head>
+<body id="body">
+
+  <a target="_blank"
+     title="Add tests for text editing and navigating"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=1459677">Mozilla Bug 1459677</a>
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test">
+  </pre>
+</body>
+</html>
--- a/browser/base/content/test/about/browser_aboutHome_search_searchbar.js
+++ b/browser/base/content/test/about/browser_aboutHome_search_searchbar.js
@@ -1,25 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
+ChromeUtils.import("resource://testing-common/CustomizableUITestUtils.jsm", this);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
 ignoreAllUncaughtExceptions();
 
+add_task(async function test_setup() {
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
+});
+
 add_task(async function() {
   info("Cmd+k should focus the search box in the toolbar when it's present");
 
-  Services.prefs.setBoolPref("browser.search.widget.inNavBar", true);
-
   await BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, async function(browser) {
     await BrowserTestUtils.synthesizeMouseAtCenter("#brandLogo", {}, browser);
 
     let doc = window.document;
-    let searchInput = doc.getElementById("searchbar").textbox.inputField;
+    let searchInput = BrowserSearch.searchBar.textbox.inputField;
     isnot(searchInput, doc.activeElement, "Search bar should not be the active element.");
 
     EventUtils.synthesizeKey("k", { accelKey: true });
     await TestUtils.waitForCondition(() => doc.activeElement === searchInput);
     is(searchInput, doc.activeElement, "Search bar should be the active element.");
   });
-
-  Services.prefs.clearUserPref("browser.search.widget.inNavBar");
 });
--- a/browser/components/customizableui/test/CustomizableUITestUtils.jsm
+++ b/browser/components/customizableui/test/CustomizableUITestUtils.jsm
@@ -7,16 +7,20 @@
  * Shared functions generally available for tests involving PanelMultiView and
  * the CustomizableUI elements in the browser window.
  */
 
 var EXPORTED_SYMBOLS = ["CustomizableUITestUtils"];
 
 ChromeUtils.import("resource://testing-common/Assert.jsm");
 ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm");
+ChromeUtils.import("resource://testing-common/TestUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "CustomizableUI",
+                               "resource:///modules/CustomizableUI.jsm");
 
 class CustomizableUITestUtils {
   /**
    * Constructs an instance that operates with the specified browser window.
    */
   constructor(window) {
     this.window = window;
     this.document = window.document;
@@ -78,9 +82,64 @@ class CustomizableUITestUtils {
 
   /**
    * Closes the main menu and waits for the operation to complete.
    */
   async hideMainMenu() {
     await this.hidePanelMultiView(this.PanelUI.panel,
                                   () => this.PanelUI.hide());
   }
+
+  /**
+   * Add the search bar into the nav bar and verify it does not overflow.
+   *
+   * @returns {Promise}
+   * @resolves The search bar element.
+   * @rejects If search bar is not found, or overflows.
+   */
+  async addSearchBar() {
+    CustomizableUI.addWidgetToArea(
+      "search-container", CustomizableUI.AREA_NAVBAR,
+      CustomizableUI.getPlacementOfWidget("urlbar-container").position + 1);
+
+    // addWidgetToArea adds the search bar into the nav bar first.  If the
+    // search bar overflows, OverflowableToolbar for the nav bar moves the
+    // search bar into the overflow panel in its overflow event handler
+    // asynchronously.
+    //
+    // We should first wait for the layout flush to make sure either the search
+    // bar fits into the nav bar, or overflow event gets dispatched and the
+    // overflow event handler is called.
+    await this.window.promiseDocumentFlushed(() => {});
+
+    // Check if the OverflowableToolbar is handling the overflow event.
+    // _lastOverflowCounter property is incremented synchronously at the top
+    // of the overflow event handler, and is set to 0 when it finishes.
+    let navbar = this.window.document.getElementById(CustomizableUI.AREA_NAVBAR);
+    await TestUtils.waitForCondition(() => {
+      // This test is using a private variable, that can be renamed or removed
+      // in the future.  Use === so that this won't silently skip if the value
+      // becomes undefined.
+      return navbar.overflowable._lastOverflowCounter === 0;
+    });
+
+    let searchbar = this.window.document.getElementById("searchbar");
+    if (!searchbar) {
+      throw new Error("The search bar should exist.");
+    }
+
+    // If the search bar overflows, it's placed inside the overflow panel.
+    //
+    // We cannot use navbar's property to check if overflow happens, since it
+    // can be different widget than the search bar that overflows.
+    if (searchbar.closest("#widget-overflow")) {
+      throw new Error("The search bar should not overflow from the nav bar. " +
+                      "This test fails if the screen resolution is small and " +
+                      "the search bar overflows from the nav bar.");
+    }
+
+    return searchbar;
+  }
+
+  removeSearchBar() {
+    CustomizableUI.removeWidgetFromArea("search-container");
+  }
 }
--- a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
+++ b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
@@ -10,34 +10,34 @@ async function waitForSearchBarFocus() {
   let searchbar = document.getElementById("searchbar");
   await waitForCondition(function() {
     logActiveElement();
     return document.activeElement === searchbar.textbox.inputField;
   });
 }
 
 // Ctrl+K should open the menu panel and focus the search bar if the search bar is in the panel.
-add_task(async function() {
+add_task(async function check_shortcut_when_in_closed_overflow_panel_closed() {
   CustomizableUI.addWidgetToArea("search-container",
                                  CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
 
   let shownPanelPromise = promiseOverflowShown(window);
   sendWebSearchKeyCommand();
   await shownPanelPromise;
 
   await waitForSearchBarFocus();
 
   let hiddenPanelPromise = promiseOverflowHidden(window);
   EventUtils.synthesizeKey("KEY_Escape");
   await hiddenPanelPromise;
   CustomizableUI.reset();
 });
 
 // Ctrl+K should give focus to the searchbar when the searchbar is in the menupanel and the panel is already opened.
-add_task(async function() {
+add_task(async function check_shortcut_when_in_opened_overflow_panel() {
   CustomizableUI.addWidgetToArea("search-container",
                                  CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
 
   await document.getElementById("nav-bar").overflowable.show();
 
   sendWebSearchKeyCommand();
 
   await waitForSearchBarFocus();
@@ -81,23 +81,25 @@ add_task(async function check_shortcut_w
 
   navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
   window.resizeTo(this.originalWindowWidth, window.outerHeight);
   await waitForCondition(() => !navbar.hasAttribute("overflowing"));
   ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar.");
 });
 
 // Ctrl+K should focus the search bar if it is in the navbar and not overflowing.
-add_task(async function() {
+add_task(async function check_shortcut_when_not_in_overflow() {
   Services.prefs.setBoolPref("browser.search.widget.inNavBar", true);
   let placement = CustomizableUI.getPlacementOfWidget("search-container");
   is(placement.area, CustomizableUI.AREA_NAVBAR, "Should be in nav-bar");
 
   sendWebSearchKeyCommand();
 
+  // This fails if the screen resolution is small and the search bar overflows
+  // from the nav bar even with the original window width.
   await waitForSearchBarFocus();
 
   Services.prefs.setBoolPref("browser.search.widget.inNavBar", false);
 });
 
 
 function sendWebSearchKeyCommand() {
   document.documentElement.focus();
--- a/browser/components/customizableui/test/browser_customization_context_menus.js
+++ b/browser/components/customizableui/test/browser_customization_context_menus.js
@@ -146,16 +146,18 @@ add_task(async function urlbar_context()
 // and back should move the search-container instead.
 add_task(async function searchbar_context_move_to_panel_and_back() {
   // This is specifically testing the addToPanel function for the search bar, so
   // we have to move it to its correct position in the navigation toolbar first.
   // The preference will be restored when the customizations are reset later.
   Services.prefs.setBoolPref("browser.search.widget.inNavBar", true);
 
   let searchbar = document.getElementById("searchbar");
+  // This fails if the screen resolution is small and the search bar overflows
+  // from the nav bar.
   await gCustomizeMode.addToPanel(searchbar);
   let placement = CustomizableUI.getPlacementOfWidget("search-container");
   is(placement.area, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, "Should be in panel");
 
   await waitForOverflowButtonShown();
 
   let shownPanelPromise = popupShown(overflowPanel);
   overflowButton.click();
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
@@ -1,27 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+ChromeUtils.import("resource://testing-common/CustomizableUITestUtils.jsm", this);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("browser.policies.runonce.setDefaultSearchEngine");
   Services.prefs.clearUserPref("browser.policies.runOncePerModification.addSearchEngines");
 });
 
+add_task(async function test_setup() {
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
+});
+
 // |shouldWork| should be true if opensearch is expected to work and false if
 // it is not.
 async function test_opensearch(shouldWork) {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
+  let searchBar = BrowserSearch.searchBar;
+
   let rootDir = getRootDirectory(gTestPath);
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, rootDir + "opensearch.html");
   let searchPopup = document.getElementById("PopupSearchAutoComplete");
-  let searchBar = document.getElementById("searchbar");
   let promiseSearchPopupShown = BrowserTestUtils.waitForEvent(searchPopup, "popupshown");
   let searchBarButton = document.getAnonymousElementByAttribute(searchBar,
                                                                 "anonid",
                                                                 "searchbar-search-button");
   searchBarButton.click();
   await promiseSearchPopupShown;
   let oneOffsContainer = document.getAnonymousElementByAttribute(searchPopup,
                                                                  "anonid",
--- a/browser/components/search/test/browser_426329.js
+++ b/browser/components/search/test/browser_426329.js
@@ -119,19 +119,20 @@ async function prepareTest() {
 
   let focusPromise = BrowserTestUtils.waitForEvent(searchBar, "focus");
   gURLBar.focus();
   searchBar.focus();
   await focusPromise;
 }
 
 add_task(async function testSetup() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
 });
 
 add_task(async function testSetupEngine() {
   await promiseSetEngine();
 });
 
 add_task(async function testReturn() {
   await prepareTest();
--- a/browser/components/search/test/browser_google_behavior.js
+++ b/browser/components/search/test/browser_google_behavior.js
@@ -112,26 +112,28 @@ async function testSearchEngine(engineDe
         gURLBar.value = `${engineDetails.alias} foo`;
         gURLBar.focus();
         EventUtils.synthesizeKey("KEY_Enter");
       }
     },
     {
       name: "search bar search",
       searchURL: base + engineDetails.codes.submission,
+      async preTest() {
+        await gCUITestUtils.addSearchBar();
+      },
       run() {
-        Services.prefs.setBoolPref("browser.search.widget.inNavBar", true);
         let sb = BrowserSearch.searchBar;
         sb.focus();
         sb.value = "foo";
         EventUtils.synthesizeKey("KEY_Enter");
       },
       postTest() {
         BrowserSearch.searchBar.value = "";
-        Services.prefs.setBoolPref("browser.search.widget.inNavBar", false);
+        gCUITestUtils.removeSearchBar();
       }
     },
     {
       name: "new tab search",
       searchURL: base + engineDetails.codes.newTab,
       async preTest(tab) {
         let browser = tab.linkedBrowser;
         await BrowserTestUtils.loadURI(browser, "about:newtab");
--- a/browser/components/search/test/browser_healthreport.js
+++ b/browser/components/search/test/browser_healthreport.js
@@ -64,25 +64,24 @@ function test() {
         if (!calledTestTelemetry) {
           is(Services.search.currentEngine.name, "Foo", "Current engine is Foo");
           testTelemetry();
         }
         break;
 
       case "engine-removed":
         Services.obs.removeObserver(observer, "browser-search-engine-modified");
+        gCUITestUtils.removeSearchBar();
         finish();
         break;
     }
   }
 
   Services.obs.addObserver(observer, "browser-search-engine-modified");
-  SpecialPowers.pushPrefEnv({set: [
-    ["browser.search.widget.inNavBar", true],
-  ]}).then(function() {
+  gCUITestUtils.addSearchBar().then(function() {
     Services.search.addEngine("http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml",
                               null, "data:image/x-icon,%00", false);
   });
 }
 
 function resetPreferences() {
   Preferences.resetBranch("datareporting.policy.");
   Preferences.set("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
--- a/browser/components/search/test/browser_hiddenOneOffs_diacritics.js
+++ b/browser/components/search/test/browser_hiddenOneOffs_diacritics.js
@@ -5,25 +5,23 @@
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
 
 const diacritic_engine = "Foo \u2661";
 
 var Preferences =
   ChromeUtils.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
 
-let searchbar;
 let searchIcon;
 
 add_task(async function init() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
-  searchbar = document.getElementById("searchbar");
+  let searchbar = await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
   searchIcon = document.getAnonymousElementByAttribute(
     searchbar, "anonid", "searchbar-search-button"
   );
 
   let currentEngine = Services.search.currentEngine;
   await promiseNewEngine("testEngine_diacritics.xml", {setAsCurrent: false});
   registerCleanupFunction(() => {
     Services.search.currentEngine = currentEngine;
--- a/browser/components/search/test/browser_oneOffContextMenu.js
+++ b/browser/components/search/test/browser_oneOffContextMenu.js
@@ -16,21 +16,20 @@ const oneOffButtons = document.getAnonym
 const searchInNewTabMenuItem = document.getAnonymousElementByAttribute(
   oneOffBinding, "anonid", "search-one-offs-context-open-in-new-tab"
 );
 
 let searchbar;
 let searchIcon;
 
 add_task(async function init() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
-  searchbar = document.getElementById("searchbar");
+  searchbar = await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
   searchIcon = document.getAnonymousElementByAttribute(
     searchbar, "anonid", "searchbar-search-button"
   );
 
   await promiseNewEngine(TEST_ENGINE_BASENAME, {
     setAsCurrent: false,
   });
 });
--- a/browser/components/search/test/browser_oneOffContextMenu_setDefault.js
+++ b/browser/components/search/test/browser_oneOffContextMenu_setDefault.js
@@ -19,25 +19,23 @@ const urlBarOneOffBinding = document.get
 let originalEngine = Services.search.currentEngine;
 
 function resetEngine() {
   Services.search.currentEngine = originalEngine;
 }
 
 registerCleanupFunction(resetEngine);
 
-let searchbar;
 let searchIcon;
 
 add_task(async function init() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
-  searchbar = document.getElementById("searchbar");
+  let searchbar = await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
   searchIcon = document.getAnonymousElementByAttribute(
     searchbar, "anonid", "searchbar-search-button"
   );
 
   await promiseNewEngine(TEST_ENGINE_BASENAME, {
     setAsCurrent: false,
   });
 });
--- a/browser/components/search/test/browser_oneOffHeader.js
+++ b/browser/components/search/test/browser_oneOffHeader.js
@@ -50,21 +50,20 @@ function synthesizeNativeMouseMove(aElem
     utils.sendNativeMouseEvent(x * scale, y * scale, msg, 0, null);
   });
 }
 
 let searchbar;
 let searchIcon;
 
 add_task(async function init() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
-  searchbar = document.getElementById("searchbar");
+  searchbar = await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
   searchIcon = document.getAnonymousElementByAttribute(
     searchbar, "anonid", "searchbar-search-button"
   );
 
   await promiseNewEngine("testEngine.xml");
 });
 
 add_task(async function test_notext() {
@@ -95,19 +94,16 @@ add_task(async function test_notext() {
   promise = promiseEvent(searchPopup, "popuphidden");
   info("Closing search panel");
   EventUtils.synthesizeKey("KEY_Escape");
   await promise;
 });
 
 add_task(async function test_text() {
   searchbar._textbox.value = "foo";
-  registerCleanupFunction(() => {
-    searchbar._textbox.value = "";
-  });
 
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
   SimpleTest.executeSoon(() => {
     EventUtils.synthesizeMouseAtCenter(searchIcon, {});
   });
   await promise;
 
@@ -142,8 +138,12 @@ add_task(async function test_text() {
 
   let url = Services.search.currentEngine
                            .getSubmission(searchbar._textbox.value).uri.spec;
   await promiseTabLoadEvent(gBrowser.selectedTab, url);
 
   // Move the cursor out of the panel area to avoid messing with other tests.
   await synthesizeNativeMouseMove(searchbar);
 });
+
+add_task(async function cleanup() {
+  searchbar._textbox.value = "";
+});
--- a/browser/components/search/test/browser_private_search_perwindowpb.js
+++ b/browser/components/search/test/browser_private_search_perwindowpb.js
@@ -1,17 +1,20 @@
 // This test performs a search in a public window, then a different
 // search in a private window, and then checks in the public window
 // whether there is an autocomplete entry for the private search.
 
+add_task(async function test_setup() {
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
+});
+
 add_task(async function() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
   let windowsToClose = [];
 
   function performSearch(aWin, aIsPrivate) {
     let searchBar = aWin.BrowserSearch.searchBar;
     ok(searchBar, "got search bar");
 
     let loadPromise = BrowserTestUtils.browserLoaded(aWin.gBrowser.selectedBrowser);
 
--- a/browser/components/search/test/browser_searchEngine_behaviors.js
+++ b/browser/components/search/test/browser_searchEngine_behaviors.js
@@ -67,20 +67,21 @@ const SEARCH_ENGINE_DETAILS = [{
 function promiseContentSearchReady(browser) {
   return ContentTask.spawn(browser, {}, async function(args) {
     await ContentTaskUtils.waitForCondition(() => content.wrappedJSObject.gContentSearchController &&
       content.wrappedJSObject.gContentSearchController.defaultEngine
     );
   });
 }
 
-add_task(async function() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
+add_task(async function test_setup() {
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
 });
 
 for (let engine of SEARCH_ENGINE_DETAILS) {
   add_task(async function() {
     let previouslySelectedEngine = Services.search.currentEngine;
 
     registerCleanupFunction(function() {
       Services.search.currentEngine = previouslySelectedEngine;
--- a/browser/components/search/test/browser_searchbar_keyboard_navigation.js
+++ b/browser/components/search/test/browser_searchbar_keyboard_navigation.js
@@ -19,21 +19,20 @@ function getOpenSearchItems() {
 
   return os;
 }
 
 let searchbar;
 let textbox;
 
 add_task(async function init() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
-  searchbar = document.getElementById("searchbar");
+  searchbar = await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
   textbox = searchbar._textbox;
 
   await promiseNewEngine("testEngine.xml");
 
   // First cleanup the form history in case other tests left things there.
   await new Promise((resolve, reject) => {
     info("cleanup the search history");
     searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
@@ -44,35 +43,22 @@ add_task(async function init() {
   await new Promise((resolve, reject) => {
     info("adding search history values: " + kValues);
     let addOps = kValues.map(value => {
  return {op: "add",
                                              fieldname: "searchbar-history",
                                              value};
                                    });
     searchbar.FormHistory.update(addOps, {
-      handleCompletion() {
-        registerCleanupFunction(() => {
-          info("removing search history values: " + kValues);
-          let removeOps =
-            kValues.map(value => {
- return {op: "remove",
-                                           fieldname: "searchbar-history",
-                                           value};
-                                 });
-          searchbar.FormHistory.update(removeOps);
-        });
-        resolve();
-      },
+      handleCompletion: resolve,
       handleError: reject
     });
   });
 
   textbox.value = kUserValue;
-  registerCleanupFunction(() => { textbox.value = ""; });
 });
 
 
 add_task(async function test_arrows() {
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
   searchbar.focus();
   await promise;
@@ -439,8 +425,18 @@ add_task(async function test_open_search
      "the settings item should be selected");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   searchPopup.hidePopup();
   await promise;
 
   gBrowser.removeCurrentTab();
 });
+
+add_task(async function cleanup() {
+  info("removing search history values: " + kValues);
+  let removeOps = kValues.map(value => {
+    return {op: "remove", fieldname: "searchbar-history", value};
+  });
+  searchbar.FormHistory.update(removeOps);
+
+  textbox.value = "";
+});
--- a/browser/components/search/test/browser_searchbar_openpopup.js
+++ b/browser/components/search/test/browser_searchbar_openpopup.js
@@ -1,14 +1,11 @@
 // Tests that the suggestion popup appears at the right times in response to
 // focus and user events (mouse, keyboard, drop).
 
-ChromeUtils.import("resource://testing-common/CustomizableUITestUtils.jsm", this);
-let gCUITestUtils = new CustomizableUITestUtils(window);
-
 // Instead of loading EventUtils.js into the test scope in browser-test.js for all tests,
 // we only need EventUtils.js for a few files which is why we are using loadSubScript.
 var EventUtils = {};
 Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
 const kValues = ["long text", "long text 2", "long text 3"];
 
@@ -58,21 +55,20 @@ async function startCustomizing(aWindow 
 }
 
 let searchbar;
 let textbox;
 let searchIcon;
 let goButton;
 
 add_task(async function init() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
-  searchbar = document.getElementById("searchbar");
+  searchbar = await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
   textbox = searchbar._textbox;
   searchIcon = document.getAnonymousElementByAttribute(
     searchbar, "anonid", "searchbar-search-button"
   );
   goButton = document.getAnonymousElementByAttribute(
     searchbar, "anonid", "search-go-button"
   );
 
@@ -89,29 +85,17 @@ add_task(async function init() {
   await new Promise((resolve, reject) => {
     info("adding search history values: " + kValues);
     let addOps = kValues.map(value => {
  return {op: "add",
                                              fieldname: "searchbar-history",
                                              value};
                                    });
     searchbar.FormHistory.update(addOps, {
-      handleCompletion() {
-        registerCleanupFunction(() => {
-          info("removing search history values: " + kValues);
-          let removeOps =
-            kValues.map(value => {
- return {op: "remove",
-                                           fieldname: "searchbar-history",
-                                           value};
-                                 });
-          searchbar.FormHistory.update(removeOps);
-        });
-        resolve();
-      },
+      handleCompletion: resolve,
       handleError: reject
     });
   });
 });
 
 // Adds a task that shouldn't show the search suggestions popup.
 function add_no_popup_task(task) {
   add_task(async function() {
@@ -579,8 +563,16 @@ add_task(async function dont_open_in_cus
   await promise;
 
   searchPopup.removeEventListener("popupshowing", listener);
   ok(!sawPopup, "Shouldn't have seen the suggestions popup");
 
   await endCustomizing();
   textbox.value = "";
 });
+
+add_task(async function cleanup() {
+  info("removing search history values: " + kValues);
+  let removeOps = kValues.map(value => {
+    return {op: "remove", fieldname: "searchbar-history", value};
+  });
+  searchbar.FormHistory.update(removeOps);
+});
--- a/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
+++ b/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -19,21 +19,20 @@ function getOpenSearchItems() {
   return os;
 }
 
 let searchbar;
 let textbox;
 let searchIcon;
 
 add_task(async function init() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
-  searchbar = document.getElementById("searchbar");
+  searchbar = await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
   textbox = searchbar._textbox;
   searchIcon = document.getAnonymousElementByAttribute(
     searchbar, "anonid", "searchbar-search-button"
   );
 
   await promiseNewEngine("testEngine.xml");
 
   // First cleanup the form history in case other tests left things there.
@@ -47,29 +46,17 @@ add_task(async function init() {
   await new Promise((resolve, reject) => {
     info("adding search history values: " + kValues);
     let addOps = kValues.map(value => {
  return {op: "add",
                                              fieldname: "searchbar-history",
                                              value};
                                    });
     searchbar.FormHistory.update(addOps, {
-      handleCompletion() {
-        registerCleanupFunction(() => {
-          info("removing search history values: " + kValues);
-          let removeOps =
-            kValues.map(value => {
- return {op: "remove",
-                                           fieldname: "searchbar-history",
-                                           value};
-                                 });
-          searchbar.FormHistory.update(removeOps);
-        });
-        resolve();
-      },
+      handleCompletion: resolve,
       handleError: reject
     });
   });
 });
 
 
 add_task(async function test_arrows() {
   let promise = promiseEvent(searchPopup, "popupshown");
@@ -359,8 +346,16 @@ add_task(async function test_open_search
      "the settings item should be selected");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   searchPopup.hidePopup();
   await promise;
 
   gBrowser.removeCurrentTab();
 });
+
+add_task(async function cleanup() {
+  info("removing search history values: " + kValues);
+  let removeOps = kValues.map(value => {
+    return {op: "remove", fieldname: "searchbar-history", value};
+  });
+  searchbar.FormHistory.update(removeOps);
+});
--- a/browser/components/search/test/browser_tooManyEnginesOffered.js
+++ b/browser/components/search/test/browser_tooManyEnginesOffered.js
@@ -4,22 +4,25 @@
 // popup shows a submenu that lists them instead of showing them in the popup
 // itself.
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
 const oneOffsContainer =
   document.getAnonymousElementByAttribute(searchPopup, "anonid",
                                           "search-one-off-buttons");
 
+add_task(async function test_setup() {
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
+});
+
 add_task(async function test() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
-
-  let searchbar = document.getElementById("searchbar");
+  let searchbar = BrowserSearch.searchBar;
 
   let rootDir = getRootDirectory(gTestPath);
   let url = rootDir + "tooManyEnginesOffered.html";
   await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
 
   // Open the search popup.
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
--- a/browser/components/search/test/head.js
+++ b/browser/components/search/test/head.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+ChromeUtils.import("resource://testing-common/CustomizableUITestUtils.jsm", this);
+let gCUITestUtils = new CustomizableUITestUtils(window);
 
 /**
  * Recursively compare two objects and check that every property of expectedObj has the same value
  * on actualObj.
  */
 function isSubObjectOf(expectedObj, actualObj, name) {
   for (let prop in expectedObj) {
     if (typeof expectedObj[prop] == "function")
deleted file mode 100644
--- a/browser/docs/UITelemetry.rst
+++ /dev/null
@@ -1,146 +0,0 @@
-.. _uitelemetry:
-
-==================================
-UITelemetry data format (obsolete)
-==================================
-
-.. note::
-
-    ``UITelemetry`` is deprecated. As of Firefox 61, ``UITelemetry`` is no longer reported.
-
-UI Telemetry sends its data as a JSON blob. This document describes the different parts
-of the JSON blob.
-
-``toolbars``
-============
-
-This tracks the state of the user's UI customizations. It has the following properties:
-
-- ``sizemode`` - string indicating whether the window is in maximized, normal (restored) or
-  fullscreen mode;
-- ``bookmarksBarEnabled`` - boolean indicating whether the bookmarks bar is visible;
-- ``menuBarEnabled`` - boolean indicating whether the menu bar is visible (always false on OS X);
-- ``titleBarEnabled`` - boolean indicating whether the (real) titlebar is visible (rather than
-  having tabs in the titlebar);
-- ``defaultKept`` - list of strings identifying toolbar buttons and items that are still in their
-  default position. Only the IDs of builtin widgets are sent (ie not add-on widgets);
-- ``defaultMoved`` - list of strings identifying toolbar buttons and items that are no longer in
-  their default position, but have not been removed to the palette. Only the IDs of builtin widgets
-  are sent (ie not add-on widgets);
-- ``nondefaultAdded`` - list of strings identifying toolbar buttons and items that have been added
-  from the palette. Only the IDs of builtin widgets are sent (ie not add-on widgets);
-- ``defaultRemoved`` - list of strings identifying toolbar buttons and items that are in the
-  palette that are elsewhere by default. Only the IDs of builtin widgets are sent
-  (ie not add-on widgets);
-- ``addonToolbars`` - the number of non-default toolbars that are customizable. 1 by default
-  because it counts the add-on bar shim;
-- ``visibleTabs`` - array of the number of visible tabs per window;
-- ``hiddenTabs`` - array of the number of hidden tabs per window (ie tabs in panorama groups which
-  are not the current group);
-- ``countableEvents`` - please refer to the next section.
-- ``durations`` - an object mapping descriptions to duration records, which records the amount of
-  time a user spent doing something. Currently only has one property:
-
-  - ``customization`` - how long a user spent customizing the browser. This is an array of
-    objects, where each object has a ``duration`` property indicating the time in milliseconds,
-    and a ``bucket`` property indicating a bucket in which the duration info falls.
-
-
-.. _UITelemetry_countableEvents:
-
-``countableEvents``
-===================
-
-Countable events are stored under the ``toolbars`` section. They count the number of times certain
-events happen. No timing or other correlating information is stored - purely the number of times
-things happen.
-
-``countableEvents`` contains a list of buckets as its properties. A bucket represents the state the browser was in when these events occurred, such as currently running an interactive tour. There are 3 types of buckets:
-
-- ``__DEFAULT__`` - No bucket, for times when the browser is not in any special state.
-- ``bucket_<NAME>`` - Normal buckets, for when the browser is in a special state. The ``<NAME>`` in the bucket ID is the name associated with the bucket and may be further broken down into parts by the ``|`` character.
-- ``bucket_<NAME>|<INTERVAL>`` - Expiring buckets, which are similar to a countdown timer. The ``<INTERVAL>`` in the bucket ID describes the time interval the recorded event happened in. The intervals are ``1m`` (one minute), ``3m`` (three minutes), ``10m`` (ten minutes), and ``1h`` (one hour). After one hour, the ``__DEFAULT__`` bucket is automatically used again.
-
-Each bucket is an object with the following properties:
-
-- ``click-builtin-item`` is an object tracking clicks on builtin customizable toolbar items, keyed
-  off the item IDs, with an object for each item with keys ``left``, ``middle`` and ``right`` each
-  storing a number indicating how often the respective type of click has happened.
-- ``click-menu-button`` is the same, except the item ID is always 'button'.
-- ``click-bookmarks-bar`` is the same, with the item IDs being replaced by either ``container`` for
-  clicks on bookmark or livemark folders, and ``item`` for individual bookmarks.
-- ``click-menubar`` is similar, with the item IDs being replaced by one of ``menu``, ``menuitem``
-  or ``other``, depending on the kind of item clicked. Note that this is not tracked on OS X, where
-  we can't listen for these events because of the global menubar.
-- ``click-bookmarks-menu-button`` is also similar, with the item IDs being replaced by:
-
-  - ``menu`` for clicks on the 'menu' part of the item;
-  - ``add`` for clicks that add a bookmark;
-  - ``edit`` for clicks that open the panel to edit an existing bookmark;
-  - ``in-panel`` for clicks when the button is in the menu panel, and clicking it does none of the
-     above;
-- ``customize`` tracks different types of customization events without the ``left``, ``middle`` and
-  ``right`` distinctions. The different events are the following, with each storing a count of the
-  number of times they occurred:
-
-  - ``start`` counts the number of times the user starts customizing;
-  - ``add`` counts the number of times an item is added somewhere from the palette;
-  - ``move`` counts the number of times an item is moved somewhere else (but not to the palette);
-  - ``remove`` counts the number of times an item is removed to the palette;
-  - ``reset`` counts the number of times the 'restore defaults' button is used;
-- ``search`` is an object tracking searches of various types, keyed off the search
-    location, storing a number indicating how often the respective type of search
-    has happened.
-
-  - There are also two special keys that mean slightly different things.
-
-    - ``urlbar-keyword`` records searches that would have been an invalid-protocol
-      error, but are now keyword searches.  They are also counted in the ``urlbar``
-      keyword (along with all the other urlbar searches).
-    - ``selection`` searches records selections of search suggestions.  They include
-      the source, the index of the selection, and the kind of selection (mouse or
-      enter key).  Selection searches are also counted in their sources.
-
-
-
-``UITour``
-==========
-
-The UITour API provides ways for pages on trusted domains to safely interact with the browser UI and request it to perform actions such as opening menus and showing highlights over the browser chrome - for the purposes of interactive tours. We track some usage of this API via the ``UITour`` object in the UI Telemetry output.
-
-Each page is able to register itself with an identifier, a ``Page ID``. A list of Page IDs that have been seen over the last 8 weeks is available via ``seenPageIDs``.
-
-Page IDs are also used to identify buckets for :ref:`UITelemetry_countableEvents`, in the following circumstances:
-
-- The current tab is a tour page. This will be a normal bucket with the name ``UITour|<PAGEID>``, where ``<PAGEID>`` is the page's registered ID. This will result in bucket IDs such as ``bucket_UITour|australis-tour``.
-- A tour tab is open but another tab is active. This will be an expiring bucket with the name ``UITour|<PAGEID>|inactive``. This will result in bucket IDs such as ``bucket_UITour|australis-tour|inactive|1m``.
-- A tour tab has recently been open but has been closed. This will be an expiring bucket with the name ``UITour|<PAGEID>|closed``. This will result in bucket IDs such as ``bucket_UITour|australis-tour|closed|10m``.
-
-
-
-``contextmenu``
-===============
-
-We track context menu interactions to figure out which ones are most often used and/or how
-effective they are. In the ``contextmenu`` object, we first store things per-bucket. Next, we
-divide the following different context menu situations:
-
-- ``selection`` if there is content on the page that's selected on which the user clicks;
-- ``link`` if the user opened the context menu for a link
-- ``image-link`` if the user opened the context menu on an image or canvas that's a link;
-- ``image`` if the user opened the context menu on an image (that isn't a link);
-- ``canvas`` if the user opened the context menu on a canvas (that isn't a link);
-- ``media`` if the user opened the context menu on an HTML video or audio element;
-- ``input`` if the user opened the context menu on a text input element;
-- ``other`` for all other openings of the content menu;
-
-Each of these objects (if they exist) then gets a "withcustom" and/or a "withoutcustom" property
-for context menus opened with custom page-created items and without them, and each of those
-properties holds an object with IDs corresponding to a count of how often an item with that ID was
-activated in the context menu. Only builtin context menu items are tracked, and besides those items
-there are four special items which get counts:
-
-- ``close-without-interaction`` is incremented when the user closes the context menu without interacting with it;
-- ``custom-page-item`` is incremented when the user clicks an item that was created by the page;
-- ``unknown`` is incremented when an item without an ID was clicked;
-- ``other-item`` is incremented when an add-on-provided menuitem is clicked.
--- a/browser/docs/index.rst
+++ b/browser/docs/index.rst
@@ -2,11 +2,10 @@
 Firefox
 =======
 
 This is the nascent documentation of the Firefox front-end code.
 
 .. toctree::
    :maxdepth: 1
 
-   UITelemetry
    BrowserUsageTelemetry
    BrowserErrorReporter
--- a/browser/extensions/formautofill/test/browser/browser.ini
+++ b/browser/extensions/formautofill/test/browser/browser.ini
@@ -14,14 +14,15 @@ support-files =
 [browser_creditCard_doorhanger.js]
 skip-if = (os == "linux") || (os == "mac" && debug) || (os == "win") # bug 1425884
 [browser_creditCard_fill_master_password.js]
 [browser_dropdown_layout.js]
 [browser_editAddressDialog.js]
 [browser_editCreditCardDialog.js]
 [browser_first_time_use_doorhanger.js]
 [browser_insecure_form.js]
+skip-if = (os == 'linux' && !debug) || (os == 'win') # bug 1456284
 [browser_manageAddressesDialog.js]
 [browser_manageCreditCardsDialog.js]
 [browser_privacyPreferences.js]
 [browser_submission_in_private_mode.js]
 [browser_update_doorhanger.js]
-skip-if = (os == "linux") || (os == "mac" && debug) || (os == "win") # bug 1426981
\ No newline at end of file
+skip-if = (os == "linux") || (os == "mac" && debug) || (os == "win") # bug 1426981
--- a/browser/modules/test/browser/browser_UsageTelemetry_searchbar.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_searchbar.js
@@ -1,15 +1,18 @@
 "use strict";
 
 const SCALAR_SEARCHBAR = "browser.engagement.navigation.searchbar";
 
 ChromeUtils.defineModuleGetter(this, "URLBAR_SELECTED_RESULT_METHODS",
                                "resource:///modules/BrowserUsageTelemetry.jsm");
 
+ChromeUtils.import("resource://testing-common/CustomizableUITestUtils.jsm", this);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
 function checkHistogramResults(resultIndexes, expected, histogram) {
   for (let i = 0; i < resultIndexes.counts.length; i++) {
     if (i == expected) {
       Assert.equal(resultIndexes.counts[i], 1,
         `expected counts should match for ${histogram} index ${i}`);
     } else {
       Assert.equal(resultIndexes.counts[i], 0,
         `unexpected counts should be zero for ${histogram} index ${i}`);
@@ -45,19 +48,20 @@ function clickSearchbarSuggestion(entryN
                      item => item.getAttribute("ac-value") == entryName);
 
   // Make sure the suggestion is visible and simulate the click.
   richlistbox.ensureElementIsVisible(richlistitem);
   EventUtils.synthesizeMouseAtCenter(richlistitem, {});
 }
 
 add_task(async function setup() {
-  await SpecialPowers.pushPrefEnv({ set: [
-    ["browser.search.widget.inNavBar", true],
-  ]});
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
 
   // Create two new search engines. Mark one as the default engine, so
   // the test don't crash. We need to engines for this test as the searchbar
   // doesn't display the default search engine among the one-off engines.
   Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
                                        "http://example.com/?q={searchTerms}");
 
   Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET",
--- a/devtools/client/debugger/new/README.mozilla
+++ b/devtools/client/debugger/new/README.mozilla
@@ -1,13 +1,13 @@
 This is the debugger.html project output.
 See https://github.com/devtools-html/debugger.html
 
-Version 46
+Version 48
 
-Comparison: https://github.com/devtools-html/debugger.html/compare/release-45.1...release-46
+Comparison: https://github.com/devtools-html/debugger.html/compare/release-47...release-48
 
 Packages:
 - babel-plugin-transform-es2015-modules-commonjs @6.26.2
 - babel-preset-react @6.24.1
 - react @16.2.0
 - react-dom @16.2.0
 - webpack @3.11.0
--- a/devtools/client/debugger/new/debugger.css
+++ b/devtools/client/debugger/new/debugger.css
@@ -958,22 +958,31 @@ img.close::before {
   width: 100%;
   height: 100%;
   padding: 6px;
 }
 /* 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/>. */
 
+.search-shadow {
+  margin: 1px;
+}
+
+.search-shadow.focused {
+  box-shadow: var(--theme-focus-box-shadow-textbox);
+}
+
 .search-field {
+  position: relative;
+
   width: calc(100% - 1px);
-  height: 27px;
   background-color: var(--theme-toolbar-background);
   border-bottom: 1px solid var(--theme-splitter-color);
-  padding-right: 10px;
+  padding: 5px 10px;
   display: flex;
   flex-shrink: 0;
 }
 
 .search-field.big {
   height: 40px;
 }
 
@@ -987,53 +996,61 @@ img.close::before {
   width: 16px;
 }
 
 .search-field.big input {
   line-height: 40px;
 }
 
 .search-field input {
+  position: relative;
+  margin-left: 30px;
   border: none;
   line-height: 30px;
   background-color: var(--theme-toolbar-background);
   color: var(--theme-body-color-inactive);
   width: calc(100% - 38px);
   flex: 1;
+  font-size: 13px;
+  text-indent: 12px;
+}
+
+.search-field input:focus {
+  outline: none;
 }
 
 .theme-dark .search-field input {
   color: var(--theme-body-color-inactive);
 }
 
 .search-field i.magnifying-glass,
 .search-field i.sad-face {
+  position: absolute;
+  top: 50%;
   padding: 6px;
   width: 24px;
+  margin-top: -12px;
 }
 
 .search-field.big i.magnifying-glass,
 .search-field.big i.sad-face {
   padding: 14px;
   width: 40px;
+  margin-top: -20px;
 }
 
 .search-field .magnifying-glass path,
 .search-field .magnifying-glass ellipse {
   stroke: var(--theme-comment);
 }
 
 .search-field input::placeholder {
   color: var(--theme-toolbar-color);
 }
 
-.search-field input:focus {
-  outline-width: 0;
-}
-
 .search-field input.empty {
   color: var(--theme-highlight-orange);
 }
 
 .search-field.big .summary {
   line-height: 40px;
 }
 
@@ -1153,17 +1170,17 @@ img.close::before {
 
 .project-text-search .search-field {
   display: flex;
   align-self: stretch;
   flex-grow: 1;
 }
 
 .project-text-search .search-field .close-btn.big {
-  margin-top: 12px;
+  margin-top: 6px;
 }
 
 .project-text-search .managed-tree {
   overflow-y: auto;
   height: 100%;
 }
 
 .project-text-search .managed-tree .tree {
@@ -1844,17 +1861,17 @@ html .toggle-button.end.vertical svg {
 }
 
 .search-bar .search-field {
   padding-left: 7px;
   height: var(--editor-searchbar-height);
 }
 
 .search-field .close-btn {
-  margin-top: 7px;
+  margin-top: 12px;
 }
 
 .search-bottom-bar * {
   -moz-user-select: none;
   user-select: none;
 }
 
 .search-bottom-bar {
@@ -3067,16 +3084,17 @@ html[dir="rtl"] .breakpoints-list .break
   overflow: hidden;
   text-overflow: ellipsis;
   display: inline-block;
 }
 
 .CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-code,
 .CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-scroll {
   cursor: default;
+  pointer-events: none;
 }
 /* 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/>. */
 
 .expression-input-form {
   width: 100%;
 }
@@ -3136,17 +3154,17 @@ html[dir="rtl"] .breakpoints-list .break
 }
 
 .expression-input-container.error {
   border: 1px solid red;
 }
 
 .expression-container {
   border: 1px;
-  padding: 0.25em 1em 0.25em 0.5em;
+  padding: 0.6em 1em 0.6em 0.5em;
   width: 100%;
   color: var(--theme-body-color);
   background-color: var(--theme-body-background);
   display: block;
   position: relative;
 }
 
 .expression-container > .tree {
@@ -3999,16 +4017,31 @@ html .welcomebox .toggle-button-end.coll
 .source-tab .blackBox path {
   fill: var(--theme-textbox-box-shadow);
 }
 
 .theme-dark .source-tab .blackBox circle {
   fill: var(--theme-body-color);
 }
 
+img.moreTabs {
+  mask: url("chrome://devtools/skin/images/command-chevron.svg") no-repeat;
+  mask-size: 100%;
+  width: 12px;
+  height: 12px;
+  display: block;
+  background: var(--theme-body-color);
+  margin-left: 6px;
+}
+
+html[dir="rtl"] img.moreTabs {
+  transform: rotate(180deg);
+  margin-right: 6px;
+}
+
 .source-tab .filename {
   white-space: nowrap;
   text-overflow: ellipsis;
   overflow: hidden;
   padding: 0 4px;
   align-self: flex-start;
 }
 
@@ -4027,16 +4060,17 @@ html .welcomebox .toggle-button-end.coll
 /* 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/>. */
 
 .dropdown {
   --width: 150px;
   background: var(--theme-body-background);
   border: 1px solid var(--theme-splitter-color);
+  border-radius: 4px;
   box-shadow: 0 4px 4px 0 var(--search-overlays-semitransparent);
   max-height: 300px;
   position: absolute;
   right: 0;
   top: 23px;
   width: var(--width);
   z-index: 1000;
   overflow: auto;
@@ -4066,16 +4100,17 @@ html[dir="rtl"] .dropdown {
 
 .dropdown li {
   transition: all 0.25s ease;
   padding: 2px 10px 10px 5px;
   overflow: hidden;
   height: 30px;
   text-overflow: ellipsis;
   white-space: nowrap;
+  display: block;
 }
 
 .dropdown li:hover {
   background-color: var(--search-overlays-semitransparent);
 }
 
 .dropdown-icon {
   width: var(--icon-size);
@@ -4095,16 +4130,18 @@ html[dir="rtl"] .dropdown {
   mask: url("chrome://devtools/skin/images/debugger/blackBox.svg") no-repeat;
   mask-size: 100%;
   background: var(--theme-highlight-blue);
 }
 
 .dropdown-icon.file {
   mask: url("chrome://devtools/skin/images/debugger/file.svg") no-repeat;
   mask-size: 100%;
+  margin-bottom: 7px;
+
 }
 
 .dropdown ul {
   list-style: none;
   line-height: 2em;
   font-size: 1em;
   margin: 0;
   padding: 0;
@@ -4139,20 +4176,20 @@ html[dir="rtl"] .dropdown {
 
 .result-list li {
   color: var(--theme-body-color);
   padding: 4px 13px;
   display: flex;
 }
 
 .result-list.big li {
-  padding: 10px;
+  padding: 5px;
   flex-direction: row;
-  border-bottom: 1px solid var(--theme-splitter-color);
   line-height: 18px;
+  font-size: 12px;
 }
 
 .result-list.small li {
   justify-content: space-between;
 }
 
 .result-list li:hover {
   background: var(--theme-tab-toolbar-background);
@@ -4195,16 +4232,24 @@ html[dir="rtl"] .dropdown {
   width: 16px;
   height: 16px;
 }
 
 .result-list li .title {
   word-break: break-all;
   text-overflow: ellipsis;
   white-space: nowrap;
+
+  /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+  color: var(--grey-90);
+}
+
+.theme-dark .result-list li .title {
+  /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+  color: var(--grey-30);
 }
 
 .result-list li.selected .title {
   color: white;
 }
 
 .result-list.big li.selected {
   background-color: var(--theme-selection-background);
@@ -4217,17 +4262,18 @@ html[dir="rtl"] .dropdown {
 
 .result-list.big li.selected .subtitle .highlight {
   color: white;
   font-weight: bold;
 }
 
 .result-list.big li .subtitle {
   word-break: break-all;
-  color: var(--theme-body-color-inactive);
+  /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+  color: var(--grey-50);
   margin-left: 15px;
   text-overflow: ellipsis;
   overflow: hidden;
   white-space: nowrap;
 }
 
 .result-list.big li .subtitle {
   line-height: 1.5em;
--- a/devtools/client/debugger/new/debugger.js
+++ b/devtools/client/debugger/new/debugger.js
@@ -3826,28 +3826,28 @@ exports.default = update;
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
+var _Close = __webpack_require__(1374);
+
+var _Close2 = _interopRequireDefault(_Close);
+
 var _Svg = __webpack_require__(1359);
 
 var _Svg2 = _interopRequireDefault(_Svg);
 
 var _classnames = __webpack_require__(175);
 
 var _classnames2 = _interopRequireDefault(_classnames);
 
-var _Close = __webpack_require__(1374);
-
-var _Close2 = _interopRequireDefault(_Close);
-
 __webpack_require__(1313);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 const arrowBtn = (onClick, type, className, tooltip) => {
   const props = {
     className,
     key: type,
@@ -3862,16 +3862,42 @@ const arrowBtn = (onClick, type, classNa
     _react2.default.createElement(_Svg2.default, { name: type })
   );
 }; /* 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/>. */
 
 class SearchInput extends _react.Component {
 
+  constructor(props) {
+    super(props);
+
+    this.onFocus = e => {
+      const { onFocus } = this.props;
+
+      this.setState({ inputFocused: true });
+      if (onFocus) {
+        onFocus(e);
+      }
+    };
+
+    this.onBlur = e => {
+      const { onBlur } = this.props;
+
+      this.setState({ inputFocused: false });
+      if (onBlur) {
+        onBlur(e);
+      }
+    };
+
+    this.state = {
+      inputFocused: false
+    };
+  }
+
   componentDidMount() {
     this.setFocus();
   }
 
   componentDidUpdate(prevProps) {
     if (this.props.shouldFocus && !prevProps.shouldFocus) {
       this.setFocus();
     }
@@ -3915,19 +3941,17 @@ class SearchInput extends _react.Compone
       this.renderArrowButtons()
     );
   }
 
   render() {
     const {
       expanded,
       handleClose,
-      onBlur,
       onChange,
-      onFocus,
       onKeyDown,
       onKeyUp,
       placeholder,
       query,
       selectedItemId,
       showErrorEmoji,
       size,
       summaryMsg
@@ -3935,45 +3959,53 @@ class SearchInput extends _react.Compone
 
     const inputProps = {
       className: (0, _classnames2.default)({
         empty: showErrorEmoji
       }),
       onChange,
       onKeyDown,
       onKeyUp,
-      onFocus,
-      onBlur,
+      onFocus: e => this.onFocus(e),
+      onBlur: e => this.onBlur(e),
       "aria-autocomplete": "list",
       "aria-controls": "result-list",
       "aria-activedescendant": expanded && selectedItemId ? `${selectedItemId}-title` : "",
       placeholder,
       value: query,
       spellCheck: false,
       ref: c => this.$input = c
     };
 
     return _react2.default.createElement(
       "div",
       {
-        className: (0, _classnames2.default)("search-field", size),
-        role: "combobox",
-        "aria-haspopup": "listbox",
-        "aria-owns": "result-list",
-        "aria-expanded": expanded
+        className: (0, _classnames2.default)("search-shadow", {
+          focused: this.state.inputFocused
+        })
       },
-      this.renderSvg(),
-      _react2.default.createElement("input", inputProps),
-      summaryMsg && _react2.default.createElement(
+      _react2.default.createElement(
         "div",
-        { className: "summary" },
-        summaryMsg
-      ),
-      this.renderNav(),
-      _react2.default.createElement(_Close2.default, { handleClick: handleClose, buttonClass: size })
+        {
+          className: (0, _classnames2.default)("search-field", size),
+          role: "combobox",
+          "aria-haspopup": "listbox",
+          "aria-owns": "result-list",
+          "aria-expanded": expanded
+        },
+        this.renderSvg(),
+        _react2.default.createElement("input", inputProps),
+        summaryMsg && _react2.default.createElement(
+          "div",
+          { className: "summary" },
+          summaryMsg
+        ),
+        this.renderNav(),
+        _react2.default.createElement(_Close2.default, { handleClick: handleClose, buttonClass: size })
+      )
     );
   }
 }
 
 SearchInput.defaultProps = {
   expanded: false,
   hasPrefix: false,
   selectedItemId: "",
@@ -5526,17 +5558,17 @@ exports.getGeneratedLocation = getGenera
 
 var _selectors = __webpack_require__(3590);
 
 async function getGeneratedLocation(state, source, location, sourceMaps) {
   if (!sourceMaps.isOriginalId(location.sourceId)) {
     return location;
   }
 
-  const { line, sourceId, column } = await sourceMaps.getGeneratedLocation(location, source);
+  const { line, sourceId, column } = await sourceMaps.getGeneratedLocation(location, source.toJS());
 
   const generatedSource = (0, _selectors.getSource)(state, sourceId);
   if (!generatedSource) {
     return location;
   }
 
   return {
     line,
@@ -5555,16 +5587,17 @@ async function getGeneratedLocation(stat
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.addExpression = addExpression;
+exports.autocomplete = autocomplete;
 exports.clearExpressionError = clearExpressionError;
 exports.updateExpression = updateExpression;
 exports.deleteExpression = deleteExpression;
 exports.evaluateExpressions = evaluateExpressions;
 exports.getMappedExpression = getMappedExpression;
 
 var _selectors = __webpack_require__(3590);
 
@@ -5607,16 +5640,27 @@ function addExpression(input) {
     if (newExpression) {
       return dispatch(evaluateExpression(newExpression));
     }
   };
 } /* 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/>. */
 
+function autocomplete(input, cursor) {
+  return async ({ dispatch, getState, client }) => {
+    if (!input) {
+      return;
+    }
+    const frameId = (0, _selectors.getSelectedFrameId)(getState());
+    const result = await client.autocomplete(input, cursor, frameId);
+    await dispatch({ type: "AUTOCOMPLETE", input, result });
+  };
+}
+
 function clearExpressionError() {
   return { type: "CLEAR_EXPRESSION_ERROR" };
 }
 
 function updateExpression(input, expression) {
   return async ({ dispatch, getState }) => {
     if (!input) {
       return;
@@ -5700,16 +5744,20 @@ function evaluateExpression(expression) 
 
 /**
  * Gets information about original variable names from the source map
  * and replaces all posible generated names.
  */
 function getMappedExpression(expression) {
   return async function ({ dispatch, getState, client, sourceMaps }) {
     const mappings = (0, _selectors.getSelectedScopeMappings)(getState());
+    if (!mappings) {
+      return expression;
+    }
+
     return parser.mapOriginalExpression(expression, mappings);
   };
 }
 
 /***/ }),
 
 /***/ 1399:
 /***/ (function(module, exports, __webpack_require__) {
@@ -6775,18 +6823,19 @@ async function findScopeByName(source, n
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.getExpressionError = exports.getExpressions = exports.createExpressionState = undefined;
+exports.getExpressionError = exports.getAutocompleteMatches = exports.getExpressions = exports.createExpressionState = undefined;
 exports.getExpression = getExpression;
+exports.getAutocompleteMatchset = getAutocompleteMatchset;
 
 var _makeRecord = __webpack_require__(1361);
 
 var _makeRecord2 = _interopRequireDefault(_makeRecord);
 
 var _immutable = __webpack_require__(3594);
 
 var _lodash = __webpack_require__(2);
@@ -6794,17 +6843,18 @@ var _lodash = __webpack_require__(2);
 var _reselect = __webpack_require__(993);
 
 var _prefs = __webpack_require__(226);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 const createExpressionState = exports.createExpressionState = (0, _makeRecord2.default)({
   expressions: (0, _immutable.List)(restoreExpressions()),
-  expressionError: false
+  expressionError: false,
+  autocompleteMatches: (0, _immutable.Map)({})
 }); /* 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/>. */
 
 /**
  * Expressions reducer
  * @module reducers/expressions
  */
@@ -6848,19 +6898,21 @@ function update(state = createExpression
     case "DELETE_EXPRESSION":
       return deleteExpression(state, action.input);
 
     case "CLEAR_EXPRESSION_ERROR":
       return state.set("expressionError", false);
 
     // respond to time travel
     case "TRAVEL_TO":
-      {
-        return travelTo(state, action);
-      }
+      return travelTo(state, action);
+
+    case "AUTOCOMPLETE":
+      const { matchProp, matches } = action.result;
+      return state.updateIn(["autocompleteMatches", matchProp], list => matches);
   }
 
   return state;
 }
 
 function travelTo(state, action) {
   const { expressions } = action.data;
   if (!expressions) {
@@ -6911,20 +6963,26 @@ function deleteExpression(state, input) 
   storeExpressions(newState);
   return newState;
 }
 
 const getExpressionsWrapper = state => state.expressions;
 
 const getExpressions = exports.getExpressions = (0, _reselect.createSelector)(getExpressionsWrapper, expressions => expressions.expressions);
 
+const getAutocompleteMatches = exports.getAutocompleteMatches = (0, _reselect.createSelector)(getExpressionsWrapper, expressions => expressions.autocompleteMatches);
+
 function getExpression(state, input) {
   return getExpressions(state).find(exp => exp.input == input);
 }
 
+function getAutocompleteMatchset(state, input) {
+  return getAutocompleteMatches(state).get(input);
+}
+
 const getExpressionError = exports.getExpressionError = (0, _reselect.createSelector)(getExpressionsWrapper, expressions => expressions.expressionError);
 
 exports.default = update;
 
 /***/ }),
 
 /***/ 1418:
 /***/ (function(module, exports, __webpack_require__) {
@@ -8162,18 +8220,18 @@ function searchSource(sourceId, query) {
 
     const matches = await (0, _search.findSourceMatches)(sourceRecord.toJS(), query);
     if (!matches.length) {
       return;
     }
     dispatch({
       type: "ADD_SEARCH_RESULT",
       result: {
-        sourceId: sourceRecord.get("id"),
-        filepath: sourceRecord.get("url"),
+        sourceId: sourceRecord.id,
+        filepath: sourceRecord.url,
         matches
       }
     });
   };
 }
 
 /***/ }),
 
@@ -10671,16 +10729,25 @@ function evaluate(script, { frameId } = 
     return Promise.resolve({});
   }
 
   return new Promise(resolve => {
     tabTarget.activeConsole.evaluateJSAsync(script, result => resolve(result), params);
   });
 }
 
+function autocomplete(input, cursor, frameId) {
+  if (!tabTarget || !tabTarget.activeConsole || !input) {
+    return Promise.resolve({});
+  }
+  return new Promise(resolve => {
+    tabTarget.activeConsole.autocomplete(input, cursor, result => resolve(result), frameId);
+  });
+}
+
 function debuggeeCommand(script) {
   tabTarget.activeConsole.evaluateJS(script, () => {}, {});
 
   if (!debuggerClient) {
     return;
   }
 
   const consoleActor = tabTarget.form.consoleActor;
@@ -10818,16 +10885,17 @@ async function fetchWorkers() {
   if (!threadClient._parent || typeof threadClient._parent.listWorkers != "function" || !supportsListWorkers) {
     return Promise.resolve({ workers: [] });
   }
 
   return threadClient._parent.listWorkers();
 }
 
 const clientCommands = {
+  autocomplete,
   blackBox,
   interrupt,
   eventListeners,
   pauseGrip,
   resume,
   stepIn,
   stepOut,
   stepOver,
@@ -11370,18 +11438,16 @@ var _propTypes = __webpack_require__(364
 var _propTypes2 = _interopRequireDefault(_propTypes);
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
-var _redux = __webpack_require__(3593);
-
 var _prefs = __webpack_require__(226);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _ShortcutsModal = __webpack_require__(1535);
 
@@ -11428,19 +11494,21 @@ var _Tabs = __webpack_require__(1614);
 var _Tabs2 = _interopRequireDefault(_Tabs);
 
 var _QuickOpenModal = __webpack_require__(1652);
 
 var _QuickOpenModal2 = _interopRequireDefault(_QuickOpenModal);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-const shortcuts = new _devtoolsModules.KeyShortcuts({ window }); /* 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 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/>. */
+
+const shortcuts = new _devtoolsModules.KeyShortcuts({ window });
 
 const { appinfo } = _devtoolsModules.Services;
 
 const isMacOS = appinfo.OS === "Darwin";
 
 const horizontalLayoutBreakpoint = window.matchMedia("(min-width: 800px)");
 const verticalLayoutBreakpoint = window.matchMedia("(min-width: 10px) and (max-width: 800px)");
 
@@ -11645,28 +11713,26 @@ class App extends _react.Component {
       }),
       this.renderShortcutsModal()
     );
   }
 }
 
 App.childContextTypes = { shortcuts: _propTypes2.default.object };
 
-function mapStateToProps(state) {
-  return {
-    selectedSource: (0, _selectors.getSelectedSource)(state),
-    startPanelCollapsed: (0, _selectors.getPaneCollapse)(state, "start"),
-    endPanelCollapsed: (0, _selectors.getPaneCollapse)(state, "end"),
-    activeSearch: (0, _selectors.getActiveSearch)(state),
-    quickOpenEnabled: (0, _selectors.getQuickOpenEnabled)(state),
-    orientation: (0, _selectors.getOrientation)(state)
-  };
-}
-
-exports.default = (0, _reactRedux.connect)(mapStateToProps, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(App);
+const mapStateToProps = state => ({
+  selectedSource: (0, _selectors.getSelectedSource)(state),
+  startPanelCollapsed: (0, _selectors.getPaneCollapse)(state, "start"),
+  endPanelCollapsed: (0, _selectors.getPaneCollapse)(state, "end"),
+  activeSearch: (0, _selectors.getActiveSearch)(state),
+  quickOpenEnabled: (0, _selectors.getQuickOpenEnabled)(state),
+  orientation: (0, _selectors.getOrientation)(state)
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(App);
 
 /***/ }),
 
 /***/ 1519:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -11687,17 +11753,17 @@ var _sourceMaps = __webpack_require__(13
 
 var _source = __webpack_require__(1356);
 
 exports.default = async function addBreakpoint(getState, client, sourceMaps, breakpoint) {
   const state = getState();
   const source = (0, _selectors.getSource)(state, breakpoint.location.sourceId);
 
   const location = _extends({}, breakpoint.location, { sourceUrl: source.url });
-  const generatedLocation = await (0, _sourceMaps.getGeneratedLocation)(state, source.toJS(), location, sourceMaps);
+  const generatedLocation = await (0, _sourceMaps.getGeneratedLocation)(state, source, location, sourceMaps);
 
   const generatedSource = (0, _selectors.getSource)(state, generatedLocation.sourceId);
 
   (0, _breakpoint.assertLocation)(location);
   (0, _breakpoint.assertLocation)(generatedLocation);
 
   if ((0, _breakpoint.breakpointExists)(state, location)) {
     const newBreakpoint = _extends({}, breakpoint, { location, generatedLocation });
@@ -11837,17 +11903,17 @@ async function syncClientBreakpoint(getS
 
   const generatedSource = (0, _selectors.getSource)(getState(), generatedSourceId);
 
   const { location, astLocation } = pendingBreakpoint;
   const previousLocation = _extends({}, location, { sourceId });
 
   const scopedLocation = await makeScopedLocation(astLocation, previousLocation, source);
 
-  const scopedGeneratedLocation = await (0, _sourceMaps.getGeneratedLocation)(getState(), source.toJS(), scopedLocation, sourceMaps);
+  const scopedGeneratedLocation = await (0, _sourceMaps.getGeneratedLocation)(getState(), source, scopedLocation, sourceMaps);
 
   // this is the generatedLocation of the pending breakpoint, with
   // the source id updated to reflect the new connection
   const generatedLocation = _extends({}, pendingBreakpoint.generatedLocation, {
     sourceId: generatedSourceId
   });
 
   const isSameLocation = !(0, _breakpoint.locationMoved)(generatedLocation, scopedGeneratedLocation);
@@ -13709,18 +13775,16 @@ function createTree({ sources, debuggeeU
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 var _text = __webpack_require__(1387);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
@@ -13739,16 +13803,20 @@ var _Outline = __webpack_require__(1552)
 var _Outline2 = _interopRequireDefault(_Outline);
 
 var _SourcesTree = __webpack_require__(1553);
 
 var _SourcesTree2 = _interopRequireDefault(_SourcesTree);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
+/* 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/>. */
+
 class PrimaryPanes extends _react.Component {
 
   constructor(props) {
     super(props);
 
     this.showPane = selectedPane => {
       this.props.setPrimaryPaneTab(selectedPane);
     };
@@ -13828,25 +13896,25 @@ class PrimaryPanes extends _react.Compon
       { className: "sources-panel" },
       this.renderTabs(),
       selectedTab === "sources" ? _react2.default.createElement(_SourcesTree2.default, null) : _react2.default.createElement(_Outline2.default, {
         alphabetizeOutline: this.state.alphabetizeOutline,
         onAlphabetizeClick: this.onAlphabetizeClick
       })
     );
   }
-} /* 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/>. */
-
-exports.default = (0, _reactRedux.connect)(state => ({
+}
+
+const mapStateToProps = state => ({
   selectedTab: (0, _selectors.getSelectedPrimaryPaneTab)(state),
   sources: (0, _selectors.getSources)(state),
   sourceSearchOn: (0, _selectors.getActiveSearch)(state) === "source"
-}), dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(PrimaryPanes);
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(PrimaryPanes);
 
 /***/ }),
 
 /***/ 1552:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -13855,18 +13923,16 @@ Object.defineProperty(exports, "__esModu
   value: true
 });
 exports.Outline = undefined;
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _redux = __webpack_require__(3593);
-
 var _devtoolsContextmenu = __webpack_require__(1413);
 
 var _reactRedux = __webpack_require__(3592);
 
 var _clipboard = __webpack_require__(1388);
 
 var _function = __webpack_require__(1597);
 
@@ -13881,16 +13947,20 @@ var _selectors = __webpack_require__(359
 var _PreviewFunction = __webpack_require__(1446);
 
 var _PreviewFunction2 = _interopRequireDefault(_PreviewFunction);
 
 var _lodash = __webpack_require__(2);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
+/* 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/>. */
+
 class Outline extends _react.Component {
   selectItem(location) {
     const { selectedSource, selectLocation } = this.props;
     if (!selectedSource) {
       return;
     }
     const selectedSourceId = selectedSource.get("id");
     const startLine = location.start.line;
@@ -14063,29 +14133,29 @@ class Outline extends _react.Component {
     return _react2.default.createElement(
       "div",
       { className: "outline" },
       symbolsToDisplay.length > 0 ? this.renderFunctions(symbols.functions) : this.renderPlaceholder()
     );
   }
 }
 
-exports.Outline = Outline; /* 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/>. */
-
-exports.default = (0, _reactRedux.connect)(state => {
+exports.Outline = Outline;
+const mapStateToProps = state => {
   const selectedSource = (0, _selectors.getSelectedSource)(state);
-  return {
-    symbols: (0, _selectors.getSymbols)(state, selectedSource && selectedSource.toJS()),
+  const symbols = (0, _selectors.getSymbols)(state, selectedSource);
+  return {
+    symbols,
     selectedSource,
     selectedLocation: (0, _selectors.getSelectedLocation)(state),
-    getFunctionText: line => (0, _function.findFunctionText)(line, selectedSource.toJS(), (0, _selectors.getSymbols)(state, selectedSource.toJS()))
-  };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Outline);
+    getFunctionText: line => (0, _function.findFunctionText)(line, selectedSource, symbols)
+  };
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Outline);
 
 /***/ }),
 
 /***/ 1553:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -14532,18 +14602,16 @@ var _source = __webpack_require__(1356);
 var _devtoolsConfig = __webpack_require__(1355);
 
 var _prefs = __webpack_require__(226);
 
 var _indentation = __webpack_require__(1438);
 
 var _selectors = __webpack_require__(3590);
 
-var _redux = __webpack_require__(3593);
-
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _Footer = __webpack_require__(1555);
 
 var _Footer2 = _interopRequireDefault(_Footer);
 
@@ -14600,27 +14668,27 @@ var _editor = __webpack_require__(1358);
 var _ui = __webpack_require__(1439);
 
 __webpack_require__(1332);
 
 __webpack_require__(1333);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
+// Redux actions
+/* 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/>. */
+
 const cssVars = {
   searchbarHeight: "var(--editor-searchbar-height)",
   secondSearchbarHeight: "var(--editor-second-searchbar-height)",
   footerHeight: "var(--editor-footer-height)"
 };
 
-// Redux actions
-/* 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/>. */
-
 class Editor extends _react.PureComponent {
   constructor(props) {
     super(props);
 
     this.onToggleBreakpoint = (key, e) => {
       e.preventDefault();
       e.stopPropagation();
       const { selectedSource, conditionalPanelLine } = this.props;
@@ -15074,28 +15142,29 @@ class Editor extends _react.PureComponen
 
 Editor.contextTypes = {
   shortcuts: _propTypes2.default.object
 };
 
 const mapStateToProps = state => {
   const selectedSource = (0, _selectors.getSelectedSource)(state);
   const sourceId = selectedSource ? selectedSource.get("id") : "";
+
   return {
     selectedLocation: (0, _selectors.getSelectedLocation)(state),
     selectedSource,
     searchOn: (0, _selectors.getActiveSearch)(state) === "file",
     hitCount: (0, _selectors.getHitCountForSource)(state, sourceId),
     coverageOn: (0, _selectors.getCoverageEnabled)(state),
     conditionalPanelLine: (0, _selectors.getConditionalPanelLine)(state),
     symbols: (0, _selectors.getSymbols)(state, selectedSource && selectedSource.toJS())
   };
 };
 
-exports.default = (0, _reactRedux.connect)(mapStateToProps, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Editor);
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Editor);
 
 /***/ }),
 
 /***/ 1555:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -15105,18 +15174,16 @@ Object.defineProperty(exports, "__esModu
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
-var _redux = __webpack_require__(3593);
-
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _selectors = __webpack_require__(3590);
 
 var _classnames = __webpack_require__(175);
 
@@ -15133,20 +15200,16 @@ var _editor = __webpack_require__(1358);
 var _PaneToggle = __webpack_require__(1407);
 
 var _PaneToggle2 = _interopRequireDefault(_PaneToggle);
 
 __webpack_require__(1322);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-/* 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/>. */
-
 class SourceFooter extends _react.PureComponent {
   prettyPrintButton() {
     const { selectedSource, togglePrettyPrint } = this.props;
     const sourceLoaded = selectedSource && (0, _source.isLoaded)(selectedSource);
 
     if (!(0, _editor.shouldShowPrettyPrint)(selectedSource)) {
       return;
     }
@@ -15295,29 +15358,34 @@ class SourceFooter extends _react.PureCo
     return _react2.default.createElement(
       "div",
       { className: "source-footer" },
       this.renderCommands(),
       this.renderSourceSummary(),
       this.renderToggleButton()
     );
   }
-}
-
-exports.default = (0, _reactRedux.connect)(state => {
+} /* 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/>. */
+
+const mapStateToProps = state => {
   const selectedSource = (0, _selectors.getSelectedSource)(state);
   const selectedId = selectedSource.get("id");
   const source = selectedSource.toJS();
+
   return {
     selectedSource,
     mappedSource: (0, _sources.getGeneratedSource)(state, source),
     prettySource: (0, _selectors.getPrettySource)(state, selectedId),
     endPanelCollapsed: (0, _selectors.getPaneCollapse)(state, "end")
   };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(SourceFooter);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(SourceFooter);
 
 /***/ }),
 
 /***/ 1556:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -15331,18 +15399,16 @@ var _propTypes = __webpack_require__(364
 var _propTypes2 = _interopRequireDefault(_propTypes);
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
-var _redux = __webpack_require__(3593);
-
 var _Svg = __webpack_require__(1359);
 
 var _Svg2 = _interopRequireDefault(_Svg);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
@@ -15361,29 +15427,31 @@ var _SearchInput = __webpack_require__(1
 var _SearchInput2 = _interopRequireDefault(_SearchInput);
 
 var _lodash = __webpack_require__(2);
 
 __webpack_require__(1323);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
+/* 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/>. */
+
 function getShortcuts() {
   const searchAgainKey = L10N.getStr("sourceSearch.search.again.key2");
   const searchAgainPrevKey = L10N.getStr("sourceSearch.search.againPrev.key2");
   const searchKey = L10N.getStr("sourceSearch.search.key2");
 
   return {
     shiftSearchAgainShortcut: searchAgainPrevKey,
     searchAgainShortcut: searchAgainKey,
     searchShortcut: searchKey
   };
-} /* 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/>. */
+}
 
 class SearchBar extends _react.Component {
   constructor(props) {
     super(props);
 
     this.onEscape = e => {
       this.closeSearch(e);
     };
@@ -15642,27 +15710,27 @@ class SearchBar extends _react.Component
     );
   }
 }
 
 SearchBar.contextTypes = {
   shortcuts: _propTypes2.default.object
 };
 
-exports.default = (0, _reactRedux.connect)(state => {
-  return {
-    searchOn: (0, _selectors.getActiveSearch)(state) === "file",
-    selectedSource: (0, _selectors.getSelectedSource)(state),
-    selectedLocation: (0, _selectors.getSelectedLocation)(state),
-    query: (0, _selectors.getFileSearchQuery)(state),
-    modifiers: (0, _selectors.getFileSearchModifiers)(state),
-    highlightedLineRange: (0, _selectors.getHighlightedLineRange)(state),
-    searchResults: (0, _selectors.getFileSearchResults)(state)
-  };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(SearchBar);
+const mapStateToProps = state => ({
+  searchOn: (0, _selectors.getActiveSearch)(state) === "file",
+  selectedSource: (0, _selectors.getSelectedSource)(state),
+  selectedLocation: (0, _selectors.getSelectedLocation)(state),
+  query: (0, _selectors.getFileSearchQuery)(state),
+  modifiers: (0, _selectors.getFileSearchModifiers)(state),
+  highlightedLineRange: (0, _selectors.getHighlightedLineRange)(state),
+  searchResults: (0, _selectors.getFileSearchResults)(state)
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(SearchBar);
 
 /***/ }),
 
 /***/ 1557:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -15864,32 +15932,36 @@ class Preview extends _react.PureCompone
       extra: extra,
       onClose: e => this.onClose(e)
     });
   }
 } /* 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/>. */
 
+const mapStateToProps = state => ({
+  preview: (0, _selectors.getPreview)(state),
+  selectedSource: (0, _selectors.getSelectedSource)(state)
+});
+
 const {
   addExpression,
   setPopupObjectProperties,
   updatePreview,
   clearPreview
 } = _actions2.default;
 
-exports.default = (0, _reactRedux.connect)(state => ({
-  preview: (0, _selectors.getPreview)(state),
-  selectedSource: (0, _selectors.getSelectedSource)(state)
-}), {
+const mapDispatchToProps = {
   addExpression,
   setPopupObjectProperties,
   updatePreview,
   clearPreview
-})(Preview);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(Preview);
 
 /***/ }),
 
 /***/ 1559:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -16179,33 +16251,37 @@ class Popup extends _react.Component {
         editorRef: editorRef
       },
       this.renderPreview()
     );
   }
 }
 
 exports.Popup = Popup;
+const mapStateToProps = state => ({
+  popupObjectProperties: (0, _selectors.getAllPopupObjectProperties)(state)
+});
+
 const {
   addExpression,
   selectSourceURL,
   selectLocation,
   setPopupObjectProperties,
   openLink
 } = _actions2.default;
 
-exports.default = (0, _reactRedux.connect)(state => ({
-  popupObjectProperties: (0, _selectors.getAllPopupObjectProperties)(state)
-}), {
+const mapDispatchToProps = {
   addExpression,
   selectSourceURL,
   selectLocation,
   setPopupObjectProperties,
   openLink
-})(Popup);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(Popup);
 
 /***/ }),
 
 /***/ 1586:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -16890,37 +16966,35 @@ function getCallSites(symbols, breakpoin
     if (breakpointId) {
       return bpLocationMap[breakpointId];
     }
   }
 
   return callSites.filter(({ location }) => location.start.line === location.end.line).map(callSite => _extends({}, callSite, { breakpoint: findBreakpoint(callSite) }));
 }
 
-const { addBreakpoint, removeBreakpoint } = _actions2.default;
-
-exports.default = (0, _reactRedux.connect)(state => {
+const mapStateToProps = state => {
   const selectedLocation = (0, _selectors.getSelectedLocation)(state);
   const selectedSource = (0, _selectors.getSelectedSource)(state);
   const sourceId = selectedLocation && selectedLocation.sourceId;
-  const source = selectedSource && selectedSource.toJS();
-
-  const symbols = (0, _selectors.getSymbols)(state, source);
+  const symbols = (0, _selectors.getSymbols)(state, selectedSource);
   const breakpoints = (0, _selectors.getBreakpointsForSource)(state, sourceId);
 
   return {
     selectedLocation,
     selectedSource,
     callSites: getCallSites(symbols, breakpoints),
     breakpoints: breakpoints
   };
-}, {
-  addBreakpoint,
-  removeBreakpoint
-})(CallSites);
+};
+
+const { addBreakpoint, removeBreakpoint } = _actions2.default;
+const mapDispatchToProps = { addBreakpoint, removeBreakpoint };
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(CallSites);
 
 /***/ }),
 
 /***/ 1592:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -17107,23 +17181,23 @@ class DebugLine extends _react.Component
   }
 
   render() {
     return null;
   }
 }
 
 exports.DebugLine = DebugLine;
-exports.default = (0, _reactRedux.connect)(state => {
-  return {
-    selectedFrame: (0, _selectors.getVisibleSelectedFrame)(state),
-    selectedSource: (0, _selectors.getSelectedSource)(state),
-    why: (0, _selectors.getPauseReason)(state)
-  };
-})(DebugLine);
+const mapStateToProps = state => ({
+  selectedFrame: (0, _selectors.getVisibleSelectedFrame)(state),
+  selectedSource: (0, _selectors.getSelectedSource)(state),
+  why: (0, _selectors.getPauseReason)(state)
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps)(DebugLine);
 
 /***/ }),
 
 /***/ 1594:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -17183,23 +17257,27 @@ class EmptyLines extends _react.Componen
     });
   }
 
   render() {
     return null;
   }
 }
 
-exports.default = (0, _reactRedux.connect)(state => {
+const mapStateToProps = state => {
   const selectedSource = (0, _selectors.getSelectedSource)(state);
+  const foundEmptyLines = (0, _selectors.getEmptyLines)(state, selectedSource.toJS());
+
   return {
     selectedSource,
-    emptyLines: selectedSource ? (0, _selectors.getEmptyLines)(state, selectedSource.toJS()) : []
-  };
-})(EmptyLines);
+    emptyLines: selectedSource ? foundEmptyLines : []
+  };
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps)(EmptyLines);
 
 /***/ }),
 
 /***/ 1595:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -17213,18 +17291,16 @@ var _extends = Object.assign || function
                                                                                                                                                                                                                                                                    * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 exports.gutterMenu = gutterMenu;
 
 var _react = __webpack_require__(0);
 
 var _devtoolsContextmenu = __webpack_require__(1413);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 var _editor = __webpack_require__(1358);
 
 var _selectors = __webpack_require__(3590);
 
 var _actions = __webpack_require__(1354);
 
@@ -17351,27 +17427,30 @@ class GutterContextMenuComponent extends
     gutterMenu(_extends({ event, sourceId, line, breakpoint }, props));
   }
 
   render() {
     return null;
   }
 }
 
-exports.default = (0, _reactRedux.connect)(state => {
+const mapStateToProps = state => {
   const selectedSource = (0, _selectors.getSelectedSource)(state);
+
   return {
     selectedLocation: (0, _selectors.getSelectedLocation)(state),
     selectedSource: selectedSource,
     breakpoints: (0, _selectors.getVisibleBreakpoints)(state),
     isPaused: (0, _selectors.isPaused)(state),
     contextMenu: (0, _selectors.getContextMenu)(state),
     emptyLines: (0, _selectors.getEmptyLines)(state, selectedSource.toJS())
   };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(GutterContextMenuComponent);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(GutterContextMenuComponent);
 
 /***/ }),
 
 /***/ 1596:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -17580,48 +17659,54 @@ class EditorMenu extends _react.Componen
     (0, _devtoolsContextmenu.showMenu)(event, getMenuItems(event, options));
   }
 
   render() {
     return null;
   }
 }
 
+const mapStateToProps = state => {
+  const selectedSource = (0, _selectors.getSelectedSource)(state);
+  const symbols = (0, _selectors.getSymbols)(state, selectedSource);
+
+  return {
+    selectedLocation: (0, _selectors.getSelectedLocation)(state),
+    selectedSource,
+    hasPrettyPrint: !!(0, _selectors.getPrettySource)(state, selectedSource.get("id")),
+    contextMenu: (0, _selectors.getContextMenu)(state),
+    getFunctionText: line => (0, _function.findFunctionText)(line, selectedSource.toJS(), symbols),
+    getFunctionLocation: line => (0, _ast.findClosestFunction)(symbols, {
+      line,
+      column: Infinity
+    })
+  };
+};
+
 const {
   addExpression,
   evaluateInConsole,
   flashLineRange,
   jumpToMappedLocation,
   setContextMenu,
   showSource,
   toggleBlackBox
 } = _actions2.default;
 
-exports.default = (0, _reactRedux.connect)(state => {
-  const selectedSource = (0, _selectors.getSelectedSource)(state);
-  return {
-    selectedLocation: (0, _selectors.getSelectedLocation)(state),
-    selectedSource,
-    hasPrettyPrint: !!(0, _selectors.getPrettySource)(state, selectedSource.get("id")),
-    contextMenu: (0, _selectors.getContextMenu)(state),
-    getFunctionText: line => (0, _function.findFunctionText)(line, selectedSource.toJS(), (0, _selectors.getSymbols)(state, selectedSource)),
-    getFunctionLocation: line => (0, _ast.findClosestFunction)((0, _selectors.getSymbols)(state, selectedSource), {
-      line,
-      column: Infinity
-    })
-  };
-}, {
+const mapDispatchToProps = {
   addExpression,
   evaluateInConsole,
   flashLineRange,
   jumpToMappedLocation,
   setContextMenu,
   showSource,
   toggleBlackBox
-})(EditorMenu);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(EditorMenu);
 
 /***/ }),
 
 /***/ 1597:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -17841,35 +17926,40 @@ class ConditionalPanel extends _react.Pu
     return null;
   }
 }
 
 exports.ConditionalPanel = ConditionalPanel; /* 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/>. */
 
+const mapStateToProps = state => {
+  const line = (0, _selectors.getConditionalPanelLine)(state);
+  const selectedLocation = (0, _selectors.getSelectedLocation)(state);
+
+  return {
+    selectedLocation,
+    breakpoint: (0, _selectors.getBreakpointForLine)(state, selectedLocation.sourceId, line),
+    line
+  };
+};
+
 const {
   setBreakpointCondition,
   openConditionalPanel,
   closeConditionalPanel
 } = _actions2.default;
 
-exports.default = (0, _reactRedux.connect)(state => {
-  const line = (0, _selectors.getConditionalPanelLine)(state);
-  const selectedLocation = (0, _selectors.getSelectedLocation)(state);
-  return {
-    selectedLocation,
-    breakpoint: (0, _selectors.getBreakpointForLine)(state, selectedLocation.sourceId, line),
-    line
-  };
-}, {
+const mapDispatchToProps = {
   setBreakpointCondition,
   openConditionalPanel,
   closeConditionalPanel
-})(ConditionalPanel);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(ConditionalPanel);
 
 /***/ }),
 
 /***/ 1599:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -17883,17 +17973,17 @@ var _propTypes = __webpack_require__(364
 var _propTypes2 = _interopRequireDefault(_propTypes);
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
-var _redux = __webpack_require__(3593);
+var _immutable = __webpack_require__(3594);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _selectors = __webpack_require__(3590);
 
 var _Svg = __webpack_require__(1359);
@@ -18014,16 +18104,22 @@ class SecondaryPanes extends _react.Comp
       },
       title: breakpointsDisabled ? L10N.getStr("breakpoints.enable") : L10N.getStr("breakpoints.disable")
     };
 
     return _react2.default.createElement("input", inputProps);
   }
 
   watchExpressionHeaderButtons() {
+    const { expressions } = this.props;
+
+    if (!expressions.size) {
+      return [];
+    }
+
     return [debugBtn(evt => {
       evt.stopPropagation();
       this.props.evaluateExpressions();
     }, "refresh", "refresh", L10N.getStr("watchExpressions.refreshButton")), debugBtn(evt => {
       evt.stopPropagation();
       this.setState({ showExpressionsInput: true });
     }, "plus", "plus", L10N.getStr("expressions.placeholder"))];
   }
@@ -18240,27 +18336,30 @@ class SecondaryPanes extends _react.Comp
     );
   }
 }
 
 SecondaryPanes.contextTypes = {
   shortcuts: _propTypes2.default.object
 };
 
-exports.default = (0, _reactRedux.connect)(state => ({
+const mapStateToProps = state => ({
+  expressions: (0, _selectors.getExpressions)(state),
   extra: (0, _selectors.getExtra)(state),
   hasFrames: !!(0, _selectors.getTopFrame)(state),
   breakpoints: (0, _selectors.getBreakpoints)(state),
   breakpointsDisabled: (0, _selectors.getBreakpointsDisabled)(state),
   breakpointsLoading: (0, _selectors.getBreakpointsLoading)(state),
   isWaitingOnBreak: (0, _selectors.getIsWaitingOnBreak)(state),
   shouldPauseOnExceptions: (0, _selectors.getShouldPauseOnExceptions)(state),
   shouldIgnoreCaughtExceptions: (0, _selectors.getShouldIgnoreCaughtExceptions)(state),
   workers: (0, _selectors.getWorkers)(state)
-}), dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(SecondaryPanes);
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(SecondaryPanes);
 
 /***/ }),
 
 /***/ 1600:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -18276,18 +18375,16 @@ var _extends = Object.assign || function
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _classnames = __webpack_require__(175);
 
 var _classnames2 = _interopRequireDefault(_classnames);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 var _immutable = __webpack_require__(3594);
 
 var I = _interopRequireWildcard(_immutable);
 
 var _reselect = __webpack_require__(993);
 
@@ -18467,20 +18564,22 @@ function updateLocation(sources, frame, 
   const locationId = (0, _breakpoint.makeLocationId)(bp.location);
   const localBP = _extends({}, bp, { locationId, isCurrentlyPaused, source });
 
   return localBP;
 }
 
 const _getBreakpoints = (0, _reselect.createSelector)(_selectors.getBreakpoints, _selectors.getSources, _selectors.getTopFrame, _selectors.getPauseReason, (breakpoints, sources, frame, why) => breakpoints.map(bp => updateLocation(sources, frame, why, bp)).filter(bp => bp.source && !bp.source.isBlackBoxed));
 
-exports.default = (0, _reactRedux.connect)((state, props) => ({
+const mapStateToProps = state => ({
   breakpoints: _getBreakpoints(state),
   selectedSource: (0, _selectors.getSelectedSource)(state)
-}), dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Breakpoints);
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Breakpoints);
 
 /***/ }),
 
 /***/ 1601:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -18755,20 +18854,22 @@ class Expressions extends _react.Compone
       "ul",
       { className: "pane expressions-list" },
       expressions.map(this.renderExpression),
       (showInput || !expressions.size) && this.renderNewExpressionInput()
     );
   }
 }
 
-exports.default = (0, _reactRedux.connect)(state => ({
+const mapStateToProps = state => ({
   expressions: (0, _selectors.getExpressions)(state),
   expressionError: (0, _selectors.getExpressionError)(state)
-}), _actions2.default)(Expressions);
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Expressions);
 
 /***/ }),
 
 /***/ 1602:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -18776,18 +18877,16 @@ exports.default = (0, _reactRedux.connec
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 var _Frame = __webpack_require__(1453);
 
 var _Frame2 = _interopRequireDefault(_Frame);
 
 var _Group = __webpack_require__(1603);
 
@@ -18806,19 +18905,21 @@ var _frames = __webpack_require__(3605);
 var _clipboard = __webpack_require__(1388);
 
 var _selectors = __webpack_require__(3590);
 
 __webpack_require__(1338);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-const NUM_FRAMES_SHOWN = 7; /* 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 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/>. */
+
+const NUM_FRAMES_SHOWN = 7;
 
 class Frames extends _react.Component {
 
   constructor(props) {
     super(props);
 
     this.toggleFramesDisplay = () => {
       this.setState(prevState => ({
@@ -18938,23 +19039,25 @@ class Frames extends _react.Component {
       { className: "pane frames" },
       this.renderFrames(frames),
       (0, _WhyPaused2.default)(why),
       this.renderToggleButton(frames)
     );
   }
 }
 
-exports.default = (0, _reactRedux.connect)(state => ({
+const mapStateToProps = state => ({
   frames: (0, _selectors.getCallStackFrames)(state),
   why: (0, _selectors.getPauseReason)(state),
   frameworkGroupingOn: (0, _selectors.getFrameworkGroupingState)(state),
   selectedFrame: (0, _selectors.getSelectedFrame)(state),
   pause: (0, _selectors.isPaused)(state)
-}), dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Frames);
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Frames);
 
 /***/ }),
 
 /***/ 1603:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -19210,18 +19313,16 @@ Object.defineProperty(exports, "__esModu
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* 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 _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _selectors = __webpack_require__(3590);
 
@@ -19300,29 +19401,31 @@ class EventListeners extends _react.Comp
     return _react2.default.createElement(
       "div",
       { className: "pane event-listeners" },
       listeners.map(this.renderListener)
     );
   }
 }
 
-exports.default = (0, _reactRedux.connect)(state => {
-  const listeners = (0, _selectors.getEventListeners)(state).map(l => {
-    return _extends({}, l, {
+const mapStateToProps = state => {
+  const listeners = (0, _selectors.getEventListeners)(state).map(listener => {
+    return _extends({}, listener, {
       breakpoint: (0, _selectors.getBreakpoint)(state, {
-        sourceId: l.sourceId,
-        line: l.line,
+        sourceId: listener.sourceId,
+        line: listener.line,
         column: null
       })
     });
   });
 
   return { listeners };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(EventListeners);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(EventListeners);
 
 /***/ }),
 
 /***/ 1606:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -19331,32 +19434,34 @@ Object.defineProperty(exports, "__esModu
   value: true
 });
 exports.Workers = undefined;
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 __webpack_require__(1340);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _selectors = __webpack_require__(3590);
 
 var _path = __webpack_require__(1393);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
+/* 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/>. */
+
 class Workers extends _react.PureComponent {
 
   renderWorkers(workers) {
     const { openWorkerToolbox } = this.props;
     return workers.map(worker => _react2.default.createElement(
       "div",
       {
         className: "worker",
@@ -19381,23 +19486,22 @@ class Workers extends _react.PureCompone
     return _react2.default.createElement(
       "div",
       { className: "pane workers-list" },
       workers && workers.size > 0 ? this.renderWorkers(workers) : this.renderNoWorkersPlaceholder()
     );
   }
 }
 
-exports.Workers = Workers; /* 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/>. */
-
-exports.default = (0, _reactRedux.connect)(state => {
-  return { workers: (0, _selectors.getWorkers)(state) };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Workers);
+exports.Workers = Workers;
+const mapStateToProps = state => ({
+  workers: (0, _selectors.getWorkers)(state)
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Workers);
 
 /***/ }),
 
 /***/ 1607:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -19501,18 +19605,16 @@ var _propTypes = __webpack_require__(364
 var _propTypes2 = _interopRequireDefault(_propTypes);
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
-var _redux = __webpack_require__(3593);
-
 var _classnames = __webpack_require__(175);
 
 var _classnames2 = _interopRequireDefault(_classnames);
 
 var _prefs = __webpack_require__(226);
 
 var _selectors = __webpack_require__(3590);
 
@@ -19525,22 +19627,20 @@ var _actions2 = _interopRequireDefault(_
 var _CommandBarButton = __webpack_require__(1764);
 
 __webpack_require__(1295);
 
 var _devtoolsModules = __webpack_require__(1376);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
-/* 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/>. */
-
-const { appinfo } = _devtoolsModules.Services;
+const { appinfo } = _devtoolsModules.Services; /* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* 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/>. */
 
 const isMacOS = appinfo.OS === "Darwin";
 
 const COMMANDS = ["resume", "stepOver", "stepIn", "stepOut"];
 
 const KEYS = {
   WINNT: {
     resume: "F8",
@@ -19766,26 +19866,26 @@ class CommandBar extends _react.Componen
     );
   }
 }
 
 CommandBar.contextTypes = {
   shortcuts: _propTypes2.default.object
 };
 
-exports.default = (0, _reactRedux.connect)(state => {
-  return {
-    isPaused: (0, _selectors.isPaused)(state),
-    history: (0, _selectors.getHistory)(state),
-    historyPosition: (0, _selectors.getHistoryPosition)(state),
-    isWaitingOnBreak: (0, _selectors.getIsWaitingOnBreak)(state),
-    canRewind: (0, _selectors.getCanRewind)(state),
-    skipPausing: (0, _selectors.getSkipPausing)(state)
-  };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(CommandBar);
+const mapStateToProps = state => ({
+  isPaused: (0, _selectors.isPaused)(state),
+  history: (0, _selectors.getHistory)(state),
+  historyPosition: (0, _selectors.getHistoryPosition)(state),
+  isWaitingOnBreak: (0, _selectors.getIsWaitingOnBreak)(state),
+  canRewind: (0, _selectors.getCanRewind)(state),
+  skipPausing: (0, _selectors.getSkipPausing)(state)
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(CommandBar);
 
 /***/ }),
 
 /***/ 1609:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -19962,18 +20062,16 @@ exports.default = UtilsBar;
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _firefox = __webpack_require__(1500);
 
@@ -19982,16 +20080,20 @@ var _selectors = __webpack_require__(359
 var _scopes = __webpack_require__(1792);
 
 var _devtoolsReps = __webpack_require__(3655);
 
 __webpack_require__(1296);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
+/* 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/>. */
+
 class Scopes extends _react.PureComponent {
   constructor(props, ...args) {
     const {
       why,
       selectedFrame,
       originalFrameScopes,
       generatedFrameScopes
     } = props;
@@ -20076,21 +20178,19 @@ class Scopes extends _react.PureComponen
       { className: "pane scopes-list" },
       _react2.default.createElement(
         "div",
         { className: "pane-info" },
         stateText
       )
     );
   }
-} /* 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/>. */
-
-exports.default = (0, _reactRedux.connect)(state => {
+}
+
+const mapStateToProps = state => {
   const selectedFrame = (0, _selectors.getSelectedFrame)(state);
   const selectedSource = (0, _selectors.getSelectedSource)(state);
 
   const {
     scope: originalFrameScopes,
     pending: originalPending
   } = (0, _selectors.getOriginalFrameScope)(state, selectedSource && selectedSource.get("id"), selectedFrame && selectedFrame.id) || { scope: null, pending: false };
 
@@ -20105,17 +20205,19 @@ exports.default = (0, _reactRedux.connec
   return {
     selectedFrame,
     isPaused: (0, _selectors.isPaused)(state),
     isLoading: generatedPending || originalPending,
     why: (0, _selectors.getPauseReason)(state),
     originalFrameScopes,
     generatedFrameScopes
   };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Scopes);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Scopes);
 
 /***/ }),
 
 /***/ 1613:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -20125,38 +20227,32 @@ Object.defineProperty(exports, "__esModu
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
-var _redux = __webpack_require__(3593);
-
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _selectors = __webpack_require__(3590);
 
 var _text = __webpack_require__(1387);
 
 var _PaneToggle = __webpack_require__(1407);
 
 var _PaneToggle2 = _interopRequireDefault(_PaneToggle);
 
 __webpack_require__(1343);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-/* 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/>. */
-
 class WelcomeBox extends _react.Component {
   renderToggleButton() {
     const { horizontal, endPanelCollapsed, togglePaneCollapse } = this.props;
     if (horizontal) {
       return;
     }
 
     return _react2.default.createElement(_PaneToggle2.default, {
@@ -20213,21 +20309,25 @@ class WelcomeBox extends _react.Componen
               searchProjectLabel
             )
           )
         ),
         this.renderToggleButton()
       )
     );
   }
-}
-
-exports.default = (0, _reactRedux.connect)(state => ({
+} /* 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/>. */
+
+const mapStateToProps = state => ({
   endPanelCollapsed: (0, _selectors.getPaneCollapse)(state, "end")
-}), dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(WelcomeBox);
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(WelcomeBox);
 
 /***/ }),
 
 /***/ 1614:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -20237,18 +20337,16 @@ Object.defineProperty(exports, "__esModu
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
-var _redux = __webpack_require__(3593);
-
 var _immutable = __webpack_require__(3594);
 
 var I = _interopRequireWildcard(_immutable);
 
 var _selectors = __webpack_require__(3590);
 
 var _ui = __webpack_require__(1439);
 
@@ -20275,20 +20373,16 @@ var _PaneToggle2 = _interopRequireDefaul
 var _Dropdown = __webpack_require__(1615);
 
 var _Dropdown2 = _interopRequireDefault(_Dropdown);
 
 function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-/* 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/>. */
-
 class Tabs extends _react.PureComponent {
 
   constructor(props) {
     super(props);
 
     this.updateHiddenTabs = () => {
       if (!this.refs.sourceTabs) {
         return;
@@ -20383,18 +20477,19 @@ class Tabs extends _react.PureComponent 
       return null;
     }
 
     const Panel = _react2.default.createElement(
       "ul",
       null,
       hiddenTabs.map(this.renderDropdownSource)
     );
-
-    return _react2.default.createElement(_Dropdown2.default, { panel: Panel, icon: "»" });
+    const icon = _react2.default.createElement("img", { className: "moreTabs" });
+
+    return _react2.default.createElement(_Dropdown2.default, { panel: Panel, icon: icon });
   }
 
   renderStartPanelToggleButton() {
     return _react2.default.createElement(_PaneToggle2.default, {
       position: "start",
       collapsed: !this.props.startPanelCollapsed,
       handleClick: this.props.togglePaneCollapse
     });
@@ -20419,24 +20514,26 @@ class Tabs extends _react.PureComponent 
       "div",
       { className: "source-header" },
       this.renderStartPanelToggleButton(),
       this.renderTabs(),
       this.renderDropdown(),
       this.renderEndPanelToggleButton()
     );
   }
-}
-
-exports.default = (0, _reactRedux.connect)(state => {
-  return {
-    selectedSource: (0, _selectors.getSelectedSource)(state),
-    tabSources: (0, _selectors.getSourcesForTabs)(state)
-  };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Tabs);
+} /* 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/>. */
+
+const mapStateToProps = state => ({
+  selectedSource: (0, _selectors.getSelectedSource)(state),
+  tabSources: (0, _selectors.getSourcesForTabs)(state)
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Tabs);
 
 /***/ }),
 
 /***/ 1615:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -25325,17 +25422,17 @@ function loadSourceMap(sourceId) {
     return urls.map(url => createOriginalSource(url, source, sourceMaps));
   };
 }
 
 // If a request has been made to show this source, go ahead and
 // select it.
 function checkSelectedSource(sourceId) {
   return async ({ dispatch, getState }) => {
-    const source = (0, _selectors.getSource)(getState(), sourceId).toJS();
+    const source = (0, _selectors.getSource)(getState(), sourceId);
 
     const pendingLocation = (0, _selectors.getPendingSelectedLocation)(getState());
 
     if (!pendingLocation || !pendingLocation.url || !source.url) {
       return;
     }
 
     const pendingUrl = pendingLocation.url;
@@ -25663,17 +25760,17 @@ function jumpToMappedLocation(location) 
   return async function ({ dispatch, getState, client, sourceMaps }) {
     if (!client) {
       return;
     }
 
     const source = (0, _selectors.getSource)(getState(), location.sourceId);
     let pairedLocation;
     if ((0, _devtoolsSourceMap.isOriginalId)(location.sourceId)) {
-      pairedLocation = await (0, _sourceMaps.getGeneratedLocation)(getState(), source.toJS(), location, sourceMaps);
+      pairedLocation = await (0, _sourceMaps.getGeneratedLocation)(getState(), source, location, sourceMaps);
     } else {
       pairedLocation = await sourceMaps.getOriginalLocation(location, source.toJS());
     }
 
     return dispatch(selectLocation(_extends({}, pairedLocation)));
   };
 }
 
@@ -26280,18 +26377,16 @@ var _react = __webpack_require__(0);
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
 var _classnames = __webpack_require__(175);
 
 var _classnames2 = _interopRequireDefault(_classnames);
 
-var _redux = __webpack_require__(3593);
-
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _editor = __webpack_require__(1358);
 
 var _projectSearch = __webpack_require__(2010);
 
@@ -26545,23 +26640,25 @@ class ProjectSearch extends _react.Compo
     );
   }
 }
 exports.ProjectSearch = ProjectSearch;
 ProjectSearch.contextTypes = {
   shortcuts: _propTypes2.default.object
 };
 
-exports.default = (0, _reactRedux.connect)(state => ({
+const mapStateToProps = state => ({
   sources: (0, _selectors.getSources)(state),
   activeSearch: (0, _selectors.getActiveSearch)(state),
   results: (0, _selectors.getTextSearchResults)(state),
   query: (0, _selectors.getTextSearchQuery)(state),
   status: (0, _selectors.getTextSearchStatus)(state)
-}), dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(ProjectSearch);
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(ProjectSearch);
 
 /***/ }),
 
 /***/ 2010:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -26630,18 +26727,16 @@ var _extends = Object.assign || function
                                                                                                                                                                                                                                                                    * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactRedux = __webpack_require__(3592);
 
-var _redux = __webpack_require__(3593);
-
 var _devtoolsContextmenu = __webpack_require__(1413);
 
 var _Close = __webpack_require__(1374);
 
 var _Close2 = _interopRequireDefault(_Close);
 
 var _actions = __webpack_require__(1354);
 
@@ -26799,26 +26894,29 @@ class Tab extends _react.PureComponent {
       ),
       _react2.default.createElement(_Close2.default, {
         handleClick: onClickClose,
         tooltip: L10N.getStr("sourceTabs.closeTabButtonTooltip")
       })
     );
   }
 }
-exports.default = (0, _reactRedux.connect)((state, props) => {
+
+const mapStateToProps = (state, { source }) => {
   const selectedSource = (0, _selectors.getSelectedSource)(state);
-  const { source } = props;
+
   return {
     tabSources: (0, _selectors.getSourcesForTabs)(state),
     selectedSource: selectedSource,
     sourceMetaData: (0, _selectors.getSourceMetaData)(state, source.id),
     activeSearch: (0, _selectors.getActiveSearch)(state)
   };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(Tab);
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(Tab);
 
 /***/ }),
 
 /***/ 22:
 /***/ (function(module, exports) {
 
 module.exports = __WEBPACK_EXTERNAL_MODULE_22__;
 
@@ -30847,39 +30945,36 @@ function formatPausePoints(text, pausePo
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _firefox = __webpack_require__(1500);
 
 var _selectors = __webpack_require__(3590);
 
 var _devtoolsReps = __webpack_require__(3655);
 
 var _preview = __webpack_require__(1807);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-/* 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/>. */
-
-const { createNode, getChildren } = _devtoolsReps.ObjectInspectorUtils.node;
+const { createNode, getChildren } = _devtoolsReps.ObjectInspectorUtils.node; /* 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/>. */
+
 const { loadItemProperties } = _devtoolsReps.ObjectInspectorUtils.loadProperties;
 
 class FrameworkComponent extends _react.PureComponent {
   async componentWillMount() {
     const expression = "this;";
     const { selectedFrame, setPopupObjectProperties } = this.props;
     const value = selectedFrame.this;
 
@@ -30932,22 +31027,22 @@ class FrameworkComponent extends _react.
     if (selectedFrame && (0, _preview.isReactComponent)(selectedFrame.this)) {
       return this.renderReactComponent();
     }
 
     return null;
   }
 }
 
-exports.default = (0, _reactRedux.connect)(state => {
-  return {
-    selectedFrame: (0, _selectors.getSelectedFrame)(state),
-    popupObjectProperties: (0, _selectors.getAllPopupObjectProperties)(state)
-  };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(FrameworkComponent);
+const mapStateToProps = state => ({
+  selectedFrame: (0, _selectors.getSelectedFrame)(state),
+  popupObjectProperties: (0, _selectors.getAllPopupObjectProperties)(state)
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(FrameworkComponent);
 
 /***/ }),
 
 /***/ 3625:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -31866,34 +31961,28 @@ module.exports = {
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
 var _react = __webpack_require__(0);
 
 var _react2 = _interopRequireDefault(_react);
 
-var _redux = __webpack_require__(3593);
-
 var _reactRedux = __webpack_require__(3592);
 
 var _actions = __webpack_require__(1354);
 
 var _actions2 = _interopRequireDefault(_actions);
 
 var _selectors = __webpack_require__(3590);
 
 __webpack_require__(1338);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-/* 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/>. */
-
 class ReactComponentStack extends _react.PureComponent {
   render() {
     const { componentStack } = this.props.extra.react;
     return _react2.default.createElement(
       "div",
       { className: "pane frames" },
       _react2.default.createElement(
         "ul",
@@ -31901,23 +31990,25 @@ class ReactComponentStack extends _react
         componentStack.slice().reverse().map((component, index) => _react2.default.createElement(
           "li",
           { key: index },
           component
         ))
       )
     );
   }
-}
-
-exports.default = (0, _reactRedux.connect)(state => {
-  return {
-    extra: (0, _selectors.getExtra)(state)
-  };
-}, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(ReactComponentStack);
+} /* 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/>. */
+
+const mapStateToProps = state => ({
+  extra: (0, _selectors.getExtra)(state)
+});
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, _actions2.default)(ReactComponentStack);
 
 /***/ }),
 
 /***/ 364:
 /***/ (function(module, exports) {
 
 module.exports = "<!-- 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/. --><svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 33 12\"><path id=\"base-path\" d=\"M27.1,0H1C0.4,0,0,0.4,0,1v10c0,0.6,0.4,1,1,1h26.1 c0.6,0,1.2-0.3,1.5-0.7L33,6l-4.4-5.3C28.2,0.3,27.7,0,27.1,0z\"></path></svg>"
 
@@ -33727,16 +33818,17 @@ function trimUrlQuery(url) {
 // Map suffix to content type.
 const contentMap = {
   js: "text/javascript",
   jsm: "text/javascript",
   mjs: "text/javascript",
   ts: "text/typescript",
   tsx: "text/typescript-jsx",
   jsx: "text/jsx",
+  vue: "text/vue",
   coffee: "text/coffeescript",
   elm: "text/elm",
   cljs: "text/x-clojure"
 };
 
 /**
  * Returns the content type for the specified URL.  If no specific
  * content type can be determined, "text/plain" is returned.
--- a/devtools/client/debugger/new/parser-worker.js
+++ b/devtools/client/debugger/new/parser-worker.js
@@ -24778,17 +24778,17 @@ function onEnter(node, ancestors, state)
 
   if (t.isBlockStatement(node)) {
     return addEmptyPoint(state, startLocation);
   }
 
   if (isReturn(node)) {
     // We do not want to pause at the return and the call e.g. return foo()
     if (isCall(node.argument)) {
-      return addStopPoint(state, startLocation);
+      return addEmptyPoint(state, startLocation);
     }
     return addStopPoint(state, startLocation);
   }
 
   if (isAssignment(node)) {
     // We only want to pause at literal assignments `var a = foo()`
     const value = node.right || node.init;
 
@@ -24917,23 +24917,22 @@ function getFirstExpression(ast) {
 
   return statements[0].expression;
 }
 
 function locationKey(start) {
   return `${start.line}:${start.column}`;
 }
 
-function getReplacements(ast, mappings) {
-  if (!mappings) {
-    return {};
-  }
-
+function mapOriginalExpression(expression, mappings) {
+  const ast = (0, _ast.parseScript)(expression);
   const scopes = (0, _getScopes.buildScopeList)(ast, "");
+
   const nodes = new Map();
+
   const replacements = new Map();
 
   // The ref-only global bindings are the ones that are accessed, but not
   // declared anywhere in the parsed code, meaning they are either global,
   // or declared somewhere in a scope outside the parsed code, so we
   // rewrite all of those specifically to avoid rewritting declarations that
   // shadow outer mappings.
   for (const name of Object.keys(scopes[0].bindings)) {
@@ -24956,55 +24955,34 @@ function getReplacements(ast, mappings) 
       if (typeof column !== "number") {
         column = 0;
       }
 
       replacements.set(locationKey({ line, column }), node);
     }
   }
 
-  return replacements;
-}
-
-function mapOriginalExpression(expression, mappings) {
-  const ast = (0, _ast.parseScript)(expression);
-  const replacements = getReplacements(ast, mappings);
-
-  let didUpdate = false;
+  if (replacements.size === 0) {
+    // Avoid the extra code generation work and also avoid potentially
+    // reformatting the user's code unnecessarily.
+    return expression;
+  }
+
   t.traverse(ast, (node, ancestors) => {
-    const parent = ancestors[ancestors.length - 1];
-    if (!parent) {
+    if (!t.isIdentifier(node) && !t.isThisExpression(node)) {
       return;
     }
-    const parentNode = parent.node;
-
-    if (replacements.size > 0 && (t.isIdentifier(node) || t.isThisExpression(node))) {
-      const replacement = replacements.get(locationKey(node.loc.start));
-      if (replacement) {
-        didUpdate = true;
-        replaceNode(ancestors, t.cloneNode(replacement));
-      }
-    }
-
-    if (t.isVariableDeclaration(node) && !t.isBlockStatement(parentNode)) {
-      const parts = node.declarations.map(({ id, init }) => {
-        if (init) {
-          return t.ifStatement(t.unaryExpression("!", t.callExpression(t.memberExpression(t.identifier("window"), t.identifier("hasOwnProperty")), [t.stringLiteral(id.name)])), t.expressionStatement(t.assignmentExpression("=", t.memberExpression(t.identifier("window"), id), init)));
-        }
-      });
-
-      didUpdate = true;
-      const lastAncestor = ancestors[ancestors.length - 1];
-      const { index } = lastAncestor;
-      parent.node[parent.key].splice(index, 1, ...parts);
-    }
-  });
-
-  const mappedExpression = didUpdate ? (0, _generator2.default)(ast).code : expression;
-  return mappedExpression;
+
+    const replacement = replacements.get(locationKey(node.loc.start));
+    if (replacement) {
+      replaceNode(ancestors, t.cloneNode(replacement));
+    }
+  });
+
+  return (0, _generator2.default)(ast).code;
 }
 
 /***/ }),
 
 /***/ 3646:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
@@ -25127,16 +25105,17 @@ function trimUrlQuery(url) {
 // Map suffix to content type.
 const contentMap = {
   js: "text/javascript",
   jsm: "text/javascript",
   mjs: "text/javascript",
   ts: "text/typescript",
   tsx: "text/typescript-jsx",
   jsx: "text/jsx",
+  vue: "text/vue",
   coffee: "text/coffeescript",
   elm: "text/elm",
   cljs: "text/x-clojure"
 };
 
 /**
  * Returns the content type for the specified URL.  If no specific
  * content type can be determined, "text/plain" is returned.
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions-error.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions-error.js
@@ -18,17 +18,21 @@ function getLabel(dbg, index) {
   return findElement(dbg, "expressionNode", index).innerText;
 }
 
 function getValue(dbg, index) {
   return findElement(dbg, "expressionValue", index).innerText;
 }
 
 async function addExpression(dbg, input) {
-  findElementWithSelector(dbg, expressionSelectors.plusIcon).click();
+  const plusIcon = findElementWithSelector(dbg, expressionSelectors.plusIcon);
+  if(plusIcon) {
+    plusIcon.click();
+  }
+  
   const evaluation = waitForDispatch(dbg, "EVALUATE_EXPRESSION");
   findElementWithSelector(dbg, expressionSelectors.input).focus();
   type(dbg, input);
   pressKey(dbg, "Enter");
   await evaluation;
 }
 
 add_task(async function() {
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
@@ -29,17 +29,21 @@ function assertEmptyValue(dbg, index) {
     return;
   }
 
   is(value, null);
 }
 
 async function addExpression(dbg, input) {
   info("Adding an expression");
-  findElementWithSelector(dbg, expressionSelectors.plusIcon).click();
+
+  const plusIcon = findElementWithSelector(dbg, expressionSelectors.plusIcon);
+  if(plusIcon) {
+    plusIcon.click();
+  }
   findElementWithSelector(dbg, expressionSelectors.input).focus();
   type(dbg, input);
   pressKey(dbg, "Enter");
 
   await waitForDispatch(dbg, "EVALUATE_EXPRESSION");
 }
 
 async function editExpression(dbg, input) {
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -79,21 +79,19 @@ class RequestListContextMenu {
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-as-curl",
       label: L10N.getStr("netmonitor.context.copyAsCurl"),
       accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
       // Menu item will be visible even if data hasn't arrived, so we need to check
       // *Available property and then fetch data lazily once user triggers the action.
-      visible: !!(selectedRequest &&
-        (requestHeadersAvailable || requestHeaders) &&
-        (responseContentAvailable || responseContent)),
+      visible: !!selectedRequest,
       click: () =>
-        this.copyAsCurl(id, url, method, httpVersion, requestHeaders, responseContent),
+        this.copyAsCurl(id, url, method, httpVersion, requestHeaders, requestPostData),
     });
 
     copySubmenu.push({
       type: "separator",
       visible: copySubmenu.slice(0, 4).some((subMenu) => subMenu.visible),
     });
 
     copySubmenu.push({
--- a/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
@@ -20,18 +20,23 @@ add_task(async function() {
   }
 
   // Header param is formatted as -H "Header: value" or -H 'Header: value'
   function header(h) {
     return "-H " + quote(h);
   }
 
   // Construct the expected command
+  const SIMPLE_BASE = [
+    "curl " + quote(SIMPLE_SJS)
+  ];
+  const SLOW_BASE = [
+    "curl " + quote(SLOW_SJS)
+  ];
   const BASE_RESULT = [
-    "curl " + quote(SIMPLE_SJS),
     "--compressed",
     header("User-Agent: " + navigator.userAgent),
     header("Accept: */*"),
     header("Accept-Language: " + navigator.language),
     header("X-Custom-Header-1: Custom value"),
     header("X-Custom-Header-2: 8.8.8.8"),
     header("X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"),
     header("Referer: " + CURL_URL),
@@ -51,66 +56,95 @@ add_task(async function() {
   ];
 
   const HEAD_PARTIAL_RESULT = [
     "-I"
   ];
 
   // GET request, no cookies (first request)
   await performRequest("GET");
-  await testClipboardContent(BASE_RESULT);
+  await testClipboardContent([
+    ...SIMPLE_BASE,
+    ...BASE_RESULT
+  ]);
+  // Check to make sure it is still OK after we view the response (bug#1452442)
+  await selectIndexAndWaitForSourceEditor(monitor, 0);
+  await testClipboardContent([
+    ...SIMPLE_BASE,
+    ...BASE_RESULT
+  ]);
 
   // GET request, cookies set by previous response
   await performRequest("GET");
   await testClipboardContent([
+    ...SIMPLE_BASE,
+    ...BASE_RESULT,
+    ...COOKIE_PARTIAL_RESULT
+  ]);
+
+  // Unfinished request (bug#1378464, bug#1420513)
+  let waitSlow = waitForNetworkEvents(monitor, 0);
+  await ContentTask.spawn(tab.linkedBrowser, SLOW_SJS, async function(url) {
+    content.wrappedJSObject.performRequest(url, "GET", null);
+  });
+  await waitSlow;
+  await testClipboardContent([
+    ...SLOW_BASE,
     ...BASE_RESULT,
     ...COOKIE_PARTIAL_RESULT
   ]);
 
   // POST request
   await performRequest("POST", POST_PAYLOAD);
   await testClipboardContent([
+    ...SIMPLE_BASE,
     ...BASE_RESULT,
     ...COOKIE_PARTIAL_RESULT,
     ...POST_PARTIAL_RESULT
   ]);
 
   // HEAD request
   await performRequest("HEAD");
   await testClipboardContent([
+    ...SIMPLE_BASE,
     ...BASE_RESULT,
     ...COOKIE_PARTIAL_RESULT,
     ...HEAD_PARTIAL_RESULT
   ]);
 
   await teardown(monitor);
 
   async function performRequest(method, payload) {
-    let wait = waitForNetworkEvents(monitor, 1);
+    let waitRequest = waitForNetworkEvents(monitor, 1);
     await ContentTask.spawn(tab.linkedBrowser, {
       url: SIMPLE_SJS,
       method_: method,
       payload_: payload
     }, async function({url, method_, payload_}) {
       content.wrappedJSObject.performRequest(url, method_, payload_);
     });
-    await wait;
+    await waitRequest;
   }
 
   async function testClipboardContent(expectedResult) {
     let { document } = monitor.panelWin;
 
     const items = document.querySelectorAll(".request-list-item");
     EventUtils.sendMouseEvent({ type: "mousedown" }, items[items.length - 1]);
     EventUtils.sendMouseEvent({ type: "contextmenu" },
       document.querySelectorAll(".request-list-item")[0]);
 
+    /* Ensure that the copy as cURL option is always visible */
+    let copyUrlParamsNode = monitor.panelWin.parent.document
+      .querySelector("#request-list-context-copy-as-curl");
+    is(!!copyUrlParamsNode, true,
+      "The \"Copy as cURL\" context menu item should not be hidden.");
+
     await waitForClipboardPromise(function setup() {
-      monitor.panelWin.parent.document
-        .querySelector("#request-list-context-copy-as-curl").click();
+      copyUrlParamsNode.click();
     }, function validate(result) {
       if (typeof result !== "string") {
         return false;
       }
 
       // Different setups may produce the same command, but with the
       // parameters in a different order in the commandline (which is fine).
       // Here we confirm that the commands are the same even in that case.
@@ -126,11 +160,11 @@ add_task(async function() {
         return false;
       }
 
       // Must match each of the params in the middle (headers and --compressed)
       return expectedResult.length === actual.length &&
         expectedResult.every(param => actual.includes(param));
     });
 
-    info("Clipboard contains a cURL command for the currently selected item's url.");
+    info("Clipboard contains a cURL command for item " + (items.length - 1));
   }
 });
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -79,16 +79,17 @@ const SIMPLE_UNSORTED_COOKIES_SJS = EXAM
 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const HTTPS_CONTENT_TYPE_SJS = HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
 const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs";
 const CORS_SJS_PATH = "/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs";
 const HSTS_SJS = EXAMPLE_URL + "sjs_hsts-test-server.sjs";
 const METHOD_SJS = EXAMPLE_URL + "sjs_method-test-server.sjs";
+const SLOW_SJS = EXAMPLE_URL + "sjs_slow-test-server.sjs";
 
 const HSTS_BASE_URL = EXAMPLE_URL;
 const HSTS_PAGE_URL = CUSTOM_GET_URL;
 
 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
 const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
 
 /* eslint-enable no-unused-vars, max-len */
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_slow-test-server.sjs
@@ -0,0 +1,17 @@
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+let timer;
+
+const DELAY_MS = 10000;
+function handleRequest(request, response) {
+  response.processAsync();
+  timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.init(() => {
+    response.setHeader("Content-Type", "text/html", false);
+    response.write("<body>Slow loading page for netmonitor test. You should never see this.</body>");
+    response.finish();
+  }, DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+}
--- a/devtools/client/shared/source-map/index.js
+++ b/devtools/client/shared/source-map/index.js
@@ -536,16 +536,17 @@ function trimUrlQuery(url) {
 // Map suffix to content type.
 const contentMap = {
   js: "text/javascript",
   jsm: "text/javascript",
   mjs: "text/javascript",
   ts: "text/typescript",
   tsx: "text/typescript-jsx",
   jsx: "text/jsx",
+  vue: "text/vue",
   coffee: "text/coffeescript",
   elm: "text/elm",
   cljs: "text/x-clojure"
 };
 
 /**
  * Returns the content type for the specified URL.  If no specific
  * content type can be determined, "text/plain" is returned.
--- a/devtools/client/shared/source-map/worker.js
+++ b/devtools/client/shared/source-map/worker.js
@@ -475,16 +475,17 @@ function trimUrlQuery(url) {
 // Map suffix to content type.
 const contentMap = {
   js: "text/javascript",
   jsm: "text/javascript",
   mjs: "text/javascript",
   ts: "text/typescript",
   tsx: "text/typescript-jsx",
   jsx: "text/jsx",
+  vue: "text/vue",
   coffee: "text/coffeescript",
   elm: "text/elm",
   cljs: "text/x-clojure"
 };
 
 /**
  * Returns the content type for the specified URL.  If no specific
  * content type can be determined, "text/plain" is returned.
--- a/dom/indexedDB/ActorsChild.cpp
+++ b/dom/indexedDB/ActorsChild.cpp
@@ -3527,17 +3527,16 @@ PreprocessHelper::ProcessCurrentStreamPa
     return;
   }
 
   RefPtr<JS::WasmModule> module =
     JS::DeserializeWasmModule(mCurrentBytecodeFileDesc,
                               mCurrentCompiledFileDesc,
                               Move(buildId),
                               nullptr,
-                              0,
                               0);
   if (NS_WARN_IF(!module)) {
     ContinueWithStatus(NS_ERROR_FAILURE);
     return;
   }
 
   mModuleSet.AppendElement(module);
   mStreamPairs.RemoveElementAt(0);
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -9793,17 +9793,16 @@ CheckWasmModule(FileHelper* aFileHelper,
   if (NS_WARN_IF(!ok)) {
     return NS_ERROR_FAILURE;
   }
 
   RefPtr<JS::WasmModule> module = JS::DeserializeWasmModule(bytecodeFileDesc,
                                                             nullptr,
                                                             Move(buildId),
                                                             nullptr,
-                                                            0,
                                                             0);
   if (NS_WARN_IF(!module)) {
     return NS_ERROR_FAILURE;
   }
 
   size_t compiledSize = module->compiledSerializedSize();
   UniquePtr<uint8_t[]> compiled(new (fallible) uint8_t[compiledSize]);
   if (NS_WARN_IF(!compiled)) {
--- a/gfx/thebes/gfxFontEntry.cpp
+++ b/gfx/thebes/gfxFontEntry.cpp
@@ -1998,17 +1998,20 @@ gfxFontFamily::CheckForLegacyFamilyNames
 {
     if (mCheckedForLegacyFamilyNames) {
         // we already did this, so there's nothing more to add
         return false;
     }
     mCheckedForLegacyFamilyNames = true;
     bool added = false;
     const uint32_t kNAME = TRUETYPE_TAG('n','a','m','e');
-    for (auto& fe : mAvailableFonts) {
+    // Make a local copy of the array of font faces, in case of changes
+    // during the iteration.
+    AutoTArray<RefPtr<gfxFontEntry>,8> faces(mAvailableFonts);
+    for (auto& fe : faces) {
         if (!fe) {
             continue;
         }
         gfxFontEntry::AutoTable nameTable(fe, kNAME);
         if (!nameTable) {
             continue;
         }
         nsAutoString legacyName;
--- a/js/src/jit-test/lib/wasm.js
+++ b/js/src/jit-test/lib/wasm.js
@@ -264,18 +264,27 @@ WasmHelpers.assertEqImpreciseStacks = (g
             if (observed[i] !== expect[i]) {
                 print(`On stack ${i}, Got:\n${observed[i]}\nExpect:\n${expect[i]}`);
                 assertEq(observed[i], expect[i]);
             }
         }
     }
 }
 
+WasmHelpers.extractStackFrameFunction = (frameString) => {
+    var [_, name, filename, line, column] = frameString.match(/^(.*)@(.*):(.*):(.*)$/);
+    if (name)
+        return name;
+    if (/wasm-function/.test(line))
+        return line;
+    return "";
+};
+
 WasmHelpers.assertStackTrace = (exception, expected) => {
-    let callsites = exception.stack.trim().split('\n').map(line => line.split('@')[0]);
+    let callsites = exception.stack.trim().split('\n').map(WasmHelpers.extractStackFrameFunction);
     assertEq(callsites.length, expected.length);
     for (let i = 0; i < callsites.length; i++) {
         assertEq(callsites[i], expected[i]);
     }
 };
 
 WasmHelpers.nextLineNumber = (n=1) => {
     return +(new Error().stack).split('\n')[1].split(':')[1] + n;
--- a/js/src/jit-test/tests/wasm/backtrace.js
+++ b/js/src/jit-test/tests/wasm/backtrace.js
@@ -4,17 +4,18 @@ var code = `(module
   (export "test" $t)
 )`;
 var mod = wasmEvalText(code, {
   env: {
     test: function() {
        // Expecting 3 lines in the backtrace (plus last empty).
        // The middle one is for the wasm function.
        var s = getBacktrace();
-       assertEq(s.split('\n').length, 4);
-       assertEq(s.split('\n')[1].startsWith("1 wasm-function[1]("), true);
+       var frames = s.split('\n');
+       assertEq(frames.length, 4);
+       assertEq(/> WebAssembly.Module":wasm-function\[1\]:0x/.test(frames[1]), true);
 
        // Let's also run DumpBacktrace() to check if we are not crashing.
        backtrace();
     }
   }
 }).exports;
 mod.test();
--- a/js/src/jit-test/tests/wasm/binary.js
+++ b/js/src/jit-test/tests/wasm/binary.js
@@ -1,10 +1,12 @@
 load(libdir + "wasm-binary.js");
 
+const { extractStackFrameFunction } = WasmHelpers;
+
 const Module = WebAssembly.Module;
 const CompileError = WebAssembly.CompileError;
 
 const magicError = /failed to match magic number/;
 const unknownSection = /expected custom section/;
 
 function sectionError(section) {
     return RegExp(`failed to start ${section} section`);
@@ -533,35 +535,34 @@ function runStackTraceTest(moduleName, f
         if (funcNames)
             subsections.push(funcNameSubsection(funcNames));
         sections.push(nameSection(subsections));
     }
     sections.push(customSection("yay", 13));
 
     var result = "";
     var callback = () => {
-        var prevFrameEntry = new Error().stack.split('\n')[1];
-        result = prevFrameEntry.split('@')[0];
+        result = extractStackFrameFunction(new Error().stack.split('\n')[1]);
     };
     wasmEval(moduleWithSections(sections), {"env": { callback }}).run();
     assertEq(result, expectedName);
 };
 
 runStackTraceTest(null, null, 'wasm-function[1]');
 runStackTraceTest(null, [{name:'blah'}, {name:'test'}], 'test');
 runStackTraceTest(null, [{name:'test', index:1}], 'test');
 runStackTraceTest(null, [{name:'blah'}, {name:'test', locals: [{name: 'var1'}, {name: 'var2'}]}], 'test');
 runStackTraceTest(null, [{name:'blah'}, {name:'test', locals: [{name: 'var1'}, {name: 'var2'}]}], 'test');
 runStackTraceTest(null, [{name:'blah'}, {name:'test1'}], 'test1');
 runStackTraceTest(null, [{name:'blah'}, {name:'test☃'}], 'test☃');
 runStackTraceTest(null, [{name:'blah'}, {name:'te\xE0\xFF'}], 'te\xE0\xFF');
 runStackTraceTest(null, [{name:'blah'}], 'wasm-function[1]');
 runStackTraceTest(null, [], 'wasm-function[1]');
 runStackTraceTest("", [{name:'blah'}, {name:'test'}], 'test');
-runStackTraceTest("a", [{name:'blah'}, {name:'test'}], 'test');
+runStackTraceTest("a", [{name:'blah'}, {name:'test'}], 'a.test');
 // Notice that invalid names section content shall not fail the parsing
 runStackTraceTest(null, [{name:'blah'}, {name:'test', index: 2}], 'wasm-function[1]'); // invalid index
 runStackTraceTest(null, [{name:'blah'}, {name:'test', index: 100000}], 'wasm-function[1]'); // invalid index
 runStackTraceTest(null, [{name:'blah'}, {name:'test', nameLen: 100}], 'wasm-function[1]'); // invalid name size
 runStackTraceTest(null, [{name:'blah'}, {name:''}], 'wasm-function[1]'); // empty name
 
 // Enable and disable Gecko profiling mode, to ensure all live instances
 // names won't make us crash.
--- a/js/src/jit-test/tests/wasm/errors.js
+++ b/js/src/jit-test/tests/wasm/errors.js
@@ -1,44 +1,46 @@
 load(libdir + "wasm-binary.js");
 
 const Module = WebAssembly.Module;
 const Instance = WebAssembly.Instance;
 const CompileError = WebAssembly.CompileError;
 const RuntimeError = WebAssembly.RuntimeError;
 
-function isWasmFunction(name) {
-    return /^wasm-function\[\d*\]$/.test(name)
+function getWasmFunctionIndex(line) {
+    return Number(line.match(/^wasm-function\[(\d*)\]$/)[1]);
+}
+
+function getWasmBytecode(column) {
+    return parseInt(column.match(/^0x([0-9a-f]*)$/)[1], 16);
 }
 
 function parseStack(stack) {
     var frames = stack.split('\n');
     assertEq(frames[frames.length-1], "");
     frames.length--;
     return frames.map(frame => {
-        var res = frame.match(/^(.*)@.*:(\d*):(\d*)$/);
+        var res = frame.match(/^(.*)@(.*):(.*):(.*)$/);
         assertEq(res !== null, true);
-        return {name: res[1], line: Number(res[2]), column: Number(res[3])};
+        return {name: res[1], url: res[2], line: res[3], column: res[4]};
     });
 }
 
 function testExn(opcode, binary, type, msg, exn) {
     assertEq(exn instanceof type, true);
     assertEq(msg.test(exn.message), true);
 
     var stack = parseStack(exn.stack);
     assertEq(stack.length > 1, true);
     var innermost = stack[0];
-    assertEq(isWasmFunction(innermost.name), true);
-    assertEq(innermost.line, exn.lineNumber);
-    assertEq(innermost.column, exn.columnNumber);
-
-    assertEq(exn.lineNumber > 0, true);
+    var funcIndex = getWasmFunctionIndex(innermost.line);
+    var bytecode = getWasmBytecode(innermost.column);
+    assertEq(exn.lineNumber, bytecode);
     assertEq(exn.columnNumber, 1);
-    assertEq(binary[exn.lineNumber], opcode);
+    assertEq(binary[bytecode], opcode);
 
     return {stack, binary};
 }
 
 function test(opcode, text, type, msg) {
     var binary = new Uint8Array(wasmTextToBinary(text));
     var exn;
     try {
@@ -145,23 +147,23 @@ var {stack, binary} = test(UnreachableCo
     (func $c call $b)
     (table anyfunc (elem $c))
     (func $d (call_indirect $v2v (i32.const 0)))
     (func $e call $d)
     (start $e)
 )`, RuntimeError, /unreachable executed/);
 const N = 5;
 assertEq(stack.length > N, true);
+assertEq(getWasmFunctionIndex(stack[0].line), 0);
 var lastLine = stack[0].line;
 for (var i = 1; i < N; i++) {
-    assertEq(isWasmFunction(stack[i].name), true);
+    assertEq(getWasmFunctionIndex(stack[i].line), i);
     assertEq(stack[i].line > lastLine, true);
     lastLine = stack[i].line;
-    assertEq(binary[stack[i].line], i == 3 ? CallIndirectCode : CallCode);
-    assertEq(stack[i].column, 1);
+    assertEq(binary[getWasmBytecode(stack[i].column)], i == 3 ? CallIndirectCode : CallCode);
 }
 
 function testCompileError(opcode, text) {
     var binary = new Uint8Array(wasmTextToBinary(text));
     var exn;
     try {
         new Instance(new Module(binary));
     } catch (e) {
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -7719,19 +7719,19 @@ JS_PUBLIC_API(bool)
 JS::CompiledWasmModuleAssumptionsMatch(PRFileDesc* compiled, JS::BuildIdCharVector&& buildId)
 {
     return wasm::CompiledModuleAssumptionsMatch(compiled, Move(buildId));
 }
 
 JS_PUBLIC_API(RefPtr<JS::WasmModule>)
 JS::DeserializeWasmModule(PRFileDesc* bytecode, PRFileDesc* maybeCompiled,
                           JS::BuildIdCharVector&& buildId, UniqueChars file,
-                          unsigned line, unsigned column)
-{
-    return wasm::DeserializeModule(bytecode, maybeCompiled, Move(buildId), Move(file), line, column);
+                          unsigned line)
+{
+    return wasm::DeserializeModule(bytecode, maybeCompiled, Move(buildId), Move(file), line);
 }
 
 JS_PUBLIC_API(void)
 JS::SetProcessLargeAllocationFailureCallback(JS::LargeAllocationFailureCallback lafc)
 {
     MOZ_ASSERT(!OnLargeAllocationFailure);
     OnLargeAllocationFailure = lafc;
 }
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -6256,17 +6256,17 @@ IsWasmModuleObject(HandleObject obj);
 extern JS_PUBLIC_API(RefPtr<WasmModule>)
 GetWasmModule(HandleObject obj);
 
 extern JS_PUBLIC_API(bool)
 CompiledWasmModuleAssumptionsMatch(PRFileDesc* compiled, BuildIdCharVector&& buildId);
 
 extern JS_PUBLIC_API(RefPtr<WasmModule>)
 DeserializeWasmModule(PRFileDesc* bytecode, PRFileDesc* maybeCompiled, BuildIdCharVector&& buildId,
-                      JS::UniqueChars filename, unsigned line, unsigned column);
+                      JS::UniqueChars filename, unsigned line);
 
 /**
  * Convenience class for imitating a JS level for-of loop. Typical usage:
  *
  *     ForOfIterator it(cx);
  *     if (!it.init(iterable))
  *       return false;
  *     RootedValue val(cx);
--- a/js/src/jsexn.cpp
+++ b/js/src/jsexn.cpp
@@ -464,20 +464,17 @@ Error(JSContext* cx, unsigned argc, Valu
 
     /* Set the 'lineNumber' property. */
     uint32_t lineNumber, columnNumber = 0;
     if (args.length() > 2) {
         if (!ToUint32(cx, args[2], &lineNumber))
             return false;
     } else {
         lineNumber = iter.done() ? 0 : iter.computeLine(&columnNumber);
-        // XXX: Make the column 1-based as in other browsers, instead of 0-based
-        // which is how SpiderMonkey stores it internally. This will be
-        // unnecessary once bug 1144340 is fixed.
-        ++columnNumber;
+        columnNumber = FixupColumnForDisplay(columnNumber);
     }
 
     RootedObject stack(cx);
     if (!CaptureStack(cx, &stack))
         return false;
 
     /*
      * ECMA ed. 3, 15.11.1 requires Error, etc., to construct even when
@@ -968,21 +965,19 @@ ErrorReport::populateUncaughtExceptionRe
     ownedReport.flags = JSREPORT_ERROR;
     ownedReport.errorNumber = JSMSG_UNCAUGHT_EXCEPTION;
     // XXXbz this assumes the stack we have right now is still
     // related to our exception object.  It would be better if we
     // could accept a passed-in stack of some sort instead.
     NonBuiltinFrameIter iter(cx, cx->compartment()->principals());
     if (!iter.done()) {
         ownedReport.filename = iter.filename();
-        ownedReport.lineno = iter.computeLine(&ownedReport.column);
-        // XXX: Make the column 1-based as in other browsers, instead of 0-based
-        // which is how SpiderMonkey stores it internally. This will be
-        // unnecessary once bug 1144340 is fixed.
-        ++ownedReport.column;
+        uint32_t column;
+        ownedReport.lineno = iter.computeLine(&column);
+        ownedReport.column = FixupColumnForDisplay(column);
         ownedReport.isMuted = iter.mutedErrors();
     }
 
     if (!ExpandErrorArgumentsVA(cx, GetErrorMessage, nullptr,
                                 JSMSG_UNCAUGHT_EXCEPTION,
                                 nullptr, ArgumentsAreUTF8, &ownedReport, ap)) {
         return false;
     }
--- a/js/src/jsfriendapi.cpp
+++ b/js/src/jsfriendapi.cpp
@@ -1047,33 +1047,32 @@ FormatFrame(JSContext* cx, const FrameIt
     MOZ_ASSERT(!cx->isExceptionPending());
     return buf;
 }
 
 static JS::UniqueChars
 FormatWasmFrame(JSContext* cx, const FrameIter& iter, JS::UniqueChars&& inBuf, int num)
 {
     UniqueChars nameStr;
-    if (JSAtom* functionDisplayAtom = iter.functionDisplayAtom()) {
+    if (JSAtom* functionDisplayAtom = iter.maybeFunctionDisplayAtom()) {
         nameStr = StringToNewUTF8CharsZ(cx, *functionDisplayAtom);
         if (!nameStr)
             return nullptr;
     }
 
     JS::UniqueChars buf = sprintf_append(cx, Move(inBuf), "%d %s()",
                                          num,
                                          nameStr ? nameStr.get() : "<wasm-function>");
     if (!buf)
         return nullptr;
 
-    const char* filename = iter.filename();
-    uint32_t lineno = iter.computeLine();
-    buf = sprintf_append(cx, Move(buf), " [\"%s\":%d]\n",
-                         filename ? filename : "<unknown>",
-                         lineno);
+    buf = sprintf_append(cx, Move(buf), " [\"%s\":wasm-function[%d]:0x%x]\n",
+                         iter.filename() ? iter.filename() : "<unknown>",
+                         iter.wasmFuncIndex(),
+                         iter.wasmBytecodeOffset());
     if (!buf)
         return nullptr;
 
     MOZ_ASSERT(!cx->isExceptionPending());
     return buf;
 }
 
 JS_FRIEND_API(JS::UniqueChars)
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -7093,18 +7093,18 @@ class DebuggerSourceGetURLMatcher
         MOZ_ASSERT(ss);
         if (ss->filename()) {
             JSString* str = NewStringCopyZ<CanGC>(cx_, ss->filename());
             return Some(str);
         }
         return Nothing();
     }
     ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
-        if (wasmInstance->instance().metadata().baseURL) {
-            JSString* str = NewStringCopyZ<CanGC>(cx_, wasmInstance->instance().metadata().baseURL.get());
+        if (wasmInstance->instance().metadata().filenameIsURL) {
+            JSString* str = NewStringCopyZ<CanGC>(cx_, wasmInstance->instance().metadata().filename.get());
             if (!str)
                 return Nothing();
             return Some(str);
         }
         if (JSString* str = wasmInstance->instance().debug().debugDisplayURL(cx_))
             return Some(str);
         return Nothing();
     }
--- a/js/src/vm/JSContext.cpp
+++ b/js/src/vm/JSContext.cpp
@@ -264,21 +264,19 @@ PopulateReportBlame(JSContext* cx, JSErr
      * Walk stack until we find a frame that is associated with a non-builtin
      * rather than a builtin frame and which we're allowed to know about.
      */
     NonBuiltinFrameIter iter(cx, compartment->principals());
     if (iter.done())
         return;
 
     report->filename = iter.filename();
-    report->lineno = iter.computeLine(&report->column);
-    // XXX: Make the column 1-based as in other browsers, instead of 0-based
-    // which is how SpiderMonkey stores it internally. This will be
-    // unnecessary once bug 1144340 is fixed.
-    report->column++;
+    uint32_t column;
+    report->lineno = iter.computeLine(&column);
+    report->column = FixupColumnForDisplay(column);
     report->isMuted = iter.mutedErrors();
 }
 
 /*
  * Since memory has been exhausted, avoid the normal error-handling path which
  * allocates an error object, report and callstack. If code is running, simply
  * throw the static atom "out of memory". If code is not running, call the
  * error reporter directly.
--- a/js/src/vm/JSScript.cpp
+++ b/js/src/vm/JSScript.cpp
@@ -2223,19 +2223,19 @@ ScriptSource::performXDR(XDRState<mode>*
     return Ok();
 }
 
 // Format and return a cx->zone()->pod_malloc'ed URL for a generated script like:
 //   {filename} line {lineno} > {introducer}
 // For example:
 //   foo.js line 7 > eval
 // indicating code compiled by the call to 'eval' on line 7 of foo.js.
-static char*
-FormatIntroducedFilename(JSContext* cx, const char* filename, unsigned lineno,
-                         const char* introducer)
+char*
+js::FormatIntroducedFilename(JSContext* cx, const char* filename, unsigned lineno,
+                             const char* introducer)
 {
     // Compute the length of the string in advance, so we can allocate a
     // buffer of the right size on the first shot.
     //
     // (JS_smprintf would be perfect, as that allocates the result
     // dynamically as it formats the string, but it won't allocate from cx,
     // and wants us to use a special free function.)
     char linenoBuf[15];
--- a/js/src/vm/JSScript.h
+++ b/js/src/vm/JSScript.h
@@ -2424,16 +2424,20 @@ struct ScriptAndCounts
         return scriptCounts.ionCounts_;
     }
 
     void trace(JSTracer* trc) {
         TraceRoot(trc, &script, "ScriptAndCounts::script");
     }
 };
 
+extern char*
+FormatIntroducedFilename(JSContext* cx, const char* filename, unsigned lineno,
+                         const char* introducer);
+
 struct GSNCache;
 
 jssrcnote*
 GetSrcNote(GSNCache& cache, JSScript* script, jsbytecode* pc);
 
 extern jssrcnote*
 GetSrcNote(JSContext* cx, JSScript* script, jsbytecode* pc);
 
--- a/js/src/vm/SavedFrame.h
+++ b/js/src/vm/SavedFrame.h
@@ -44,16 +44,21 @@ class SavedFrame : public NativeObject {
     JSAtom*       getSource();
     uint32_t      getLine();
     uint32_t      getColumn();
     JSAtom*       getFunctionDisplayName();
     JSAtom*       getAsyncCause();
     SavedFrame*   getParent() const;
     JSPrincipals* getPrincipals();
     bool          isSelfHosted(JSContext* cx);
+    bool          isWasm();
+
+    // When isWasm():
+    uint32_t      wasmFuncIndex();
+    uint32_t      wasmBytecodeOffset();
 
     // Iterator for use with C++11 range based for loops, eg:
     //
     //     RootedSavedFrame stack(cx, getSomeSavedFrameStack());
     //     for (HandleSavedFrame frame : SavedFrame::RootedRange(cx, stack)) {
     //         ...
     //     }
     //
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -571,16 +571,39 @@ SavedFrame::create(JSContext* cx)
 
 bool
 SavedFrame::isSelfHosted(JSContext* cx)
 {
     JSAtom* source = getSource();
     return source == cx->names().selfHosted;
 }
 
+bool
+SavedFrame::isWasm()
+{
+    // See WasmFrameIter::computeLine() comment.
+    return bool(getColumn() & wasm::WasmFrameIter::ColumnBit);
+}
+
+uint32_t
+SavedFrame::wasmFuncIndex()
+{
+    // See WasmFrameIter::computeLine() comment.
+    MOZ_ASSERT(isWasm());
+    return getColumn() & ~wasm::WasmFrameIter::ColumnBit;
+}
+
+uint32_t
+SavedFrame::wasmBytecodeOffset()
+{
+    // See WasmFrameIter::computeLine() comment.
+    MOZ_ASSERT(isWasm());
+    return getLine();
+}
+
 /* static */ bool
 SavedFrame::construct(JSContext* cx, unsigned argc, Value* vp)
 {
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
                               "SavedFrame");
     return false;
 }
 
@@ -979,34 +1002,64 @@ GetSavedFrameParent(JSContext* cx, Handl
     if (subsumedParent && !(subsumedParent->getAsyncCause() || skippedAsync))
         parentp.set(parent);
     else
         parentp.set(nullptr);
     return SavedFrameResult::Ok;
 }
 
 static bool
+FormatStackFrameLine(JSContext* cx, js::StringBuffer& sb, js::HandleSavedFrame frame)
+{
+    if (frame->isWasm()) {
+        // See comment in WasmFrameIter::computeLine().
+        return sb.append("wasm-function[")
+            && NumberValueToStringBuffer(cx, NumberValue(frame->wasmFuncIndex()), sb)
+            && sb.append(']');
+    }
+
+    return NumberValueToStringBuffer(cx, NumberValue(frame->getLine()), sb);
+}
+
+static bool
+FormatStackFrameColumn(JSContext* cx, js::StringBuffer& sb, js::HandleSavedFrame frame)
+{
+    if (frame->isWasm()) {
+        // See comment in WasmFrameIter::computeLine().
+        js::ToCStringBuf cbuf;
+        const char* cstr = NumberToCString(cx, &cbuf, frame->wasmBytecodeOffset(), 16);
+        if (!cstr)
+            return false;
+
+        return sb.append("0x")
+            && sb.append(cstr, strlen(cstr));
+    }
+
+    return NumberValueToStringBuffer(cx, NumberValue(frame->getColumn()), sb);
+}
+
+static bool
 FormatSpiderMonkeyStackFrame(JSContext* cx, js::StringBuffer& sb,
                              js::HandleSavedFrame frame, size_t indent,
                              bool skippedAsync)
 {
     RootedString asyncCause(cx, frame->getAsyncCause());
     if (!asyncCause && skippedAsync)
         asyncCause.set(cx->names().Async);
 
     js::RootedAtom name(cx, frame->getFunctionDisplayName());
     return (!indent || sb.appendN(' ', indent))
         && (!asyncCause || (sb.append(asyncCause) && sb.append('*')))
         && (!name || sb.append(name))
         && sb.append('@')
         && sb.append(frame->getSource())
         && sb.append(':')
-        && NumberValueToStringBuffer(cx, NumberValue(frame->getLine()), sb)
+        && FormatStackFrameLine(cx, sb, frame)
         && sb.append(':')
-        && NumberValueToStringBuffer(cx, NumberValue(frame->getColumn()), sb)
+        && FormatStackFrameColumn(cx, sb, frame)
         && sb.append('\n');
 }
 
 static bool
 FormatV8StackFrame(JSContext* cx, js::StringBuffer& sb,
                    js::HandleSavedFrame frame, size_t indent, bool lastFrame)
 {
     js::RootedAtom name(cx, frame->getFunctionDisplayName());
@@ -1014,19 +1067,19 @@ FormatV8StackFrame(JSContext* cx, js::St
         && sb.append('a')
         && sb.append('t')
         && sb.append(' ')
         && (!name || (sb.append(name) &&
                       sb.append(' ') &&
                       sb.append('(')))
         && sb.append(frame->getSource())
         && sb.append(':')
-        && NumberValueToStringBuffer(cx, NumberValue(frame->getLine()), sb)
+        && FormatStackFrameLine(cx, sb, frame)
         && sb.append(':')
-        && NumberValueToStringBuffer(cx, NumberValue(frame->getColumn()), sb)
+        && FormatStackFrameColumn(cx, sb, frame)
         && (!name || sb.append(')'))
         && (lastFrame || sb.append('\n'));
 }
 
 JS_PUBLIC_API(bool)
 BuildStackString(JSContext* cx, HandleObject stack, MutableHandleString stringp,
                  size_t indent, js::StackFormat format)
 {
@@ -1420,17 +1473,19 @@ SavedStacks::insertFrames(JSContext* cx,
         // We'll be pushing this frame onto stackChain. Gather the information
         // needed to construct the SavedFrame::Lookup.
         Rooted<LocationValue> location(cx);
         {
             AutoCompartmentUnchecked ac(cx, iter.compartment());
             if (!cx->compartment()->savedStacks().getLocation(cx, iter, &location))
                 return false;
         }
-        auto displayAtom = (iter.isWasm() || iter.isFunctionFrame()) ? iter.functionDisplayAtom() : nullptr;
+
+        RootedAtom displayAtom(cx, iter.maybeFunctionDisplayAtom());
+
         auto principals = iter.compartment()->principals();
         MOZ_ASSERT_IF(framePtr && !iter.isWasm(), iter.pc());
 
         if (!stackChain->emplaceBack(location.source(),
                                      location.line(),
                                      location.column(),
                                      displayAtom,
                                      nullptr, // asyncCause
@@ -1695,32 +1750,31 @@ SavedStacks::getLocation(JSContext* cx, 
     // compartment gets collected.
     assertSameCompartment(cx, this, iter.compartment());
 
     // When we have a |JSScript| for this frame, use a potentially memoized
     // location from our PCLocationMap and copy it into |locationp|. When we do
     // not have a |JSScript| for this frame (wasm frames), we take a slow path
     // that doesn't employ memoization, and update |locationp|'s slots directly.
 
-    if (!iter.hasScript()) {
+    if (iter.isWasm()) {
+        // Only asm.js has a displayURL.
         if (const char16_t* displayURL = iter.displayURL()) {
             locationp.setSource(AtomizeChars(cx, displayURL, js_strlen(displayURL)));
         } else {
             const char* filename = iter.filename() ? iter.filename() : "";
             locationp.setSource(Atomize(cx, filename, strlen(filename)));
         }
         if (!locationp.source())
             return false;
 
+        // See WasmFrameIter::computeLine() comment.
         uint32_t column = 0;
         locationp.setLine(iter.computeLine(&column));
-        // XXX: Make the column 1-based as in other browsers, instead of 0-based
-        // which is how SpiderMonkey stores it internally. This will be
-        // unnecessary once bug 1144340 is fixed.
-        locationp.setColumn(column + 1);
+        locationp.setColumn(column);
         return true;
     }
 
     RootedScript script(cx, iter.script());
     jsbytecode* pc = iter.pc();
 
     PCKey key(script, pc);
     PCLocationMap::AddPtr p = pcLocationMap.lookupForAdd(key);
@@ -1833,16 +1887,33 @@ BuildUTF8StackString(JSContext* cx, Hand
     RootedString stackStr(cx);
     if (!JS::BuildStackString(cx, stack, &stackStr))
         return UTF8CharsZ();
 
     char* chars = JS_EncodeStringToUTF8(cx, stackStr);
     return UTF8CharsZ(chars, strlen(chars));
 }
 
+uint32_t
+FixupColumnForDisplay(uint32_t column)
+{
+    // As described in WasmFrameIter::computeLine(), for wasm frames, the
+    // function index is returned as the column with the high bit set. In paths
+    // that format error stacks into strings, this information can be used to
+    // synthesize a proper wasm frame. But when raw column numbers are handed
+    // out, we just fix them to 1 to avoid confusion.
+    if (column & wasm::WasmFrameIter::ColumnBit)
+        return 1;
+
+    // XXX: Make the column 1-based as in other browsers, instead of 0-based
+    // which is how SpiderMonkey stores it internally. This will be
+    // unnecessary once bug 1144340 is fixed.
+    return column + 1;
+}
+
 } /* namespace js */
 
 namespace JS {
 namespace ubi {
 
 bool
 ConcreteStackFrame<SavedFrame>::isSystem() const
 {
--- a/js/src/vm/SavedStacks.h
+++ b/js/src/vm/SavedStacks.h
@@ -320,11 +320,14 @@ struct MutableWrappedPtrOperations<Saved
     SavedStacks::LocationValue& loc() {
         return static_cast<Wrapper*>(this)->get();
     }
 };
 
 UTF8CharsZ
 BuildUTF8StackString(JSContext* cx, HandleObject stack);
 
+uint32_t
+FixupColumnForDisplay(uint32_t column);
+
 } /* namespace js */
 
 #endif /* vm_SavedStacks_h */
--- a/js/src/vm/Stack.cpp
+++ b/js/src/vm/Stack.cpp
@@ -937,27 +937,28 @@ FrameIter::isFunctionFrame() const
         }
         MOZ_ASSERT(isWasm());
         return false;
     }
     MOZ_CRASH("Unexpected state");
 }
 
 JSAtom*
-FrameIter::functionDisplayAtom() const
+FrameIter::maybeFunctionDisplayAtom() const
 {
     switch (data_.state_) {
       case DONE:
         break;
       case INTERP:
       case JIT:
         if (isWasm())
             return wasmFrame().functionDisplayAtom();
-        MOZ_ASSERT(isFunctionFrame());
-        return calleeTemplate()->displayAtom();
+        if (isFunctionFrame())
+            return calleeTemplate()->displayAtom();
+        return nullptr;
     }
 
     MOZ_CRASH("Unexpected state");
 }
 
 ScriptSource*
 FrameIter::scriptSource() const
 {
@@ -1007,21 +1008,18 @@ FrameIter::displayURL() const
 unsigned
 FrameIter::computeLine(uint32_t* column) const
 {
     switch (data_.state_) {
       case DONE:
         break;
       case INTERP:
       case JIT:
-        if (isWasm()) {
-            if (column)
-                *column = 0;
-            return wasmFrame().lineOrBytecode();
-        }
+        if (isWasm())
+            return wasmFrame().computeLine(column);
         return PCToLineNumber(script(), pc(), column);
     }
 
     MOZ_CRASH("Unexpected state");
 }
 
 bool
 FrameIter::mutedErrors() const
--- a/js/src/vm/Stack.h
+++ b/js/src/vm/Stack.h
@@ -2066,27 +2066,28 @@ class FrameIter
     bool isEvalFrame() const;
     bool isFunctionFrame() const;
     bool hasArgs() const { return isFunctionFrame(); }
 
     ScriptSource* scriptSource() const;
     const char* filename() const;
     const char16_t* displayURL() const;
     unsigned computeLine(uint32_t* column = nullptr) const;
-    JSAtom* functionDisplayAtom() const;
+    JSAtom* maybeFunctionDisplayAtom() const;
     bool mutedErrors() const;
 
     bool hasScript() const { return !isWasm(); }
 
     // -----------------------------------------------------------
     //  The following functions can only be called when isWasm()
     // -----------------------------------------------------------
 
     inline bool wasmDebugEnabled() const;
     inline wasm::Instance* wasmInstance() const;
+    inline uint32_t wasmFuncIndex() const;
     inline unsigned wasmBytecodeOffset() const;
     void wasmUpdateBytecodeOffset();
 
     // -----------------------------------------------------------
     // The following functions can only be called when hasScript()
     // -----------------------------------------------------------
 
     inline JSScript* script() const;
@@ -2341,28 +2342,36 @@ FrameIter::wasmDebugEnabled() const
     MOZ_ASSERT(isWasm());
     return wasmFrame().debugEnabled();
 }
 
 inline wasm::Instance*
 FrameIter::wasmInstance() const
 {
     MOZ_ASSERT(!done());
-    MOZ_ASSERT(isWasm() && wasmDebugEnabled());
+    MOZ_ASSERT(isWasm());
     return wasmFrame().instance();
 }
 
 inline unsigned
 FrameIter::wasmBytecodeOffset() const
 {
     MOZ_ASSERT(!done());
     MOZ_ASSERT(isWasm());
     return wasmFrame().lineOrBytecode();
 }
 
+inline uint32_t
+FrameIter::wasmFuncIndex() const
+{
+    MOZ_ASSERT(!done());
+    MOZ_ASSERT(isWasm());
+    return wasmFrame().funcIndex();
+}
+
 inline bool
 FrameIter::isIon() const
 {
     return isJSJit() && jsJitFrame().isIonJS();
 }
 
 inline bool
 FrameIter::isBaseline() const
--- a/js/src/wasm/AsmJS.cpp
+++ b/js/src/wasm/AsmJS.cpp
@@ -357,17 +357,19 @@ struct js::AsmJSMetadata : Metadata, Asm
         return scriptSource.get()->mutedErrors();
     }
     const char16_t* displayURL() const override {
         return scriptSource.get()->hasDisplayURL() ? scriptSource.get()->displayURL() : nullptr;
     }
     ScriptSource* maybeScriptSource() const override {
         return scriptSource.get();
     }
-    bool getFuncName(const Bytes* maybeBytecode, uint32_t funcIndex, UTF8Bytes* name) const override {
+    bool getFuncName(NameContext ctx, const Bytes* maybeBytecode, uint32_t funcIndex,
+                     UTF8Bytes* name) const override
+    {
         const char* p = asmJSFuncNames[funcIndex].get();
         if (!p)
             return true;
         return name->append(p, strlen(p));
     }
 
     AsmJSMetadataCacheablePod& pod() { return *this; }
     const AsmJSMetadataCacheablePod& pod() const { return *this; }
@@ -2481,17 +2483,17 @@ class MOZ_STACK_CLASS ModuleValidator
 
         TokenPos pos;
         MOZ_ALWAYS_TRUE(tokenStream().peekTokenPos(&pos, TokenStreamShared::Operand));
         uint32_t endAfterCurly = pos.end;
         asmJSMetadata_->srcLengthWithRightBrace = endAfterCurly - asmJSMetadata_->srcStart;
 
         ScriptedCaller scriptedCaller;
         if (parser_.ss->filename()) {
-            scriptedCaller.line = scriptedCaller.column = 0;  // unused
+            scriptedCaller.line = 0;  // unused
             scriptedCaller.filename = DuplicateString(parser_.ss->filename());
             if (!scriptedCaller.filename)
                 return nullptr;
         }
 
         MutableCompileArgs args = cx_->new_<CompileArgs>();
         if (!args || !args->initFromContext(cx_, Move(scriptedCaller)))
             return nullptr;
--- a/js/src/wasm/WasmCode.cpp
+++ b/js/src/wasm/WasmCode.cpp
@@ -208,17 +208,17 @@ SendCodeRangesToProfiler(const ModuleSeg
     for (const CodeRange& codeRange : codeRanges) {
         if (!codeRange.hasFuncIndex())
             continue;
 
         uintptr_t start = uintptr_t(ms.base() + codeRange.begin());
         uintptr_t size = codeRange.end() - codeRange.begin();
 
         UTF8Bytes name;
-        if (!metadata.getFuncName(&bytecode, codeRange.funcIndex(), &name))
+        if (!metadata.getFuncNameStandalone(&bytecode, codeRange.funcIndex(), &name))
             return;
 
         // Avoid "unused" warnings
         (void)start;
         (void)size;
 
 #ifdef JS_ION_PERF
         if (PerfFuncEnabled()) {
@@ -883,60 +883,56 @@ Metadata::serializedSize() const
 {
     return sizeof(pod()) +
            SerializedVectorSize(sigIds) +
            SerializedPodVectorSize(globals) +
            SerializedPodVectorSize(tables) +
            SerializedPodVectorSize(funcNames) +
            SerializedPodVectorSize(customSections) +
            filename.serializedSize() +
-           baseURL.serializedSize() +
            sourceMapURL.serializedSize();
 }
 
 size_t
 Metadata::sizeOfExcludingThis(MallocSizeOf mallocSizeOf) const
 {
     return SizeOfVectorExcludingThis(sigIds, mallocSizeOf) +
            globals.sizeOfExcludingThis(mallocSizeOf) +
            tables.sizeOfExcludingThis(mallocSizeOf) +
            funcNames.sizeOfExcludingThis(mallocSizeOf) +
            customSections.sizeOfExcludingThis(mallocSizeOf) +
            filename.sizeOfExcludingThis(mallocSizeOf) +
-           baseURL.sizeOfExcludingThis(mallocSizeOf) +
            sourceMapURL.sizeOfExcludingThis(mallocSizeOf);
 }
 
 uint8_t*
 Metadata::serialize(uint8_t* cursor) const
 {
     MOZ_ASSERT(!debugEnabled && debugFuncArgTypes.empty() && debugFuncReturnTypes.empty());
     cursor = WriteBytes(cursor, &pod(), sizeof(pod()));
     cursor = SerializeVector(cursor, sigIds);
     cursor = SerializePodVector(cursor, globals);
     cursor = SerializePodVector(cursor, tables);
     cursor = SerializePodVector(cursor, funcNames);
     cursor = SerializePodVector(cursor, customSections);
     cursor = filename.serialize(cursor);
-    cursor = baseURL.serialize(cursor);
     cursor = sourceMapURL.serialize(cursor);
     return cursor;
 }
 
 /* static */ const uint8_t*
 Metadata::deserialize(const uint8_t* cursor)
 {
     (cursor = ReadBytes(cursor, &pod(), sizeof(pod()))) &&
     (cursor = DeserializeVector(cursor, &sigIds)) &&
     (cursor = DeserializePodVector(cursor, &globals)) &&
     (cursor = DeserializePodVector(cursor, &tables)) &&
     (cursor = DeserializePodVector(cursor, &funcNames)) &&
     (cursor = DeserializePodVector(cursor, &customSections)) &&
-    (cursor = filename.deserialize(cursor));
-    (cursor = baseURL.deserialize(cursor));
+    (cursor = filename.deserialize(cursor)) &&
     (cursor = sourceMapURL.deserialize(cursor));
     debugEnabled = false;
     debugFuncArgTypes.clear();
     debugFuncReturnTypes.clear();
     return cursor;
 }
 
 struct ProjectFuncIndex
@@ -962,41 +958,57 @@ MetadataTier::lookupFuncExport(uint32_t 
 }
 
 const FuncExport&
 MetadataTier::lookupFuncExport(uint32_t funcIndex, size_t* funcExportIndex) const
 {
     return const_cast<MetadataTier*>(this)->lookupFuncExport(funcIndex, funcExportIndex);
 }
 
-bool
-Metadata::getFuncName(const Bytes* maybeBytecode, uint32_t funcIndex, UTF8Bytes* name) const
+static bool
+AppendNameInBytecode(const Bytes* maybeBytecode, const NameInBytecode& name, UTF8Bytes* bytes)
 {
-    if (funcIndex < funcNames.length()) {
-        MOZ_ASSERT(maybeBytecode, "NameInBytecode requires preserved bytecode");
+    MOZ_RELEASE_ASSERT(name.offset <= maybeBytecode->length());
+    MOZ_RELEASE_ASSERT(name.length <= maybeBytecode->length() - name.offset);
+    return bytes->append((const char*)maybeBytecode->begin() + name.offset, name.length);
+}
 
-        const NameInBytecode& n = funcNames[funcIndex];
-        if (n.length != 0) {
-            MOZ_ASSERT(n.offset + n.length <= maybeBytecode->length());
-            return name->append((const char*)maybeBytecode->begin() + n.offset, n.length);
-        }
-    }
-
-    // For names that are out of range or invalid, synthesize a name.
-
+static bool
+AppendFunctionIndexName(uint32_t funcIndex, UTF8Bytes* bytes)
+{
     const char beforeFuncIndex[] = "wasm-function[";
     const char afterFuncIndex[] = "]";
 
     ToCStringBuf cbuf;
     const char* funcIndexStr = NumberToCString(nullptr, &cbuf, funcIndex);
     MOZ_ASSERT(funcIndexStr);
 
-    return name->append(beforeFuncIndex, strlen(beforeFuncIndex)) &&
-           name->append(funcIndexStr, strlen(funcIndexStr)) &&
-           name->append(afterFuncIndex, strlen(afterFuncIndex));
+    return bytes->append(beforeFuncIndex, strlen(beforeFuncIndex)) &&
+           bytes->append(funcIndexStr, strlen(funcIndexStr)) &&
+           bytes->append(afterFuncIndex, strlen(afterFuncIndex));
+}
+
+bool
+Metadata::getFuncName(NameContext ctx, const Bytes* maybeBytecode, uint32_t funcIndex,
+                      UTF8Bytes* name) const
+{
+    if (moduleName && moduleName->length != 0) {
+        if (!AppendNameInBytecode(maybeBytecode, *moduleName, name))
+            return false;
+        if (!name->append('.'))
+            return false;
+    }
+
+    if (funcIndex < funcNames.length() && funcNames[funcIndex].length != 0)
+        return AppendNameInBytecode(maybeBytecode, funcNames[funcIndex], name);
+
+    if (ctx == NameContext::BeforeLocation)
+        return true;
+
+    return AppendFunctionIndexName(funcIndex, name);
 }
 
 size_t
 CodeTier::serializedSize() const
 {
     return segment_->serializedSize() +
            metadata_->serializedSize();
 }
@@ -1288,17 +1300,17 @@ Code::ensureProfilingLabels(const Bytes*
         if (!codeRange.isFunction())
             continue;
 
         ToCStringBuf cbuf;
         const char* bytecodeStr = NumberToCString(nullptr, &cbuf, codeRange.funcLineOrBytecode());
         MOZ_ASSERT(bytecodeStr);
 
         UTF8Bytes name;
-        if (!metadata().getFuncName(maybeBytecode, codeRange.funcIndex(), &name))
+        if (!metadata().getFuncNameStandalone(maybeBytecode, codeRange.funcIndex(), &name))
             return;
         if (!name.append(" (", 2))
             return;
 
         if (const char* filename = metadata().filename.get()) {
             if (!name.append(filename, strlen(filename)))
                 return;
         } else {
--- a/js/src/wasm/WasmCode.h
+++ b/js/src/wasm/WasmCode.h
@@ -400,37 +400,39 @@ struct MetadataCacheablePod
 {
     ModuleKind            kind;
     MemoryUsage           memoryUsage;
     HasGcTypes            temporaryHasGcTypes;
     uint32_t              minMemoryLength;
     uint32_t              globalDataLength;
     Maybe<uint32_t>       maxMemoryLength;
     Maybe<uint32_t>       startFuncIndex;
+    Maybe<NameInBytecode> moduleName;
+    bool                  filenameIsURL;
 
     explicit MetadataCacheablePod(ModuleKind kind)
       : kind(kind),
         memoryUsage(MemoryUsage::None),
         temporaryHasGcTypes(HasGcTypes::False),
         minMemoryLength(0),
-        globalDataLength(0)
+        globalDataLength(0),
+        filenameIsURL(false)
     {}
 };
 
 typedef uint8_t ModuleHash[8];
 
 struct Metadata : public ShareableBase<Metadata>, public MetadataCacheablePod
 {
     SigWithIdVector       sigIds;
     GlobalDescVector      globals;
     TableDescVector       tables;
     NameInBytecodeVector  funcNames;
     CustomSectionVector   customSections;
     CacheableChars        filename;
-    CacheableChars        baseURL;
     CacheableChars        sourceMapURL;
 
     // Debug-enabled code is not serialized.
     bool                  debugEnabled;
     FuncArgTypesVector    debugFuncArgTypes;
     FuncReturnTypesVector debugFuncReturnTypes;
     ModuleHash            debugHash;
 
@@ -463,17 +465,33 @@ struct Metadata : public ShareableBase<M
         return false;
     }
     virtual const char16_t* displayURL() const {
         return nullptr;
     }
     virtual ScriptSource* maybeScriptSource() const {
         return nullptr;
     }
-    virtual bool getFuncName(const Bytes* maybeBytecode, uint32_t funcIndex, UTF8Bytes* name) const;
+
+    // The Developer-Facing Display Conventions section of the WebAssembly Web
+    // API spec defines two cases for displaying a wasm function name:
+    //  1. the function name stands alone
+    //  2. the function name precedes the location
+
+    enum NameContext { Standalone, BeforeLocation };
+
+    virtual bool getFuncName(NameContext ctx, const Bytes* maybeBytecode, uint32_t funcIndex,
+                             UTF8Bytes* name) const;
+
+    bool getFuncNameStandalone(const Bytes* maybeBytecode, uint32_t funcIndex, UTF8Bytes* name) const {
+        return getFuncName(NameContext::Standalone, maybeBytecode, funcIndex, name);
+    }
+    bool getFuncNameBeforeLocation(const Bytes* maybeBytecode, uint32_t funcIndex, UTF8Bytes* name) const {
+        return getFuncName(NameContext::BeforeLocation, maybeBytecode, funcIndex, name);
+    }
 
     WASM_DECLARE_SERIALIZABLE_VIRTUAL(Metadata);
 };
 
 typedef RefPtr<Metadata> MutableMetadata;
 typedef RefPtr<const Metadata> SharedMetadata;
 
 struct MetadataTier
--- a/js/src/wasm/WasmCompile.h
+++ b/js/src/wasm/WasmCompile.h
@@ -24,33 +24,29 @@
 namespace js {
 namespace wasm {
 
 // Describes the JS scripted caller of a request to compile a wasm module.
 
 struct ScriptedCaller
 {
     UniqueChars filename;
+    bool filenameIsURL;
     unsigned line;
-    unsigned column;
-};
 
-struct ResponseURLs
-{
-    UniqueChars baseURL;
-    UniqueChars sourceMapURL;
+    ScriptedCaller() : filenameIsURL(false), line(0) {}
 };
 
 // Describes all the parameters that control wasm compilation.
 
 struct CompileArgs : ShareableBase<CompileArgs>
 {
     Assumptions assumptions;
     ScriptedCaller scriptedCaller;
-    ResponseURLs responseURLs;
+    UniqueChars sourceMapURL;
     bool baselineEnabled;
     bool debugEnabled;
     bool ionEnabled;
     bool sharedMemoryEnabled;
     HasGcTypes gcTypesEnabled;
     bool testTiering;
 
     CompileArgs(Assumptions&& assumptions, ScriptedCaller&& scriptedCaller)
--- a/js/src/wasm/WasmFrameIter.cpp
+++ b/js/src/wasm/WasmFrameIter.cpp
@@ -182,32 +182,66 @@ WasmFrameIter::mutedErrors() const
 }
 
 JSAtom*
 WasmFrameIter::functionDisplayAtom() const
 {
     MOZ_ASSERT(!done());
 
     JSContext* cx = activation_->cx();
-    JSAtom* atom = instance()->getFuncAtom(cx, codeRange_->funcIndex());
+    JSAtom* atom = instance()->getFuncDisplayAtom(cx, codeRange_->funcIndex());
     if (!atom) {
         cx->clearPendingException();
         return cx->names().empty;
     }
 
     return atom;
 }
 
 unsigned
 WasmFrameIter::lineOrBytecode() const
 {
     MOZ_ASSERT(!done());
     return lineOrBytecode_;
 }
 
+uint32_t
+WasmFrameIter::funcIndex() const
+{
+    MOZ_ASSERT(!done());
+    return codeRange_->funcIndex();
+}
+
+unsigned
+WasmFrameIter::computeLine(uint32_t* column) const
+{
+    if (instance()->isAsmJS()) {
+        if (column)
+            *column = 1;
+        return lineOrBytecode_;
+    }
+
+    // As a terrible hack to avoid changing the tons of places that pass around
+    // (url, line, column) tuples to instead passing around a Variant that
+    // stores a (url, func-index, bytecode-offset) tuple for wasm frames,
+    // wasm stuffs its tuple into the existing (url, line, column) tuple,
+    // tagging the high bit of the column to indicate "this is a wasm frame".
+    // When knowing clients see this bit, they shall render the tuple
+    // (url, line, column|bit) as "url:wasm-function[column]:0xline" according
+    // to the WebAssembly Web API's Developer-Facing Display Conventions.
+    //   https://webassembly.github.io/spec/web-api/index.html#conventions
+    // The wasm bytecode offset continues to be passed as the JS line to avoid
+    // breaking existing devtools code written when this used to be the case.
+
+    MOZ_ASSERT(!(codeRange_->funcIndex() & ColumnBit));
+    if (column)
+        *column = codeRange_->funcIndex() | ColumnBit;
+    return lineOrBytecode_;
+}
+
 Instance*
 WasmFrameIter::instance() const
 {
     MOZ_ASSERT(!done());
     return fp_->tls->instance;
 }
 
 void**
--- a/js/src/wasm/WasmFrameIter.h
+++ b/js/src/wasm/WasmFrameIter.h
@@ -49,16 +49,17 @@ struct CallableOffsets;
 //
 // If you want to handle every kind of frames (including JS jit frames), use
 // JitFrameIter.
 
 class WasmFrameIter
 {
   public:
     enum class Unwind { True, False };
+    static constexpr uint32_t ColumnBit = 1u << 31;
 
   private:
     jit::JitActivation* activation_;
     const Code* code_;
     const CodeRange* codeRange_;
     unsigned lineOrBytecode_;
     Frame* fp_;
     uint8_t* unwoundIonCallerFP_;
@@ -74,16 +75,18 @@ class WasmFrameIter
     void setUnwind(Unwind unwind) { unwind_ = unwind; }
     void operator++();
     bool done() const;
     const char* filename() const;
     const char16_t* displayURL() const;
     bool mutedErrors() const;
     JSAtom* functionDisplayAtom() const;
     unsigned lineOrBytecode() const;
+    uint32_t funcIndex() const;
+    unsigned computeLine(uint32_t* column) const;
     const CodeRange* codeRange() const { return codeRange_; }
     Instance* instance() const;
     void** unwoundAddressOfReturnAddress() const;
     bool debugEnabled() const;
     DebugFrame* debugFrame() const;
     uint8_t* unwoundIonCallerFP() const { return unwoundIonCallerFP_; }
 };
 
--- a/js/src/wasm/WasmGenerator.cpp
+++ b/js/src/wasm/WasmGenerator.cpp
@@ -164,26 +164,24 @@ ModuleGenerator::init(Metadata* maybeAsm
         if (!metadata_)
             return false;
     }
 
     if (compileArgs_->scriptedCaller.filename) {
         metadata_->filename = DuplicateString(compileArgs_->scriptedCaller.filename.get());
         if (!metadata_->filename)
             return false;
+
+        metadata_->filenameIsURL = compileArgs_->scriptedCaller.filenameIsURL;
+    } else {
+        MOZ_ASSERT(!compileArgs_->scriptedCaller.filenameIsURL);
     }
 
-    if (compileArgs_->responseURLs.baseURL) {
-        metadata_->baseURL = DuplicateString(compileArgs_->responseURLs.baseURL.get());
-        if (!metadata_->baseURL)
-            return false;
-    }
-
-    if (compileArgs_->responseURLs.sourceMapURL) {
-        metadata_->sourceMapURL = DuplicateString(compileArgs_->responseURLs.sourceMapURL.get());
+    if (compileArgs_->sourceMapURL) {
+        metadata_->sourceMapURL = DuplicateString(compileArgs_->sourceMapURL.get());
         if (!metadata_->sourceMapURL)
             return false;
     }
 
     linkDataTier_ = js::MakeUnique<LinkDataTier>(tier());
     if (!linkDataTier_)
         return false;
 
@@ -839,16 +837,17 @@ ModuleGenerator::finishMetadata(const Sh
 
     // Copy over data from the ModuleEnvironment.
 
     metadata_->memoryUsage = env_->memoryUsage;
     metadata_->temporaryHasGcTypes = env_->gcTypesEnabled;
     metadata_->minMemoryLength = env_->minMemoryLength;
     metadata_->maxMemoryLength = env_->maxMemoryLength;
     metadata_->startFuncIndex = env_->startFuncIndex;
+    metadata_->moduleName = env_->moduleName;
     metadata_->tables = Move(env_->tables);
     metadata_->globals = Move(env_->globals);
     metadata_->funcNames = Move(env_->funcNames);
     metadata_->customSections = Move(env_->customSections);
 
     // Inflate the global bytes up to page size so that the total bytes are a
     // page size (as required by the allocator functions).
 
--- a/js/src/wasm/WasmInstance.cpp
+++ b/js/src/wasm/WasmInstance.cpp
@@ -938,27 +938,23 @@ Instance::callExport(JSContext* cx, uint
     if (expectsObject)
         args.rval().set(ObjectOrNullValue(retObj));
     else if (retObj)
         args.rval().set(ObjectValue(*retObj));
 
     return true;
 }
 
-bool
-Instance::getFuncName(uint32_t funcIndex, UTF8Bytes* name) const
+JSAtom*
+Instance::getFuncDisplayAtom(JSContext* cx, uint32_t funcIndex) const
 {
-    return metadata().getFuncName(debug_->maybeBytecode(), funcIndex, name);
-}
-
-JSAtom*
-Instance::getFuncAtom(JSContext* cx, uint32_t funcIndex) const
-{
+    // The "display name" of a function is primarily shown in Error.stack which
+    // also includes location, so use getFuncNameBeforeLocation.
     UTF8Bytes name;
-    if (!getFuncName(funcIndex, &name))
+    if (!metadata().getFuncNameBeforeLocation(debug_->maybeBytecode(), funcIndex, &name))
         return nullptr;
 
     return AtomizeUTF8Chars(cx, name.begin(), name.length());
 }
 
 void
 Instance::ensureProfilingLabels(bool profilingEnabled) const
 {
--- a/js/src/wasm/WasmInstance.h
+++ b/js/src/wasm/WasmInstance.h
@@ -120,18 +120,17 @@ class Instance
     // Execute the given export given the JS call arguments, storing the return
     // value in args.rval.
 
     MOZ_MUST_USE bool callExport(JSContext* cx, uint32_t funcIndex, CallArgs args);
 
     // Return the name associated with a given function index, or generate one
     // if none was given by the module.
 
-    bool getFuncName(uint32_t funcIndex, UTF8Bytes* name) const;
-    JSAtom* getFuncAtom(JSContext* cx, uint32_t funcIndex) const;
+    JSAtom* getFuncDisplayAtom(JSContext* cx, uint32_t funcIndex) const;
     void ensureProfilingLabels(bool profilingEnabled) const;
 
     // Initially, calls to imports in wasm code call out through the generic
     // callImport method. If the imported callee gets JIT compiled and the types
     // match up, callImport will patch the code to instead call through a thunk
     // directly into the JIT code. If the JIT code is released, the Instance must
     // be notified so it can go back to the generic callImport.
 
--- a/js/src/wasm/WasmJS.cpp
+++ b/js/src/wasm/WasmJS.cpp
@@ -287,36 +287,36 @@ GetImports(JSContext* cx,
         }
     }
 
     MOZ_ASSERT(globalIndex == globals.length() || !globals[globalIndex].isImport());
 
     return true;
 }
 
-// ============================================================================
-// Fuzzing support
-
 static bool
-DescribeScriptedCaller(JSContext* cx, ScriptedCaller* scriptedCaller)
+DescribeScriptedCaller(JSContext* cx, ScriptedCaller* caller, const char* introducer)
 {
     // Note: JS::DescribeScriptedCaller returns whether a scripted caller was
     // found, not whether an error was thrown. This wrapper function converts
     // back to the more ordinary false-if-error form.
 
     JS::AutoFilename af;
-    if (JS::DescribeScriptedCaller(cx, &af, &scriptedCaller->line, &scriptedCaller->column)) {
-        scriptedCaller->filename = DuplicateString(cx, af.get());
-        if (!scriptedCaller->filename)
+    if (JS::DescribeScriptedCaller(cx, &af, &caller->line)) {
+        caller->filename.reset(FormatIntroducedFilename(cx, af.get(), caller->line, introducer));
+        if (!caller->filename)
             return false;
     }
 
     return true;
 }
 
+// ============================================================================
+// Fuzzing support
+
 bool
 wasm::Eval(JSContext* cx, Handle<TypedArrayObject*> code, HandleObject importObj,
            MutableHandleWasmInstanceObject instanceObj)
 {
     if (!GlobalObject::ensureConstructor(cx, cx->global(), JSProto_WebAssembly))
         return false;
 
     MutableBytes bytecode = cx->new_<ShareableBytes>();
@@ -324,17 +324,17 @@ wasm::Eval(JSContext* cx, Handle<TypedAr
         return false;
 
     if (!bytecode->append((uint8_t*)code->viewDataEither().unwrap(), code->byteLength())) {
         ReportOutOfMemory(cx);
         return false;
     }
 
     ScriptedCaller scriptedCaller;
-    if (!DescribeScriptedCaller(cx, &scriptedCaller))
+    if (!DescribeScriptedCaller(cx, &scriptedCaller, "wasm_eval"))
         return false;
 
     MutableCompileArgs compileArgs = cx->new_<CompileArgs>();
     if (!compileArgs || !compileArgs->initFromContext(cx, Move(scriptedCaller)))
         return false;
 
     UniqueChars error;
     UniqueCharsVector warnings;
@@ -851,20 +851,20 @@ GetBufferSource(JSContext* cx, JSObject*
         ReportOutOfMemory(cx);
         return false;
     }
 
     return true;
 }
 
 static MutableCompileArgs
-InitCompileArgs(JSContext* cx)
+InitCompileArgs(JSContext* cx, const char* introducer)
 {
     ScriptedCaller scriptedCaller;
-    if (!DescribeScriptedCaller(cx, &scriptedCaller))
+    if (!DescribeScriptedCaller(cx, &scriptedCaller, introducer))
         return nullptr;
 
     MutableCompileArgs compileArgs = cx->new_<CompileArgs>();
     if (!compileArgs)
         return nullptr;
 
     if (!compileArgs->initFromContext(cx, Move(scriptedCaller)))
         return nullptr;
@@ -908,17 +908,17 @@ WasmModuleObject::construct(JSContext* c
         JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_BUF_ARG);
         return false;
     }
 
     MutableBytes bytecode;
     if (!GetBufferSource(cx, &callArgs[0].toObject(), JSMSG_WASM_BAD_BUF_ARG, &bytecode))
         return false;
 
-    SharedCompileArgs compileArgs = InitCompileArgs(cx);
+    SharedCompileArgs compileArgs = InitCompileArgs(cx, "WebAssembly.Module");
     if (!compileArgs)
         return false;
 
     UniqueChars error;
     UniqueCharsVector warnings;
     SharedModule module = CompileBuffer(*compileArgs, *bytecode, &error, &warnings);
     if (!module) {
         if (error) {
@@ -1300,17 +1300,17 @@ WasmInstanceObject::getExportedFunction(
         return false;
 
     const Sig& sig = funcExport.sig();
     unsigned numArgs = sig.args().length();
 
     if (instance.isAsmJS()) {
         // asm.js needs to act like a normal JS function which means having the
         // name from the original source and being callable as a constructor.
-        RootedAtom name(cx, instance.getFuncAtom(cx, funcIndex));
+        RootedAtom name(cx, instance.getFuncDisplayAtom(cx, funcIndex));
         if (!name)
             return false;
         fun.set(NewNativeConstructor(cx, WasmCall, numArgs, name, gc::AllocKind::FUNCTION_EXTENDED,
                                      SingletonObject, JSFunction::ASMJS_CTOR));
         if (!fun)
             return false;
         fun->setAsmJSIndex(funcIndex);
     } else {
@@ -2369,31 +2369,30 @@ Reject(JSContext* cx, const CompileArgs&
     }
 
     RootedObject stack(cx, promise->allocationSite());
     RootedString filename(cx, JS_NewStringCopyZ(cx, args.scriptedCaller.filename.get()));
     if (!filename)
         return false;
 
     unsigned line = args.scriptedCaller.line;
-    unsigned column = args.scriptedCaller.column;
 
     // Ideally we'd report a JSMSG_WASM_COMPILE_ERROR here, but there's no easy
     // way to create an ErrorObject for an arbitrary error code with multiple
     // replacements.
     UniqueChars str(JS_smprintf("wasm validation error: %s", error.get()));
     if (!str)
         return false;
 
     RootedString message(cx, NewLatin1StringZ(cx, Move(str)));
     if (!message)
         return false;
 
     RootedObject errorObj(cx,
-        ErrorObject::create(cx, JSEXN_WASMCOMPILEERROR, stack, filename, line, column, nullptr, message));
+        ErrorObject::create(cx, JSEXN_WASMCOMPILEERROR, stack, filename, line, 0, nullptr, message));
     if (!errorObj)
         return false;
 
     RootedValue rejectionValue(cx, ObjectValue(*errorObj));
     return PromiseObject::reject(cx, promise, rejectionValue);
 }
 
 static bool
@@ -2454,18 +2453,18 @@ struct CompileBufferTask : PromiseHelper
         importObj(cx, importObj)
     {}
 
     CompileBufferTask(JSContext* cx, Handle<PromiseObject*> promise)
       : PromiseHelperTask(cx, promise),
         instantiate(false)
     {}
 
-    bool init(JSContext* cx) {
-        compileArgs = InitCompileArgs(cx);
+    bool init(JSContext* cx, const char* introducer) {
+        compileArgs = InitCompileArgs(cx, introducer);
         if (!compileArgs)
             return false;
         return PromiseHelperTask::init(cx);
     }
 
     void execute() override {
         module = CompileBuffer(*compileArgs, *bytecode, &error, &warnings);
     }
@@ -2517,17 +2516,17 @@ WebAssembly_compile(JSContext* cx, unsig
     if (!EnsurePromiseSupport(cx))
         return false;
 
     Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
     if (!promise)
         return false;
 
     auto task = cx->make_unique<CompileBufferTask>(cx, promise);
-    if (!task || !task->init(cx))
+    if (!task || !task->init(cx, "WebAssembly.compile"))
         return false;
 
     CallArgs callArgs = CallArgsFromVp(argc, vp);
 
     if (!GetBufferSource(cx, callArgs, "WebAssembly.compile", &task->bytecode))
         return RejectWithPendingException(cx, promise, callArgs);
 
     if (!StartOffThreadPromiseHelperTask(cx, Move(task)))
@@ -2577,17 +2576,17 @@ WebAssembly_instantiate(JSContext* cx, u
         if (!Instantiate(cx, *module, importObj, &instanceObj))
             return RejectWithPendingException(cx, promise, callArgs);
 
         RootedValue resolutionValue(cx, ObjectValue(*instanceObj));
         if (!PromiseObject::resolve(cx, promise, resolutionValue))
             return false;
     } else {
         auto task = cx->make_unique<CompileBufferTask>(cx, promise, importObj);
-        if (!task || !task->init(cx))
+        if (!task || !task->init(cx, "WebAssembly.instantiate"))
             return false;
 
         if (!GetBufferSource(cx, firstArg, JSMSG_WASM_BAD_BUF_MOD_ARG, &task->bytecode))
             return RejectWithPendingException(cx, promise, callArgs);
 
         if (!StartOffThreadPromiseHelperTask(cx, Move(task)))
             return false;
     }
@@ -2671,20 +2670,22 @@ class CompileStreamTask : public Promise
     // Mutated on helper thread (execute()):
     SharedModule                 module_;
     UniqueChars                  compileError_;
     UniqueCharsVector            warnings_;
 
     // Called on some thread before consumeChunk() or streamClosed():
 
     void noteResponseURLs(const char* url, const char* sourceMapUrl) override {
-        if (url)
-            compileArgs_->responseURLs.baseURL = DuplicateString(url);
+        if (url) {
+            compileArgs_->scriptedCaller.filename = DuplicateString(url);
+            compileArgs_->scriptedCaller.filenameIsURL = true;
+        }
         if (sourceMapUrl)
-            compileArgs_->responseURLs.sourceMapURL = DuplicateString(sourceMapUrl);
+            compileArgs_->sourceMapURL = DuplicateString(sourceMapUrl);
     }
 
     // Called on a stream thread:
 
     // Until StartOffThreadPromiseHelperTask succeeds, we are responsible for
     // dispatching ourselves back to the JS thread.
     //
     // Warning: After this function returns, 'this' can be deleted at any time, so the
@@ -3012,17 +3013,21 @@ ResolveResponse_OnRejected(JSContext* cx
 }
 
 static bool
 ResolveResponse(JSContext* cx, CallArgs callArgs, Handle<PromiseObject*> promise,
                 bool instantiate = false, HandleObject importObj = nullptr)
 {
     MOZ_ASSERT_IF(importObj, instantiate);
 
-    MutableCompileArgs compileArgs = InitCompileArgs(cx);
+    const char* introducer = instantiate
+                             ? "WebAssembly.instantiateStreaming"
+                             : "WebAssembly.compileStreaming";
+
+    MutableCompileArgs compileArgs = InitCompileArgs(cx, introducer);
     if (!compileArgs)
         return false;
 
     RootedObject closure(cx, ResolveResponseClosure::create(cx, *compileArgs, promise,
                                                             instantiate, importObj));
     if (!closure)
         return false;
 
--- a/js/src/wasm/WasmModule.cpp
+++ b/js/src/wasm/WasmModule.cpp
@@ -548,17 +548,17 @@ wasm::CompiledModuleAssumptionsMatch(PRF
 
     Assumptions assumptions(Move(buildId));
     return Module::assumptionsMatch(assumptions, mapping.get(), info.size);
 }
 
 SharedModule
 wasm::DeserializeModule(PRFileDesc* bytecodeFile, PRFileDesc* maybeCompiledFile,
                         JS::BuildIdCharVector&& buildId, UniqueChars filename,
-                        unsigned line, unsigned column)
+                        unsigned line)
 {
     PRFileInfo bytecodeInfo;
     UniqueMapping bytecodeMapping = MapFile(bytecodeFile, &bytecodeInfo);
     if (!bytecodeMapping)
         return nullptr;
 
     if (PRFileDesc* compiledFile = maybeCompiledFile) {
         PRFileInfo compiledInfo;
@@ -578,17 +578,16 @@ wasm::DeserializeModule(PRFileDesc* byte
     if (!bytecode || !bytecode->bytes.initLengthUninitialized(bytecodeInfo.size))
         return nullptr;
 
     memcpy(bytecode->bytes.begin(), bytecodeMapping.get(), bytecodeInfo.size);
 
     ScriptedCaller scriptedCaller;
     scriptedCaller.filename = Move(filename);
     scriptedCaller.line = line;
-    scriptedCaller.column = column;
 
     MutableCompileArgs args = js_new<CompileArgs>(Assumptions(Move(buildId)), Move(scriptedCaller));
     if (!args)
         return nullptr;
 
     // The true answer to whether shared memory is enabled is provided by
     // cx->compartment()->creationOptions().getSharedMemoryAndAtomicsEnabled()
     // where cx is the context that originated the call that caused this
--- a/js/src/wasm/WasmModule.h
+++ b/js/src/wasm/WasmModule.h
@@ -252,14 +252,14 @@ typedef RefPtr<Module> SharedModule;
 
 // JS API implementations:
 
 bool
 CompiledModuleAssumptionsMatch(PRFileDesc* compiled, JS::BuildIdCharVector&& buildId);
 
 SharedModule
 DeserializeModule(PRFileDesc* bytecode, PRFileDesc* maybeCompiled, JS::BuildIdCharVector&& buildId,
-                  UniqueChars filename, unsigned line, unsigned column);
+                  UniqueChars filename, unsigned line);
 
 } // namespace wasm
 } // namespace js
 
 #endif // wasm_module_h
--- a/js/src/wasm/WasmValidate.cpp
+++ b/js/src/wasm/WasmValidate.cpp
@@ -1941,39 +1941,40 @@ DecodeDataSection(Decoder& d, ModuleEnvi
         if (!env->dataSegments.append(seg))
             return false;
     }
 
     return d.finishSection(*range, "data");
 }
 
 static bool
-DecodeModuleNameSubsection(Decoder& d)
+DecodeModuleNameSubsection(Decoder& d, ModuleEnvironment* env)
 {
     Maybe<uint32_t> endOffset;
     if (!d.startNameSubsection(NameType::Module, &endOffset))
         return false;
     if (!endOffset)
         return true;
 
     // Don't use NameInBytecode for module name; instead store a copy of the
     // string. This way supplying a module name doesn't need to save the whole
     // bytecode. While function names are likely to be stripped in practice,
     // module names aren't necessarily.
 
     uint32_t nameLength;
     if (!d.readVarU32(&nameLength))
         return d.fail("failed to read module name length");
 
+    NameInBytecode moduleName(d.currentOffset(), nameLength);
+
     const uint8_t* bytes;
     if (!d.readBytes(nameLength, &bytes))
         return d.fail("failed to read module name bytes");
 
-    // Do nothing with module name for now; a future patch will incorporate the
-    // module name into the callstack format.
+    env->moduleName.emplace(moduleName);
 
     return d.finishNameSubsection(*endOffset);
 }
 
 static bool
 DecodeFunctionNameSubsection(Decoder& d, ModuleEnvironment* env)
 {
     Maybe<uint32_t> endOffset;
@@ -2002,20 +2003,22 @@ DecodeFunctionNameSubsection(Decoder& d,
             return d.fail("unable to read function name length");
 
         if (!nameLength)
             continue;
 
         if (!funcNames.resize(funcIndex + 1))
             return false;
 
-        funcNames[funcIndex] = NameInBytecode(d.currentOffset(), nameLength);
+        NameInBytecode funcName(d.currentOffset(), nameLength);
 
         if (!d.readBytes(nameLength))
             return d.fail("unable to read function name bytes");
+
+        funcNames[funcIndex] = funcName;
     }
 
     if (!d.finishNameSubsection(*endOffset))
         return false;
 
     // To encourage fully valid function names subsections; only save names if
     // the entire subsection decoded correctly.
     env->funcNames = Move(funcNames);
@@ -2028,17 +2031,17 @@ DecodeNameSection(Decoder& d, ModuleEnvi
     MaybeSectionRange range;
     if (!d.startCustomSection(NameSectionName, env, &range))
         return false;
     if (!range)
         return true;
 
     // Once started, custom sections do not report validation errors.
 
-    if (!DecodeModuleNameSubsection(d))
+    if (!DecodeModuleNameSubsection(d, env))
         goto finish;
 
     if (!DecodeFunctionNameSubsection(d, env))
         goto finish;
 
     while (d.currentOffset() < range->end()) {
         if (!d.skipNameSubsection())
             goto finish;
--- a/js/src/wasm/WasmValidate.h
+++ b/js/src/wasm/WasmValidate.h
@@ -77,16 +77,17 @@ struct ModuleEnvironment
     ImportVector              imports;
     ExportVector              exports;
     Maybe<uint32_t>           startFuncIndex;
     MaybeSectionRange         codeSection;
 
     // Fields decoded as part of the wasm module tail:
     ElemSegmentVector         elemSegments;
     DataSegmentVector         dataSegments;
+    Maybe<NameInBytecode>     moduleName;
     NameInBytecodeVector      funcNames;
     CustomSectionVector       customSections;
 
     explicit ModuleEnvironment(CompileMode mode,
                                Tier tier,
                                DebugEnabled debug,
                                HasGcTypes hasGcTypes,
                                Shareable sharedMemoryEnabled,
--- a/layout/xul/test/browser_bug1163304.js
+++ b/layout/xul/test/browser_bug1163304.js
@@ -1,35 +1,41 @@
-function test() {
-  waitForExplicitFinish();
+ChromeUtils.import("resource://testing-common/CustomizableUITestUtils.jsm", this);
+let gCUITestUtils = new CustomizableUITestUtils(window);
 
-  Services.prefs.setBoolPref("browser.search.widget.inNavBar", true);
-  let searchBar = BrowserSearch.searchBar;
-  searchBar.focus();
+add_task(async function test_setup() {
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
+});
+
+add_task(async function() {
+  BrowserSearch.searchBar.focus();
 
   let DOMWindowUtils = EventUtils._getDOMWindowUtils();
   is(DOMWindowUtils.IMEStatus, DOMWindowUtils.IME_STATUS_ENABLED,
      "IME should be available when searchbar has focus");
 
   let searchPopup = document.getElementById("PopupSearchAutoComplete");
-  searchPopup.addEventListener("popupshown", function (aEvent) {
-    setTimeout(function () {
-      is(DOMWindowUtils.IMEStatus, DOMWindowUtils.IME_STATUS_ENABLED,
-         "IME should be available even when the popup of searchbar is open");
-      searchPopup.addEventListener("popuphidden", function (aEvent) {
-        setTimeout(function () {
-          is(DOMWindowUtils.IMEStatus, DOMWindowUtils.IME_STATUS_DISABLED,
-             "IME should not be available when menubar is active");
-          // Inactivate the menubar (and restore the focus to the searchbar
-          EventUtils.synthesizeKey("KEY_Escape");
-          is(DOMWindowUtils.IMEStatus, DOMWindowUtils.IME_STATUS_ENABLED,
-             "IME should be available after focus is back to the searchbar");
-          Services.prefs.clearUserPref("browser.search.widget.inNavBar");
-          finish();
-        }, 0);
-      }, {once: true});
-      // Activate the menubar, then, the popup should be closed
-      EventUtils.synthesizeKey("KEY_Alt");
-    }, 0);
-  }, {once: true});
+
+  let shownPromise = BrowserTestUtils.waitForEvent(searchPopup, "popupshown");
   // Open popup of the searchbar
   EventUtils.synthesizeKey("KEY_F4");
-}
+  await shownPromise;
+  await new Promise(r => setTimeout(r, 0));
+
+  is(DOMWindowUtils.IMEStatus, DOMWindowUtils.IME_STATUS_ENABLED,
+     "IME should be available even when the popup of searchbar is open");
+
+  // Activate the menubar, then, the popup should be closed
+  let hiddenPromise = BrowserTestUtils.waitForEvent(searchPopup, "popuphidden");
+  EventUtils.synthesizeKey("KEY_Alt");
+  await hiddenPromise;
+  await new Promise(r => setTimeout(r, 0));
+
+  is(DOMWindowUtils.IMEStatus, DOMWindowUtils.IME_STATUS_DISABLED,
+     "IME should not be available when menubar is active");
+  // Inactivate the menubar (and restore the focus to the searchbar
+  EventUtils.synthesizeKey("KEY_Escape");
+  is(DOMWindowUtils.IMEStatus, DOMWindowUtils.IME_STATUS_ENABLED,
+     "IME should be available after focus is back to the searchbar");
+});
--- a/mfbt/WindowsVersion.h
+++ b/mfbt/WindowsVersion.h
@@ -143,28 +143,46 @@ IsWin8Point1OrLater()
 
 MOZ_ALWAYS_INLINE bool
 IsWin10OrLater()
 {
   return IsWindowsVersionOrLater(0x0a000000ul);
 }
 
 MOZ_ALWAYS_INLINE bool
+IsWin10November2015UpdateOrLater()
+{
+  return IsWindows10BuildOrLater(10586);
+}
+
+MOZ_ALWAYS_INLINE bool
+IsWin10AnniversaryUpdateOrLater()
+{
+  return IsWindows10BuildOrLater(14393);
+}
+
+MOZ_ALWAYS_INLINE bool
 IsWin10CreatorsUpdateOrLater()
 {
   return IsWindows10BuildOrLater(15063);
 }
 
 MOZ_ALWAYS_INLINE bool
 IsWin10FallCreatorsUpdateOrLater()
 {
   return IsWindows10BuildOrLater(16299);
 }
 
 MOZ_ALWAYS_INLINE bool
+IsWin10April2018UpdateOrLater()
+{
+  return IsWindows10BuildOrLater(17134);
+}
+
+MOZ_ALWAYS_INLINE bool
 IsNotWin7PreRTM()
 {
   return IsWin7SP1OrLater() || IsWindowsBuildOrLater(7600);
 }
 
 inline bool
 IsWin7AndPre2000Compatible() {
   /*
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -58,16 +58,20 @@
 #include "nsStreamUtils.h"
 #include "nsThreadUtils.h"
 #include "nsCORSListenerProxy.h"
 
 #ifdef MOZ_TASK_TRACER
 #include "GeckoTaskTracer.h"
 #endif
 
+#ifdef MOZ_GECKO_PROFILER
+#include "ProfilerMarkerPayload.h"
+#endif
+
 using namespace mozilla::dom;
 using namespace mozilla::ipc;
 
 namespace mozilla {
 namespace net {
 
 #if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION) || defined(DEBUG)
 static bool gIPCSecurityDisabled = false;
@@ -186,16 +190,17 @@ HttpChannelChild::HttpChannelChild()
   , mSuspendParentAfterSynthesizeResponse(false)
   , mBgChildMutex("HttpChannelChild::BgChildMutex")
   , mEventTargetMutex("HttpChannelChild::EventTargetMutex")
 {
   LOG(("Creating HttpChannelChild @%p\n", this));
 
   mChannelCreationTime = PR_Now();
   mChannelCreationTimestamp = TimeStamp::Now();
+  mLastStatusReported = mChannelCreationTimestamp; // in case we enable the profiler after Init()
   mAsyncOpenTime = TimeStamp::Now();
   mEventQ = new ChannelEventQueue(static_cast<nsIHttpChannel*>(this));
 
 #if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION) || defined(DEBUG)
   static bool sSecurityPrefChecked = false;
   if (!sSecurityPrefChecked) {
     Preferences::AddBoolVarCache(&gIPCSecurityDisabled,
                                  "network.disable.ipc.security");
@@ -1144,16 +1149,27 @@ HttpChannelChild::OnStopRequest(const ns
   mRedirectEndTimeStamp = timing.redirectEnd;
   mTransferSize = timing.transferSize;
   mEncodedBodySize = timing.encodedBodySize;
   mProtocolVersion = timing.protocolVersion;
 
   mCacheReadStart = timing.cacheReadStart;
   mCacheReadEnd = timing.cacheReadEnd;
 
+#ifdef MOZ_GECKO_PROFILER
+  if (profiler_is_active()) {
+    int32_t priority = PRIORITY_NORMAL;
+    GetPriority(&priority);
+    profiler_add_network_marker(mURI, priority, mChannelId, NetworkLoadType::LOAD_STOP,
+                                mLastStatusReported, TimeStamp::Now(),
+                                mTransferSize,
+                                &mTransactionTimings);
+  }
+#endif
+
   mResponseTrailers = new nsHttpHeaderArray(aResponseTrailers);
 
   DoPreOnStopRequest(channelStatus);
 
   { // We must flush the queue before we Send__delete__
     // (although we really shouldn't receive any msgs after OnStop),
     // so make sure this goes out of scope before then.
     AutoEventEnqueuer ensureSerialDispatch(mEventQ);
@@ -1773,16 +1789,22 @@ HttpChannelChild::Redirect1Begin(const u
   nsresult rv;
 
   LOG(("HttpChannelChild::Redirect1Begin [this=%p]\n", this));
 
   ipc::MergeParentLoadInfoForwarder(loadInfoForwarder, mLoadInfo);
 
   nsCOMPtr<nsIURI> uri = DeserializeURI(newOriginalURI);
 
+  PROFILER_ADD_NETWORK_MARKER(mURI, mPriority, channelId, NetworkLoadType::LOAD_REDIRECT,
+                              mLastStatusReported, TimeStamp::Now(),
+                              0,
+                              &mTransactionTimings,
+                              uri);
+
   if (!securityInfoSerialization.IsEmpty()) {
     NS_DeserializeObject(securityInfoSerialization,
                          getter_AddRefs(mSecurityInfo));
   }
 
   nsCOMPtr<nsIChannel> newChannel;
   rv = SetupRedirect(uri,
                       &responseHead,
@@ -2498,16 +2520,21 @@ HttpChannelChild::AsyncOpen(nsIStreamLis
   //
 
   // We notify "http-on-opening-request" observers in the child
   // process so that devtools can capture a stack trace at the
   // appropriate spot.  See bug 806753 for some information about why
   // other http-* notifications are disabled in child processes.
   gHttpHandler->OnOpeningRequest(this);
 
+  mLastStatusReported = TimeStamp::Now();
+  PROFILER_ADD_NETWORK_MARKER(mURI, mPriority, mChannelId, NetworkLoadType::LOAD_START,
+                              mChannelCreationTimestamp, mLastStatusReported,
+                              0, nullptr, nullptr);
+
   mIsPending = true;
   mWasOpened = true;
   mListener = listener;
   mListenerContext = aContext;
 
   // add ourselves to the load group.
   if (mLoadGroup)
     mLoadGroup->AddRequest(this, nullptr);
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -311,16 +311,18 @@ private:
   int32_t      mCacheFetchCount;
   uint32_t     mCacheExpirationTime;
   nsCString    mCachedCharset;
 
   nsCOMPtr<nsICacheInfoChannel> mSynthesizedCacheInfo;
 
   nsCString mProtocolVersion;
 
+  TimeStamp mLastStatusReported;
+
   // If ResumeAt is called before AsyncOpen, we need to send extra data upstream
   bool mSendResumeAt;
 
   // To ensure only one SendDeletingChannel is triggered.
   Atomic<bool> mDeletingChannelSent;
 
   Atomic<bool> mIPCOpen;
   bool mKeptAlive;            // IPC kept open, but only for security info
--- a/netwerk/protocol/http/TimingStruct.h
+++ b/netwerk/protocol/http/TimingStruct.h
@@ -2,16 +2,17 @@
  * 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/. */
 
 
 #ifndef TimingStruct_h_
 #define TimingStruct_h_
 
 #include "mozilla/TimeStamp.h"
+#include "nsString.h"
 
 namespace mozilla { namespace net {
 
 struct TimingStruct {
   TimeStamp domainLookupStart;
   TimeStamp domainLookupEnd;
   TimeStamp connectStart;
   TimeStamp tcpConnectEnd;
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -390,32 +390,21 @@ nsHttpChannel::Init(nsIURI *uri,
     nsresult rv = HttpBaseChannel::Init(uri, caps, proxyInfo,
                                         proxyResolveFlags, proxyURI, channelId);
     if (NS_FAILED(rv))
         return rv;
 
 #ifdef MOZ_GECKO_PROFILER
     mLastStatusReported = TimeStamp::Now(); // in case we enable the profiler after Init()
     if (profiler_is_active()) {
-        // These do allocations/frees/etc; avoid if not active
-        nsAutoCString spec;
-        if (uri) {
-            uri->GetAsciiSpec(spec);
-        }
-        // top 32 bits are process id of the load
-        int32_t id = static_cast<int32_t>(channelId & 0xFFFFFFFF);
-        char name[64];
-        SprintfLiteral(name, "Load: %d", id);
-        rv = NS_OK; // ensure it's a fixed value (0)
-        profiler_add_marker(name,
-                            MakeUnique<NetworkMarkerPayload>(id,
-                                                             PromiseFlatCString(spec).get(),
-                                                             rv, // NS_OK means START
-                                                             mLastStatusReported,
-                                                             mLastStatusReported));
+        int32_t priority = PRIORITY_NORMAL;
+        GetPriority(&priority);
+        profiler_add_network_marker(uri, priority, channelId, NetworkLoadType::LOAD_START,
+                                    mChannelCreationTimestamp, mLastStatusReported,
+                                    0);
     }
 #endif
 
     LOG(("nsHttpChannel::Init [this=%p]\n", this));
 
     return rv;
 }
 
@@ -5635,16 +5624,27 @@ nsHttpChannel::ContinueProcessRedirectio
                                                    mRequestHead.ParsedMethod());
 
     // prompt if the method is not safe (such as POST, PUT, DELETE, ...)
     if (!rewriteToGET && !mRequestHead.IsSafeMethod()) {
         rv = PromptTempRedirect();
         if (NS_FAILED(rv)) return rv;
     }
 
+#ifdef MOZ_GECKO_PROFILER
+    if (profiler_is_active()) {
+        int32_t priority = PRIORITY_NORMAL;
+        GetPriority(&priority);
+        profiler_add_network_marker(mURI, priority, mChannelId, NetworkLoadType::LOAD_REDIRECT,
+                                    mLastStatusReported, TimeStamp::Now(),
+                                    mLogicalOffset, nullptr,
+                                    mRedirectURI);
+    }
+#endif
+
     nsCOMPtr<nsIIOService> ioService;
     rv = gHttpHandler->GetIOService(getter_AddRefs(ioService));
     if (NS_FAILED(rv)) return rv;
 
     uint32_t redirectFlags;
     if (nsHttp::IsPermanentRedirect(mRedirectType))
         redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT;
     else
@@ -7243,16 +7243,30 @@ nsHttpChannel::OnStopRequest(nsIRequest 
             // We only need the domainLookup timestamps when not using a
             // persistent connection, meaning if the endTimestamp < connectStart
             mTransactionTimings.domainLookupStart =
                 mDNSPrefetch->StartTimestamp();
             mTransactionTimings.domainLookupEnd =
                 mDNSPrefetch->EndTimestamp();
         }
         mDNSPrefetch = nullptr;
+#ifdef MOZ_GECKO_PROFILER
+        if (profiler_is_active() && !mRedirectURI) {
+            // Don't include this if we already redirected
+            // These do allocations/frees/etc; avoid if not active
+            nsCOMPtr<nsIURI> uri;
+            GetURI(getter_AddRefs(uri));
+            int32_t priority = PRIORITY_NORMAL;
+            GetPriority(&priority);
+            profiler_add_network_marker(uri, priority, mChannelId, NetworkLoadType::LOAD_STOP,
+                                        mLastStatusReported, TimeStamp::Now(),
+                                        mLogicalOffset,
+                                        &mTransactionTimings);
+        }
+#endif
 
         // handle auth retry...
         if (authRetry) {
             mAuthRetryPending = false;
             status = DoAuthRetry(conn);
             if (NS_SUCCEEDED(status))
                 return NS_OK;
         }
@@ -7729,34 +7743,16 @@ nsHttpChannel::OnTransportStatus(nsITran
                 do_QueryInterface(trans);
             if (socketTransport) {
                 socketTransport->GetSelfAddr(&mSelfAddr);
                 socketTransport->GetPeerAddr(&mPeerAddr);
             }
         }
     }
 
-#ifdef MOZ_GECKO_PROFILER
-    if (profiler_is_active()) {
-        // These do allocations/frees/etc; avoid if not active
-        mozilla::TimeStamp now = TimeStamp::Now();
-        // top 32 bits are process id of the load
-        int32_t id = static_cast<int32_t>(mChannelId & 0xFFFFFFFF);
-        char name[64];
-        SprintfLiteral(name, "Load: %d", id);
-        profiler_add_marker(name,
-                            MakeUnique<NetworkMarkerPayload>(static_cast<int64_t>(mChannelId),
-                                                             nullptr,
-                                                             status,
-                                                             mLastStatusReported,
-                                                             now));
-        mLastStatusReported = now;
-    }
-#endif
-
     // block socket status event after Cancel or OnStopRequest has been called.
     if (mProgressSink && NS_SUCCEEDED(mStatus) && mIsPending) {
         LOG(("sending progress%s notification [this=%p status=%" PRIx32
              " progress=%" PRId64 "/%" PRId64 "]\n",
             (mLoadFlags & LOAD_BACKGROUND)? "" : " and status",
              this, static_cast<uint32_t>(status), progress, progressMax));
 
         if (!(mLoadFlags & LOAD_BACKGROUND)) {
--- a/python/mozbuild/mozbuild/action/xpidl-process.py
+++ b/python/mozbuild/mozbuild/action/xpidl-process.py
@@ -62,18 +62,23 @@ def process(input_dir, inc_paths, bindin
             print_header(idl, fh, path)
 
         with FileAvoidWrite(rs_rt_path) as fh:
             print_rust_bindings(idl, fh, path)
 
         with FileAvoidWrite(rs_bt_path) as fh:
             print_rust_macros_bindings(idl, fh, path)
 
+    # NOTE: We don't use FileAvoidWrite here as we may re-run this code due to a
+    # number of different changes in the code, which may not cause the .xpt
+    # files to be changed in any way. This means that make will re-run us every
+    # time a build is run whether or not anything changed. To fix this we
+    # unconditionally write out the file.
     xpt_path = os.path.join(xpt_dir, '%s.xpt' % module)
-    with FileAvoidWrite(xpt_path) as fh:
+    with open(xpt_path, 'w') as fh:
         jsonxpt.write(jsonxpt.link(xpts), fh)
 
     rule.add_targets([xpt_path])
     if deps_dir:
         deps_path = os.path.join(deps_dir, '%s.pp' % module)
         with FileAvoidWrite(deps_path) as fh:
             mk.dump(fh)
 
--- a/testing/profiles/common/user.js
+++ b/testing/profiles/common/user.js
@@ -79,18 +79,16 @@ user_pref("browser.tabs.disableBackgroun
 // Only load extensions from the application and user profile
 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
 user_pref("extensions.enabledScopes", 5);
 user_pref("extensions.autoDisableScopes", 0);
 // Disable metadata caching for installed add-ons by default
 user_pref("extensions.getAddons.cache.enabled", false);
 // Disable intalling any distribution add-ons
 user_pref("extensions.installDistroAddons", false);
-// XPI extensions are required for test harnesses to load
-user_pref("extensions.defaultProviders.enabled", true);
 user_pref("xpinstall.signatures.required", false);
 user_pref("extensions.legacy.enabled", true);
 
 user_pref("geo.wifi.uri", "http://{server}/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
 user_pref("geo.wifi.timeToWaitBeforeSending", 2000);
 user_pref("geo.wifi.scan", false);
 user_pref("geo.wifi.logging.enabled", true);
 
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js
@@ -1,21 +1,30 @@
 "use strict";
 
 // This test checks whether applied WebExtension themes that attempt to change
 // the background color and the color of the navbar text fields are applied properly.
 
+ChromeUtils.import("resource://testing-common/CustomizableUITestUtils.jsm", this);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({set: [
     ["extensions.webextensions.themes.enabled", true],
-    ["browser.search.widget.inNavBar", true],
   ]});
+
+  await gCUITestUtils.addSearchBar();
+  registerCleanupFunction(() => {
+    gCUITestUtils.removeSearchBar();
+  });
 });
 
 add_task(async function test_support_toolbar_field_properties() {
+  let searchbar = BrowserSearch.searchBar;
+
   const TOOLBAR_FIELD_BACKGROUND = "#ff00ff";
   const TOOLBAR_FIELD_COLOR = "#00ff00";
   const TOOLBAR_FIELD_BORDER = "#aaaaff";
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "theme": {
         "images": {
           "headerURL": "image1.png",
@@ -39,17 +48,16 @@ add_task(async function test_support_too
   let root = document.documentElement;
   // Remove the `remotecontrol` attribute since it interferes with the urlbar styling.
   root.removeAttribute("remotecontrol");
   registerCleanupFunction(() => {
     root.setAttribute("remotecontrol", "true");
   });
 
   let toolbox = document.querySelector("#navigator-toolbox");
-  let searchbar = document.querySelector("#searchbar");
   let fields = [
     toolbox.querySelector("#urlbar"),
     document.getAnonymousElementByAttribute(searchbar, "anonid", "searchbar-textbox"),
   ].filter(field => {
     let bounds = field.getBoundingClientRect();
     return bounds.width > 0 && bounds.height > 0;
   });
 
deleted file mode 100644
--- a/toolkit/components/telemetry/docs/fhr/architecture.rst
+++ /dev/null
@@ -1,226 +0,0 @@
-.. _healthreport_architecture:
-
-============
-Architecture
-============
-
-``healthreporter.jsm`` contains the main interface for FHR, the
-``HealthReporter`` type. An instance of this is created by the
-``data_reporting_service``.
-
-``providers.jsm`` contains numerous ``Metrics.Provider`` and
-``Metrics.Measurement`` used for collecting application metrics. If you
-are looking for the FHR probes, this is where they are.
-
-Storage
-=======
-
-Firefox Health Report stores data in 3 locations:
-
-* Metrics measurements and provider state is stored in a SQLite database
-  (via ``Metrics.Storage``).
-* Service state (such as the IDs of documents uploaded) is stored in a
-  JSON file on disk (via OS.File).
-* Lesser state and run-time options are stored in preferences.
-
-Preferences
-===========
-
-Preferences controlling behavior of Firefox Health Report live in the
-``datareporting.healthreport.*`` branch.
-
-Service and Data Control
-------------------------
-
-The follow preferences control behavior of the service and data upload.
-
-service.enabled
-   Controls whether the entire health report service runs. The overall
-   service performs data collection, storing, and submission.
-
-   This is the primary kill switch for Firefox Health Report
-   outside of the build system variable. i.e. if you are using an
-   official Firefox build and wish to disable FHR, this is what you
-   should set to false to prevent FHR from not only submitting but
-   also collecting data.
-
-uploadEnabled
-   Whether uploading of data is enabled. This is the preference the
-   checkbox in the preferences UI reflects. If this is
-   disabled, FHR still collects data - it just doesn't upload it.
-
-service.loadDelayMsec
-   How long (in milliseconds) after initial application start should FHR
-   wait before initializing.
-
-   FHR may initialize sooner than this if the FHR service is requested.
-   This will happen if e.g. the user goes to ``about:healthreport``.
-
-service.loadDelayFirstRunMsec
-   How long (in milliseconds) FHR should wait to initialize on first
-   application run.
-
-   FHR waits longer than normal to initialize on first application run
-   because first-time initialization can use a lot of I/O to initialize
-   the SQLite database and this I/O should not interfere with the
-   first-run user experience.
-
-documentServerURI
-   The URI of a Bagheera server that FHR should interface with for
-   submitting documents.
-
-   You typically do not need to change this.
-
-documentServerNamespace
-   The namespace on the document server FHR should upload documents to.
-
-   You typically do not need to change this.
-
-infoURL
-   The URL of a page containing more info about FHR, it's privacy
-   policy, etc.
-
-about.reportUrl
-   The URL to load in ``about:healthreport``.
-
-about.reportUrlUnified
-   The URL to load in ``about:healthreport``. This is used instead of ``reportUrl`` for UnifiedTelemetry when it is not opt-in.
-
-service.providerCategories
-   A comma-delimited list of category manager categories that contain
-   registered ``Metrics.Provider`` records. Read below for how provider
-   registration works.
-
-If the entire service is disabled, you lose data collection. This means
-that **local** data analysis won't be available because there is no data
-to analyze! Keep in mind that Firefox Health Report can be useful even
-if it's not submitting data to remote servers!
-
-Logging
--------
-
-The following preferences allow you to control the logging behavior of
-Firefox Health Report.
-
-logging.consoleEnabled
-   Whether to write log messages to the web console. This is true by
-   default.
-
-logging.consoleLevel
-   The minimum log level FHR messages must have to be written to the
-   web console. By default, only FHR warnings or errors will be written
-   to the web console. During normal/expected operation, no messages of
-   this type should be produced.
-
-logging.dumpEnabled
-   Whether to write log messages via ``dump()``. If true, FHR will write
-   messages to stdout/stderr.
-
-   This is typically only enabled when developing FHR.
-
-logging.dumpLevel
-   The minimum log level messages must have to be written via
-   ``dump()``.
-
-State
------
-
-currentDaySubmissionFailureCount
-   How many submission failures the client has encountered while
-   attempting to upload the most recent document.
-
-lastDataSubmissionFailureTime
-   The time of the last failed document upload.
-
-lastDataSubmissionRequestedTime
-   The time of the last document upload attempt.
-
-lastDataSubmissionSuccessfulTime
-   The time of the last successful document upload.
-
-nextDataSubmissionTime
-   The time the next data submission is scheduled for. FHR will not
-   attempt to upload a new document before this time.
-
-pendingDeleteRemoteData
-   Whether the client currently has a pending request to delete remote
-   data. If true, the client will attempt to delete all remote data
-   before an upload is performed.
-
-FHR stores various state in preferences.
-
-Registering Providers
-=====================
-
-Firefox Health Report providers are registered via the category manager.
-See ``HealthReportComponents.manifest`` for providers defined in this
-directory.
-
-Essentially, the category manager receives the name of a JS type and the
-URI of a JSM to import that exports this symbol. At run-time, the
-providers registered in the category manager are instantiated.
-
-Providers are registered via the category manager to make registration
-simple and less prone to errors. Any XPCOM component can create a
-category manager entry. Therefore, new data providers can be added
-without having to touch core Firefox Health Report code. Additionally,
-category manager registration means providers are more likely to be
-registered on FHR's terms, when it wants. If providers were registered
-in code at application run-time, there would be the risk of other
-components prematurely instantiating FHR (causing a performance hit if
-performed at an inopportune time) or semi-complicated code around
-observers or listeners. Category manager entries are only 1 line per
-provider and leave FHR in control: they are simple and safe.
-
-Document Generation and Lifecycle
-=================================
-
-FHR will attempt to submit a JSON document containing data every 24 wall
-clock hours.
-
-At upload time, FHR will query the database for **all** information from
-the last 180 days and assemble this data into a JSON document. We
-attempt to upload this JSON document with a client-generated UUID to the
-configured server.
-
-Before we attempt upload, the generated UUID is stored in the JSON state
-file on local disk. At this point, the client assumes the document with
-that UUID has been successfully stored on the server.
-
-If the client is aware of other document UUIDs that presumably exist on
-the server, those UUIDs are sent with the upload request so the client
-can request those UUIDs be deleted. This helps ensure that each client
-only has 1 document/UUID on the server at any one time.
-
-Importance of Persisting UUIDs
-------------------------------
-
-The choices of how, where, and when document UUIDs are stored and updated
-are very important. One should not attempt to change things unless she
-has a very detailed understanding of why things are the way they are.
-
-The client is purposefully very conservative about forgetting about
-generated UUIDs. In other words, once a UUID is generated, the client
-deliberately holds on to that UUID until it's very confident that UUID
-is no longer stored on the server. The reason we do this is because
-*orphaned* documents/UUIDs on the server can lead to faulty analysis,
-such as over-reporting the number of Firefox installs that stop being
-used.
-
-When uploading a new UUID, we update the state and save the state file
-to disk *before* an upload attempt because if the upload succeeds but
-the response never makes it back to the client, we want the client to
-know about the uploaded UUID so it can delete it later to prevent an
-orphan.
-
-We maintain a list of UUIDs locally (not simply the last UUID) because
-multiple upload attempts could fail the same way as the previous
-paragraph describes and we have no way of knowing which (if any)
-actually succeeded. The safest approach is to assume every document
-produced managed to get uploaded some how.
-
-We store the UUIDs on a file on disk and not anywhere else because we
-want storage to be robust. We originally stored UUIDs in preferences,
-which only flush to disk periodically. Writes to preferences were
-apparently getting lost. We switched to writing directly to files to
-eliminate this window.
deleted file mode 100644
--- a/toolkit/components/telemetry/docs/fhr/dataformat.rst
+++ /dev/null
@@ -1,1998 +0,0 @@
-.. _healthreport_dataformat:
-
-==============
-Payload Format
-==============
-
-Currently, the Firefox Health Report is submitted as a compressed JSON
-document. The root JSON element is an object. A *version* field defines
-the version of the payload which in turn defines the expected contents
-the object.
-
-As of 2013-07-03, desktop submits Version 2, and Firefox for Android submits
-Version 3 payloads.
-
-Version 3
-=========
-
-Version 3 is a complete rebuild of the document format. Events are tracked in
-an "environment". Environments are computed from a large swath of local data
-(e.g., add-ons, CPU count, versions), and a new environment comes into being
-when one of its attributes changes.
-
-Client documents, then, will include descriptions of many environments, and
-measurements will be attributed to one particular environment.
-
-A map of environments is present at the top level of the document, with the
-current named "current" in the map. Each environment has a hash identifier and
-a set of attributes. The current environment is completely described, and has
-its hash present in a "hash" attribute. All other environments are represented
-as a tree diff from the current environment, with their hash as the key in the
-"environments" object.
-
-A removed add-on has the value 'null'.
-
-There is no "last" data at present.
-
-Daily data is hierarchical: by day, then by environment, and then by
-measurement, and is present in "data", just as in v2.
-
-Leading by example::
-
-    {
-      "lastPingDate": "2013-06-29",
-      "thisPingDate": "2013-07-03",
-      "version": 3,
-      "environments": {
-        "current": {
-          "org.mozilla.sysinfo.sysinfo": {
-            "memoryMB": 1567,
-            "cpuCount": 4,
-            "architecture": "armeabi-v7a",
-            "_v": 1,
-            "version": "4.1.2",
-            "name": "Android"
-          },
-          "org.mozilla.profile.age": {
-            "_v": 1,
-            "profileCreation": 15827
-          },
-          "org.mozilla.addons.active": {
-            "QuitNow@TWiGSoftware.com": {
-              "appDisabled": false,
-              "userDisabled": false,
-              "scope": 1,
-              "updateDay": 15885,
-              "foreignInstall": false,
-              "hasBinaryComponents": false,
-              "blocklistState": 0,
-              "type": "extension",
-              "installDay": 15885,
-              "version": "1.18.02"
-            },
-            "{dbbf9331-b713-6eda-1006-205efead09dc}": {
-              "appDisabled": false,
-              "userDisabled": "askToActivate",
-              "scope": 8,
-              "updateDay": 15779,
-              "foreignInstall": true,
-              "blocklistState": 0,
-              "type": "plugin",
-              "installDay": 15779,
-              "version": "11.1 r115"
-            },
-            "desktopbydefault@bnicholson.mozilla.org": {
-              "appDisabled": false,
-              "userDisabled": true,
-              "scope": 1,
-              "updateDay": 15870,
-              "foreignInstall": false,
-              "hasBinaryComponents": false,
-              "blocklistState": 0,
-              "type": "extension",
-              "installDay": 15870,
-              "version": "1.1"
-            },
-            "{6e092a7f-ba58-4abb-88c1-1a4e50b217e4}": {
-              "appDisabled": false,
-              "userDisabled": false,
-              "scope": 1,
-              "updateDay": 15828,
-              "foreignInstall": false,
-              "hasBinaryComponents": false,
-              "blocklistState": 0,
-              "type": "extension",
-              "installDay": 15828,
-              "version": "1.1.0"
-            },
-            "{46551EC9-40F0-4e47-8E18-8E5CF550CFB8}": {
-              "appDisabled": false,
-              "userDisabled": true,
-              "scope": 1,
-              "updateDay": 15879,
-              "foreignInstall": false,
-              "hasBinaryComponents": false,
-              "blocklistState": 0,
-              "type": "extension",
-              "installDay": 15879,
-              "version": "1.3.2"
-            },
-            "_v": 1
-          },
-          "org.mozilla.appInfo.appinfo": {
-            "_v": 3,
-            "appLocale": "en_us",
-            "osLocale": "en_us",
-            "distribution": "",
-            "acceptLangIsUserSet": 0,
-            "isTelemetryEnabled": 1,
-            "isBlocklistEnabled": 1
-          },
-          "geckoAppInfo": {
-            "updateChannel": "nightly",
-            "id": "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
-            "os": "Android",
-            "platformBuildID": "20130703031323",
-            "platformVersion": "25.0a1",
-            "vendor": "Mozilla",
-            "name": "fennec",
-            "xpcomabi": "arm-eabi-gcc3",
-            "appBuildID": "20130703031323",
-            "_v": 1,
-            "version": "25.0a1"
-          },
-          "hash": "tB4Pnnep9yTxnMDymc3dAB2RRB0=",
-          "org.mozilla.addons.counts": {
-            "extension": 4,
-            "plugin": 1,
-            "_v": 1,
-            "theme": 0
-          }
-        },
-        "k2O3hlreMeS7L1qtxeMsYWxgWWQ=": {
-          "geckoAppInfo": {
-            "platformBuildID": "20130630031138",
-            "appBuildID": "20130630031138",
-            "_v": 1
-          },
-          "org.mozilla.appInfo.appinfo": {
-            "_v": 2,
-          }
-        },
-        "1+KN9TutMpzdl4TJEl+aCxK+xcw=": {
-          "geckoAppInfo": {
-            "platformBuildID": "20130626031100",
-            "appBuildID": "20130626031100",
-            "_v": 1
-          },
-          "org.mozilla.addons.active": {
-            "QuitNow@TWiGSoftware.com": null,
-            "{dbbf9331-b713-6eda-1006-205efead09dc}": null,
-            "desktopbydefault@bnicholson.mozilla.org": null,
-            "{6e092a7f-ba58-4abb-88c1-1a4e50b217e4}": null,
-            "{46551EC9-40F0-4e47-8E18-8E5CF550CFB8}": null,
-            "_v": 1
-          },
-          "org.mozilla.addons.counts": {
-            "extension": 0,
-            "plugin": 0,
-            "_v": 1
-          }
-        }
-      },
-      "data": {
-        "last": {},
-        "days": {
-          "2013-07-03": {
-            "tB4Pnnep9yTxnMDymc3dAB2RRB0=": {
-              "org.mozilla.appSessions": {
-                "normal": [
-                  {
-                    "r": "P",
-                    "d": 2,
-                    "sj": 653
-                  },
-                  {
-                    "r": "P",
-                    "d": 22
-                  },
-                  {
-                    "r": "P",
-                    "d": 5
-                  },
-                  {
-                    "r": "P",
-                    "d": 0
-                  },
-                  {
-                    "r": "P",
-                    "sg": 3560,
-                    "d": 171,
-                    "sj": 518
-                  },
-                  {
-                    "r": "P",
-                    "d": 16
-                  },
-                  {
-                    "r": "P",
-                    "d": 1079
-                  }
-                ],
-                "_v": "4"
-              }
-            },
-            "k2O3hlreMeS7L1qtxeMsYWxgWWQ=": {
-              "org.mozilla.appSessions": {
-                "normal": [
-                  {
-                    "r": "P",
-                    "d": 27
-                  },
-                  {
-                    "r": "P",
-                    "d": 19
-                  },
-                  {
-                    "r": "P",
-                    "d": 55
-                  }
-                ],
-                "_v": "4"
-              },
-              "org.mozilla.searches.counts": {
-                "bartext": {
-                  "google": 1
-                },
-                "_v": "4"
-              },
-              "org.mozilla.experiment": {
-                "lastActive": "some.experiment.id"
-                "_v": "1"
-              }
-            }
-          }
-        }
-      }
-    }
-
-App sessions in Version 3
--------------------------
-
-Sessions are divided into "normal" and "abnormal". Session objects are stored as discrete JSON::
-
-    "org.mozilla.appSessions": {
-      _v: 4,
-      "normal": [
-        {"r":"P", "d": 123},
-      ],
-      "abnormal": [
-        {"r":"A", "oom": true, "stopped": false}
-      ]
-    }
-
-Keys are:
-
-"r"
-    reason. Values are "P" (activity paused), "A" (abnormal termination).
-"d"
-    duration. Value in seconds.
-"sg"
-    Gecko startup time (msec). Present if this is a clean launch. This
-    corresponds to the telemetry timer *FENNEC_STARTUP_TIME_GECKOREADY*.
-"sj"
-    Java activity init time (msec). Present if this is a clean launch. This
-    corresponds to the telemetry timer *FENNEC_STARTUP_TIME_JAVAUI*,
-    and includes initialization tasks beyond initial
-    *onWindowFocusChanged*.
-
-Abnormal terminations will be missing a duration and will feature these keys:
-
-"oom"
-    was the session killed by an OOM exception?
-"stopped"
-    was the session stopped gently?
-
-Version 3.2
------------
-
-As of Firefox 35, the search counts measurement is now bumped to v6, including the *activity* location for the search activity.
-
-Version 3.1
------------
-
-As of Firefox 27, *appinfo* is now bumped to v3, including *osLocale*,
-*appLocale* (currently always the same as *osLocale*), *distribution* (a string
-containing the distribution ID and version, separated by a colon), and
-*acceptLangIsUserSet*, an integer-boolean that describes whether the user set
-an *intl.accept_languages* preference.
-
-The search counts measurement is now at version 5, which indicates that
-non-partner searches are recorded. You'll see identifiers like "other-Foo Bar"
-rather than "other".
-
-
-Version 3.2
------------
-
-In Firefox 32, Firefox for Android includes a device configuration section
-in the environment description::
-
-    "org.mozilla.device.config": {
-      "hasHardwareKeyboard": false,
-      "screenXInMM": 58,
-      "screenLayout": 2,
-      "uiType": "default",
-      "screenYInMM": 103,
-      "_v": 1,
-      "uiMode": 1
-    }
-
-Of these, the only keys that need explanation are:
-
-uiType
-    One of "default", "smalltablet", "largetablet".
-uiMode
-    A mask of the Android *Configuration.uiMode* value, e.g.,
-    *UI_MODE_TYPE_CAR*.
-screenLayout
-    A mask of the Android *Configuration.screenLayout* value. One of the
-    *SCREENLAYOUT_SIZE_* constants.
-
-Note that screen dimensions can be incorrect due to device inaccuracies and platform limitations.
-
-Other notable differences from Version 2
-----------------------------------------
-
-* There is no default browser indicator on Android.
-* Add-ons include a *blocklistState* attribute, as returned by AddonManager.
-* Searches are now version 4, and are hierarchical: how the search was started
-  (bartext, barkeyword, barsuggest), and then counts per provider.
-
-Version 2
-=========
-
-Version 2 is the same as version 1 with the exception that it has an additional
-top-level field, *geckoAppInfo*, which contains basic application info.
-
-geckoAppInfo
-------------
-
-This field is an object that is a simple map of string keys and values
-describing basic application metadata. It is very similar to the *appinfo*
-measurement in the *last* section. The difference is this field is almost
-certainly guaranteed to exist whereas the one in the data part of the
-payload may be omitted in certain scenarios (such as catastrophic client
-error).
-
-Its keys are as follows:
-
-appBuildID
-    The build ID/date of the application. e.g. "20130314113542".
-
-version
-    The value of nsXREAppData.version. This is the application's version. e.g.
-    "21.0.0".
-
-vendor
-    The value of nsXREAppData.vendor. Can be empty an empty string. For
-    official Mozilla builds, this will be "Mozilla".
-
-name
-    The value of nsXREAppData.name. For official Firefox builds, this
-    will be "Firefox".
-
-id
-    The value of nsXREAppData.ID.
-
-platformVersion
-    The version of the Gecko platform (as opposed to the app version). For
-    Firefox, this is almost certainly equivalent to the *version* field.
-
-platformBuildID
-    The build ID/date of the Gecko platfor (as opposed to the app version).
-    This is commonly equivalent to *appBuildID*.
-
-os
-    The name of the operating system the application is running on.
-
-xpcomabi
-    The binary architecture of the build.
-
-updateChannel
-    The name of the channel used for application updates. Official Mozilla
-    builds have one of the values {release, beta, aurora, nightly}. Local and
-    test builds have *default* as the channel.
-
-Version 1
-=========
-
-Top-level Properties
---------------------
-
-The main JSON object contains the following properties:
-
-lastPingDate
-    UTC date of the last upload. If this is the first upload from this client,
-    this will not be present.
-
-thisPingDate
-    UTC date when this payload was constructed.
-
-version
-    Integer version of this payload format. Currently only 1 is defined.
-
-clientID
-    An identifier that identifies the client that is submitting data.
-
-    This property may not be present in older clients.
-
-    See :ref:`healthreport_identifiers` for more info on identifiers.
-
-clientIDVersion
-    Integer version associated with the generation semantics for the
-    ``clientID``.
-
-    If the value is ``1``, ``clientID`` is a randomly-generated UUID.
-
-    This property may not be present in older clients.
-
-data
-    Object holding data constituting health report.
-
-Data Properties
----------------
-
-The bulk of the health report is contained within the *data* object. This
-object has the following keys:
-
-days
-   Object mapping UTC days to measurements from that day. Keys are in the
-   *YYYY-MM-DD* format. e.g. "2013-03-14"
-
-last
-   Object mapping measurement names to their values.
-
-
-The value of *days* and *last* are objects mapping measurement names to that
-measurement's values. The values are always objects. Each object contains
-a *_v* property. This property defines the version of this measurement.
-Additional non-underscore-prefixed properties are defined by the measurement
-itself (see sections below).
-
-Example
--------
-
-Here is an example JSON document for version 1::
-
-    {
-      "version": 1,
-      "thisPingDate": "2013-03-11",
-      "lastPingDate": "2013-03-10",
-      "data": {
-        "last": {
-          "org.mozilla.addons.active": {
-            "masspasswordreset@johnathan.nightingale": {
-              "userDisabled": false,
-              "appDisabled": false,
-              "version": "1.05",
-              "type": "extension",
-              "scope": 1,
-              "foreignInstall": false,
-              "hasBinaryComponents": false,
-              "installDay": 14973,
-              "updateDay": 15317
-            },
-            "places-maintenance@bonardo.net": {
-              "userDisabled": false,
-              "appDisabled": false,
-              "version": "1.3",
-              "type": "extension",
-              "scope": 1,
-              "foreignInstall": false,
-              "hasBinaryComponents": false,
-              "installDay": 15268,
-              "updateDay": 15379
-            },
-            "_v": 1
-          },
-          "org.mozilla.appInfo.appinfo": {
-            "_v": 1,
-            "appBuildID": "20130309030841",
-            "distributionID": "",
-            "distributionVersion": "",
-            "hotfixVersion": "",
-            "id": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
-            "locale": "en-US",
-            "name": "Firefox",
-            "os": "Darwin",
-            "platformBuildID": "20130309030841",
-            "platformVersion": "22.0a1",
-            "updateChannel": "nightly",
-            "vendor": "Mozilla",
-            "version": "22.0a1",
-            "xpcomabi": "x86_64-gcc3"
-          },
-          "org.mozilla.profile.age": {
-            "_v": 1,
-            "profileCreation": 12444
-          },
-          "org.mozilla.appSessions.current": {
-            "_v": 3,
-            "startDay": 15773,
-            "activeTicks": 522,
-            "totalTime": 70858,
-            "main": 1245,
-            "firstPaint": 2695,
-            "sessionRestored": 3436
-          },
-          "org.mozilla.sysinfo.sysinfo": {
-            "_v": 1,
-            "cpuCount": 8,
-            "memoryMB": 16384,
-            "architecture": "x86-64",
-            "name": "Darwin",
-            "version": "12.2.1"
-          }
-        },
-        "days": {
-          "2013-03-11": {
-            "org.mozilla.addons.counts": {
-              "_v": 1,
-              "extension": 15,
-              "plugin": 12,
-              "theme": 1
-            },
-            "org.mozilla.places.places": {
-              "_v": 1,
-              "bookmarks": 757,
-              "pages": 104858
-            },
-            "org.mozilla.appInfo.appinfo": {
-              "_v": 1,
-              "isDefaultBrowser": 1
-            }
-          },
-          "2013-03-10": {
-            "org.mozilla.addons.counts": {
-              "_v": 1,
-              "extension": 15,
-              "plugin": 12,
-              "theme": 1
-            },
-            "org.mozilla.places.places": {
-              "_v": 1,
-              "bookmarks": 757,
-              "pages": 104857
-            },
-            "org.mozilla.searches.counts": {
-              "_v": 1,
-              "google.urlbar": 4
-            },
-            "org.mozilla.appInfo.appinfo": {
-              "_v": 1,
-              "isDefaultBrowser": 1
-            }
-          }
-        }
-      }
-    }
-
-Measurements
-============
-
-The bulk of payloads consists of measurement data. An individual measurement
-is merely a collection of related values e.g. *statistics about the Places
-database* or *system information*.
-
-Each measurement has an integer version number attached. When the fields in
-a measurement or the semantics of data within that measurement change, the
-version number is incremented.
-
-All measurements are defined alphabetically in the sections below.
-
-org.mozilla.addons.addons
--------------------------
-
-This measurement contains information about the currently-installed add-ons.
-
-Version 2
-^^^^^^^^^
-
-This version adds the human-readable fields *name* and *description*, both
-coming directly from the Addon instance as most properties in version 1.
-Also, all plugin details are now in org.mozilla.addons.plugins.
-
-Version 1
-^^^^^^^^^
-
-The measurement object is a mapping of add-on IDs to objects containing
-add-on metadata.
-
-Each add-on contains the following properties:
-
-* userDisabled
-* appDisabled
-* version
-* type
-* scope
-* foreignInstall
-* hasBinaryComponents
-* installDay
-* updateDay
-
-With the exception of *installDay* and *updateDay*, all these properties
-come direct from the Addon instance. See https://developer.mozilla.org/en-US/docs/Addons/Add-on_Manager/Addon.
-*installDay* and *updateDay* are the number of days since UNIX epoch of
-the add-ons *installDate* and *updateDate* properties, respectively.
-
-Notes
-^^^^^
-
-Add-ons that have opted out of AMO updates via the
-*extensions._id_.getAddons.cache.enabled* preference are, since Bug 868306
-(Firefox 24), included in the list of submitted add-ons.
-
-Example
-^^^^^^^
-::
-
-    "org.mozilla.addons.addons": {
-      "_v": 2,
-      "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}": {
-        "userDisabled": false,
-        "appDisabled": false,
-        "name": "Adblock Plus",
-        "version": "2.4.1",
-        "type": "extension",
-        "scope": 1,
-        "description": "Ads were yesterday!",
-        "foreignInstall": false,
-        "hasBinaryComponents": false,
-        "installDay": 16093,
-        "updateDay": 16093
-      },
-      "{e4a8a97b-f2ed-450b-b12d-ee082ba24781}": {
-        "userDisabled": true,
-        "appDisabled": false,
-        "name": "Greasemonkey",
-        "version": "1.14",
-        "type": "extension",
-        "scope": 1,
-        "description": "A User Script Manager for Firefox",
-        "foreignInstall": false,
-        "hasBinaryComponents": false,
-        "installDay": 16093,
-        "updateDay": 16093
-      }
-    }
-
-org.mozilla.addons.plugins
---------------------------
-
-This measurement contains information about the currently-installed plugins.
-
-Version 1
-^^^^^^^^^
-
-The measurement object is a mapping of plugin IDs to objects containing
-plugin metadata.
-
-The plugin ID is constructed of the plugins filename, name, version and
-description. Every plugin has at least a filename and a name.
-
-Each plugin contains the following properties:
-
-* name
-* version
-* description
-* blocklisted
-* disabled
-* clicktoplay
-* mimeTypes
-* updateDay
-
-With the exception of *updateDay* and *mimeTypes*, all these properties come
-directly from ``nsIPluginTag`` via ``nsIPluginHost``.
-*updateDay* is the number of days since UNIX epoch of the plugins last modified
-time.
-*mimeTypes* is the list of mimetypes the plugin supports, see
-``nsIPluginTag.getMimeTypes()``.
-
-Example
-^^^^^^^
-
-::
-
-    "org.mozilla.addons.plugins": {
-      "_v": 1,
-      "Flash Player.plugin:Shockwave Flash:12.0.0.38:Shockwave Flash 12.0 r0": {
-        "mimeTypes": [
-          "application/x-shockwave-flash",
-          "application/futuresplash"
-        ],
-        "name": "Shockwave Flash",
-        "version": "12.0.0.38",
-        "description": "Shockwave Flash 12.0 r0",
-        "blocklisted": false,
-        "disabled": false,
-        "clicktoplay": false
-      },
-      "Default Browser.plugin:Default Browser Helper:537:Provides information about the default web browser": {
-        "mimeTypes": [
-          "application/apple-default-browser"
-        ],
-        "name": "Default Browser Helper",
-        "version": "537",
-        "description": "Provides information about the default web browser",
-        "blocklisted": false,
-        "disabled": true,
-        "clicktoplay": false
-      }
-    }
-
-org.mozilla.addons.counts
--------------------------
-
-This measurement contains information about historical add-on counts.
-
-Version 1
-^^^^^^^^^
-
-The measurement object consists of counts of different add-on types. The
-properties are:
-
-extension
-    Integer count of installed extensions.
-plugin
-    Integer count of installed plugins.
-theme
-    Integer count of installed themes.
-lwtheme
-    Integer count of installed lightweight themes.
-
-Notes
-^^^^^
-
-Add-ons opted out of AMO updates are included in the counts. This differs from
-the behavior of the active add-ons measurement.
-
-If no add-ons of a particular type are installed, the property for that type
-will not be present (as opposed to an explicit property with value of 0).
-
-Example
-^^^^^^^
-
-::
-
-    "2013-03-14": {
-      "org.mozilla.addons.counts": {
-        "_v": 1,
-        "extension": 21,
-        "plugin": 4,
-        "theme": 1
-      }
-    }
-
-
-
-org.mozilla.appInfo.appinfo
----------------------------
-
-This measurement contains basic XUL application and Gecko platform
-information. It is reported in the *last* section.
-
-Version 2
-^^^^^^^^^
-
-In addition to fields present in version 1, this version has the following
-fields appearing in the *days* section:
-
-isBlocklistEnabled
-    Whether the blocklist ping is enabled. This is an integer, 0 or 1.
-    This does not indicate whether the blocklist ping was sent but merely
-    whether the application will try to send the blocklist ping.
-
-isTelemetryEnabled
-    Whether Telemetry is enabled. This is an integer, 0 or 1.
-
-Version 1
-^^^^^^^^^
-
-The measurement object contains mostly string values describing the
-current application and build. The properties are:
-
-* vendor
-* name
-* id
-* version
-* appBuildID
-* platformVersion
-* platformBuildID
-* os
-* xpcomabi
-* updateChannel
-* distributionID
-* distributionVersion
-* hotfixVersion
-* locale
-* isDefaultBrowser
-
-Notes
-^^^^^
-
-All of the properties appear in the *last* section except for
-*isDefaultBrowser*, which appears under *days*.
-
-Example
-^^^^^^^
-
-This example comes from an official macOS Nightly build::
-
-    "org.mozilla.appInfo.appinfo": {
-      "_v": 1,
-      "appBuildID": "20130311030946",
-      "distributionID": "",
-      "distributionVersion": "",
-      "hotfixVersion": "",
-      "id": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
-      "locale": "en-US",
-      "name": "Firefox",
-      "os": "Darwin",
-      "platformBuildID": "20130311030946",
-      "platformVersion": "22.0a1",
-      "updateChannel": "nightly",
-      "vendor": "Mozilla",
-      "version": "22.0a1",
-      "xpcomabi": "x86_64-gcc3"
-    },
-
-org.mozilla.appInfo.update
---------------------------
-
-This measurement contains information about the application update mechanism
-in the application.
-
-Version 1
-^^^^^^^^^
-
-The following daily values are reported:
-
-enabled
-    Whether automatic application update checking is enabled. 1 for yes,
-    0 for no.
-autoDownload
-    Whether automatic download of available updates is enabled.
-
-Notes
-^^^^^
-
-This measurement was merged to mozilla-central for JS FHR on 2013-07-15.
-
-Example
-^^^^^^^
-
-::
-
-    "2013-07-15": {
-      "org.mozilla.appInfo.update": {
-        "_v": 1,
-        "enabled": 1,
-        "autoDownload": 1,
-      }
-    }
-
-org.mozilla.appInfo.versions
-----------------------------
-
-This measurement contains a history of application version numbers.
-
-Version 2
-^^^^^^^^^
-
-Version 2 reports more fields than version 1 and is not backwards compatible.
-The following fields are present in version 2:
-
-appVersion
-    An array of application version strings.
-appBuildID
-    An array of application build ID strings.
-platformVersion
-    An array of platform version strings.
-platformBuildID
-    An array of platform build ID strings.
-
-When the application is upgraded, the new version and/or build IDs are
-appended to their appropriate fields.
-
-Version 1
-^^^^^^^^^
-
-When the application version (*version* from *org.mozilla.appinfo.appinfo*)
-changes, we record the new version on the day the change was seen. The new
-versions for a day are recorded in an array under the *version* property.
-
-Notes
-^^^^^
-
-If the application isn't upgraded, this measurement will not be present.
-This means this measurement will not be present for most days if a user is
-on the release channel (since updates are typically released every 6 weeks).
-However, users on the Nightly and Aurora channels will likely have a lot
-of these entries since those builds are updated every day.
-
-Values for this measurement are collected when performing the daily
-collection (typically occurs at upload time). As a result, it's possible
-the actual upgrade day may not be attributed to the proper day - the
-reported day may lag behind.
-
-The app and platform versions and build IDs should be identical for most
-clients. If they are different, we are possibly looking at a *Frankenfox*.
-
-Example
-^^^^^^^
-
-::
-
-    "2013-03-27": {
-      "org.mozilla.appInfo.versions": {
-        "_v": 2,
-        "appVersion": [
-           "22.0.0"
-        ],
-        "appBuildID": [
-          "20130325031100"
-        ],
-        "platformVersion": [
-          "22.0.0"
-        ],
-        "platformBuildID": [
-          "20130325031100"
-        ]
-      }
-    }
-
-org.mozilla.appSessions.current
--------------------------------
-
-This measurement contains information about the currently running XUL
-application's session.
-
-Version 3
-^^^^^^^^^
-
-This measurement has the following properties:
-
-startDay
-    Integer days since UNIX epoch when this session began.
-activeTicks
-    Integer count of *ticks* the session was active for. Gecko periodically
-    sends out a signal when the session is active. Session activity
-    involves keyboard or mouse interaction with the application. Each tick
-    represents a window of 5 seconds where there was interaction.
-totalTime
-    Integer seconds the session has been alive.
-main
-    Integer milliseconds it took for the Gecko process to start up.
-firstPaint
-    Integer milliseconds from process start to first paint.
-sessionRestored
-    Integer milliseconds from process start to session restore.
-
-Example
-^^^^^^^
-
-::
-
-    "org.mozilla.appSessions.current": {
-      "_v": 3,
-      "startDay": 15775,
-      "activeTicks": 4282,
-      "totalTime": 249422,
-      "main": 851,
-      "firstPaint": 3271,
-      "sessionRestored": 5998
-    }
-
-org.mozilla.appSessions.previous
---------------------------------
-
-This measurement contains information about previous XUL application sessions.
-
-Version 3
-^^^^^^^^^
-
-This measurement contains per-day lists of all the sessions started on that
-day. The following properties may be present:
-
-cleanActiveTicks
-    Active ticks of sessions that were properly shut down.
-cleanTotalTime
-    Total number of seconds for sessions that were properly shut down.
-abortedActiveTicks
-    Active ticks of sessions that were not properly shut down.
-abortedTotalTime
-    Total number of seconds for sessions that were not properly shut down.
-main
-    Time in milliseconds from process start to main process initialization.
-firstPaint
-    Time in milliseconds from process start to first paint.
-sessionRestored
-    Time in milliseconds from process start to session restore.
-
-Notes
-^^^^^
-
-Sessions are recorded on the date on which they began.
-
-If a session was aborted/crashed, the total time may be less than the actual
-total time. This is because we don't always update total time during periods
-of inactivity and the abort/crash could occur after a long period of idle,
-before we've updated the total time.
-
-The lengths of the arrays for {cleanActiveTicks, cleanTotalTime},
-{abortedActiveTicks, abortedTotalTime}, and {main, firstPaint, sessionRestored}
-should all be identical.
-
-The length of the clean sessions plus the length of the aborted sessions should
-be equal to the length of the {main, firstPaint, sessionRestored} properties.
-
-It is not possible to distinguish the main, firstPaint, and sessionRestored
-values from a clean vs aborted session: they are all lumped together.
-
-For sessions spanning multiple UTC days, it's not possible to know which
-days the session was active for. It's possible a week long session only
-had activity for 2 days and there's no way for us to tell which days.
-
-Example
-^^^^^^^
-
-::
-
-    "org.mozilla.appSessions.previous": {
-      "_v": 3,
-      "cleanActiveTicks": [
-        78,
-        1785
-      ],
-      "cleanTotalTime": [
-        4472,
-        88908
-      ],
-      "main": [
-        32,
-        952
-      ],
-      "firstPaint": [
-        2755,
-        3497
-      ],
-      "sessionRestored": [
-        5149,
-        5520
-      ]
-    }
-
-org.mozilla.crashes.crashes
----------------------------
-
-This measurement contains a historical record of application crashes.
-
-Version 6
-^^^^^^^^^
-
-This version adds tracking for out-of-memory (OOM) crashes in the main process.
-An OOM crash will be counted as both main-crash and main-crash-oom.
-
-This measurement will be reported on each day there was a crash or crash
-submission. Records may contain the following fields, whose values indicate
-the number of crashes, hangs, or submissions that occurred on the given day:
-
-* content-crash
-* content-crash-submission-succeeded
-* content-crash-submission-failed
-* content-hang
-* content-hang-submission-succeeded
-* content-hang-submission-failed
-* gmplugin-crash
-* gmplugin-crash-submission-succeeded
-* gmplugin-crash-submission-failed
-* main-crash
-* main-crash-oom
-* main-crash-submission-succeeded
-* main-crash-submission-failed
-* main-hang
-* main-hang-submission-succeeded
-* main-hang-submission-failed
-* plugin-crash
-* plugin-crash-submission-succeeded
-* plugin-crash-submission-failed
-* plugin-hang
-* plugin-hang-submission-succeeded
-* plugin-hang-submission-failed
-
-Version 5
-^^^^^^^^^
-
-This version adds support for Gecko media plugin (GMP) crashes.
-
-This measurement will be reported on each day there was a crash or crash
-submission. Records may contain the following fields, whose values indicate
-the number of crashes, hangs, or submissions that occurred on the given day:
-
-* content-crash
-* content-crash-submission-succeeded
-* content-crash-submission-failed
-* content-hang
-* content-hang-submission-succeeded
-* content-hang-submission-failed
-* gmplugin-crash
-* gmplugin-crash-submission-succeeded
-* gmplugin-crash-submission-failed
-* main-crash
-* main-crash-submission-succeeded
-* main-crash-submission-failed
-* main-hang
-* main-hang-submission-succeeded
-* main-hang-submission-failed
-* plugin-crash
-* plugin-crash-submission-succeeded
-* plugin-crash-submission-failed
-* plugin-hang
-* plugin-hang-submission-succeeded
-* plugin-hang-submission-failed
-
-Version 4
-^^^^^^^^^
-
-This version follows up from version 3, adding submissions which are now
-tracked by the :ref:`crashes_crashmanager`.
-
-This measurement will be reported on each day there was a crash or crash
-submission. Records may contain the following fields, whose values indicate
-the number of crashes, hangs, or submissions that occurred on the given day:
-
-* main-crash
-* main-crash-submission-succeeded
-* main-crash-submission-failed
-* main-hang
-* main-hang-submission-succeeded
-* main-hang-submission-failed
-* content-crash
-* content-crash-submission-succeeded
-* content-crash-submission-failed
-* content-hang
-* content-hang-submission-succeeded
-* content-hang-submission-failed
-* plugin-crash
-* plugin-crash-submission-succeeded
-* plugin-crash-submission-failed
-* plugin-hang
-* plugin-hang-submission-succeeded
-* plugin-hang-submission-failed
-
-Version 3
-^^^^^^^^^
-
-This version follows up from version 2, building on improvements to
-the :ref:`crashes_crashmanager`.
-
-This measurement will be reported on each day there was a
-crash. Records may contain the following fields, whose values indicate
-the number of crashes or hangs that occurred on the given day:
-
-* main-crash
-* main-hang
-* content-crash
-* content-hang
-* plugin-crash
-* plugin-hang
-
-Version 2
-^^^^^^^^^
-
-The switch to version 2 coincides with the introduction of the
-:ref:`crashes_crashmanager`, which provides a more robust source of
-crash data.
-
-This measurement will be reported on each day there was a crash. The
-following fields may be present in each record:
-
-mainCrash
-   The number of main process crashes that occurred on the given day.
-
-Yes, version 2 does not track submissions like version 1. It is very
-likely submissions will be re-added later.
-
-Also absent from version 2 are plugin crashes and hangs. These will be
-re-added, likely in version 3.
-
-Version 1
-^^^^^^^^^
-
-This measurement will be reported on each day there was a crash. The
-following properties are reported:
-
-pending
-    The number of crash reports that haven't been submitted.
-submitted
-    The number of crash reports that were submitted.
-
-Notes
-^^^^^
-
-Main process crashes are typically submitted immediately after they
-occur (by checking a box in the crash reporter, which should appear