Bug 756888 - Rollup of a bunch of small GCLI changes; r=dcamp
authorJoe Walker <jwalker@mozilla.com>
Tue, 22 May 2012 08:50:02 +0100
changeset 94482 a84e147b4d22cc2562c95f34420fb9c70bda0085
parent 94481 c72ca7cdac11a702e3bd1285900f7f7fbe913060
child 94483 b1dc93af542d380daa03f1afabe842ba583bc721
push id777
push userjwalker@mozilla.com
push dateTue, 22 May 2012 07:50:52 +0000
treeherderfx-team@a84e147b4d22 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs756888
milestone15.0a1
Bug 756888 - Rollup of a bunch of small GCLI changes; r=dcamp
browser/devtools/commandline/gcli.jsm
browser/devtools/commandline/gcliblank.xhtml
browser/devtools/commandline/gclioutput.xhtml
browser/devtools/commandline/gclitooltip.xhtml
browser/devtools/jar.mn
browser/devtools/shared/DeveloperToolbar.jsm
browser/devtools/shared/test/Makefile.in
browser/devtools/shared/test/browser_toolbar_basic.js
browser/devtools/shared/test/browser_toolbar_tooltip.js
browser/devtools/shared/test/head.js
browser/locales/en-US/chrome/browser/browser.dtd
--- a/browser/devtools/commandline/gcli.jsm
+++ b/browser/devtools/commandline/gcli.jsm
@@ -6196,17 +6196,17 @@ var eagerHelperSettingSpec = {
   type: {
     name: 'selection',
     lookup: [
       { name: 'never', value: Eagerness.NEVER },
       { name: 'sometimes', value: Eagerness.SOMETIMES },
       { name: 'always', value: Eagerness.ALWAYS },
     ]
   },
-  defaultValue: 1,
+  defaultValue: Eagerness.SOMETIMES,
   description: l10n.lookup('eagerHelperDesc'),
   ignoreTypeDifference: true
 };
 var eagerHelper;
 
 /**
  * Register (and unregister) the hide-intro setting
  */
@@ -6341,17 +6341,18 @@ FocusManager.prototype.removeMonitoredEl
 };
 
 /**
  * Monitor for new command executions
  */
 FocusManager.prototype.updatePosition = function(dimensions) {
   var ev = {
     tooltipVisible: this.isTooltipVisible,
-    outputVisible: this.isOutputVisible
+    outputVisible: this.isOutputVisible,
+    dimensions: dimensions
   };
   this.onVisibilityChange(ev);
 };
 
 /**
  * Monitor for new command executions
  */
 FocusManager.prototype._outputted = function(ev) {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/gclioutput.xhtml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"
+  [
+    <!ENTITY % webConsoleDTD SYSTEM "chrome://browser/locale/devtools/webConsole.dtd">
+    %webConsoleDTD;
+  ]
+>
+
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (the "License"); you may not use this file except in compliance with
+   - the License. You may obtain a copy of the License at
+   - http://www.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is GCLI.
+   -
+   - The Initial Developer of the Original Code is
+   - Mozilla Foundation.
+   - Portions created by the Initial Developer are Copyright (C) 2012
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -   Joe Walker <jwalker@mozilla.com>
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the LGPL or the GPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
+
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+  <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/content/devtools/gcli.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/skin/devtools/gcli.css" type="text/css"/>
+</head>
+<body class="gcli-body">
+<div id="gcli-output-root"></div>
+</body>
+</html>
rename from browser/devtools/commandline/gcliblank.xhtml
rename to browser/devtools/commandline/gclitooltip.xhtml
--- a/browser/devtools/commandline/gcliblank.xhtml
+++ b/browser/devtools/commandline/gclitooltip.xhtml
@@ -14,13 +14,13 @@
 
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://browser/content/devtools/gcli.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://browser/skin/devtools/gcli.css" type="text/css"/>
 </head>
-<body id="gclichrome-body">
-<div>
-</div>
+<body class="gcli-body">
+<div id="gcli-tooltip-root"></div>
+<div id="gcli-tooltip-connector"></div>
 </body>
 </html>
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -18,9 +18,10 @@ browser.jar:
     content/browser/devtools/layoutview/view.css  (layoutview/view.css)
     content/browser/orion.js                      (sourceeditor/orion/orion.js)
 *   content/browser/source-editor-overlay.xul     (sourceeditor/source-editor-overlay.xul)
 *   content/browser/debugger.xul                  (debugger/debugger.xul)
     content/browser/debugger.css                  (debugger/debugger.css)
     content/browser/debugger-controller.js        (debugger/debugger-controller.js)
     content/browser/debugger-view.js              (debugger/debugger-view.js)
     content/browser/devtools/gcli.css             (commandline/gcli.css)
-    content/browser/devtools/gcliblank.xhtml      (commandline/gcliblank.xhtml)
+    content/browser/devtools/gclioutput.xhtml     (commandline/gclioutput.xhtml)
+    content/browser/devtools/gclitooltip.xhtml    (commandline/gclitooltip.xhtml)
--- a/browser/devtools/shared/DeveloperToolbar.jsm
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -2,17 +2,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const EXPORTED_SYMBOLS = [ "DeveloperToolbar" ];
 
 const NS_XHTML = "http://www.w3.org/1999/xhtml";
-const URI_GCLIBLANK = "chrome://browser/content/devtools/gcliblank.xhtml";
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gcli", function() {
   let obj = {};
   Components.utils.import("resource:///modules/devtools/gcli.jsm", obj);
   Components.utils.import("resource:///modules/devtools/GcliCommands.jsm", {});
@@ -74,17 +73,16 @@ Object.defineProperty(DeveloperToolbar.p
  * toggle the toolbar
  */
 DeveloperToolbar.prototype.toggle = function DT_toggle()
 {
   if (this.visible) {
     this.hide();
   } else {
     this.show();
-    this._input.focus();
   }
 };
 
 /**
  * Even if the user has not clicked on 'Got it' in the intro, we only show it
  * once per session.
  * Warning this is slightly messed up because this.DeveloperToolbar is not the
  * same as this.DeveloperToolbar when in browser.js context.
@@ -149,19 +147,21 @@ DeveloperToolbar.prototype._onload = fun
     scratchpad: null
   });
 
   this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged, this.outputPanel);
   this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged, this.tooltipPanel);
   this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
 
   this._chromeWindow.getBrowser().tabContainer.addEventListener("TabSelect", this, false);
-  this._chromeWindow.getBrowser().addEventListener("load", this, true); 
+  this._chromeWindow.getBrowser().addEventListener("load", this, true);
+  this._chromeWindow.addEventListener("resize", this, false);
 
   this._element.hidden = false;
+  this._input.focus();
 
   this._notify(NOTIFICATIONS.SHOW);
   if (this._pendingShowCallback) {
     this._pendingShowCallback.call();
     this._pendingShowCallback = undefined;
   }
 
   // If a hide event happened while we were loading, then we need to hide.
@@ -258,111 +258,55 @@ DeveloperToolbar.prototype.handleEvent =
         chromeWindow: this._chromeWindow,
         environment: {
           chromeDocument: this._doc,
           contentDocument: contentDocument
         },
       });
     }
   }
+  else if (aEvent.type == "resize") {
+    this.outputPanel._resize();
+  }
 };
 
 /**
- * Add class="gcli-panel-inner-arrowcontent" to a panel's
- * |<xul:box class="panel-inner-arrowcontent">| so we can alter the styling
- * without complex CSS expressions.
- * @param aPanel The panel to affect
- */
-function getContentBox(aPanel)
-{
-  let container = aPanel.ownerDocument.getAnonymousElementByAttribute(
-          aPanel, "anonid", "container");
-  return container.querySelector(".panel-inner-arrowcontent");
-}
-
-/**
- * Helper function to calculate the sum of the vertical padding and margins
- * between a nested node |aNode| and an ancestor |aRoot|. Iff all of the
- * children of aRoot are 'only-childs' until you get to aNode then to avoid
- * scroll-bars, the 'correct' height of aRoot is verticalSpacing + aNode.height.
- * @param aNode The child node whose height is known.
- * @param aRoot The parent height whose height we can affect.
- * @return The sum of the vertical padding/margins in between aNode and aRoot.
- */
-function getVerticalSpacing(aNode, aRoot)
-{
-  let win = aNode.ownerDocument.defaultView;
-
-  function pxToNum(styles, property) {
-    return parseInt(styles.getPropertyValue(property).replace(/px$/, ''), 10);
-  }
-
-  let vertSpacing = 0;
-  do {
-    let styles = win.getComputedStyle(aNode);
-    vertSpacing += pxToNum(styles, "padding-top");
-    vertSpacing += pxToNum(styles, "padding-bottom");
-    vertSpacing += pxToNum(styles, "margin-top");
-    vertSpacing += pxToNum(styles, "margin-bottom");
-    vertSpacing += pxToNum(styles, "border-top-width");
-    vertSpacing += pxToNum(styles, "border-bottom-width");
-
-    let prev = aNode.previousSibling;
-    while (prev != null) {
-      vertSpacing += prev.clientHeight;
-      prev = prev.previousSibling;
-    }
-
-    let next = aNode.nextSibling;
-    while (next != null) {
-      vertSpacing += next.clientHeight;
-      next = next.nextSibling;
-    }
-
-    aNode = aNode.parentNode;
-  } while (aNode !== aRoot);
-
-  return vertSpacing + 9;
-}
-
-/**
  * Panel to handle command line output.
  * @param aChromeDoc document from which we can pull the parts we need.
  * @param aInput the input element that should get focus.
  * @param aLoadCallback called when the panel is loaded properly.
  */
 function OutputPanel(aChromeDoc, aInput, aLoadCallback)
 {
   this._input = aInput;
-  this._anchor = aChromeDoc.getElementById("developer-toolbar");
+  this._toolbar = aChromeDoc.getElementById("developer-toolbar");
 
   this._loadCallback = aLoadCallback;
 
   /*
   <panel id="gcli-output"
-         type="arrow"
          noautofocus="true"
          noautohide="true"
          class="gcli-panel">
-    <iframe id="gcli-output-frame"
-            src=URI_GCLIBLANK
-            flex="1"/>
+    <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+                 id="gcli-output-frame"
+                 src="chrome://browser/content/devtools/gclioutput.xhtml"
+                 flex="1"/>
   </panel>
   */
   this._panel = aChromeDoc.createElement("panel");
   this._panel.id = "gcli-output";
   this._panel.classList.add("gcli-panel");
-  this._panel.setAttribute("type", "arrow");
   this._panel.setAttribute("noautofocus", "true");
   this._panel.setAttribute("noautohide", "true");
-  this._anchor.parentElement.insertBefore(this._panel, this._anchor);
+  this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
 
-  this._frame = aChromeDoc.createElement("iframe");
+  this._frame = aChromeDoc.createElementNS(NS_XHTML, "iframe");
   this._frame.id = "gcli-output-frame";
-  this._frame.setAttribute("src", URI_GCLIBLANK);
+  this._frame.setAttribute("src", "chrome://browser/content/devtools/gclioutput.xhtml");
   this._frame.setAttribute("flex", "1");
   this._panel.appendChild(this._frame);
 
   this.displayedOutput = undefined;
 
   this._onload = this._onload.bind(this);
   this._frame.addEventListener("load", this._onload, true);
 
@@ -372,57 +316,59 @@ function OutputPanel(aChromeDoc, aInput,
 /**
  * Wire up the element from the iframe, and inform the _loadCallback.
  */
 OutputPanel.prototype._onload = function OP_onload()
 {
   this._frame.removeEventListener("load", this._onload, true);
   delete this._onload;
 
-  this._content = getContentBox(this._panel);
-  this._content.classList.add("gcli-panel-inner-arrowcontent");
+  this.document = this._frame.contentDocument;
 
-  this.document = this._frame.contentDocument;
-  this.document.body.classList.add("gclichrome-output");
-
-  this._div = this.document.querySelector("div");
+  this._div = this.document.getElementById("gcli-output-root");
   this._div.classList.add('gcli-row-out');
   this._div.setAttribute('aria-live', 'assertive');
 
   this.loaded = true;
   if (this._loadCallback) {
     this._loadCallback();
     delete this._loadCallback;
   }
 };
 
 /**
  * Display the OutputPanel.
  */
 OutputPanel.prototype.show = function OP_show()
 {
+  // This is nasty, but displaying the panel causes it to re-flow, which can
+  // change the size it should be, so we need to resize the iframe after the
+  // panel has displayed
   this._panel.ownerDocument.defaultView.setTimeout(function() {
     this._resize();
   }.bind(this), 0);
 
+  this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null);
   this._resize();
-  this._panel.openPopup(this._anchor, "before_end", -300, 0, false, false, null);
 
   this._input.focus();
 };
 
 /**
  * Internal helper to set the height of the output panel to fit the available
  * content;
  */
 OutputPanel.prototype._resize = function CLP_resize()
 {
-  let vertSpacing = getVerticalSpacing(this._content, this._panel);
-  let idealHeight = this.document.body.scrollHeight + vertSpacing;
-  this._panel.sizeTo(400, Math.min(idealHeight, 500));
+  if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+    return
+  }
+
+  this._frame.height = this.document.body.scrollHeight;
+  this._frame.width = this._input.clientWidth + 2;
 };
 
 /**
  * Called by GCLI when a command is executed.
  */
 OutputPanel.prototype._outputChanged = function OP_outputChanged(aEvent)
 {
   if (aEvent.output.hidden) {
@@ -471,20 +417,20 @@ OutputPanel.prototype.remove = function 
 /**
  * Detach listeners from the currently displayed Output.
  */
 OutputPanel.prototype.destroy = function OP_destroy()
 {
   this.remove();
 
   this._panel.removeChild(this._frame);
-  this._anchor.parentElement.removeChild(this._panel);
+  this._toolbar.parentElement.removeChild(this._panel);
 
   delete this._input;
-  delete this._anchor;
+  delete this._toolbar;
   delete this._panel;
   delete this._frame;
   delete this._content;
   delete this._div;
   delete this.document;
 };
 
 /**
@@ -505,83 +451,122 @@ OutputPanel.prototype._visibilityChanged
  * Panel to handle tooltips.
  * @param aChromeDoc document from which we can pull the parts we need.
  * @param aInput the input element that should get focus.
  * @param aLoadCallback called when the panel is loaded properly.
  */
 function TooltipPanel(aChromeDoc, aInput, aLoadCallback)
 {
   this._input = aInput;
-  this._anchor = aChromeDoc.getElementById("developer-toolbar");
+  this._toolbar = aChromeDoc.getElementById("developer-toolbar");
+  this._dimensions = { start: 0, end: 0 };
 
   this._onload = this._onload.bind(this);
   this._loadCallback = aLoadCallback;
   /*
   <panel id="gcli-tooltip"
          type="arrow"
          noautofocus="true"
          noautohide="true"
          class="gcli-panel">
-    <iframe id="gcli-tooltip-frame"
-            src=URI_GCLIBLANK
-            flex="1"/>
+    <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+                 id="gcli-tooltip-frame"
+                 src="chrome://browser/content/devtools/gclitooltip.xhtml"
+                 flex="1"/>
   </panel>
   */
   this._panel = aChromeDoc.createElement("panel");
   this._panel.id = "gcli-tooltip";
   this._panel.classList.add("gcli-panel");
-  this._panel.setAttribute("type", "arrow");
   this._panel.setAttribute("noautofocus", "true");
   this._panel.setAttribute("noautohide", "true");
-  this._anchor.parentElement.insertBefore(this._panel, this._anchor);
+  this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
 
-  this._frame = aChromeDoc.createElement("iframe");
+  this._frame = aChromeDoc.createElementNS(NS_XHTML, "iframe");
   this._frame.id = "gcli-tooltip-frame";
-  this._frame.setAttribute("src", URI_GCLIBLANK);
+  this._frame.setAttribute("src", "chrome://browser/content/devtools/gclitooltip.xhtml");
   this._frame.setAttribute("flex", "1");
   this._panel.appendChild(this._frame);
 
   this._frame.addEventListener("load", this._onload, true);
   this.loaded = false;
 }
 
 /**
  * Wire up the element from the iframe, and inform the _loadCallback.
  */
 TooltipPanel.prototype._onload = function TP_onload()
 {
   this._frame.removeEventListener("load", this._onload, true);
 
-  this._content = getContentBox(this._panel);
-  this._content.classList.add("gcli-panel-inner-arrowcontent");
-
   this.document = this._frame.contentDocument;
-  this.document.body.classList.add("gclichrome-tooltip");
-
-  this.hintElement = this.document.querySelector("div");
+  this.hintElement = this.document.getElementById("gcli-tooltip-root");
+  this._connector = this.document.getElementById("gcli-tooltip-connector");
 
   this.loaded = true;
 
   if (this._loadCallback) {
     this._loadCallback();
     delete this._loadCallback;
   }
 };
 
 /**
  * Display the TooltipPanel.
  */
-TooltipPanel.prototype.show = function TP_show()
+TooltipPanel.prototype.show = function TP_show(aDimensions)
 {
-  let vertSpacing = getVerticalSpacing(this._content, this._panel);
-  let idealHeight = this.document.body.scrollHeight + vertSpacing;
-  this._panel.sizeTo(350, Math.min(idealHeight, 500));
-  this._panel.openPopup(this._anchor, "before_start", 0, 0, false, false, null);
+  if (!aDimensions) {
+    aDimensions = { start: 0, end: 0 };
+  }
+  this._dimensions = aDimensions;
+
+  // This is nasty, but displaying the panel causes it to re-flow, which can
+  // change the size it should be, so we need to resize the iframe after the
+  // panel has displayed
+  this._panel.ownerDocument.defaultView.setTimeout(function() {
+    this._resize();
+  }.bind(this), 0);
+
+  this._resize();
+  this._panel.openPopup(this._input, "before_start", aDimensions.start * 10, 0, false, false, null);
+  this._input.focus();
+};
 
-  this._input.focus();
+/**
+ * One option is to spend lots of time taking an average width of characters
+ * in the current font, dynamically, and weighting for the frequency of use of
+ * various characters, or even to render the given string off screen, and then
+ * measure the width.
+ * Or we could do this...
+ */
+const AVE_CHAR_WIDTH = 4.5;
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype._resize = function TP_resize()
+{
+  if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+    return
+  }
+
+  let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH);
+  this._frame.style.marginLeft = offset + "px";
+
+  /*
+  // Bug 744906: UX review - Not sure if we want this code to fatten connector
+  // with param width
+  let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH);
+  width = Math.min(width, 100);
+  width = Math.max(width, 10);
+  this._connector.style.width = width + "px";
+  */
+
+  this._frame.height = this.document.body.scrollHeight;
 };
 
 /**
  * Hide the TooltipPanel.
  */
 TooltipPanel.prototype.remove = function TP_remove()
 {
   this._panel.hidePopup();
@@ -590,32 +575,34 @@ TooltipPanel.prototype.remove = function
 /**
  * Hide the TooltipPanel.
  */
 TooltipPanel.prototype.destroy = function TP_destroy()
 {
   this.remove();
 
   this._panel.removeChild(this._frame);
-  this._anchor.parentElement.removeChild(this._panel);
+  this._toolbar.parentElement.removeChild(this._panel);
 
+  delete this._connector;
+  delete this._dimensions;
   delete this._input;
   delete this._onload;
   delete this._panel;
   delete this._frame;
-  delete this._anchor;
+  delete this._toolbar;
   delete this._content;
   delete this.document;
   delete this.hintElement;
 };
 
 /**
  * Called by GCLI to indicate that we should show or hide one either the
  * tooltip panel or the output panel.
  */
 TooltipPanel.prototype._visibilityChanged = function TP_visibilityChanged(aEvent)
 {
   if (aEvent.tooltipVisible === true) {
-    this.show();
+    this.show(aEvent.dimensions);
   } else {
     this._panel.hidePopup();
   }
 };
--- a/browser/devtools/shared/test/Makefile.in
+++ b/browser/devtools/shared/test/Makefile.in
@@ -13,16 +13,17 @@ include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
   browser_browser_basic.js \
   browser_promise_basic.js \
   browser_require_basic.js \
   browser_templater_basic.js \
   browser_toolbar_basic.js \
+  browser_toolbar_tooltip.js \
   head.js \
   $(NULL)
 
 _BROWSER_TEST_PAGES = \
   browser_templater_basic.html \
   browser_toolbar_basic.html \
   $(NULL)
 
--- a/browser/devtools/shared/test/browser_toolbar_basic.js
+++ b/browser/devtools/shared/test/browser_toolbar_basic.js
@@ -4,20 +4,20 @@
 // Tests that the developer toolbar works properly
 
 let imported = {};
 Components.utils.import("resource:///modules/HUDService.jsm", imported);
 registerCleanupFunction(function() {
   imported = {};
 });
 
-const URL = "http://example.com/browser/browser/devtools/shared/test/browser_toolbar_basic.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_toolbar_basic.html";
 
 function test() {
-  addTab(URL, function(browser, tab) {
+  addTab(TEST_URI, function(browser, tab) {
     info("Starting browser_toolbar_basic.js");
     runTest();
   });
 }
 
 function runTest() {
   ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
 
new file mode 100755
--- /dev/null
+++ b/browser/devtools/shared/test/browser_toolbar_tooltip.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Tooltip Tests</p>";
+
+function test() {
+  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
+    runTest();
+    finish();
+  });
+}
+
+function runTest() {
+  let tooltipPanel = DeveloperToolbar.tooltipPanel;
+
+  DeveloperToolbar.display.focusManager.helpRequest();
+  DeveloperToolbar.display.inputter.setInput('help help');
+
+  DeveloperToolbar.display.inputter.setCursor({ start: 'help help'.length });
+  is(tooltipPanel._dimensions.start, 'help '.length,
+          'search param start, when cursor at end');
+  ok(getLeftMargin() > 30, 'tooltip offset, when cursor at end')
+
+  DeveloperToolbar.display.inputter.setCursor({ start: 'help'.length });
+  is(tooltipPanel._dimensions.start, 0,
+          'search param start, when cursor at end of command');
+  ok(getLeftMargin() > 9, 'tooltip offset, when cursor at end of command')
+
+  DeveloperToolbar.display.inputter.setCursor({ start: 'help help'.length - 1 });
+  is(tooltipPanel._dimensions.start, 'help '.length,
+          'search param start, when cursor at penultimate position');
+  ok(getLeftMargin() > 30, 'tooltip offset, when cursor at penultimate position')
+
+  DeveloperToolbar.display.inputter.setCursor({ start: 0 });
+  is(tooltipPanel._dimensions.start, 0,
+          'search param start, when cursor at start');
+  ok(getLeftMargin() > 9, 'tooltip offset, when cursor at start')
+}
+
+function getLeftMargin() {
+  let style = DeveloperToolbar.tooltipPanel._frame.style.marginLeft;
+  return parseInt(style.slice(0, -2), 10);
+}
--- a/browser/devtools/shared/test/head.js
+++ b/browser/devtools/shared/test/head.js
@@ -62,17 +62,65 @@ let DeveloperToolbarTest = {
   hide: function DTT_hide() {
     if (!DeveloperToolbar.visible) {
       ok(false, "!DeveloperToolbar.visible at start of closeDeveloperToolbar");
     }
     else {
       DeveloperToolbar.display.inputter.setInput("");
       DeveloperToolbar.hide();
     }
-  }
+  },
+
+  /**
+   * Quick wrapper around the things you need to do to run DeveloperToolbar
+   * command tests:
+   * - Set the pref 'devtools.toolbar.enabled' to true
+   * - Add a tab pointing at |uri|
+   * - Open the DeveloperToolbar
+   * - Register a cleanup function to undo the above
+   * - Run the tests
+   *
+   * @param uri The uri of a page to load. Can be 'about:blank' or 'data:...'
+   * @param testFunc A function containing the tests to run. This should
+   * arrange for 'finish()' to be called on completion.
+   */
+  test: function DTT_test(uri, testFunc) {
+    let menuItem = document.getElementById("menu_devToolbar");
+    let command = document.getElementById("Tools:DevToolbar");
+    let appMenuItem = document.getElementById("appmenu_devToolbar");
+
+    registerCleanupFunction(function() {
+      DeveloperToolbarTest.hide();
+
+      // a.k.a Services.prefs.clearUserPref("devtools.toolbar.enabled");
+      if (menuItem) menuItem.hidden = true;
+      if (command) command.setAttribute("disabled", "true");
+      if (appMenuItem) appMenuItem.hidden = true;
+    });
+
+    // a.k.a: Services.prefs.setBoolPref("devtools.toolbar.enabled", true);
+    if (menuItem) menuItem.hidden = false;
+    if (command) command.removeAttribute("disabled");
+    if (appMenuItem) appMenuItem.hidden = false;
+
+    addTab(uri, function(browser, tab) {
+      DeveloperToolbarTest.show(function() {
+
+        try {
+          testFunc(browser, tab);
+        }
+        catch (ex) {
+          ok(false, "" + ex);
+          console.error(ex);
+          finish();
+          throw ex;
+        }
+      });
+    });
+  },
 };
 
 function catchFail(func) {
   return function() {
     try {
       return func.apply(null, arguments);
     }
     catch (ex) {
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -224,19 +224,19 @@ These should match what Safari and other
 <!ENTITY scratchpad.label             "Scratchpad">
 <!ENTITY scratchpad.accesskey         "s">
 <!ENTITY scratchpad.keycode           "VK_F4">
 <!ENTITY scratchpad.keytext           "F4">
 
 <!ENTITY inspectCloseButton.tooltiptext "Close Inspector">
 
 <!ENTITY devToolbarCloseButton.tooltiptext "Close Developer Toolbar">
-<!ENTITY devToolbarMenu.label "Developer Toolbar">
-<!ENTITY devToolbarMenu.accesskey "v">
-<!ENTITY devToolbar.commandkey "v">
+<!ENTITY devToolbarMenu.label              "Developer Toolbar">
+<!ENTITY devToolbarMenu.accesskey          "v">
+<!ENTITY devToolbar.commandkey             "v">
 
 <!ENTITY webConsoleButton.label "Web Console">
 <!ENTITY inspectorButton.label "Inspector">
 <!ENTITY scriptsButton.label "Scripts">
 
 <!ENTITY inspectorHTMLCopyInner.label       "Copy Inner HTML">
 <!ENTITY inspectorHTMLCopyInner.accesskey   "I">