Bug 893965 - Autocomplete CSS properties in the rule view, r=mratcliffe, msucan
authorGirish Sharma <scrapmachines@gmail.com>
Fri, 26 Jul 2013 04:35:05 +0530
changeset 152659 d8e9483d9436ec46e22f90a70ba5bd4ba6488f05
parent 152658 14cdbdae216067894c15978143f54ec2e5e0b635
child 152660 ea93ffd484cf16a8210d7f84896f43a2da0382c5
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe, msucan
bugs893965
milestone25.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
Bug 893965 - Autocomplete CSS properties in the rule view, r=mratcliffe, msucan
browser/devtools/shared/AutocompletePopup.jsm
browser/devtools/shared/inplace-editor.js
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/test/Makefile.in
browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_existing_property.js
browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_new_property.js
browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
browser/devtools/webconsole/test/browser_webconsole_completion.js
--- a/browser/devtools/shared/AutocompletePopup.jsm
+++ b/browser/devtools/shared/AutocompletePopup.jsm
@@ -1,21 +1,26 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const Cu = Components.utils;
+const Ci = Components.interfaces;
 
 // The XUL and XHTML namespace.
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyGetter(this, "gDevTools", function() {
+  return Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools;
+});
+
 this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
 
 /**
  * Autocomplete popup UI implementation.
  *
  * @constructor
  * @param nsIDOMDocument aDocument
  *        The document you want the popup attached to.
@@ -44,16 +49,24 @@ function AutocompletePopup(aDocument, aO
   this.direction = aOptions.direction || "ltr";
 
   this.onSelect = aOptions.onSelect;
   this.onClick = aOptions.onClick;
   this.onKeypress = aOptions.onKeypress;
 
   let id = aOptions.panelId || "devtools_autoCompletePopup";
   let theme = aOptions.theme || "dark";
+  // If theme is auto, use the devtools.theme pref
+  if (theme == "auto") {
+    theme = Services.prefs.getCharPref("devtools.theme");
+    this.autoThemeEnabled = true;
+    // Setup theme change listener.
+    this._handleThemeChange = this._handleThemeChange.bind(this);
+    gDevTools.on("pref-changed", this._handleThemeChange);
+  }
   // Reuse the existing popup elements.
   this._panel = this._document.getElementById(id);
   if (!this._panel) {
     this._panel = this._document.createElementNS(XUL_NS, "panel");
     this._panel.setAttribute("id", id);
     this._panel.className = "devtools-autocomplete-popup " + theme + "-theme";
 
     this._panel.setAttribute("noautofocus", "true");
@@ -168,16 +181,20 @@ AutocompletePopup.prototype = {
     if (this.onClick) {
       this._list.removeEventListener("click", this.onClick, false);
     }
 
     if (this.onKeypress) {
       this._list.removeEventListener("keypress", this.onKeypress, false);
     }
 
+    if (this.autoThemeEnabled) {
+      gDevTools.off("pref-changed", this._handleThemeChange);
+    }
+
     this._document = null;
     this._list = null;
     this._panel = null;
   },
 
   /**
    * Get the autocomplete items array.
    *
@@ -435,31 +452,31 @@ AutocompletePopup.prototype = {
    *         The newly selected item object.
    */
   selectNextItem: function AP_selectNextItem()
   {
     if (this.selectedIndex < (this.itemCount - 1)) {
       this.selectedIndex++;
     }
     else {
-      this.selectedIndex = -1;
+      this.selectedIndex = 0;
     }
 
     return this.selectedItem;
   },
 
   /**
    * Select the previous item in the list.
    *
    * @return object
    *         The newly selected item object.
    */
   selectPreviousItem: function AP_selectPreviousItem()
   {
-    if (this.selectedIndex > -1) {
+    if (this.selectedIndex > 0) {
       this.selectedIndex--;
     }
     else {
       this.selectedIndex = this.itemCount - 1;
     }
 
     return this.selectedItem;
   },
@@ -491,10 +508,33 @@ AutocompletePopup.prototype = {
     hbox.appendChild(scrollbar);
 
     this._document.documentElement.appendChild(hbox);
     this.__scrollbarWidth = scrollbar.clientWidth;
     this._document.documentElement.removeChild(hbox);
 
     return this.__scrollbarWidth;
   },
+
+  /**
+   * Manages theme switching for the popup based on the devtools.theme pref.
+   *
+   * @private
+   *
+   * @param String aEvent
+   *        The name of the event. In this case, "pref-changed".
+   * @param Object aData
+   *        An object passed by the emitter of the event. In this case, the
+   *        object consists of three properties:
+   *        - pref {String} The name of the preference that was modified.
+   *        - newValue {Object} The new value of the preference.
+   *        - oldValue {Object} The old value of the preference.
+   */
+  _handleThemeChange: function AP__handleThemeChange(aEvent, aData)
+  {
+    if (aData.pref == "devtools.theme") {
+      this._panel.classList.toggle(aData.oldValue + "-theme", false);
+      this._panel.classList.toggle(aData.newValue + "-theme", true);
+      this._list.classList.toggle(aData.oldValue + "-theme", false);
+      this._list.classList.toggle(aData.newValue + "-theme", true);
+    }
+  },
 };
-
--- a/browser/devtools/shared/inplace-editor.js
+++ b/browser/devtools/shared/inplace-editor.js
@@ -19,19 +19,26 @@
  *   trigger: "dblclick"
  * });
  *
  * See editableField() for more options.
  */
 
 "use strict";
 
-const {Ci, Cu} = require("chrome");
+const {Ci, Cu, Cc} = require("chrome");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
+const CONTENT_TYPES = {
+  PLAIN_TEXT: 0,
+  CSS_VALUE: 1,
+  CSS_MIXED: 2,
+  CSS_PROPERTY: 3,
+};
+const MAX_POPUP_ENTRIES = 10;
 
 const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
 const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /**
@@ -155,16 +162,18 @@ function InplaceEditor(aOptions, aEvent)
   this.elt.inplaceEditor = this;
 
   this.change = aOptions.change;
   this.done = aOptions.done;
   this.destroy = aOptions.destroy;
   this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
   this.multiline = aOptions.multiline || false;
   this.stopOnReturn = !!aOptions.stopOnReturn;
+  this.contentType = aOptions.contentType || CONTENT_TYPES.PLAIN_TEXT;
+  this.popup = aOptions.popup;
 
   this._onBlur = this._onBlur.bind(this);
   this._onKeyPress = this._onKeyPress.bind(this);
   this._onInput = this._onInput.bind(this);
   this._onKeyup = this._onKeyup.bind(this);
 
   this._createInput();
   this._autosize();
@@ -203,16 +212,18 @@ function InplaceEditor(aOptions, aEvent)
 
   if (aOptions.start) {
     aOptions.start(this, aEvent);
   }
 }
 
 exports.InplaceEditor = InplaceEditor;
 
+InplaceEditor.CONTENT_TYPES = CONTENT_TYPES;
+
 InplaceEditor.prototype = {
   _createInput: function InplaceEditor_createEditor()
   {
     this.input =
       this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
     this.input.inplaceEditor = this;
     this.input.classList.add("styleinspector-propertyeditor");
     this.input.value = this.initial;
@@ -674,16 +685,20 @@ InplaceEditor.prototype = {
    * Call the client's done handler and clear out.
    */
   _apply: function InplaceEditor_apply(aEvent)
   {
     if (this._applied) {
       return;
     }
 
+    if (this.popup) {
+      this.popup.hidePopup();
+    }
+
     this._applied = true;
 
     if (this.done) {
       let val = this.input.value.trim();
       return this.done(this.cancelled ? this.initial : val, !this.cancelled);
     }
 
     return null;
@@ -727,19 +742,41 @@ InplaceEditor.prototype = {
         increment *= largeIncrement;
       } else {
         increment *= mediumIncrement;
       }
     } else if (aEvent.altKey && !aEvent.shiftKey) {
       increment *= smallIncrement;
     }
 
+    let cycling = false;
     if (increment && this._incrementValue(increment) ) {
       this._updateSize();
       prevent = true;
+    } else if (increment && this.popup && this.popup.isOpen) {
+      cycling = true;
+      prevent = true;
+      if (increment > 0) {
+        this.popup.selectPreviousItem();
+      } else {
+        this.popup.selectNextItem();
+      }
+      this.input.value = this.popup.selectedItem.label;
+      this._updateSize();
+    }
+
+    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE ||
+        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DELETE ||
+        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_LEFT ||
+        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RIGHT) {
+      if (this.popup && this.popup.isOpen) {
+        this.popup.hidePopup();
+      }
+    } else if (!cycling) {
+      this._maybeSuggestCompletion();
     }
 
     if (this.multiline &&
         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
         aEvent.shiftKey) {
       prevent = false;
     } else if (aEvent.charCode in this._advanceCharCodes
        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
@@ -751,16 +788,19 @@ InplaceEditor.prototype = {
           aEvent.shiftKey) {
         this.cancelled = true;
         direction = FOCUS_BACKWARD;
       }
       if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
         direction = null;
       }
 
+      // Now we don't want to suggest anything as we are moving out.
+      this._preventSuggestions = true;
+
       let input = this.input;
 
       this._apply();
 
       if (direction !== null && focusManager.focusedElement === input) {
         // If the focused element wasn't changed by the done callback,
         // move the focus as requested.
         let next = moveFocus(this.doc.defaultView, direction);
@@ -770,16 +810,18 @@ InplaceEditor.prototype = {
         if (next && next.ownerDocument === this.doc && next._editable) {
           next.click();
         }
       }
 
       this._clear();
     } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
       // Cancel and blur ourselves.
+      // Now we don't want to suggest anything as we are moving out.
+      this._preventSuggestions = true;
       prevent = true;
       this.cancelled = true;
       this._apply();
       this._clear();
       aEvent.stopPropagation();
     } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
       // No need for leading spaces here.  This is particularly
       // noticable when adding a property: it's very natural to type
@@ -816,16 +858,89 @@ InplaceEditor.prototype = {
     if (this._measurement) {
       this._updateSize();
     }
 
     // Call the user's change handler if available.
     if (this.change) {
       this.change(this.input.value.trim());
     }
+  },
+
+  /**
+   * Handles displaying suggestions based on the current input.
+   */
+  _maybeSuggestCompletion: function() {
+    // Since we are calling this method from a keypress event handler, the
+    // |input.value| does not include currently typed character. Thus we perform
+    // this method async.
+    this.doc.defaultView.setTimeout(() => {
+      if (this._preventSuggestions) {
+        this._preventSuggestions = false;
+        return;
+      }
+      if (this.contentType == CONTENT_TYPES.PLAIN_TEXT) {
+        return;
+      }
+
+      let input = this.input;
+      // Input can be null in cases when you intantaneously switch out of it.
+      if (!input) {
+        return;
+      }
+      let query = input.value.slice(0, input.selectionStart);
+      if (!query) {
+        return;
+      }
+
+      let list = [];
+      if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
+        list = CSSPropertyList;
+      }
+
+      list.some(item => {
+        if (item.startsWith(query)) {
+          input.value = item;
+          input.setSelectionRange(query.length, item.length);
+          this._updateSize();
+          return true;
+        }
+      });
+
+      if (!this.popup) {
+        return;
+      }
+      let finalList = [];
+      let length = list.length;
+      for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
+        if (list[i].startsWith(query)) {
+          count++;
+          finalList.push({
+            preLabel: query,
+            label: list[i]
+          });
+        }
+        else if (count > 0) {
+          // Since count was incremented, we had already crossed the entries
+          // which would have started with query, assuming that list is sorted.
+          break;
+        }
+        else if (list[i][0] > query[0]) {
+          // We have crossed all possible matches alphabetically.
+          break;
+        }
+      }
+
+      if (finalList.length > 1) {
+        this.popup.setItems(finalList);
+        this.popup.openPopup(this.input);
+      } else {
+        this.popup.hidePopup();
+      }
+    }, 0);
   }
 };
 
 /**
  * Copy text-related styles from one element to another.
  */
 function copyTextStyles(aFrom, aTo)
 {
@@ -844,8 +959,13 @@ function moveFocus(aWin, aDirection)
 {
   return focusManager.moveFocus(aWin, null, aDirection, 0);
 }
 
 
 XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
   return Services.focus;
 });
+
+XPCOMUtils.defineLazyGetter(this, "CSSPropertyList", function() {
+  let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+  return domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES).sort();
+});
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -871,16 +871,23 @@ function CssRuleView(aDoc, aStore)
   this.store = aStore;
   this.element = this.doc.createElementNS(HTML_NS, "div");
   this.element.className = "ruleview devtools-monospace";
   this.element.flex = 1;
 
   this._boundCopy = this._onCopy.bind(this);
   this.element.addEventListener("copy", this._boundCopy);
 
+  let options = {
+    fixedWidth: true,
+    autoSelect: true,
+    theme: "auto"
+  };
+  this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
+
   this._showEmpty();
 }
 
 exports.CssRuleView = CssRuleView;
 
 CssRuleView.prototype = {
   // The element that we're inspecting.
   _viewedElement: null,
@@ -897,16 +904,18 @@ CssRuleView.prototype = {
     this.clear();
 
     this.element.removeEventListener("copy", this._boundCopy);
     delete this._boundCopy;
 
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
     }
+
+    this.popup.destroy();
   },
 
   /**
    * Update the highlighted element.
    *
    * @param {nsIDOMElement} aElement
    *        The node whose style rules we'll inspect.
    */
@@ -1265,17 +1274,19 @@ RuleEditor.prototype = {
       class: "ruleview-propertyname",
       tabindex: "0"
     });
 
     new InplaceEditor({
       element: this.newPropSpan,
       done: this._onNewProperty,
       destroy: this._newPropertyDestroy,
-      advanceChars: ":"
+      advanceChars: ":",
+      contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+      popup: this.ruleView.popup
     });
   },
 
   /**
    * Called when the new property input has been dismissed.
    * Will create a new TextProperty if necessary.
    *
    * @param {string} aValue
@@ -1317,16 +1328,17 @@ RuleEditor.prototype = {
  *        The rule editor that owns this TextPropertyEditor.
  * @param {TextProperty} aProperty
  *        The text property to edit.
  * @constructor
  */
 function TextPropertyEditor(aRuleEditor, aProperty)
 {
   this.doc = aRuleEditor.doc;
+  this.popup = aRuleEditor.ruleView.popup;
   this.prop = aProperty;
   this.prop.editor = this;
   this.browserWindow = this.doc.defaultView.top;
 
   let sheet = this.prop.rule.sheet;
   let href = sheet ? CssLogic.href(sheet) : null;
   if (href) {
     this.sheetURI = IOService.newURI(href, null, null);
@@ -1385,17 +1397,19 @@ TextPropertyEditor.prototype = {
       class: "ruleview-propertyname theme-fg-color5",
       tabindex: "0",
     });
 
     editableField({
       start: this._onStartEditing,
       element: this.nameSpan,
       done: this._onNameDone,
-      advanceChars: ':'
+      advanceChars: ':',
+      contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+      popup: this.popup
     });
 
     appendText(this.nameContainer, ": ");
 
     // Create a span that will hold the property and semicolon.
     // Use this span to create a slightly larger click target
     // for the value.
     let propertyContainer = createChild(this.element, "span", {
@@ -1868,8 +1882,12 @@ XPCOMUtils.defineLazyGetter(this, "clipb
 XPCOMUtils.defineLazyGetter(this, "_strings", function() {
   return Services.strings.createBundle(
     "chrome://browser/locale/devtools/styleinspector.properties");
 });
 
 XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
+
+XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function() {
+  return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
+});
--- a/browser/devtools/styleinspector/test/Makefile.in
+++ b/browser/devtools/styleinspector/test/Makefile.in
@@ -33,16 +33,18 @@ MOCHITEST_BROWSER_FILES = \
   browser_bug705707_is_content_stylesheet.js \
   browser_bug722196_property_view_media_queries.js \
   browser_bug722196_rule_view_media_queries.js \
   browser_bug_592743_specificity.js \
   browser_bug722691_rule_view_increment.js \
   browser_computedview_734259_style_editor_link.js \
   browser_computedview_copy.js\
   browser_styleinspector_bug_677930_urls_clickable.js \
+  browser_bug893965_css_property_completion_new_property.js \
+  browser_bug893965_css_property_completion_existing_property.js \
   head.js \
   $(NULL)
 
 MOCHITEST_BROWSER_FILES += \
   browser_bug683672.html \
   browser_bug705707_is_content_stylesheet.html \
   browser_bug705707_is_content_stylesheet_imported.css \
   browser_bug705707_is_content_stylesheet_imported2.css \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_existing_property.js
@@ -0,0 +1,149 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that CSS property names are autocompleted and cycled correctly.
+
+const MAX_ENTRIES = 10;
+
+let doc;
+let inspector;
+let ruleViewWindow;
+let editor;
+let state;
+// format :
+//  [
+//    what key to press,
+//    expected input box value after keypress,
+//    selectedIndex of the popup,
+//    total items in the popup
+//  ]
+let testData = [
+  ["VK_RIGHT", "border", -1, 0],
+  ["-","border-bottom", 0, 10],
+  ["b","border-bottom", 0, 6],
+  ["VK_BACK_SPACE", "border-b", -1, 0],
+  ["VK_BACK_SPACE", "border-", -1, 0],
+  ["VK_BACK_SPACE", "border", -1, 0],
+  ["VK_BACK_SPACE", "borde", -1, 0],
+  ["VK_BACK_SPACE", "bord", -1, 0],
+  ["VK_BACK_SPACE", "bor", -1, 0],
+  ["VK_BACK_SPACE", "bo", -1, 0],
+  ["VK_BACK_SPACE", "b", -1, 0],
+  ["VK_BACK_SPACE", "", -1, 0],
+  ["d", "direction", 0, 3],
+  ["VK_DOWN", "display", 1, 3],
+  ["VK_DOWN", "dominant-baseline", 2, 3],
+  ["VK_DOWN", "direction", 0, 3],
+  ["VK_DOWN", "display", 1, 3],
+  ["VK_UP", "direction", 0, 3],
+  ["VK_UP", "dominant-baseline", 2, 3],
+  ["VK_UP", "display", 1, 3],
+  ["VK_BACK_SPACE", "displa", -1, 0],
+  ["VK_BACK_SPACE", "displ", -1, 0],
+  ["VK_BACK_SPACE", "disp", -1, 0],
+  ["VK_BACK_SPACE", "dis", -1, 0],
+  ["VK_BACK_SPACE", "di", -1, 0],
+  ["VK_BACK_SPACE", "d", -1, 0],
+  ["i", "direction", 0, 2],
+  ["s", "display", -1, 0],
+  ["VK_BACK_SPACE", "dis", -1, 0],
+  ["VK_BACK_SPACE", "di", -1, 0],
+  ["VK_BACK_SPACE", "d", -1, 0],
+  ["VK_BACK_SPACE", "", -1, 0],
+  ["f", "fill", 0, MAX_ENTRIES],
+  ["i", "fill", 0, 4],
+  ["VK_ESCAPE", null, -1, 0],
+];
+
+function openRuleView()
+{
+  var target = TargetFactory.forTab(gBrowser.selectedTab);
+  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+    inspector = toolbox.getCurrentPanel();
+    inspector.sidebar.select("ruleview");
+
+    // Highlight a node.
+    let node = content.document.getElementsByTagName("h1")[0];
+    inspector.selection.setNode(node);
+
+    inspector.sidebar.once("ruleview-ready", testCompletion);
+  });
+}
+
+function testCompletion()
+{
+  ruleViewWindow = inspector.sidebar.getWindowForTab("ruleview");
+  let brace = ruleViewWindow.document.querySelector(".ruleview-propertyname");
+
+  waitForEditorFocus(brace.parentNode, function onNewElement(aEditor) {
+    editor = aEditor;
+    editor.input.addEventListener("keypress", checkState, false);
+    checkStateAndMoveOn(0);
+  });
+
+  brace.click();
+}
+
+function checkStateAndMoveOn(index) {
+  if (index == testData.length) {
+    finishUp();
+    return;
+  }
+
+  let [key] = testData[index];
+  state = index;
+
+  info("pressing key " + key + " to get result: [" + testData[index].slice(1) +
+       "] for state " + state);
+  EventUtils.synthesizeKey(key, {}, ruleViewWindow);
+}
+
+function checkState(event) {
+  // The keypress handler is async and can take some time to compute, filter and
+  // display the popup and autocompletion so we need to use setTimeout here.
+  window.setTimeout(function() {
+    info("After keypress for state " + state);
+    let [key, completion, index, total] = testData[state];
+    if (completion != null) {
+      is(editor.input.value, completion,
+         "Correct value is autocompleted for state " + state);
+    }
+    if (total == 0) {
+      ok(!(editor.popup && editor.popup.isOpen), "Popup is closed for state " +
+         state);
+    }
+    else {
+      ok(editor.popup._panel.state == "open" ||
+         editor.popup._panel.state == "showing",
+         "Popup is open for state " + state);
+      is(editor.popup.getItems().length, total,
+         "Number of suggestions match for state " + state);
+      is(editor.popup.selectedIndex, index,
+         "Correct item is selected for state " + state);
+    }
+    checkStateAndMoveOn(state + 1);
+  }, 200);
+}
+
+function finishUp()
+{
+  doc = inspector = editor = ruleViewWindow = state = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    doc = content.document;
+    doc.title = "Rule View Test";
+    waitForFocus(openRuleView, content);
+  }, true);
+
+  content.location = "data:text/html,<h1 style='border: 1px solid red'>Filename" +
+                     ": browser_bug893965_css_property_completion_existing_property.js</h1>";
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_new_property.js
@@ -0,0 +1,137 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that CSS property names are autocompleted and cycled correctly.
+
+const MAX_ENTRIES = 10;
+
+let doc;
+let inspector;
+let ruleViewWindow;
+let editor;
+let state;
+// format :
+//  [
+//    what key to press,
+//    expected input box value after keypress,
+//    selectedIndex of the popup,
+//    total items in the popup
+//  ]
+let testData = [
+  ["d", "direction", 0, 3],
+  ["VK_DOWN", "display", 1, 3],
+  ["VK_DOWN", "dominant-baseline", 2, 3],
+  ["VK_DOWN", "direction", 0, 3],
+  ["VK_DOWN", "display", 1, 3],
+  ["VK_UP", "direction", 0, 3],
+  ["VK_UP", "dominant-baseline", 2, 3],
+  ["VK_UP", "display", 1, 3],
+  ["VK_BACK_SPACE", "displa", -1, 0],
+  ["VK_BACK_SPACE", "displ", -1, 0],
+  ["VK_BACK_SPACE", "disp", -1, 0],
+  ["VK_BACK_SPACE", "dis", -1, 0],
+  ["VK_BACK_SPACE", "di", -1, 0],
+  ["VK_BACK_SPACE", "d", -1, 0],
+  ["i", "direction", 0, 2],
+  ["s", "display", -1, 0],
+  ["VK_BACK_SPACE", "dis", -1, 0],
+  ["VK_BACK_SPACE", "di", -1, 0],
+  ["VK_BACK_SPACE", "d", -1, 0],
+  ["VK_BACK_SPACE", "", -1, 0],
+  ["f", "fill", 0, MAX_ENTRIES],
+  ["i", "fill", 0, 4],
+  ["VK_ESCAPE", null, -1, 0],
+];
+
+function openRuleView()
+{
+  var target = TargetFactory.forTab(gBrowser.selectedTab);
+  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+    inspector = toolbox.getCurrentPanel();
+    inspector.sidebar.select("ruleview");
+
+    // Highlight a node.
+    let node = content.document.getElementsByTagName("h1")[0];
+    inspector.selection.setNode(node);
+
+    inspector.sidebar.once("ruleview-ready", testCompletion);
+  });
+}
+
+function testCompletion()
+{
+  ruleViewWindow = inspector.sidebar.getWindowForTab("ruleview");
+  let brace = ruleViewWindow.document.querySelector(".ruleview-ruleclose");
+
+  waitForEditorFocus(brace.parentNode, function onNewElement(aEditor) {
+    editor = aEditor;
+    editor.input.addEventListener("keypress", checkState, false);
+    checkStateAndMoveOn(0);
+  });
+
+  brace.click();
+}
+
+function checkStateAndMoveOn(index) {
+  if (index == testData.length) {
+    finishUp();
+    return;
+  }
+
+  let [key] = testData[index];
+  state = index;
+
+  info("pressing key " + key + " to get result: [" + testData[index].slice(1) +
+       "] for state " + state);
+  EventUtils.synthesizeKey(key, {}, ruleViewWindow);
+}
+
+function checkState(event) {
+  // The keypress handler is async and can take some time to compute, filter and
+  // display the popup and autocompletion so we need to use setTimeout here.
+  window.setTimeout(function() {
+    info("After keypress for state " + state);
+    let [key, completion, index, total] = testData[state];
+    if (completion != null) {
+      is(editor.input.value, completion,
+         "Correct value is autocompleted for state " + state);
+    }
+    if (total == 0) {
+      ok(!(editor.popup && editor.popup.isOpen), "Popup is closed for state " +
+         state);
+    }
+    else {
+      ok(editor.popup._panel.state == "open" ||
+         editor.popup._panel.state == "showing",
+         "Popup is open for state " + state);
+      is(editor.popup.getItems().length, total,
+         "Number of suggestions match for state " + state);
+      is(editor.popup.selectedIndex, index,
+         "Correct item is selected for state " + state);
+    }
+    checkStateAndMoveOn(state + 1);
+  }, 200);
+}
+
+function finishUp()
+{
+  doc = inspector = editor = ruleViewWindow = state = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    doc = content.document;
+    doc.title = "Rule View Test";
+    waitForFocus(openRuleView, content);
+  }, true);
+
+  content.location = "data:text/html,<h1 style='border: 1px solid red'>Filename:" +
+                     "browser_bug893965_css_property_completion_new_property.js</h1>";
+}
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
@@ -64,17 +64,16 @@ function consoleOpened(aHud) {
         "unwatch",
         "valueOf",
         "watch",
       ][index] === prop}), "getItems returns the items we expect");
 
     is(popup.selectedIndex, 17,
        "Index of the first item from bottom is selected.");
     EventUtils.synthesizeKey("VK_DOWN", {});
-    EventUtils.synthesizeKey("VK_DOWN", {});
 
     let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
 
     is(popup.selectedIndex, 0, "index 0 is selected");
     is(popup.selectedItem.label, "watch", "watch is selected");
     is(completeNode.value, prefix + "watch",
         "completeNode.value holds watch");
 
@@ -118,17 +117,16 @@ function popupHideAfterTab()
     popup._panel.removeEventListener("popupshown", onShown, false);
 
     ok(popup.isOpen, "popup is open");
 
     is(popup.itemCount, 18, "popup.itemCount is correct");
 
     is(popup.selectedIndex, 17, "First index from bottom is selected");
     EventUtils.synthesizeKey("VK_DOWN", {});
-    EventUtils.synthesizeKey("VK_DOWN", {});
 
     let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
 
     is(popup.selectedIndex, 0, "index 0 is selected");
     is(popup.selectedItem.label, "watch", "watch is selected");
     is(completeNode.value, prefix + "watch",
         "completeNode.value holds watch");
 
@@ -164,17 +162,16 @@ function testReturnKey()
     popup._panel.removeEventListener("popupshown", onShown, false);
 
     ok(popup.isOpen, "popup is open");
 
     is(popup.itemCount, 18, "popup.itemCount is correct");
 
     is(popup.selectedIndex, 17, "First index from bottom is selected");
     EventUtils.synthesizeKey("VK_DOWN", {});
-    EventUtils.synthesizeKey("VK_DOWN", {});
 
     let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
 
     is(popup.selectedIndex, 0, "index 0 is selected");
     is(popup.selectedItem.label, "watch", "watch is selected");
     is(completeNode.value, prefix + "watch",
         "completeNode.value holds watch");
 
@@ -288,17 +285,16 @@ function testCompletionInText()
 
   popup._panel.addEventListener("popupshown", function onShown() {
     popup._panel.removeEventListener("popupshown", onShown);
 
     ok(popup.isOpen, "popup is open");
     is(popup.itemCount, 2, "popup.itemCount is correct");
 
     EventUtils.synthesizeKey("VK_DOWN", {});
-    EventUtils.synthesizeKey("VK_DOWN", {});
     is(popup.selectedIndex, 0, "popup.selectedIndex is correct");
     ok(!completeNode.value, "completeNode.value is empty");
 
     let items = popup.getItems().reverse().map(e => e.label);
     let sameItems = items.every((prop, index) =>
       ["testBug873250a", "testBug873250b"][index] === prop);
     ok(sameItems, "getItems returns the items we expect");
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
@@ -59,20 +59,20 @@ function consoleOpened(HUD) {
     is(popup.selectedIndex, 1, "index 1 is selected");
     is(popup.selectedItem, items[1], "item1 is selected");
 
     is(popup.selectNextItem(), items[2], "selectPreviousItem() works");
 
     is(popup.selectedIndex, 2, "index 2 is selected");
     is(popup.selectedItem, items[2], "item2 is selected");
 
-    ok(!popup.selectNextItem(), "selectPreviousItem() works");
+    ok(popup.selectNextItem(), "selectPreviousItem() works");
 
-    is(popup.selectedIndex, -1, "no index is selected");
-    ok(!popup.selectedItem, "no item is selected");
+    is(popup.selectedIndex, 0, "index 0 is selected");
+    is(popup.selectedItem, items[0], "item0 is selected");
 
     items.push({label: "label3", value: "value3"});
     popup.appendItem(items[3]);
 
     is(popup.itemCount, items.length, "item3 appended");
 
     popup.selectedIndex = 3;
     is(popup.selectedItem, items[3], "item3 is selected");
--- a/browser/devtools/webconsole/test/browser_webconsole_completion.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_completion.js
@@ -61,31 +61,31 @@ function testCompletion(hud) {
 
   // Test typing 'document.getElem'.
   input.value = "document.getElem";
   input.setSelectionRange(16, 16);
   jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
   yield undefined;
 
   is(input.value, "document.getElem", "'document.getElem' completion");
-  is(jsterm.completeNode.value, "", "'document.getElem' completion");
+  is(jsterm.completeNode.value, "                entsByTagNameNS", "'document.getElem' completion");
 
   // Test pressing tab another time.
   jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
   yield undefined;
 
   is(input.value, "document.getElem", "'document.getElem' completion");
-  is(jsterm.completeNode.value, "                entsByTagNameNS", "'document.getElem' another tab completion");
+  is(jsterm.completeNode.value, "                entsByTagName", "'document.getElem' another tab completion");
 
   // Test pressing shift_tab.
   jsterm.complete(jsterm.COMPLETE_BACKWARD, testNext);
   yield undefined;
 
   is(input.value, "document.getElem", "'document.getElem' untab completion");
-  is(jsterm.completeNode.value, "", "'document.getElem' completion");
+  is(jsterm.completeNode.value, "                entsByTagNameNS", "'document.getElem' completion");
 
   jsterm.clearOutput();
 
   input.value = "docu";
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
   yield undefined;
 
   is(jsterm.completeNode.value, "    ment", "'docu' completion");