Bug 1202458 - part1: inline text nodes in markupview only if they are short enough;r=pbro
authorJulian Descottes <jdescottes@mozilla.com>
Thu, 02 Jun 2016 10:41:49 +0200
changeset 375177 00ee1688f8a29667b941be32f4b94b407926b022
parent 375176 be6fddfc1390a14cd560eb843162f0cccd804e63
child 375178 e3622cb1ef4266152f887583a621df599452905f
push id20186
push userrhelmer@mozilla.com
push dateFri, 03 Jun 2016 16:55:36 +0000
reviewerspbro
bugs1202458
milestone49.0a1
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
@@ -158,19 +158,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");
 const nodeConstants = require("devtools/shared/dom-node-constants");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -985,16 +983,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;
@@ -1545,39 +1547,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);
       }
@@ -2004,17 +2006,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;
   },
@@ -2713,23 +2715,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();
@@ -2901,35 +2903,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 () {
@@ -3110,17 +3101,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,25 +1,23 @@
 /* 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");
 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 === nodeConstants.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);
         }