merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 02 Jul 2014 15:00:10 +0200
changeset 191874 f58734605afd7063b2d02715075ce9f1303c838c
parent 191830 49e7fc49dd4e1e2bb9a0bd52c63907fa6a98f267 (current diff)
parent 191873 5bc01fa51b243d41b15c316c3e8b2864ab91eb16 (diff)
child 191901 e82a9700f94b386bed26a45704a4177a3f251141
push id45685
push usercbook@mozilla.com
push dateWed, 02 Jul 2014 13:09:48 +0000
treeherdermozilla-inbound@60133a85f8ae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -777,17 +777,17 @@ let CustomizableUIInternal = {
 
       let container = areaNode.customizationTarget;
       let widgetNode = window.document.getElementById(aWidgetId);
       if (widgetNode && isOverflowable) {
         container = areaNode.overflowable.getContainerFor(widgetNode);
       }
 
       if (!widgetNode || !container.contains(widgetNode)) {
-        INFO("Widget not found, unable to remove");
+        INFO("Widget " + aWidgetId + " not found, unable to remove from " + aArea);
         continue;
       }
 
       this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true);
 
       // We remove location attributes here to make sure they're gone too when a
       // widget is removed from a toolbar to the palette. See bug 930950.
       this.removeLocationAttributes(widgetNode);
--- a/browser/devtools/canvasdebugger/test/browser.ini
+++ b/browser/devtools/canvasdebugger/test/browser.ini
@@ -1,25 +1,27 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   doc_simple-canvas.html
   doc_simple-canvas-bitmasks.html
   doc_simple-canvas-deep-stack.html
   doc_simple-canvas-transparent.html
+  doc_webgl-enum.html
   head.js
 
 [browser_canvas-actor-test-01.js]
 [browser_canvas-actor-test-02.js]
 [browser_canvas-actor-test-03.js]
 [browser_canvas-actor-test-04.js]
 [browser_canvas-actor-test-05.js]
 [browser_canvas-actor-test-06.js]
 [browser_canvas-actor-test-07.js]
 [browser_canvas-actor-test-08.js]
+[browser_canvas-actor-test-09.js]
 [browser_canvas-frontend-call-highlight.js]
 [browser_canvas-frontend-call-list.js]
 [browser_canvas-frontend-call-search.js]
 [browser_canvas-frontend-call-stack-01.js]
 [browser_canvas-frontend-call-stack-02.js]
 [browser_canvas-frontend-call-stack-03.js]
 [browser_canvas-frontend-clear.js]
 [browser_canvas-frontend-img-screenshots.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/canvasdebugger/test/browser_canvas-actor-test-09.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that integers used in arguments are not cast to their constant, enum value
+ * forms if the method's signature does not expect an enum. Bug 999687.
+ */
+
+function ifTestingSupported() {
+  let [target, debuggee, front] = yield initCanavsDebuggerBackend(WEBGL_ENUM_URL);
+
+  let navigated = once(target, "navigate");
+
+  yield front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  yield navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  let snapshotActor = yield front.recordAnimationFrame();
+  let animationOverview = yield snapshotActor.getOverview();
+  let functionCalls = animationOverview.calls;
+
+  is(functionCalls[0].name, "clear",
+    "The function's name is correct.");
+  is(functionCalls[0].argsPreview, "DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT",
+    "The bits passed into `gl.clear` have been cast to their enum values.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/canvasdebugger/test/doc_webgl-enum.html
@@ -0,0 +1,33 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+  </head>
+
+  <body>
+    <canvas id="canvas" width="128" height="128"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas, gl;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+        gl.clearColor(0.0, 0.0, 0.0, 1.0);
+        drawScene();
+      }
+
+      function drawScene() {
+        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+        window.requestAnimationFrame(drawScene);
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/canvasdebugger/test/head.js
+++ b/browser/devtools/canvasdebugger/test/head.js
@@ -24,16 +24,17 @@ let TiltGL = devtools.require("devtools/
 let TargetFactory = devtools.TargetFactory;
 let Toolbox = devtools.Toolbox;
 
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/canvasdebugger/test/";
 const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
 const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.html";
 const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html";
 const SIMPLE_CANVAS_DEEP_STACK_URL = EXAMPLE_URL + "doc_simple-canvas-deep-stack.html";
+const WEBGL_ENUM_URL = EXAMPLE_URL + "doc_webgl-enum.html";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 let gToolEnabled = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
 
 registerCleanupFunction(() => {
   info("finish() was called, cleaning up...");
--- a/browser/devtools/inspector/breadcrumbs.js
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -393,16 +393,18 @@ HTMLBreadcrumbs.prototype = {
     this.container.removeEventListener("mousedown", this, true);
     this.container.removeEventListener("keypress", this, true);
     this.container = null;
 
     this.separators.remove();
     this.separators = null;
 
     this.nodeHierarchy = null;
+
+    this.isDestroyed = true;
   },
 
   /**
    * Empty the breadcrumbs container.
    */
   empty: function BC_empty()
   {
     while (this.container.hasChildNodes()) {
@@ -620,16 +622,20 @@ HTMLBreadcrumbs.prototype = {
     this.chromeWin.clearTimeout(this._ensureVisibleTimeout);
     this._ensureVisibleTimeout = this.chromeWin.setTimeout(function() {
       scrollbox.ensureElementIsVisible(element);
     }, ENSURE_SELECTION_VISIBLE_DELAY);
   },
 
   updateSelectors: function BC_updateSelectors()
   {
+    if (this.isDestroyed) {
+      return;
+    }
+
     for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
       let crumb = this.nodeHierarchy[i];
       let button = crumb.button;
 
       while(button.hasChildNodes()) {
         button.removeChild(button.firstChild);
       }
       button.appendChild(this.prettyPrintNodeAsXUL(crumb.node));
@@ -637,16 +643,20 @@ HTMLBreadcrumbs.prototype = {
     }
   },
 
   /**
    * Update the breadcrumbs display when a new node is selected.
    */
   update: function BC_update(reason)
   {
+    if (this.isDestroyed) {
+      return;
+    }
+
     if (reason !== "markupmutation") {
       this.inspector.hideNodeMenu();
     }
 
     let cmdDispatcher = this.chromeDoc.commandDispatcher;
     this.hadFocus = (cmdDispatcher.focusedElement &&
                      cmdDispatcher.focusedElement.parentNode == this.container);
 
@@ -683,16 +693,20 @@ HTMLBreadcrumbs.prototype = {
       // we select the current node button
       idx = this.indexOf(this.selection.nodeFront);
       this.setCursor(idx);
     }
 
     let doneUpdating = this.inspector.updating("breadcrumbs");
     // Add the first child of the very last node of the breadcrumbs if possible.
     this.ensureFirstChild().then(this.selectionGuard()).then(() => {
+      if (this.isDestroyed) {
+        return;
+      }
+
       this.updateSelectors();
 
       // Make sure the selected node and its neighbours are visible.
       this.scroll();
       return resolveNextTick().then(() => {
         this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
         doneUpdating();
       });
--- a/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js
@@ -1,78 +1,69 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
+"use strict";
 
-  let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+// Test that the inspector has the correct pseudo-class locking menu items and
+// that these items actually work
 
-  let pseudos = ["hover", "active", "focus"];
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+const PSEUDOS = ["hover", "active", "focus"];
 
-  let doc;
-  let div;
-  let menu;
-  let inspector;
+let test = asyncTest(function*() {
+  yield addTab("data:text/html,pseudo-class lock node menu tests");
 
-  ignoreAllUncaughtExceptions();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    doc = content.document;
-    waitForFocus(createDocument, content);
-  }, true);
+  info("Creating the test element");
+  let div = content.document.createElement("div");
+  div.textContent = "test div";
+  content.document.body.appendChild(div);
 
-  content.location = "data:text/html,pseudo-class lock node menu tests";
+  let {inspector} = yield openInspector();
+  yield selectNode(div, inspector);
 
-  function createDocument()
-  {
-    div = doc.createElement("div");
-    div.textContent = "test div";
+  info("Getting the inspector ctx menu and opening it");
+  let menu = inspector.panelDoc.getElementById("inspector-node-popup");
+  yield openMenu(menu);
 
-    doc.body.appendChild(div);
+  yield testMenuItems(div, menu, inspector);
 
-    openInspector(selectNode);
-  }
+  gBrowser.removeCurrentTab();
+});
 
-  function selectNode(aInspector)
-  {
-    inspector = aInspector;
-    inspector.selection.setNode(div);
-    inspector.once("inspector-updated", performTests);
-  }
+function openMenu(menu) {
+  let def = promise.defer();
 
-  function performTests()
-  {
-    menu = inspector.panelDoc.getElementById("inspector-node-popup");
-    menu.addEventListener("popupshowing", testMenuItems, true);
-    menu.openPopup();
-  }
+  menu.addEventListener("popupshowing", function onOpen() {
+    menu.removeEventListener("popupshowing", onOpen, true);
+    def.resolve();
+  }, true);
+  menu.openPopup();
 
-  function testMenuItems()
-  {
-    menu.removeEventListener("popupshowing", testMenuItems, true);
+  return def.promise;
+}
 
-    var tryNext = () => {
-      if (pseudos.length === 0) {
-        finishUp();
-        return;
-      }
-      let pseudo = pseudos.shift();
-
-      let menuitem = inspector.panelDoc.getElementById("node-menu-pseudo-" + pseudo);
-      ok(menuitem, ":" + pseudo + " menuitem exists");
+function* testMenuItems(div,menu, inspector) {
+  for (let pseudo of PSEUDOS) {
+    let menuitem = inspector.panelDoc.getElementById("node-menu-pseudo-" + pseudo);
+    ok(menuitem, ":" + pseudo + " menuitem exists");
 
-      menuitem.doCommand();
-      inspector.selection.once("pseudoclass", () => {
-        is(DOMUtils.hasPseudoClassLock(div, ":" + pseudo), true,
-          "pseudo-class lock has been applied");
-        tryNext();
-      });
-    }
-    tryNext();
-  }
+    // Give the inspector panels a chance to update when the pseudoclass changes
+    let onPseudo = inspector.selection.once("pseudoclass");
+    let onRefresh = inspector.once("rule-view-refreshed");
+    let onMutations = waitForMutation(inspector);
+
+    menuitem.doCommand();
 
-  function finishUp()
-  {
-    gBrowser.removeCurrentTab();
-    finish();
+    yield onPseudo;
+    yield onRefresh;
+    yield onMutations;
+
+    is(DOMUtils.hasPseudoClassLock(div, ":" + pseudo), true,
+      "pseudo-class lock has been applied");
   }
 }
+
+function waitForMutation(inspector) {
+  let def = promise.defer();
+  inspector.walker.once("mutations", def.resolve);
+  return def.promise;
+}
--- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
@@ -11,48 +11,41 @@ const TEST_URL = 'data:text/html,' +
                  '</head>' +
                  '<body>' +
                  '  <div id="parent-div">' +
                  '    <div id="div-1">test div</div>' +
                  '    <div id="div-2">test div2</div>' +
                  '  </div>' +
                  '</body>';
 
-function test() {
-  ignoreAllUncaughtExceptions();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(startTests, content);
-  }, true);
+let test = asyncTest(function*() {
+  info("Creating the test tab and opening the rule-view");
+  yield addTab(TEST_URL);
+  let {toolbox, inspector, view} = yield openRuleView();
 
-  content.location = TEST_URL;
-}
-
-let startTests = Task.async(function*() {
-  let {toolbox, inspector, view} = yield openRuleView();
+  info("Selecting the test node");
   yield selectNode("#div-1", inspector);
 
-  yield performTests(inspector, view);
-
-  yield finishUp(toolbox);
-  finish();
-});
-
-function* performTests(inspector, ruleview) {
   yield togglePseudoClass(inspector);
-  yield assertPseudoAddedToNode(inspector, ruleview);
+  yield assertPseudoAddedToNode(inspector, view);
 
   yield togglePseudoClass(inspector);
   yield assertPseudoRemovedFromNode();
-  yield assertPseudoRemovedFromView(inspector, ruleview);
+  yield assertPseudoRemovedFromView(inspector, view);
 
   yield togglePseudoClass(inspector);
-  yield testNavigate(inspector, ruleview);
-}
+  yield testNavigate(inspector, view);
+
+  info("Destroying the toolbox");
+  yield toolbox.destroy();
+  yield assertPseudoRemovedFromNode(getNode("#div-1"));
+
+  gBrowser.removeCurrentTab();
+});
+
 
 function* togglePseudoClass(inspector) {
   info("Toggle the pseudoclass, wait for it to be applied");
 
   // Give the inspector panels a chance to update when the pseudoclass changes
   let onPseudo = inspector.selection.once("pseudoclass");
   let onRefresh = inspector.once("rule-view-refreshed");
   let onMutations = waitForMutation(inspector);
@@ -134,17 +127,8 @@ function* assertPseudoRemovedFromView(in
   is(rules.length, 2, "rule view is showing 2 rules after removing lock");
 
   yield showPickerOn(getNode("#div-1"), inspector);
 
   let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
   is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
   yield inspector.toolbox.highlighter.hideBoxModel();
 }
-
-function* finishUp(toolbox) {
-  let onDestroy = gDevTools.once("toolbox-destroyed");
-  toolbox.destroy();
-  yield onDestroy;
-
-  yield assertPseudoRemovedFromNode(getNode("#div-1"));
-  gBrowser.removeCurrentTab();
-}
--- a/browser/devtools/shared/Parser.jsm
+++ b/browser/devtools/shared/Parser.jsm
@@ -213,17 +213,17 @@ SyntaxTreesPool.prototype = {
           parseResults.scriptLength = syntaxTree.length;
           parseResults.scriptOffset = syntaxTree.offset;
           results.push(parseResults);
         }
       } catch (e) {
         // Can't guarantee that the tree traversal logic is forever perfect :)
         // Language features may be added, in which case the recursive methods
         // need to be updated. If an exception is thrown here, file a bug.
-        DevToolsUtils.reportException("Syntax tree visitor for " + aUrl, e);
+        DevToolsUtils.reportException("Syntax tree visitor for " + this._url, e);
       }
     }
     this._cache.set(requestId, results);
     return results;
   },
 
   _trees: null,
   _cache: null
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -190,21 +190,23 @@ function Tooltip(doc, options) {
     })(event);
     this.panel.addEventListener("popup" + event,
       this["_onPopup" + event], false);
   }
 
   // Listen to keypress events to close the tooltip if configured to do so
   let win = this.doc.querySelector("window");
   this._onKeyPress = event => {
+    if (this.panel.hidden) {
+      return;
+    }
+
     this.emit("keypress", event.keyCode);
     if (this.options.get("closeOnKeys").indexOf(event.keyCode) !== -1) {
-      if (!this.panel.hidden) {
-        event.stopPropagation();
-      }
+      event.stopPropagation();
       this.hide();
     }
   };
   win.addEventListener("keypress", this._onKeyPress, false);
 
   // Listen to custom emitters' events to close the tooltip
   this.hide = this.hide.bind(this);
   let closeOnEvents = this.options.get("closeOnEvents");
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -220,17 +220,18 @@ StyleSheetEditor.prototype = {
   },
 
   /**
    * Start fetching the full text source for this editor's sheet.
    */
   fetchSource: function(callback) {
     return this.styleSheet.getText().then((longStr) => {
       longStr.string().then((source) => {
-        this._state.text = CssLogic.prettifyCSS(source);
+        let ruleCount = this.styleSheet.ruleCount;
+        this._state.text = CssLogic.prettifyCSS(source, ruleCount);
         this.sourceLoaded = true;
 
         if (callback) {
           callback(source);
         }
         return source;
       });
     }, e => {
--- a/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js
@@ -18,17 +18,17 @@ function test()
 
   content.location = TESTCASE_URI;
 }
 
 let editorTestedCount = 0;
 function testEditor(aEditor)
 {
   if (aEditor.styleSheet.styleSheetIndex == 0) {
-    let prettifiedSource = "body\{\r?\n\tbackground\:white;\r?\n\}\r?\n\r?\ndiv\{\r?\n\tfont\-size\:4em;\r?\n\tcolor\:red\r?\n\}\r?\n";
+    let prettifiedSource = "body\{\r?\n\tbackground\:white;\r?\n\}\r?\n\r?\ndiv\{\r?\n\tfont\-size\:4em;\r?\n\tcolor\:red\r?\n\}\r?\n\r?\nspan\{\r?\n\tcolor\:green;\r?\n\}\r?\n";
     let prettifiedSourceRE = new RegExp(prettifiedSource);
 
     ok(prettifiedSourceRE.test(aEditor.sourceEditor.getText()),
        "minified source has been prettified automatically");
     editorTestedCount++;
     let summary = gUI.editors[1].summary;
     EventUtils.synthesizeMouseAtCenter(summary, {}, gPanelWindow);
   }
--- a/browser/devtools/styleeditor/test/pretty.css
+++ b/browser/devtools/styleeditor/test/pretty.css
@@ -1,5 +1,2 @@
 
-
-body{background:white;}div{font-size:4em;color:red}
-
-
+body{background:white;}div{font-size:4em;color:red}span{color:green;}
--- a/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js
@@ -1,59 +1,52 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Testing various inplace-editor behaviors in the rule-view
-// FIXME: To be split in several test files, and some of the inplace-editor
-// focus/blur/commit/revert stuff should be factored out in head.js
 
 let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")';
 let PAGE_CONTENT = [
   '<style type="text/css">',
   '  #testid {',
   '    background-color: blue;',
   '  }',
-  '  .testclass {',
-  '    background-color: green;',
-  '  }',
   '</style>',
-  '<div id="testid" class="testclass" style="background-color:red;">Styled Node</div>'
+  '<div id="testid">Styled Node</div>'
 ].join("\n");
 
 let test = asyncTest(function*() {
   yield addTab("data:text/html,test rule view user changes");
 
   info("Creating the test document");
   content.document.body.innerHTML = PAGE_CONTENT;
 
   info("Opening the rule-view");
   let {toolbox, inspector, view} = yield openRuleView();
 
   info("Selecting the test element");
   yield selectNode("#testid", inspector);
 
-  yield testCreateNewEscape(view);
-});
-
-function* testCreateNewEscape(view) {
   info("Test creating a new property and escaping");
 
-  let elementRuleEditor = getRuleViewRuleEditor(view, 0);
+  let elementRuleEditor = getRuleViewRuleEditor(view, 1);
 
   info("Focusing a new property name in the rule-view");
   let editor = yield focusEditableField(elementRuleEditor.closeBrace);
 
   is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "The new property editor got focused.");
   let input = editor.input;
 
   info("Entering a value in the property name editor");
+  let onModifications = elementRuleEditor.rule._applyingModifications;
   input.value = "color";
+  yield onModifications;
 
   info("Pressing return to commit and focus the new value field");
   let onValueFocus = once(elementRuleEditor.element, "focus", true);
   let onModifications = elementRuleEditor.rule._applyingModifications;
   EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
   yield onValueFocus;
   yield onModifications;
 
@@ -64,21 +57,23 @@ function* testCreateNewEscape(view) {
   is(elementRuleEditor.rule.textProps.length,  2, "Created a new text property.");
   is(elementRuleEditor.propertyList.children.length, 2, "Created a property editor.");
   is(editor, inplaceEditor(textProp.editor.valueSpan), "Editing the value span now.");
 
   info("Entering a property value");
   editor.input.value = "red";
 
   info("Escaping out of the field");
+  let onModifications = elementRuleEditor.rule._applyingModifications;
   EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+  yield onModifications;
 
   info("Checking that the previous field is focused");
   let focusedElement = inplaceEditor(elementRuleEditor.rule.textProps[0].editor.valueSpan).input;
   is(focusedElement, focusedElement.ownerDocument.activeElement, "Correct element has focus");
 
   let onModifications = elementRuleEditor.rule._applyingModifications;
   EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
   yield onModifications;
 
   is(elementRuleEditor.rule.textProps.length,  1, "Removed the new text property.");
   is(elementRuleEditor.propertyList.children.length, 1, "Removed the property editor.");
-}
+});
--- a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js
@@ -1,33 +1,46 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that SVG nodes and edges were created for the Graph View.
  */
 
+let connectCount = 0;
+
 function spawnTest() {
   let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
   let { panelWin } = panel;
   let { gFront, $, $$, EVENTS } = panelWin;
 
   let started = once(gFront, "start-context");
 
   reload(target);
 
+  panelWin.on(EVENTS.CONNECT_NODE, onConnectNode);
+
   let [actors] = yield Promise.all([
     get3(gFront, "create-node"),
     waitForGraphRendered(panelWin, 3, 2)
   ]);
 
   let [destId, oscId, gainId] = actors.map(actor => actor.actorID);
 
   ok(findGraphNode(panelWin, oscId).classList.contains("type-OscillatorNode"), "found OscillatorNode with class");
   ok(findGraphNode(panelWin, gainId).classList.contains("type-GainNode"), "found GainNode with class");
   ok(findGraphNode(panelWin, destId).classList.contains("type-AudioDestinationNode"), "found AudioDestinationNode with class");
   is(findGraphEdge(panelWin, oscId, gainId).toString(), "[object SVGGElement]", "found edge for osc -> gain");
   is(findGraphEdge(panelWin, gainId, destId).toString(), "[object SVGGElement]", "found edge for gain -> dest");
 
+  yield wait(1000);
+
+  is(connectCount, 2, "Only two node connect events should be fired.");
+
+  panelWin.off(EVENTS.CONNECT_NODE, onConnectNode);
+
   yield teardown(panel);
   finish();
 }
 
+function onConnectNode () {
+  ++connectCount;
+}
--- a/browser/devtools/webaudioeditor/test/doc_simple-context.html
+++ b/browser/devtools/webaudioeditor/test/doc_simple-context.html
@@ -12,15 +12,22 @@
 
     <script type="text/javascript;version=1.8">
       "use strict";
 
       let ctx = new AudioContext();
       let osc = ctx.createOscillator();
       let gain = ctx.createGain();
       gain.gain.value = 0;
+
+      // Connect multiple times to test that it's disregarded.
       osc.connect(gain);
       gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
       osc.start(0);
     </script>
   </body>
 
 </html>
--- a/browser/devtools/webaudioeditor/webaudioeditor-controller.js
+++ b/browser/devtools/webaudioeditor/webaudioeditor-controller.js
@@ -89,29 +89,34 @@ function AudioNodeView (actor) {
 // A proxy for the underlying AudioNodeActor to fetch its type
 // and subsequently assign the type to the instance.
 AudioNodeView.prototype.getType = Task.async(function* () {
   this.type = yield this.actor.getType();
   return this.type;
 });
 
 // Helper method to create connections in the AudioNodeConnections
-// WeakMap for rendering
+// WeakMap for rendering. Returns a boolean indicating
+// if the connection was successfully created. Will return `false`
+// when the connection was previously made.
 AudioNodeView.prototype.connect = function (destination) {
-  let connections = AudioNodeConnections.get(this);
-  if (!connections) {
-    connections = [];
-    AudioNodeConnections.set(this, connections);
+  let connections = AudioNodeConnections.get(this) || new Set();
+  AudioNodeConnections.set(this, connections);
+
+  // Don't duplicate add.
+  if (!connections.has(destination)) {
+    connections.add(destination);
+    return true;
   }
-  connections.push(destination);
+  return false;
 };
 
 // Helper method to remove audio connections from the current AudioNodeView
 AudioNodeView.prototype.disconnect = function () {
-  AudioNodeConnections.set(this, []);
+  AudioNodeConnections.set(this, new Set());
 };
 
 // Returns a promise that resolves to an array of objects containing
 // both a `param` name property and a `value` property.
 AudioNodeView.prototype.getParams = function () {
   return this.actor.getParams();
 };
 
@@ -288,18 +293,20 @@ let WebAudioEditorController = {
   _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) {
     // Since node create and connect are probably executed back to back,
     // and the controller's `_onCreateNode` needs to look up type,
     // the edge creation could be called before the graph node is actually
     // created. This way, we can check and listen for the event before
     // adding an edge.
     let [source, dest] = yield waitForNodeCreation(sourceActor, destActor);
 
-    source.connect(dest);
-    window.emit(EVENTS.CONNECT_NODE, source.id, dest.id);
+    // Connect nodes, and only emit if it's a new connection.
+    if (source.connect(dest)) {
+      window.emit(EVENTS.CONNECT_NODE, source.id, dest.id);
+    }
 
     function waitForNodeCreation (sourceActor, destActor) {
       let deferred = defer();
       let source = getViewNodeByActor(sourceActor);
       let dest = getViewNodeByActor(destActor);
 
       if (!source || !dest)
         window.on(EVENTS.CREATE_NODE, function createNodeListener (_, id) {
--- a/browser/devtools/webaudioeditor/webaudioeditor-view.js
+++ b/browser/devtools/webaudioeditor/webaudioeditor-view.js
@@ -156,17 +156,17 @@ let WebAudioGraphView = {
 
     AudioNodes.forEach(node => {
       // Add node to graph
       graph.addNode(node.id, { label: node.type, id: node.id });
 
       // Add all of the connections from this node to the edge array to be added
       // after all the nodes are added, otherwise edges will attempted to be created
       // for nodes that have not yet been added
-      AudioNodeConnections.get(node, []).forEach(dest => edges.push([node, dest]));
+      AudioNodeConnections.get(node, new Set()).forEach(dest => edges.push([node, dest]));
     });
 
     edges.forEach(([node, dest]) => graph.addEdge(null, node.id, dest.id, {
       source: node.id,
       target: dest.id
     }));
 
     let renderer = new dagreD3.Renderer();
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -2090,16 +2090,18 @@ public class GeckoAppShell
             return null;
         }
     }
 
     private static ContextGetter sContextGetter;
 
     @WrapElementForJNI(allowMultithread = true)
     public static Context getContext() {
+        if (sContextGetter == null)
+            return null;
         return sContextGetter.getContext();
     }
 
     public static void setContextGetter(ContextGetter cg) {
         sContextGetter = cg;
     }
 
     public static SharedPreferences getSharedPreferences() {
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -97,16 +97,18 @@ public class GeckoApplication extends Ap
                 }
             });
         }
         GeckoConnectivityReceiver.getInstance().stop();
         GeckoNetworkManager.getInstance().stop();
     }
 
     public void onActivityResume(GeckoActivityStatus activity) {
+	GeckoAppShell.setContextGetter(this);
+
         if (mPausedGecko) {
             GeckoAppShell.sendEventToGecko(GeckoEvent.createAppForegroundingEvent());
             mPausedGecko = false;
         }
 
         final Context applicationContext = getApplicationContext();
         GeckoBatteryManager.getInstance().start(applicationContext);
         GeckoConnectivityReceiver.getInstance().start(applicationContext);
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -28,16 +28,19 @@ public class BrowserContract {
     public static final Uri HOME_AUTHORITY_URI = Uri.parse("content://" + HOME_AUTHORITY);
 
     public static final String PROFILES_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".profiles";
     public static final Uri PROFILES_AUTHORITY_URI = Uri.parse("content://" + PROFILES_AUTHORITY);
 
     public static final String READING_LIST_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.readinglist";
     public static final Uri READING_LIST_AUTHORITY_URI = Uri.parse("content://" + READING_LIST_AUTHORITY);
 
+    public static final String SEARCH_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.searchhistory";
+    public static final Uri SEARCH_HISTORY_AUTHORITY_URI = Uri.parse("content://" + SEARCH_HISTORY_AUTHORITY);
+
     public static final String PARAM_PROFILE = "profile";
     public static final String PARAM_PROFILE_PATH = "profilePath";
     public static final String PARAM_LIMIT = "limit";
     public static final String PARAM_IS_SYNC = "sync";
     public static final String PARAM_SHOW_DELETED = "show_deleted";
     public static final String PARAM_IS_TEST = "test";
     public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
     public static final String PARAM_INCREMENT_VISITS = "increment_visits";
@@ -429,14 +432,25 @@ public class BrowserContract {
         public static final String BOOKMARK_ID = "bookmark_id";
         public static final String HISTORY_ID = "history_id";
         public static final String DISPLAY = "display";
 
         public static final String TYPE = "type";
     }
 
     @RobocopTarget
+    public static final class SearchHistory implements CommonColumns, HistoryColumns {
+        private SearchHistory() {}
+
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searchhistory";
+        public static final String QUERY = "query";
+        public static final String TABLE_NAME = "searchhistory";
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(SEARCH_HISTORY_AUTHORITY_URI, "searchhistory");
+    }
+
+    @RobocopTarget
     public static final class SuggestedSites implements CommonColumns, URLColumns {
         private SuggestedSites() {}
 
         public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "suggestedsites");
     }
 }
--- a/mobile/android/base/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/db/BrowserDatabaseHelper.java
@@ -11,16 +11,17 @@ import java.util.List;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Obsolete;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.sync.Utils;
 
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.SQLException;
@@ -29,17 +30,17 @@ import android.database.sqlite.SQLiteOpe
 import android.net.Uri;
 import android.os.Build;
 import android.util.Log;
 
 
 final class BrowserDatabaseHelper extends SQLiteOpenHelper {
 
     private static final String LOGTAG = "GeckoBrowserDBHelper";
-    public static final int DATABASE_VERSION = 18;
+    public static final int DATABASE_VERSION = 19;
     public static final String DATABASE_NAME = "browser.db";
 
     final protected Context mContext;
 
     static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
     static final String TABLE_HISTORY = History.TABLE_NAME;
     static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
     static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
@@ -744,16 +745,30 @@ final class BrowserDatabaseHelper extend
         createHistoryWithFaviconsView(db);
         createCombinedViewOn16(db);
 
         createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
             R.string.bookmarks_folder_places, 0);
 
         createOrUpdateAllSpecialFolders(db);
         createReadingListTable(db);
+        createSearchHistoryTable(db);
+    }
+
+    private void createSearchHistoryTable(SQLiteDatabase db) {
+        debug("Creating " + SearchHistory.TABLE_NAME + " table");
+
+        db.execSQL("CREATE TABLE " + SearchHistory.TABLE_NAME + "(" +
+                    SearchHistory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                    SearchHistory.QUERY + " TEXT UNIQUE NOT NULL, " +
+                    SearchHistory.DATE_LAST_VISITED + " INTEGER, " +
+                    SearchHistory.VISITS + " INTEGER ) ");
+
+        db.execSQL("CREATE INDEX idx_search_history_last_visited ON " +
+                SearchHistory.TABLE_NAME + "(" + SearchHistory.DATE_LAST_VISITED + ")");
     }
 
     private void createReadingListTable(SQLiteDatabase db) {
         debug("Creating " + TABLE_READING_LIST + " table");
 
         db.execSQL("CREATE TABLE " + TABLE_READING_LIST + "(" +
                     ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                     ReadingListItems.URL + " TEXT NOT NULL, " +
@@ -1373,16 +1388,20 @@ final class BrowserDatabaseHelper extend
         } finally {
             if (cursor != null) {
                 cursor.close();
             }
             db.endTransaction();
         }
     }
 
+    private void upgradeDatabaseFrom18to19(SQLiteDatabase db) {
+        createSearchHistoryTable(db);
+    }
+
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
         debug("Upgrading browser.db: " + db.getPath() + " from " +
                 oldVersion + " to " + newVersion);
 
         // We have to do incremental upgrades until we reach the current
         // database schema version.
         for (int v = oldVersion + 1; v <= newVersion; v++) {
@@ -1449,16 +1468,20 @@ final class BrowserDatabaseHelper extend
 
                 case 17:
                     upgradeDatabaseFrom16to17(db);
                     break;
 
                 case 18:
                     upgradeDatabaseFrom17to18(db);
                     break;
+
+                case 19:
+                    upgradeDatabaseFrom18to19(db);
+                    break;
             }
         }
 
         // If an upgrade after 12->13 fails, the entire upgrade is rolled
         // back, but we can't undo the deletion of favicon_urls.db if we
         // delete this in step 13; therefore, we wait until all steps are
         // complete before removing it.
         if (oldVersion < 13 && newVersion >= 13
@@ -1561,9 +1584,9 @@ final class BrowserDatabaseHelper extend
                 bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_BOOKMARK);
             } else {
                 bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
             }
 
             bookmark.remove("folder");
         }
     }
-}
\ No newline at end of file
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/SearchHistoryProvider.java
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.db;
+
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.text.TextUtils;
+
+public class SearchHistoryProvider extends SharedBrowserDatabaseProvider {
+
+    /**
+     * Collapse whitespace.
+     */
+    private String stripWhitespace(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return "";
+        }
+
+        // Collapse whitespace
+        return query.trim().replaceAll("\\s+", " ");
+    }
+
+
+    @Override
+    public Uri insertInTransaction(Uri uri, ContentValues cv) {
+        final String query = stripWhitespace(cv.getAsString(SearchHistory.QUERY));
+
+        // We don't support inserting empty search queries.
+        if (TextUtils.isEmpty(query)) {
+            return null;
+        }
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
+        /*
+         * FIRST: Try incrementing the VISITS counter and updating the DATE_LAST_VISITED.
+         */
+        final String sql = "UPDATE " + SearchHistory.TABLE_NAME + " SET " +
+                SearchHistory.VISITS + " = " + SearchHistory.VISITS + " + 1, " +
+                SearchHistory.DATE_LAST_VISITED + " = " + System.currentTimeMillis() +
+                " WHERE " + SearchHistory.QUERY + " = ?";
+        final Cursor c = db.rawQuery(sql, new String[] { query });
+
+        try {
+            if (c.getCount() > 1) {
+                // There is a UNIQUE constraint on the QUERY column,
+                // so there should only be one match.
+                return null;
+            }
+            if (c.moveToFirst()) {
+                return ContentUris
+                    .withAppendedId(uri, c.getInt(c.getColumnIndex(SearchHistory._ID)));
+            }
+        } finally {
+            c.close();
+        }
+
+        /*
+         * SECOND: If the update failed, then insert a new record.
+         */
+        cv.put(SearchHistory.QUERY, query);
+        cv.put(SearchHistory.VISITS, 1);
+        cv.put(SearchHistory.DATE_LAST_VISITED, System.currentTimeMillis());
+
+        long id = db.insert(SearchHistory.TABLE_NAME, null, cv);
+
+        if (id < 0) {
+            return null;
+        }
+
+        return ContentUris.withAppendedId(uri, id);
+    }
+
+    @Override
+    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
+        return getWritableDatabase(uri)
+                .delete(SearchHistory.TABLE_NAME, selection, selectionArgs);
+    }
+
+    /**
+     * Since we are managing counts and the full-text db, an update
+     * could mangle the internal state. So we disable it.
+     */
+    @Override
+    public int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        throw new UnsupportedOperationException(
+                "This content provider does not support updating items");
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        String groupBy = null;
+        String having = null;
+        return getReadableDatabase(uri)
+                .query(SearchHistory.TABLE_NAME, projection, selection, selectionArgs,
+                        groupBy, having, sortOrder);
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return SearchHistory.CONTENT_TYPE;
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -145,16 +145,17 @@ gbjar.sources += [
     'db/DBUtils.java',
     'db/FormHistoryProvider.java',
     'db/HomeProvider.java',
     'db/LocalBrowserDB.java',
     'db/PasswordsProvider.java',
     'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListProvider.java',
+    'db/SearchHistoryProvider.java',
     'db/SharedBrowserDatabaseProvider.java',
     'db/SQLiteBridgeContentProvider.java',
     'db/SuggestedSites.java',
     'db/TabsProvider.java',
     'db/TopSitesCursorWrapper.java',
     'distribution/Distribution.java',
     'DoorHangerPopup.java',
     'DynamicToolbar.java',
--- a/mobile/android/base/preferences/GeckoPreferenceFragment.java
+++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java
@@ -117,17 +117,19 @@ public class GeckoPreferenceFragment ext
             return;
         }
 
         Log.v(LOGTAG, "Setting activity title to " + newTitle);
         activity.setTitle(newTitle);
 
         if (Build.VERSION.SDK_INT >= 14) {
             final ActionBar actionBar = activity.getActionBar();
-            actionBar.setTitle(newTitle);
+            if (actionBar != null) {
+                actionBar.setTitle(newTitle);
+            }
         }
     }
 
     @Override
     public void onResume() {
         // This is a little delicate. Ensure that you do nothing prior to
         // super.onResume that you wouldn't do in onCreate.
         applyLocale(Locale.getDefault());
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -135,17 +135,19 @@ OnSharedPreferenceChangeListener
 
     private void updateActionBarTitle(int title) {
         if (Build.VERSION.SDK_INT >= 14) {
             final String newTitle = getString(title);
             if (newTitle != null) {
                 Log.v(LOGTAG, "Setting action bar title to " + newTitle);
 
                 final ActionBar actionBar = getActionBar();
-                actionBar.setTitle(newTitle);
+                if (actionBar != null) {
+                    actionBar.setTitle(newTitle);
+                }
             }
         }
     }
 
     private void updateTitle(String newTitle) {
         if (newTitle != null) {
             Log.v(LOGTAG, "Setting activity title to " + newTitle);
             setTitle(newTitle);
@@ -355,17 +357,19 @@ OnSharedPreferenceChangeListener
                     return longClickListener.onLongClick(view);
                 }
                 return false;
             }
         });
 
         if (Build.VERSION.SDK_INT >= 14) {
             final ActionBar actionBar = getActionBar();
-            actionBar.setHomeButtonEnabled(true);
+            if (actionBar != null) {
+                actionBar.setHomeButtonEnabled(true);
+            }
         }
 
         // N.B., if we ever need to redisplay the locale selection UI without
         // just finishing and recreating the activity, right here we'll need to
         // capture EXTRA_SHOW_FRAGMENT_TITLE from the intent and store the title ID.
 
         // If launched from notification, explicitly cancel the notification.
         if (intentExtras != null && intentExtras.containsKey(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION)) {
@@ -972,25 +976,25 @@ OnSharedPreferenceChangeListener
             return onLocaleSelected(BrowserLocaleManager.getLanguageTag(lastLocale), (String) newValue);
         }
 
         if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) {
             setCharEncodingState(((String) newValue).equals("true"));
         } else if (PREFS_ANNOUNCEMENTS_ENABLED.equals(prefName)) {
             // Send a broadcast intent to the product announcements service, either to start or
             // to stop the repeated background checks.
-            broadcastAnnouncementsPref(GeckoAppShell.getContext(), ((Boolean) newValue).booleanValue());
+            broadcastAnnouncementsPref(this, ((Boolean) newValue).booleanValue());
         } else if (PREFS_UPDATER_AUTODOWNLOAD.equals(prefName)) {
-            org.mozilla.gecko.updater.UpdateServiceHelper.registerForUpdates(GeckoAppShell.getContext(), (String) newValue);
+            org.mozilla.gecko.updater.UpdateServiceHelper.registerForUpdates(this, (String) newValue);
         } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(prefName)) {
             // The healthreport pref only lives in Android, so we do not persist
             // to Gecko, but we do broadcast intent to the health report
             // background uploader service, which will start or stop the
             // repeated background upload attempts.
-            broadcastHealthReportUploadPref(GeckoAppShell.getContext(), ((Boolean) newValue).booleanValue());
+            broadcastHealthReportUploadPref(this, ((Boolean) newValue).booleanValue());
         } else if (PREFS_GEO_REPORTING.equals(prefName)) {
             // Translate boolean value to int for geo reporting pref.
             newValue = ((Boolean) newValue) ? 1 : 0;
         }
 
         // Send Gecko-side pref changes to Gecko
         if (isGeckoPref(prefName)) {
             PrefsHelper.setPref(prefName, newValue);
@@ -1008,17 +1012,17 @@ OnSharedPreferenceChangeListener
             final FontSizePreference fontSizePref = (FontSizePreference) preference;
             fontSizePref.setSummary(fontSizePref.getSavedFontSizeName());
         }
 
         return true;
     }
 
     private EditText getTextBox(int aHintText) {
-        EditText input = new FloatingHintEditText(GeckoAppShell.getContext());
+        EditText input = new FloatingHintEditText(this);
         int inputtype = InputType.TYPE_CLASS_TEXT;
         inputtype |= InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
         input.setInputType(inputtype);
 
         input.setHint(aHintText);
         return input;
     }
 
--- a/mobile/android/base/tests/helpers/WaitHelper.java
+++ b/mobile/android/base/tests/helpers/WaitHelper.java
@@ -22,17 +22,17 @@ import com.jayway.android.robotium.solo.
 /**
  * Provides functionality related to waiting on certain events to happen.
  */
 public final class WaitHelper {
     // TODO: Make public for when Solo.waitForCondition is used directly (i.e. do not want
     // assertion from waitFor)?
     private static final int DEFAULT_MAX_WAIT_MS = 5000;
     private static final int PAGE_LOAD_WAIT_MS = 10000;
-    private static final int CHANGE_WAIT_MS = 10000;
+    private static final int CHANGE_WAIT_MS = 15000;
 
     // TODO: via lucasr - Add ThrobberVisibilityChangeVerifier?
     private static final ChangeVerifier[] PAGE_LOAD_VERIFIERS = new ChangeVerifier[] {
         new ToolbarTitleTextChangeVerifier()
     };
 
     private static UITestContext sContext;
     private static Solo sSolo;
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -51,17 +51,17 @@ skip-if = processor == "x86"
 skip-if = android_version == "10" || processor == "x86"
 [testInputUrlBar]
 [testJarReader]
 [testLinkContextMenu]
 # [testHomeListsProvider] # see bug 952310
 [testHomeProvider]
 [testLoad]
 [testMailToContextMenu]
-[testMasterPassword]
+# [testMasterPassword] disabled for being finicky, see bug 1033013
 # disabled on 2.3; bug 979603
 # disabled on 4.0; bug 1006242
 skip-if = android_version == "10" || android_version == "15"
 [testNewTab]
 # disabled on 2.3; bug 995696
 skip-if = android_version == "10"
 [testOverscroll]
 # disabled on 2.3; bug 836818
@@ -75,16 +75,17 @@ skip-if = processor == "x86"
 [testPictureLinkContextMenu]
 [testPrefsObserver]
 [testPrivateBrowsing]
 [testPromptGridInput]
 # disabled on x86 only; bug 957185
 skip-if = processor == "x86"
 # [testReaderMode] # see bug 913254, 936224
 [testReadingListProvider]
+[testSearchHistoryProvider]
 [testSearchSuggestions]
 # disabled on x86; bug 907768
 skip-if = processor == "x86"
 [testSessionOOMSave]
 # disabled on x86 and 2.3; bug 945395
 skip-if = android_version == "10" || processor == "x86"
 [testSessionOOMRestore]
 # disabled on Android 2.3; bug 979600
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testSearchHistoryProvider.java
@@ -0,0 +1,269 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
+import org.mozilla.gecko.db.SearchHistoryProvider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class testSearchHistoryProvider extends ContentProviderTest {
+
+    // Translations of "United Kingdom" in several different languages
+    private static final String[] testStrings = {"An Ríocht Aontaithe", // Irish
+            "Angli", // Albanian
+            "Britanniarum Regnum", // Latin
+            "Britio", // Esperanto
+            "Büyük Britanya", // Turkish
+            "Egyesült Királyság", // Hungarian
+            "Erresuma Batua", // Basque
+            "Inggris Raya", // Indonesian
+            "Ir-Renju Unit", // Maltese
+            "Iso-Britannia", // Finnish
+            "Jungtinė Karalystė", // Lithuanian
+            "Lielbritānija", // Latvian
+            "Regatul Unit", // Romanian
+            "Regne Unit", // Catalan, Valencian
+            "Regno Unito", // Italian
+            "Royaume-Uni", // French
+            "Spojené království", // Czech
+            "Spojené kráľovstvo", // Slovak
+            "Storbritannia", // Norwegian
+            "Storbritannien", // Danish
+            "Suurbritannia", // Estonian
+            "Ujedinjeno Kraljevstvo", // Bosnian
+            "United Alaeze", // Igbo
+            "United Kingdom", // English
+            "Vereinigtes Königreich", // German
+            "Verenigd Koninkrijk", // Dutch
+            "Verenigde Koninkryk", // Afrikaans
+            "Vương quốc Anh", // Vietnamese
+            "Wayòm Ini", // Haitian, Haitian Creole
+            "Y Deyrnas Unedig", // Welsh
+            "Združeno kraljestvo", // Slovene
+            "Zjednoczone Królestwo", // Polish
+            "Ηνωμένο Βασίλειο", // Greek (modern)
+            "Великобритания", // Russian
+            "Нэгдсэн Вант Улс", // Mongolian
+            "Обединетото Кралство", // Macedonian
+            "Уједињено Краљевство", // Serbian
+            "Միացյալ Թագավորություն", // Armenian
+            "בריטניה", // Hebrew (modern)
+            "פֿאַראייניקטע מלכות", // Yiddish
+            "المملكة المتحدة", // Arabic
+            "برطانیہ", // Urdu
+            "پادشاهی متحده", // Persian (Farsi)
+            "यूनाइटेड किंगडम", // Hindi
+            "संयुक्त राज्य", // Nepali
+            "যুক্তরাজ্য", // Bengali, Bangla
+            "યુનાઇટેડ કિંગડમ", // Gujarati
+            "ஐக்கிய ராஜ்யம்", // Tamil
+            "สหราชอาณาจักร", // Thai
+            "ສະ​ຫະ​ປະ​ຊາ​ຊະ​ອາ​ນາ​ຈັກ", // Lao
+            "გაერთიანებული სამეფო", // Georgian
+            "イギリス", // Japanese
+            "联合王国" // Chinese
+    };
+
+
+    private static final String DB_NAME = "searchhistory.db";
+
+    /**
+     * Boilerplate alert.
+     * <p/>
+     * Make sure this method is present and that it returns a new
+     * instance of your class.
+     */
+    private static Callable<ContentProvider> sProviderFactory =
+            new Callable<ContentProvider>() {
+                @Override
+                public ContentProvider call() {
+                    return new SearchHistoryProvider();
+                }
+            };
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp(sProviderFactory, BrowserContract.SEARCH_HISTORY_AUTHORITY, DB_NAME);
+        mTests.add(new TestInsert());
+        mTests.add(new TestUnicodeQuery());
+        mTests.add(new TestTimestamp());
+        mTests.add(new TestDelete());
+        mTests.add(new TestIncrement());
+    }
+
+    public void testSearchHistory() throws Exception {
+        for (Runnable test : mTests) {
+            String testName = test.getClass().getSimpleName();
+            setTestName(testName);
+            mAsserter.dumpLog(
+                    "testBrowserProvider: Database empty - Starting " + testName + ".");
+            // Clear the db
+            mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+            test.run();
+        }
+    }
+
+    /**
+     * Verify that we can insert values into the DB, including unicode.
+     */
+    private class TestInsert extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv;
+            for (int i = 0; i < testStrings.length; i++) {
+                cv = new ContentValues();
+                cv.put(SearchHistory.QUERY, testStrings[i]);
+                mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            }
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), testStrings.length,
+                    "Should have one row for each insert");
+
+            c.close();
+        }
+    }
+
+    /**
+     * Verify that we can insert values into the DB, including unicode.
+     */
+    private class TestUnicodeQuery extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv;
+            Cursor c = null;
+            String selection = SearchHistory.QUERY + " = ?";
+
+            for (int i = 0; i < testStrings.length; i++) {
+                cv = new ContentValues();
+                cv.put(SearchHistory.QUERY, testStrings[i]);
+                mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+                c = mProvider.query(SearchHistory.CONTENT_URI, null, selection,
+                        new String[]{ testStrings[i] }, null);
+                mAsserter.is(c.getCount(), 1,
+                        "Should have one row for insert of " + testStrings[i]);
+            }
+
+            if (c != null) {
+                c.close();
+            }
+        }
+    }
+
+    /**
+     * Verify that timestamps are updated on insert.
+     */
+    private class TestTimestamp extends TestCase {
+        @Override
+        public void test() throws Exception {
+            String insertedTerm = "Courtside Seats";
+            long insertStart;
+            long insertFinish;
+            long t1Db;
+            long t2Db;
+
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+
+            // First check that the DB has a value that is close to the
+            // system time.
+            insertStart = System.currentTimeMillis();
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            c.moveToFirst();
+            t1Db = c.getLong(c.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+            c.close();
+            insertFinish = System.currentTimeMillis();
+            mAsserter.ok(t1Db >= insertStart, "DATE_LAST_VISITED",
+                    "Date last visited should be set on insert.");
+            mAsserter.ok(t1Db <= insertFinish, "DATE_LAST_VISITED",
+                    "Date last visited should be set on insert.");
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+
+            insertStart = System.currentTimeMillis();
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            c.moveToFirst();
+            t2Db = c.getLong(c.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+            c.close();
+            insertFinish = System.currentTimeMillis();
+
+            mAsserter.ok(t2Db >= insertStart, "DATE_LAST_VISITED",
+                    "Date last visited should be set on insert.");
+            mAsserter.ok(t2Db <= insertFinish, "DATE_LAST_VISITED",
+                    "Date last visited should be set on insert.");
+            mAsserter.ok(t2Db > t1Db, "DATE_LAST_VISITED",
+                    "Date last visited should be updated on key increment.");
+        }
+    }
+
+    /**
+     * Verify that sending a delete command empties the database.
+     */
+    private class TestDelete extends TestCase {
+        @Override
+        public void test() throws Exception {
+            String insertedTerm = "Courtside Seats";
+
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 1, "Should have one value");
+            mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+            c.close();
+
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 0, "Should be empty");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            c.close();
+
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 1, "Should have one value");
+            c.close();
+        }
+    }
+
+
+    /**
+     * Ensure that we only increment when the case matches.
+     */
+    private class TestIncrement extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            c.moveToFirst();
+            mAsserter.is(c.getCount(), 1, "Should have one result");
+            mAsserter.is(c.getInt(c.getColumnIndex(SearchHistory.VISITS)), 2,
+                    "Counter should be 2");
+            c.close();
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "Omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 2, "Should have two results");
+            c.close();
+        }
+    }
+}
--- a/mobile/locales/en-US/searchplugins/yahoo.xml
+++ b/mobile/locales/en-US/searchplugins/yahoo.xml
@@ -11,12 +11,12 @@
   <Param name="output" value="fxjson" />
   <Param name="appid" value="ffm" />
   <Param name="command" value="{searchTerms}" />
   <MozParam name="nresults" condition="pref" pref="maxSuggestions" />
 </Url>
 <Url type="text/html" method="GET" template="https://search.yahoo.com/search">
   <Param name="p" value="{searchTerms}" />
   <Param name="ei" value="UTF-8" />
-  <Param name="fr" value="mozilla_mobile_search" />
+  <Param name="fr" value="mozmob1" />
 </Url>
 <SearchForm>https://search.yahoo.com/</SearchForm>
 </SearchPlugin>
--- a/toolkit/devtools/discovery/discovery.js
+++ b/toolkit/devtools/discovery/discovery.js
@@ -324,16 +324,19 @@ Discovery.prototype = {
   },
 
   _onRemoteUpdate: function(e, update) {
     log("GOT REMOTE UPDATE");
 
     let remoteDevice = update.device;
     let remoteHost = update.from;
 
+    // Record the reply as received so it won't be purged as missing
+    this._expectingReplies.from.delete(remoteDevice);
+
     // First, loop over the known services
     for (let service in this.remoteServices) {
       let devicesWithService = this.remoteServices[service];
       let hadServiceForDevice = !!devicesWithService[remoteDevice];
       let haveServiceForDevice = service in update.services;
       // If the remote device used to have service, but doesn't any longer, then
       // it was deleted, so we remove it here.
       if (hadServiceForDevice && !haveServiceForDevice) {
--- a/toolkit/devtools/discovery/tests/unit/test_discovery.js
+++ b/toolkit/devtools/discovery/tests/unit/test_discovery.js
@@ -111,16 +111,19 @@ add_task(function*() {
             { tux: true,  host: "localhost" });
 
   discovery.removeService("devtools");
   yield scanForChange("devtools", "removed");
 
   discovery.addService("penguins", { tux: false });
   yield scanForChange("penguins", "updated");
 
+  // Scan again, but nothing should be removed
+  yield scanForNoChange("penguins", "removed");
+
   // Split the scanning side from the service side to simulate the machine with
   // the service becoming unreachable
   gTestTransports = {};
 
   discovery.removeService("penguins");
   yield scanForChange("penguins", "removed");
 });
 
@@ -132,8 +135,22 @@ function scanForChange(service, changeTy
   discovery.on(service + "-device-" + changeType, function onChange() {
     discovery.off(service + "-device-" + changeType, onChange);
     clearTimeout(timer);
     deferred.resolve();
   });
   discovery.scan();
   return deferred.promise;
 }
+
+function scanForNoChange(service, changeType) {
+  let deferred = promise.defer();
+  let timer = setTimeout(() => {
+    deferred.resolve();
+  }, discovery.replyTimeout + 500);
+  discovery.on(service + "-device-" + changeType, function onChange() {
+    discovery.off(service + "-device-" + changeType, onChange);
+    clearTimeout(timer);
+    deferred.reject(new Error("Unexpected change occurred"));
+  });
+  discovery.scan();
+  return deferred.promise;
+}
--- a/toolkit/devtools/server/actors/call-watcher.js
+++ b/toolkit/devtools/server/actors/call-watcher.js
@@ -217,17 +217,17 @@ let FunctionCallActor = protocol.ActorCl
         return "Function";
       }
       if (typeof arg == "object") {
         return "Object";
       }
       // If this argument matches the method's signature
       // and is an enum, change it to its constant name.
       if (enumArgs && enumArgs.indexOf(i) !== -1) {
-        return getEnumsLookupTable(global, caller)[arg] || arg;
+        return getBitToEnumValue(global, caller, arg);
       }
       return arg;
     });
 
     return serializeArgs().join(", ");
   }
 });
 
@@ -636,24 +636,53 @@ CallWatcherFront.ENUM_METHODS[CallWatche
  * assuming they look LIKE_THIS most of the time.
  *
  * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed
  * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT".
  */
 var gEnumRegex = /^[A-Z_]+$/;
 var gEnumsLookupTable = {};
 
-function getEnumsLookupTable(type, object) {
-  let cachedEnum = gEnumsLookupTable[type];
-  if (cachedEnum) {
-    return cachedEnum;
-  }
+// These values are returned from errors, or empty values,
+// and need to be ignored when checking arguments due to the bitwise math.
+var INVALID_ENUMS = [
+  "INVALID_ENUM", "NO_ERROR", "INVALID_VALUE", "OUT_OF_MEMORY", "NONE"
+];
+
+function getBitToEnumValue(type, object, arg) {
+  let table = gEnumsLookupTable[type];
 
-  let table = gEnumsLookupTable[type] = {};
+  // If mapping not yet created, do it on the first run.
+  if (!table) {
+    table = gEnumsLookupTable[type] = {};
 
-  for (let key in object) {
-    if (key.match(gEnumRegex)) {
-      table[object[key]] = key;
+    for (let key in object) {
+      if (key.match(gEnumRegex)) {
+        // Maps `16384` to `"COLOR_BUFFER_BIT"`, etc.
+        table[object[key]] = key;
+      }
     }
   }
 
-  return table;
+  // If a single bit value, just return it.
+  if (table[arg]) {
+    return table[arg];
+  }
+
+  // Otherwise, attempt to reduce it to the original bit flags:
+  // `16640` -> "COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT"
+  let flags = [];
+  for (let flag in table) {
+    if (INVALID_ENUMS.indexOf(table[flag]) !== -1) {
+      continue;
+    }
+
+    // Cast to integer as all values are stored as strings
+    // in `table`
+    flag = flag | 0;
+    if (flag && (arg & flag) === flag) {
+      flags.push(table[flag]);
+    }
+  }
+
+  // Cache the combined bitmask value
+  return table[arg] = flags.join(" | ") || arg;
 }
--- a/toolkit/devtools/styleinspector/css-logic.js
+++ b/toolkit/devtools/styleinspector/css-logic.js
@@ -896,25 +896,31 @@ const TAB_CHARS = "\t";
 
 /**
  * Prettify minified CSS text.
  * This prettifies CSS code where there is no indentation in usual places while
  * keeping original indentation as-is elsewhere.
  * @param string text The CSS source to prettify.
  * @return string Prettified CSS source
  */
-CssLogic.prettifyCSS = function(text) {
+CssLogic.prettifyCSS = function(text, ruleCount) {
   if (CssLogic.LINE_SEPARATOR == null) {
     let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
     CssLogic.LINE_SEPARATOR = (os === "WINNT" ? "\r\n" : "\n");
   }
 
   // remove initial and terminating HTML comments and surrounding whitespace
   text = text.replace(/(?:^\s*<!--[\r\n]*)|(?:\s*-->\s*$)/g, "");
 
+  // don't attempt to prettify if there's more than one line per rule.
+  let lineCount = text.split("\n").length - 1;
+  if (ruleCount !== null && lineCount >= ruleCount) {
+    return text;
+  }
+
   let parts = [];    // indented parts
   let partStart = 0; // start offset of currently parsed part
   let indent = "";
   let indentLevel = 0;
 
   for (let i = 0; i < text.length; i++) {
     let c = text[i];
     let shouldIndent = false;
--- a/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul
@@ -18,25 +18,29 @@
 
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_ERROR_PATCHING,
   buttonClick: "next"
 }, {
-  pageid: PAGEID_DOWNLOADING
+  pageid: PAGEID_DOWNLOADING,
+  extraStartFunction: createContinueFile
 }, {
   pageid: PAGEID_FINISHED,
-  buttonClick: "extra1"
+  buttonClick: "extra1",
+  extraStartFunction: removeContinueFile
 } ];
 
 function runTest() {
   debugDump("entering");
 
+  removeContinueFile();
+
   // Specify the url to update.sjs with a slowDownloadMar param so the ui can
   // load before the download completes.
   let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1";
   let patches = getLocalPatchString("partial", null, null, null, null, null,
                                     STATE_PENDING) +
                 getLocalPatchString("complete", slowDownloadURL, null, null,
                                     null, "false");
   let updates = getLocalUpdateString(patches, null, null, null,
--- a/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul
@@ -11,32 +11,37 @@
 <window title="Update Wizard pages: error patching, download, and errors (partial failed and download complete verification failure)"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="runTestDefault();">
 <script type="application/javascript"
         src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
 <script type="application/javascript"
         src="utils.js"/>
 
+<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_ERROR_PATCHING,
   buttonClick: "next"
 }, {
-  pageid: PAGEID_DOWNLOADING
+  pageid: PAGEID_DOWNLOADING,
+  extraStartFunction: createContinueFile
 }, {
   pageid: PAGEID_ERRORS,
-  buttonClick: "finish"
+  buttonClick: "finish",
+  extraStartFunction: removeContinueFile
 } ];
 
 function runTest() {
   debugDump("entering");
 
+  removeContinueFile();
+
   // Specify the url to update.sjs with a slowDownloadMar param so the ui can
   // load before the download completes.
   let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1";
   let patches = getLocalPatchString("partial", null, null, null, null, null,
                                     STATE_PENDING) +
                 getLocalPatchString("complete", slowDownloadURL, "MD5",
                                     "1234cd43a1c77e30191c53a329a3f99d", null,
                                     "false");
--- a/toolkit/mozapps/update/tests/chrome/update.sjs
+++ b/toolkit/mozapps/update/tests/chrome/update.sjs
@@ -39,33 +39,47 @@ function handleRequest(aRequest, aRespon
   }
 
   // When a mar download is started by the update service it can finish
   // downloading before the ui has loaded. By specifying a serviceURL for the
   // update patch that points to this file and has a slowDownloadMar param the
   // mar will be downloaded asynchronously which will allow the ui to load
   // before the download completes.
   if (params.slowDownloadMar) {
+    var i;
     aResponse.processAsync();
     aResponse.setHeader("Content-Type", "binary/octet-stream");
     aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR);
+    var continueFile = AUS_Cc["@mozilla.org/file/directory_service;1"].
+                       getService(AUS_Ci.nsIProperties).
+                       get("CurWorkD", AUS_Ci.nsILocalFile);
+    var continuePath = REL_PATH_DATA + "continue";
+    var continuePathParts = continuePath.split("/");
+    for (i = 0; i < continuePathParts.length; ++i) {
+      continueFile.append(continuePathParts[i]);
+    }
+
     var marFile = AUS_Cc["@mozilla.org/file/directory_service;1"].
                   getService(AUS_Ci.nsIProperties).
                   get("CurWorkD", AUS_Ci.nsILocalFile);
     var path = REL_PATH_DATA + FILE_SIMPLE_MAR;
     var pathParts = path.split("/");
-    for(var i = 0; i < pathParts.length; ++i)
+    for (i = 0; i < pathParts.length; ++i) {
       marFile.append(pathParts[i]);
+    }
     var contents = readFileBytes(marFile);
     gTimer = AUS_Cc["@mozilla.org/timer;1"].
              createInstance(AUS_Ci.nsITimer);
     gTimer.initWithCallback(function(aTimer) {
-      aResponse.write(contents);
-      aResponse.finish();
-    }, SLOW_MAR_DOWNLOAD_INTERVAL, AUS_Ci.nsITimer.TYPE_ONE_SHOT);
+      if (continueFile.exists()) {
+        gTimer.cancel();
+        aResponse.write(contents);
+        aResponse.finish();
+      }
+    }, SLOW_MAR_DOWNLOAD_INTERVAL, AUS_Ci.nsITimer.TYPE_REPEATING_SLACK);
     return;
   }
 
   if (params.uiURL) {
     var remoteType = "";
     if (!params.remoteNoTypeAttr &&
         (params.uiURL == "BILLBOARD" || params.uiURL == "LICENSE")) {
       remoteType = " " + params.uiURL.toLowerCase() + "=\"1\"";
--- a/toolkit/mozapps/update/tests/chrome/utils.js
+++ b/toolkit/mozapps/update/tests/chrome/utils.js
@@ -478,29 +478,68 @@ function delayedDefaultCallback() {
     gTest.extraDelayedCheckFunction();
   }
 
   // Used to verify that this test has been performed
   gTest.ranTest = true;
 
   if (gTest.buttonClick) {
     debugDump("clicking " + gTest.buttonClick + " button");
-    if(gTest.extraDelayedFinishFunction) {
+    if (gTest.extraDelayedFinishFunction) {
       throw("Tests cannot have a buttonClick and an extraDelayedFinishFunction property");
     }
     gDocElem.getButton(gTest.buttonClick).click();
   }
   else if (gTest.extraDelayedFinishFunction) {
     debugDump("calling extraDelayedFinishFunction " +
               gTest.extraDelayedFinishFunction.name);
     gTest.extraDelayedFinishFunction();
   }
 }
 
 /**
+ * Gets the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests without creating it.
+ *
+ * @return nsILocalFile for the continue file.
+ */
+function getContinueFile() {
+  let continueFile = AUS_Cc["@mozilla.org/file/directory_service;1"].
+                     getService(AUS_Ci.nsIProperties).
+                     get("CurWorkD", AUS_Ci.nsILocalFile);
+  let continuePath = REL_PATH_DATA + "/continue";
+  let continuePathParts = continuePath.split("/");
+  for (let i = 0; i < continuePathParts.length; ++i) {
+    continueFile.append(continuePathParts[i]);
+  }
+  return continueFile;
+}
+
+/**
+ * Creates the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests.
+ */
+function createContinueFile() {
+  debugDump("creating 'continue' file for slow mar downloads");
+  writeFile(getContinueFile(), "");
+}
+
+/**
+ * Removes the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests.
+ */
+function removeContinueFile() {
+  let continueFile = getContinueFile();
+  if (continueFile.exists()) {
+    debugDump("removing 'continue' file for slow mar downloads");
+    continueFile.remove(false);
+  }
+}
+
+/**
  * Checks the wizard page buttons' disabled and hidden attributes values are
  * correct. If an expected button id is not specified then the expected disabled
  * and hidden attribute value is true.
  */
 function checkButtonStates() {
   debugDump("entering - TESTS[" + gTestCounter + "], pageid: " + gTest.pageid);
 
   const buttonNames = ["extra1", "extra2", "back", "next", "finish", "cancel"];