Bug 1202458 - part1: inline text nodes in markupview only if they are short enough;r=pbro
☠☠ backed out by c26aac8cf086 ☠ ☠
authorJulian Descottes <jdescottes@mozilla.com>
Thu, 02 Jun 2016 10:41:49 +0200
changeset 341092 f3bdf3ebaf4cbf3bb426d10cf95e702bc8e981f4
parent 341091 b6ba2b9fbe8a89a569222914c6990a31cfbd9b56
child 341093 49d7963e43d890488ad0ad9b1a592c57bdc2be49
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1202458
milestone49.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 1202458 - part1: inline text nodes in markupview only if they are short enough;r=pbro The markup view will now inline a textnode in its container if and only if: - the text node is the only child (pseudo elements included) - the text node length is smaller than a predefined limit If a container is expanded, its text nodes will now always be rendered in full, no longer as a short version with an ellipsis. When selecting or navigating on a textnode, the layout will no longer be modified on the fly. MozReview-Commit-ID: HcDMqjbOesN
devtools/client/framework/selection.js
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/test/browser.ini
devtools/client/inspector/markup/test/browser_markup_mutation_01.js
devtools/client/inspector/markup/test/browser_markup_textcontent_display.js
devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js
devtools/client/inspector/markup/test/head.js
devtools/server/actors/inspector.js
devtools/shared/fronts/inspector.js
--- a/devtools/client/framework/selection.js
+++ b/devtools/client/framework/selection.js
@@ -157,19 +157,19 @@ Selection.prototype = {
       return this.node.ownerDocument;
     }
     return null;
   },
 
   setNodeFront: function (value, reason = "unknown") {
     this.reason = reason;
 
-    // If a singleTextChild text node is being set, then set it's parent instead.
+    // If an inlineTextChild text node is being set, then set it's parent instead.
     let parentNode = value && value.parentNode();
-    if (value && parentNode && parentNode.singleTextChild === value) {
+    if (value && parentNode && parentNode.inlineTextChild === value) {
       value = parentNode;
     }
 
     // We used to return here if the node had not changed but we now need to
     // set the node even if it is already set otherwise it is not possible to
     // e.g. highlight the same node twice.
     let rawValue = null;
     if (value && value.isLocalToBeDeprecated()) {
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -39,18 +39,16 @@ const {Tooltip} = require("devtools/clie
 const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
 const {setImageTooltip, setBrokenImageTooltip} =
       require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const Heritage = require("sdk/core/heritage");
 const {parseAttribute} =
       require("devtools/client/shared/node-attribute-parser");
-const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis",
-      Ci.nsIPrefLocalizedString).data;
 const {Task} = require("devtools/shared/task");
 const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils");
 const {PrefObserver} = require("devtools/client/styleeditor/utils");
 const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
 const {template} = require("devtools/shared/gcli/templater");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
@@ -984,16 +982,20 @@ MarkupView.prototype = {
         container.update();
       } else if (type === "childList" || type === "nativeAnonymousChildList") {
         container.childrenDirty = true;
         // Update the children to take care of changes in the markup view DOM
         // and update container (and its subtree) DOM tree depth level for
         // accessibility where necessary.
         this._updateChildren(container, {flash: true}).then(() =>
           container.updateLevel());
+      } else if (type === "inlineTextChild") {
+        container.childrenDirty = true;
+        this._updateChildren(container, {flash: true});
+        container.update();
       }
     }
 
     this._waitForChildren().then(() => {
       if (this._destroyer) {
         console.warn("Could not fully update after markup mutations, " +
           "the markup-view was destroyed while waiting for children.");
         return;
@@ -1544,39 +1546,39 @@ MarkupView.prototype = {
     if (this._queuedChildUpdates.has(container)) {
       return this._queuedChildUpdates.get(container);
     }
 
     if (!container.childrenDirty) {
       return promise.resolve(container);
     }
 
-    if (container.singleTextChild
-        && container.singleTextChild != container.node.singleTextChild) {
+    if (container.inlineTextChild
+        && container.inlineTextChild != container.node.inlineTextChild) {
       // This container was doing double duty as a container for a single
       // text child, back that out.
-      this._containers.delete(container.singleTextChild);
-      container.clearSingleTextChild();
+      this._containers.delete(container.inlineTextChild);
+      container.clearInlineTextChild();
 
       if (container.hasChildren && container.selected) {
         container.setExpanded(true);
       }
     }
 
-    if (container.node.singleTextChild) {
+    if (container.node.inlineTextChild) {
       container.setExpanded(false);
       // this container will do double duty as the container for the single
       // text child.
       while (container.children.firstChild) {
         container.children.removeChild(container.children.firstChild);
       }
 
-      container.setSingleTextChild(container.node.singleTextChild);
-
-      this._containers.set(container.node.singleTextChild, container);
+      container.setInlineTextChild(container.node.inlineTextChild);
+
+      this._containers.set(container.node.inlineTextChild, container);
       container.childrenDirty = false;
       return promise.resolve(container);
     }
 
     if (!container.hasChildren) {
       while (container.children.firstChild) {
         container.children.removeChild(container.children.firstChild);
       }
@@ -2003,17 +2005,17 @@ MarkupContainer.prototype = {
       doc.activeElement.blur();
     }
   },
 
   /**
    * True if the current node can be expanded.
    */
   get canExpand() {
-    return this._hasChildren && !this.node.singleTextChild;
+    return this._hasChildren && !this.node.inlineTextChild;
   },
 
   /**
    * True if this is the root <html> element and can't be collapsed.
    */
   get mustExpand() {
     return this.node._parent === this.markup.walker.rootNode;
   },
@@ -2712,23 +2714,23 @@ MarkupElementContainer.prototype = Herit
     // for the tooltip, because we want the full-size image
     this.node.getImageData().then(data => {
       data.data.string().then(str => {
         clipboardHelper.copyString(str);
       });
     });
   },
 
-  setSingleTextChild: function (singleTextChild) {
-    this.singleTextChild = singleTextChild;
+  setInlineTextChild: function (inlineTextChild) {
+    this.inlineTextChild = inlineTextChild;
     this.editor.updateTextEditor();
   },
 
-  clearSingleTextChild: function () {
-    this.singleTextChild = undefined;
+  clearInlineTextChild: function () {
+    this.inlineTextChild = undefined;
     this.editor.updateTextEditor();
   },
 
   /**
    * Trigger new attribute field for input.
    */
   addAttribute: function () {
     this.editor.newAttr.editMode();
@@ -2900,35 +2902,24 @@ TextEditor.prototype = {
     if (value === this._selected) {
       return;
     }
     this._selected = value;
     this.update();
   },
 
   update: function () {
-    if (!this.selected || !this.node.incompleteValue) {
-      let text = this.node.shortValue;
-      if (this.node.incompleteValue) {
-        text += ELLIPSIS;
-      }
-      this.value.textContent = text;
-    } else {
-      let longstr = null;
-      this.node.getNodeValue().then(ret => {
-        longstr = ret;
-        return longstr.string();
-      }).then(str => {
-        longstr.release().then(null, console.error);
-        if (this.selected) {
-          this.value.textContent = str;
-          this.markup.emit("text-expand");
-        }
-      }).then(null, console.error);
-    }
+    let longstr = null;
+    this.node.getNodeValue().then(ret => {
+      longstr = ret;
+      return longstr.string();
+    }).then(str => {
+      longstr.release().then(null, console.error);
+      this.value.textContent = str;
+    }).then(null, console.error);
   },
 
   destroy: function () {},
 
   /**
    * Stub method for consistency with ElementEditor.
    */
   getInfoAtNode: function () {
@@ -3109,17 +3100,17 @@ ElementEditor.prototype = {
 
     this.updateTextEditor();
   },
 
   /**
    * Update the inline text editor in case of a single text child node.
    */
   updateTextEditor: function () {
-    let node = this.node.singleTextChild;
+    let node = this.node.inlineTextChild;
 
     if (this.textEditor && this.textEditor.node != node) {
       this.elt.removeChild(this.textEditor.elt);
       this.textEditor = null;
     }
 
     if (node && !this.textEditor) {
       // Create a text editor added to this editor.
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -127,16 +127,17 @@ skip-if = e10s # Bug 1036409 - The last 
 [browser_markup_tag_edit_06.js]
 [browser_markup_tag_edit_07.js]
 [browser_markup_tag_edit_08.js]
 [browser_markup_tag_edit_09.js]
 [browser_markup_tag_edit_10.js]
 [browser_markup_tag_edit_11.js]
 [browser_markup_tag_edit_12.js]
 [browser_markup_tag_edit_13-other.js]
+[browser_markup_textcontent_display.js]
 [browser_markup_textcontent_edit_01.js]
 [browser_markup_textcontent_edit_02.js]
 [browser_markup_toggle_01.js]
 [browser_markup_toggle_02.js]
 [browser_markup_toggle_03.js]
 [browser_markup_update-on-navigtion.js]
 [browser_markup_void_elements_html.js]
 [browser_markup_void_elements_xhtml.js]
--- a/devtools/client/inspector/markup/test/browser_markup_mutation_01.js
+++ b/devtools/client/inspector/markup/test/browser_markup_mutation_01.js
@@ -84,87 +84,87 @@ const TEST_DATA = [
     test: function* (testActor) {
       yield testActor.eval(`
         let node1 = content.document.querySelector("#node1");
         node1.classList.remove("pseudo");
       `);
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(container.singleTextChild, "Has single text child.");
+      ok(container.inlineTextChild, "Has single text child.");
     }
   },
   {
     desc: "Updating the text-content",
     test: function* (testActor) {
       yield testActor.setProperty("#node1", "textContent", "newtext");
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(container.singleTextChild, "Has single text child.");
-      ok(!container.canExpand, "Can't expand container with singleTextChild.");
-      ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
+      ok(container.inlineTextChild, "Has single text child.");
+      ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+      ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
       is(container.editor.elt.querySelector(".text").textContent.trim(),
          "newtext", "Single text child editor updated.");
     }
   },
   {
     desc: "Adding a second text child",
     test: function* (testActor) {
       yield testActor.eval(`
         let node1 = content.document.querySelector("#node1");
         let newText = node1.ownerDocument.createTextNode("more");
         node1.appendChild(newText);
       `);
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(!container.singleTextChild, "Does not have single text child.");
+      ok(!container.inlineTextChild, "Does not have single text child.");
       ok(container.canExpand, "Can expand container with child nodes.");
       ok(container.editor.elt.querySelector(".text") == null,
         "Single text child editor removed.");
     },
   },
   {
     desc: "Go from 2 to 1 text child",
     test: function* (testActor) {
       yield testActor.setProperty("#node1", "textContent", "newtext");
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(container.singleTextChild, "Has single text child.");
-      ok(!container.canExpand, "Can't expand container with singleTextChild.");
-      ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
+      ok(container.inlineTextChild, "Has single text child.");
+      ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+      ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
       ok(container.editor.elt.querySelector(".text").textContent.trim(),
          "newtext", "Single text child editor updated.");
     },
   },
   {
     desc: "Removing an only text child",
     test: function* (testActor) {
       yield testActor.setProperty("#node1", "innerHTML", "");
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(!container.singleTextChild, "Does not have single text child.");
+      ok(!container.inlineTextChild, "Does not have single text child.");
       ok(!container.canExpand, "Can't expand empty container.");
       ok(container.editor.elt.querySelector(".text") == null,
         "Single text child editor removed.");
     },
   },
   {
     desc: "Go from 0 to 1 text child",
     test: function* (testActor) {
       yield testActor.setProperty("#node1", "textContent", "newtext");
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(container.singleTextChild, "Has single text child.");
-      ok(!container.canExpand, "Can't expand container with singleTextChild.");
-      ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
+      ok(container.inlineTextChild, "Has single text child.");
+      ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+      ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
       ok(container.editor.elt.querySelector(".text").textContent.trim(),
          "newtext", "Single text child editor updated.");
     },
   },
 
   {
     desc: "Updating the innerHTML",
     test: function* (testActor) {
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_display.js
@@ -0,0 +1,89 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the rendering of text nodes in the markup view.
+
+const LONG_VALUE = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do " +
+  "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.";
+const SCHEMA = "data:text/html;charset=UTF-8,";
+const TEST_URL = `${SCHEMA}<!DOCTYPE html>
+  <html>
+  <body>
+    <div id="shorttext">Short text</div>
+    <div id="longtext">${LONG_VALUE}</div>
+    <div id="shortcomment"><!--Short comment--></div>
+    <div id="longcomment"><!--${LONG_VALUE}--></div>
+    <div id="shorttext-and-node">Short text<span>Other element</span></div>
+    <div id="longtext-and-node">${LONG_VALUE}<span>Other element</span></div>
+  </body>
+  </html>`;
+
+const TEST_DATA = [{
+  desc: "Test node containing a short text, short text nodes can be inlined.",
+  selector: "#shorttext",
+  inline: true,
+  value: "Short text",
+}, {
+  desc: "Test node containing a long text, long text nodes are not inlined.",
+  selector: "#longtext",
+  inline: false,
+  value: LONG_VALUE,
+}, {
+  desc: "Test node containing a short comment, comments are not inlined.",
+  selector: "#shortcomment",
+  inline: false,
+  value: "Short comment",
+}, {
+  desc: "Test node containing a long comment, comments are not inlined.",
+  selector: "#longcomment",
+  inline: false,
+  value: LONG_VALUE,
+}, {
+  desc: "Test node containing a short text and a span.",
+  selector: "#shorttext-and-node",
+  inline: false,
+  value: "Short text",
+}, {
+  desc: "Test node containing a long text and a span.",
+  selector: "#longtext-and-node",
+  inline: false,
+  value: LONG_VALUE,
+}, ];
+
+add_task(function* () {
+  let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+  for (let data of TEST_DATA) {
+    yield checkNode(inspector, testActor, data);
+  }
+});
+
+function* checkNode(inspector, testActor, {desc, selector, inline, value}) {
+  info(desc);
+
+  let container = yield getContainerForSelector(selector, inspector);
+  let nodeValue = yield getFirstChildNodeValue(selector, testActor);
+  is(nodeValue, value, "The test node's text content is correct");
+
+  is(!!container.inlineTextChild, inline, "Container inlineTextChild is as expected");
+  is(!container.canExpand, inline, "Container canExpand property is as expected");
+
+  let textContainer;
+  if (inline) {
+    textContainer = container.elt.querySelector("pre");
+    ok(!!textContainer, "Text container is already rendered for inline text elements");
+  } else {
+    textContainer = container.elt.querySelector("pre");
+    ok(!textContainer, "Text container is not rendered for collapsed text nodes");
+    yield inspector.markup.expandNode(container.node);
+    yield waitForMultipleChildrenUpdates(inspector);
+
+    textContainer = container.elt.querySelector("pre");
+    ok(!!textContainer, "Text container is rendered after expanding the container");
+  }
+
+  is(textContainer.textContent, value, "The complete text node is rendered.");
+}
--- a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
@@ -2,16 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test editing a node's text content
 
 const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+const {DEFAULT_VALUE_SUMMARY_LENGTH} = require("devtools/server/actors/inspector");
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
 
   info("Expanding all nodes");
   yield inspector.markup.expandAll();
   yield waitForMultipleChildrenUpdates(inspector);
 
@@ -21,71 +22,63 @@ add_task(function* () {
     oldValue: "line6"
   });
 
   yield editContainer(inspector, testActor, {
     selector: "#node17",
     newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
               "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
     oldValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
-              "Donec posuere placerat magna et imperdiet.",
-    shortValue: true
+              "Donec posuere placerat magna et imperdiet."
   });
 
   yield editContainer(inspector, testActor, {
     selector: "#node17",
     newValue: "New value",
     oldValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
+              "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET."
+  });
+
+  yield editContainer(inspector, testActor, {
+    selector: "#node17",
+    newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
               "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
-    shortValue: true
+    oldValue: "New value"
   });
 });
 
-function* getNodeValue(selector, testActor) {
-  let nodeValue = yield testActor.eval(`
-    content.document.querySelector("${selector}").firstChild.nodeValue;
-  `);
-  return nodeValue;
-}
-
 function* editContainer(inspector, testActor,
-                        {selector, newValue, oldValue, shortValue}) {
-  let nodeValue = yield getNodeValue(selector, testActor);
+                        {selector, newValue, oldValue}) {
+  let nodeValue = yield getFirstChildNodeValue(selector, testActor);
   is(nodeValue, oldValue, "The test node's text content is correct");
 
   info("Changing the text content");
   let onMutated = inspector.once("markupmutation");
   let container = yield focusNode(selector, inspector);
-  let field = container.elt.querySelector("pre");
 
-  if (shortValue) {
-    is(oldValue.indexOf(
-       field.textContent.substring(0, field.textContent.length - 1)),
-       0,
-       "The shortened value starts with the full value " + field.textContent);
-    ok(oldValue.length > field.textContent.length,
-       "The shortened value is short");
-  } else {
-    is(field.textContent, oldValue,
-       "The text node has the correct original value");
-  }
+  let isOldValueInline = oldValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH;
+  is(!!container.inlineTextChild, isOldValueInline, "inlineTextChild is as expected");
+  is(!container.canExpand, isOldValueInline, "canExpand property is as expected");
 
-  inspector.markup.markNodeAsSelected(container.node);
-
-  if (shortValue) {
-    info("Waiting for the text to be updated");
-    yield inspector.markup.once("text-expand");
-  }
-
+  let field = container.elt.querySelector("pre");
   is(field.textContent, oldValue,
      "The text node has the correct original value after selecting");
   setEditableFieldValue(field, newValue, inspector);
 
   info("Listening to the markupmutation event");
   yield onMutated;
 
-  nodeValue = yield getNodeValue(selector, testActor);
+  nodeValue = yield getFirstChildNodeValue(selector, testActor);
   is(nodeValue, newValue, "The test node's text content has changed");
 
+  let isNewValueInline = newValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH;
+  is(!!container.inlineTextChild, isNewValueInline, "inlineTextChild is as expected");
+  is(!container.canExpand, isNewValueInline, "canExpand property is as expected");
+
+  if (isOldValueInline != isNewValueInline) {
+    is(container.expanded, !isNewValueInline,
+      "Container was automatically expanded/collapsed");
+  }
+
   info("Selecting the <body> to reset the selection");
   let bodyContainer = yield getContainerForSelector("body", inspector);
   inspector.markup.markNodeAsSelected(bodyContainer.node);
 }
--- a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js
@@ -12,17 +12,17 @@ const SELECTOR = ".node6";
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
 
   info("Expanding all nodes");
   yield inspector.markup.expandAll();
   yield waitForMultipleChildrenUpdates(inspector);
 
-  let nodeValue = yield getNodeValue(SELECTOR, testActor);
+  let nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor);
   let expectedValue = "line6";
   is(nodeValue, expectedValue, "The test node's text content is correct");
 
   info("Open editable field for .node6");
   let container = yield focusNode(SELECTOR, inspector);
   let field = container.elt.querySelector("pre");
   field.focus();
   EventUtils.sendKey("return", inspector.panelWin);
@@ -79,27 +79,20 @@ add_task(function* () {
   info("Caret should be back on the first line");
   checkSelectionPositions(editor, 1, 1);
 
   info("Commit the new value with RETURN, wait for the markupmutation event");
   let onMutated = inspector.once("markupmutation");
   yield sendKey("VK_RETURN", {}, editor, inspector.panelWin);
   yield onMutated;
 
-  nodeValue = yield getNodeValue(SELECTOR, testActor);
+  nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor);
   is(nodeValue, expectedValue, "The test node's text content is correct");
 });
 
-function* getNodeValue(selector, testActor) {
-  let nodeValue = yield testActor.eval(`
-    content.document.querySelector("${selector}").firstChild.nodeValue;
-  `);
-  return nodeValue;
-}
-
 /**
  * Check that the editor selection is at the expected positions.
  */
 function checkSelectionPositions(editor, expectedStart, expectedEnd) {
   is(editor.input.selectionStart, expectedStart,
     "Selection should start at " + expectedStart);
   is(editor.input.selectionEnd, expectedEnd,
     "Selection should end at " + expectedEnd);
--- a/devtools/client/inspector/markup/test/head.js
+++ b/devtools/client/inspector/markup/test/head.js
@@ -85,16 +85,30 @@ var getContainerForSelector = Task.async
   info("Getting the markup-container for node " + selector);
   let nodeFront = yield getNodeFront(selector, inspector);
   let container = getContainerForNodeFront(nodeFront, inspector);
   info("Found markup-container " + container);
   return container;
 });
 
 /**
+ * Retrieve the nodeValue for the firstChild of a provided selector on the content page.
+ *
+ * @param {String} selector
+ * @param {TestActorFront} testActor The current TestActorFront instance.
+ * @return {String} the nodeValue of the first
+ */
+function* getFirstChildNodeValue(selector, testActor) {
+  let nodeValue = yield testActor.eval(`
+    content.document.querySelector("${selector}").firstChild.nodeValue;
+  `);
+  return nodeValue;
+}
+
+/**
  * Using the markupview's _waitForChildren function, wait for all queued
  * children updates to be handled.
  * @param {InspectorPanel} inspector The instance of InspectorPanel currently
  * loaded in the toolbox
  * @return a promise that resolves when all queued children updates have been
  * handled
  */
 function waitForChildrenUpdated({markup}) {
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -243,28 +243,29 @@ var NodeActor = exports.NodeActor = prot
 
   // Returns the JSON representation of this object over the wire.
   form: function (detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
 
     let parentNode = this.walker.parentNode(this);
-    let singleTextChild = this.walker.singleTextChild(this);
+    let inlineTextChild = this.walker.inlineTextChild(this);
 
     let form = {
       actor: this.actorID,
       baseURI: this.rawNode.baseURI,
       parent: parentNode ? parentNode.actorID : undefined,
       nodeType: this.rawNode.nodeType,
       namespaceURI: this.rawNode.namespaceURI,
       nodeName: this.rawNode.nodeName,
+      nodeValue: this.rawNode.nodeValue,
       displayName: getNodeDisplayName(this.rawNode),
       numChildren: this.numChildren,
-      singleTextChild: singleTextChild ? singleTextChild.form() : undefined,
+      inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
 
       // doctype attributes
       name: this.rawNode.name,
       publicId: this.rawNode.publicId,
       systemId: this.rawNode.systemId,
 
       attrs: this.writeAttrs(),
       isBeforePseudoElement: this.isBeforePseudoElement,
@@ -280,28 +281,16 @@ var NodeActor = exports.NodeActor = prot
         this.rawNode.ownerDocument.contentType === "text/html",
       hasEventListeners: this._hasEventListeners,
     };
 
     if (this.isDocumentElement()) {
       form.isDocumentElement = true;
     }
 
-    if (this.rawNode.nodeValue) {
-      // We only include a short version of the value if it's longer than
-      // gValueSummaryLength
-      if (this.rawNode.nodeValue.length > gValueSummaryLength) {
-        form.shortValue = this.rawNode.nodeValue
-          .substring(0, gValueSummaryLength);
-        form.incompleteValue = true;
-      } else {
-        form.shortValue = this.rawNode.nodeValue;
-      }
-    }
-
     // Add an extra API for custom properties added by other
     // modules/extensions.
     form.setFormProperty = (name, value) => {
       if (!form.props) {
         form.props = {};
       }
       form.props[name] = value;
     };
@@ -325,16 +314,17 @@ var NodeActor = exports.NodeActor = prot
     // Create the observer on the node's actor.  The node will make sure
     // the observer is cleaned up when the actor is released.
     let observer = new node.defaultView.MutationObserver(callback);
     observer.mergeAttributeRecords = true;
     observer.observe(node, {
       nativeAnonymousChildList: true,
       attributes: true,
       characterData: true,
+      characterDataOldValue: true,
       childList: true,
       subtree: true
     });
     this.mutationObserver = observer;
   },
 
   get isBeforePseudoElement() {
     return this.rawNode.nodeName === "_moz_generated_content_before";
@@ -1142,38 +1132,42 @@ var WalkerActor = protocol.ActorClassWit
     let parent = walker.parentNode();
     if (parent) {
       return this._ref(parent);
     }
     return null;
   },
 
   /**
-   * If the given NodeActor only has a single text node as a child,
-   * return that child's NodeActor.
+   * If the given NodeActor only has a single text node as a child with a text
+   * content small enough to be inlined, return that child's NodeActor.
    *
    * @param NodeActor node
    */
-  singleTextChild: function (node) {
+  inlineTextChild: function (node) {
     // Quick checks to prevent creating a new walker if possible.
     if (node.isBeforePseudoElement ||
         node.isAfterPseudoElement ||
         node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE ||
         node.rawNode.children.length > 0) {
       return undefined;
     }
 
     let docWalker = this.getDocumentWalker(node.rawNode);
     let firstChild = docWalker.firstChild();
 
-    // If the first child isn't a text node, or there are multiple children
-    // then bail out
+    // Bail out if:
+    // - more than one child
+    // - unique child is not a text node
+    // - unique child is a text node, but is too long to be inlined
     if (!firstChild ||
+        docWalker.nextSibling() ||
         firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE ||
-        docWalker.nextSibling()) {
+        firstChild.nodeValue.length > gValueSummaryLength
+        ) {
       return undefined;
     }
 
     return this._ref(firstChild);
   },
 
   /**
    * Mark a node as 'retained'.
@@ -2195,28 +2189,27 @@ var WalkerActor = protocol.ActorClassWit
    * And additional attributes based on the mutation type:
    *
    * `attributes` type:
    *   attributeName: <string> - the attribute that changed
    *   attributeNamespace: <string> - the attribute's namespace URI, if any.
    *   newValue: <string> - The new value of the attribute, if any.
    *
    * `characterData` type:
-   *   newValue: <string> - the new shortValue for the node
-   *   [incompleteValue: true] - True if the shortValue was truncated.
+   *   newValue: <string> - the new nodeValue for the node
    *
    * `childList` type is returned when the set of children for a node
    * has changed.  Includes extra data, which can be used by the client to
    * maintain its ownership subtree.
    *
    *   added: array of <domnode actor ID> - The list of actors *previously
    *     seen by the client* that were added to the target node.
    *   removed: array of <domnode actor ID> The list of actors *previously
    *     seen by the client* that were removed from the target node.
-   *   singleTextChild: If the node now has a single text child, it will
+   *   inlineTextChild: If the node now has a single text child, it will
    *     be sent here.
    *
    * Actors that are included in a MutationRecord's `removed` but
    * not in an `added` have been removed from the client's ownership
    * tree (either by being moved under a node the client has seen yet
    * or by being removed from the tree entirely), and is considered
    * 'orphaned'.
    *
@@ -2283,23 +2276,18 @@ var WalkerActor = protocol.ActorClassWit
 
       if (type === "attributes") {
         mutation.attributeName = change.attributeName;
         mutation.attributeNamespace = change.attributeNamespace || undefined;
         mutation.newValue = targetNode.hasAttribute(mutation.attributeName) ?
                             targetNode.getAttribute(mutation.attributeName)
                             : null;
       } else if (type === "characterData") {
-        if (targetNode.nodeValue.length > gValueSummaryLength) {
-          mutation.newValue = targetNode.nodeValue
-            .substring(0, gValueSummaryLength);
-          mutation.incompleteValue = true;
-        } else {
-          mutation.newValue = targetNode.nodeValue;
-        }
+        mutation.newValue = targetNode.nodeValue;
+        this._maybeQueueInlineTextChildMutation(change, targetNode);
       } else if (type === "childList" || type === "nativeAnonymousChildList") {
         // Get the list of removed and added actors that the client has seen
         // so that it can keep its ownership tree up to date.
         let removedActors = [];
         let addedActors = [];
         for (let removed of change.removedNodes) {
           let removedActor = this.getNode(removed);
           if (!removedActor) {
@@ -2325,25 +2313,60 @@ var WalkerActor = protocol.ActorClassWit
           this._orphaned.delete(addedActor);
           addedActors.push(addedActor.actorID);
         }
 
         mutation.numChildren = targetActor.numChildren;
         mutation.removed = removedActors;
         mutation.added = addedActors;
 
-        let singleTextChild = this.singleTextChild(targetActor);
-        if (singleTextChild) {
-          mutation.singleTextChild = singleTextChild.form();
+        let inlineTextChild = this.inlineTextChild(targetActor);
+        if (inlineTextChild) {
+          mutation.inlineTextChild = inlineTextChild.form();
         }
       }
       this.queueMutation(mutation);
     }
   },
 
+  /**
+   * Check if the provided mutation could change the way the target element is
+   * inlined with its parent node. If it might, a custom mutation of type
+   * "inlineTextChild" will be queued.
+   *
+   * @param {MutationRecord} mutation
+   *        A characterData type mutation
+   */
+  _maybeQueueInlineTextChildMutation: function (mutation) {
+    let {oldValue, target} = mutation;
+    let newValue = target.nodeValue;
+    let limit = gValueSummaryLength;
+
+    if ((oldValue.length <= limit && newValue.length <= limit) ||
+        (oldValue.length > limit && newValue.length > limit)) {
+      // Bail out if the new & old values are both below/above the size limit.
+      return;
+    }
+
+    let parentActor = this.getNode(target.parentNode);
+    if (!parentActor || parentActor.rawNode.children.length > 0) {
+      // If the parent node has other children, a character data mutation will
+      // not change anything regarding inlining text nodes.
+      return;
+    }
+
+    let inlineTextChild = this.inlineTextChild(parentActor);
+    this.queueMutation({
+      type: "inlineTextChild",
+      target: parentActor.actorID,
+      inlineTextChild:
+        inlineTextChild ? inlineTextChild.form() : undefined
+    });
+  },
+
   onFrameLoad: function ({ window, isTopLevel }) {
     if (!this.rootDoc && isTopLevel) {
       this.rootDoc = window.document;
       this.rootNode = this.document();
       this.queueMutation({
         type: "newRoot",
         target: this.rootNode.form()
       });
--- a/devtools/shared/fronts/inspector.js
+++ b/devtools/shared/fronts/inspector.js
@@ -1,26 +1,24 @@
 /* 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/. */
 "use strict";
 
-const Services = require("Services");
 const { Ci } = require("chrome");
 require("devtools/shared/fronts/styles");
 require("devtools/shared/fronts/highlighters");
 const { ShortLongString } = require("devtools/server/actors/string");
 const {
   Front,
   FrontClassWithSpec,
   custom,
   preEvent,
   types
 } = require("devtools/shared/protocol.js");
-const { makeInfallible } = require("devtools/shared/DevToolsUtils");
 const {
   inspectorSpec,
   nodeSpec,
   nodeListSpec,
   walkerSpec
 } = require("devtools/shared/specs/inspector");
 const promise = require("promise");
 const { Task } = require("devtools/shared/task");
@@ -66,25 +64,16 @@ const AttributeModificationList = Class(
     this.setAttributeNS(ns, name, undefined);
   },
 
   removeAttribute: function (name) {
     this.setAttributeNS(undefined, name, undefined);
   }
 });
 
-// A resolve that hits the main loop first.
-function delayedResolve(value) {
-  let deferred = promise.defer();
-  Services.tm.mainThread.dispatch(makeInfallible(() => {
-    deferred.resolve(value);
-  }), 0);
-  return deferred.promise;
-}
-
 /**
  * Client side of the node actor.
  *
  * Node fronts are strored in a tree that mirrors the DOM tree on the
  * server, but with a few key differences:
  *  - Not all children will be necessary loaded for each node.
  *  - The order of children isn't guaranteed to be the same as the DOM.
  * Children are stored in a doubly-linked list, to make addition/removal
@@ -117,34 +106,42 @@ const NodeFront = FrontClassWithSpec(nod
   },
 
   // Update the object given a form representation off the wire.
   form: function (form, detail, ctx) {
     if (detail === "actorid") {
       this.actorID = form;
       return;
     }
+
+    // backward-compatibility: shortValue indicates we are connected to old server
+    if (form.shortValue) {
+      // If the value is not complete, set nodeValue to null, it will be fetched
+      // when calling getNodeValue()
+      form.nodeValue = form.incompleteValue ? null : form.shortValue;
+    }
+
     // Shallow copy of the form.  We could just store a reference, but
     // eventually we'll want to update some of the data.
     this._form = object.merge(form);
     this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
 
     if (form.parent) {
       // Get the owner actor for this actor (the walker), and find the
       // parent node of this actor from it, creating a standin node if
       // necessary.
       let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent);
       this.reparent(parentNodeFront);
     }
 
-    if (form.singleTextChild) {
-      this.singleTextChild =
-        types.getType("domnode").read(form.singleTextChild, ctx);
+    if (form.inlineTextChild) {
+      this.inlineTextChild =
+        types.getType("domnode").read(form.inlineTextChild, ctx);
     } else {
-      this.singleTextChild = undefined;
+      this.inlineTextChild = undefined;
     }
   },
 
   /**
    * Returns the parent NodeFront for this NodeFront.
    */
   parentNode: function () {
     return this._parent;
@@ -181,18 +178,17 @@ const NodeFront = FrontClassWithSpec(nod
       if (!found && change.newValue !== null) {
         this.attributes.push({
           name: change.attributeName,
           namespace: change.attributeNamespace,
           value: change.newValue
         });
       }
     } else if (change.type === "characterData") {
-      this._form.shortValue = change.newValue;
-      this._form.incompleteValue = change.incompleteValue;
+      this._form.nodeValue = change.newValue;
     } else if (change.type === "pseudoClassLock") {
       this._form.pseudoClassLocks = change.pseudoClassLocks;
     } else if (change.type === "events") {
       this._form.hasEventListeners = change.hasEventListeners;
     }
   },
 
   // Some accessors to make NodeFront feel more like an nsIDOMNode
@@ -254,22 +250,16 @@ const NodeFront = FrontClassWithSpec(nod
     return this._form.isAnonymous;
   },
   get isInHTMLDocument() {
     return this._form.isInHTMLDocument;
   },
   get tagName() {
     return this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null;
   },
-  get shortValue() {
-    return this._form.shortValue;
-  },
-  get incompleteValue() {
-    return !!this._form.incompleteValue;
-  },
 
   get isDocumentElement() {
     return !!this._form.isDocumentElement;
   },
 
   // doctype properties
   get name() {
     return this._form.name;
@@ -319,21 +309,24 @@ const NodeFront = FrontClassWithSpec(nod
         return false;
       }
       parent = parent.parentNode();
     }
     return true;
   },
 
   getNodeValue: custom(function () {
-    if (!this.incompleteValue) {
-      return delayedResolve(new ShortLongString(this.shortValue));
+    // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
+    // value of the node needs to be fetched on the server.
+    if (this._form.nodeValue === null && this._form.shortValue) {
+      return this._getNodeValue();
     }
 
-    return this._getNodeValue();
+    let str = this._form.nodeValue || "";
+    return promise.resolve(new ShortLongString(str));
   }, {
     impl: "_getNodeValue"
   }),
 
   // Accessors for custom form properties.
 
   getFormProperty: function (name) {
     return this._form.props ? this._form.props[name] : null;
@@ -801,23 +794,16 @@ const WalkerFront = FrontClassWithSpec(w
             addedFront.reparent(targetFront);
 
             // The actor is reconnected to the ownership tree, unorphan
             // it.
             this._orphaned.delete(addedFront);
             addedFronts.push(addedFront);
           }
 
-          if (change.singleTextChild) {
-            targetFront.singleTextChild =
-              types.getType("domnode").read(change.singleTextChild, this);
-          } else {
-            targetFront.singleTextChild = undefined;
-          }
-
           // Before passing to users, replace the added and removed actor
           // ids with front in the mutation record.
           emittedMutation.added = addedFronts;
           emittedMutation.removed = removedFronts;
 
           // If this is coming from a DOM mutation, the actor's numChildren
           // was passed in. Otherwise, it is simulated from a frame load or
           // unload, so don't change the front's form.
@@ -853,16 +839,29 @@ const WalkerFront = FrontClassWithSpec(w
             let releasedFront = this.get(released);
             this._retainedOrphans.delete(released);
             this._releaseFront(releasedFront, true);
           }
         } else {
           targetFront.updateMutation(change);
         }
 
+        // Update the inlineTextChild property of the target for a selected list of
+        // mutation types.
+        if (change.type === "inlineTextChild" ||
+            change.type === "childList" ||
+            change.type === "nativeAnonymousChildList") {
+          if (change.inlineTextChild) {
+            targetFront.inlineTextChild =
+              types.getType("domnode").read(change.inlineTextChild, this);
+          } else {
+            targetFront.inlineTextChild = undefined;
+          }
+        }
+
         emitMutations.push(emittedMutation);
       }
 
       if (options.cleanup) {
         for (let node of this._orphaned) {
           // This will move retained nodes to this._retainedOrphans.
           this._releaseFront(node);
         }