Bug 264412. Implement HTMLElement.innerText. r=smaug,mats
authorRobert O'Callahan <robert@ocallahan.org>
Wed, 21 Oct 2015 10:23:17 +1300
changeset 304552 a396f4262479a5695c918f59bacd542ba21448b0
parent 304551 ab657569f554a1b1dae9575fb95ca5a94949442a
child 304553 b70e89c03c5656ba5eff891fd0bcbb3ec044aa91
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, mats
bugs264412
milestone44.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 264412. Implement HTMLElement.innerText. r=smaug,mats
dom/base/FragmentOrElement.cpp
dom/base/nsRange.cpp
dom/base/nsRange.h
dom/html/nsGenericHTMLElement.cpp
dom/html/nsGenericHTMLElement.h
dom/webidl/HTMLElement.webidl
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/innerText/getter-tests.js
testing/web-platform/tests/innerText/getter.html
testing/web-platform/tests/innerText/setter-tests.js
testing/web-platform/tests/innerText/setter.html
--- a/dom/base/FragmentOrElement.cpp
+++ b/dom/base/FragmentOrElement.cpp
@@ -1154,17 +1154,17 @@ FragmentOrElement::RemoveChildAt(uint32_
     doRemoveChildAt(aIndex, aNotify, oldKid, mAttrsAndChildren);
   }
 }
 
 void
 FragmentOrElement::GetTextContentInternal(nsAString& aTextContent,
                                           ErrorResult& aError)
 {
-  if(!nsContentUtils::GetNodeTextContent(this, true, aTextContent, fallible)) {
+  if (!nsContentUtils::GetNodeTextContent(this, true, aTextContent, fallible)) {
     aError.Throw(NS_ERROR_OUT_OF_MEMORY);
   }
 }
 
 void
 FragmentOrElement::SetTextContentInternal(const nsAString& aTextContent,
                                           ErrorResult& aError)
 {
--- a/dom/base/nsRange.cpp
+++ b/dom/base/nsRange.cpp
@@ -30,16 +30,19 @@
 #include "mozilla/dom/DocumentType.h"
 #include "mozilla/dom/RangeBinding.h"
 #include "mozilla/dom/DOMRect.h"
 #include "mozilla/dom/ShadowRoot.h"
 #include "mozilla/dom/Selection.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/Likely.h"
 #include "nsCSSFrameConstructor.h"
+#include "nsStyleStruct.h"
+#include "nsStyleStructInlines.h"
+#include "nsComputedDOMStyle.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 JSObject*
 nsRange::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return RangeBinding::Wrap(aCx, this, aGivenProto);
@@ -3188,8 +3191,241 @@ nsRange::ExcludeNonSelectableNodes(nsTAr
         }
       }
       if (iter->IsDone()) {
         return;
       }
     }
   }
 }
+
+struct InnerTextAccumulator {
+  nsString& mString;
+  int8_t mRequiredLineBreakCount;
+  InnerTextAccumulator(mozilla::dom::DOMString& aValue)
+    : mString(aValue.AsAString()), mRequiredLineBreakCount(0) {}
+  void FlushLineBreaks()
+  {
+    while (mRequiredLineBreakCount > 0) {
+      // Required line breaks at the start of the text are suppressed.
+      if (!mString.IsEmpty()) {
+        mString.Append('\n');
+      }
+      --mRequiredLineBreakCount;
+    }
+  }
+  void Append(char ch)
+  {
+    Append(nsAutoString(ch));
+  }
+  void Append(const nsAString& aString)
+  {
+    if (aString.IsEmpty()) {
+      return;
+    }
+    FlushLineBreaks();
+    mString.Append(aString);
+  }
+  void AddRequiredLineBreakCount(int8_t aCount)
+  {
+    mRequiredLineBreakCount = std::max(mRequiredLineBreakCount, aCount);
+  }
+};
+
+static bool
+IsVisibleAndNotInReplacedElement(nsIFrame* aFrame)
+{
+  if (!aFrame || !aFrame->StyleVisibility()->IsVisible()) {
+    return false;
+  }
+  for (nsIFrame* f = aFrame->GetParent(); f; f = f->GetParent()) {
+    if (f->GetContent() && f->GetContent()->IsHTMLElement(nsGkAtoms::button)) {
+      continue;
+    }
+    if (f->IsFrameOfType(nsIFrame::eReplaced)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+static bool
+ElementIsVisible(nsIContent* aContent)
+{
+  Element* elem = aContent->AsElement();
+  if (!elem) {
+    return false;
+  }
+  RefPtr<nsStyleContext> sc = nsComputedDOMStyle::GetStyleContextForElement(
+      elem, nullptr, nullptr);
+  return sc && sc->StyleVisibility()->IsVisible();
+}
+
+static void
+AppendTransformedText(InnerTextAccumulator& aResult,
+                      nsGenericDOMDataNode* aTextNode,
+                      int32_t aStart, int32_t aEnd)
+{
+  nsIFrame* frame = aTextNode->GetPrimaryFrame();
+  if (!IsVisibleAndNotInReplacedElement(frame)) {
+    return;
+  }
+  nsIFrame::RenderedText text = frame->GetRenderedText(aStart, aEnd);
+  aResult.Append(text.mString);
+}
+
+enum TreeTraversalState {
+  AT_NODE,
+  AFTER_NODE
+};
+
+static int8_t
+GetRequiredInnerTextLineBreakCount(nsIFrame* aFrame)
+{
+  if (aFrame->GetContent()->IsHTMLElement(nsGkAtoms::p)) {
+    return 2;
+  }
+  const nsStyleDisplay* styleDisplay = aFrame->StyleDisplay();
+  if (styleDisplay->IsBlockOutside(aFrame) ||
+      styleDisplay->mDisplay == NS_STYLE_DISPLAY_TABLE_CAPTION) {
+    return 1;
+  }
+  return 0;
+}
+
+static bool
+IsLastCellOfRow(nsIFrame* aFrame)
+{
+  nsIAtom* type = aFrame->GetType();
+  if (type != nsGkAtoms::tableCellFrame &&
+      type != nsGkAtoms::bcTableCellFrame) {
+    return true;
+  }
+  for (nsIFrame* c = aFrame; c; c = c->GetNextContinuation()) {
+    if (c->GetNextSibling()) {
+      return false;
+    }
+  }
+  return true;
+}
+
+static bool
+IsLastRowOfRowGroup(nsIFrame* aFrame)
+{
+  if (aFrame->GetType() != nsGkAtoms::tableRowFrame) {
+    return true;
+  }
+  for (nsIFrame* c = aFrame; c; c = c->GetNextContinuation()) {
+    if (c->GetNextSibling()) {
+      return false;
+    }
+  }
+  return true;
+}
+
+static bool
+IsLastNonemptyRowGroupOfTable(nsIFrame* aFrame)
+{
+  if (aFrame->GetType() != nsGkAtoms::tableRowGroupFrame) {
+    return true;
+  }
+  for (nsIFrame* c = aFrame; c; c = c->GetNextContinuation()) {
+    for (nsIFrame* next = c->GetNextSibling(); next; next = next->GetNextSibling()) {
+      if (next->GetFirstPrincipalChild()) {
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+void
+nsRange::GetInnerTextNoFlush(mozilla::dom::DOMString& aValue,
+                             mozilla::ErrorResult& aError)
+{
+  InnerTextAccumulator result(aValue);
+  nsIContent* currentNode = mStartParent->AsContent();
+  TreeTraversalState currentState = AFTER_NODE;
+  if (mStartParent->IsNodeOfType(nsINode::eTEXT)) {
+    auto t = static_cast<nsGenericDOMDataNode*>(mStartParent.get());
+    if (mStartParent == mEndParent) {
+      AppendTransformedText(result, t, mStartOffset, mEndOffset);
+      return;
+    }
+    AppendTransformedText(result, t, mStartOffset, t->TextLength());
+  } else {
+    if (uint32_t(mStartOffset) < mStartParent->GetChildCount()) {
+      currentNode = mStartParent->GetChildAt(mStartOffset);
+      currentState = AT_NODE;
+    }
+  }
+
+  nsIContent* endNode = mEndParent->AsContent();
+  TreeTraversalState endState = AFTER_NODE;
+  if (mEndParent->IsNodeOfType(nsINode::eTEXT)) {
+    endState = AT_NODE;
+  } else {
+    if (uint32_t(mEndOffset) < mEndParent->GetChildCount()) {
+      endNode = mEndParent->GetChildAt(mEndOffset);
+      endState = AT_NODE;
+    }
+  }
+
+  while (currentNode != endNode || currentState != endState) {
+    nsIFrame* f = currentNode->GetPrimaryFrame();
+    bool isVisibleAndNotReplaced = IsVisibleAndNotInReplacedElement(f);
+    if (currentState == AT_NODE) {
+      bool isText = currentNode->IsNodeOfType(nsINode::eTEXT);
+      if (isText && currentNode->GetParent()->IsHTMLElement(nsGkAtoms::rp) &&
+          ElementIsVisible(currentNode->GetParent())) {
+        nsAutoString str;
+        currentNode->GetTextContent(str, aError);
+        result.Append(str);
+      } else if (isVisibleAndNotReplaced) {
+        result.AddRequiredLineBreakCount(GetRequiredInnerTextLineBreakCount(f));
+        if (isText) {
+          nsIFrame::RenderedText text = f->GetRenderedText();
+          result.Append(text.mString);
+        }
+      }
+      nsIContent* child = currentNode->GetFirstChild();
+      if (child) {
+        currentNode = child;
+      } else {
+        currentState = AFTER_NODE;
+      }
+    } else {
+      if (isVisibleAndNotReplaced) {
+        if (currentNode->IsHTMLElement(nsGkAtoms::br)) {
+          result.Append('\n');
+        }
+        switch (f->StyleDisplay()->mDisplay) {
+        case NS_STYLE_DISPLAY_TABLE_CELL:
+          if (!IsLastCellOfRow(f)) {
+            result.Append('\t');
+          }
+          break;
+        case NS_STYLE_DISPLAY_TABLE_ROW:
+          if (!IsLastRowOfRowGroup(f) ||
+              !IsLastNonemptyRowGroupOfTable(f->GetParent())) {
+            result.Append('\n');
+          }
+          break;
+        }
+        result.AddRequiredLineBreakCount(GetRequiredInnerTextLineBreakCount(f));
+      }
+      nsIContent* next = currentNode->GetNextSibling();
+      if (next) {
+        currentNode = next;
+        currentState = AT_NODE;
+      } else {
+        currentNode = currentNode->GetParent();
+      }
+    }
+  }
+
+  if (mEndParent->IsNodeOfType(nsINode::eTEXT)) {
+    nsGenericDOMDataNode* t = static_cast<nsGenericDOMDataNode*>(mEndParent.get());
+    AppendTransformedText(result, t, 0, mEndOffset);
+  }
+  // Do not flush trailing line breaks! Required breaks at the end of the text
+  // are suppressed.
+}
--- a/dom/base/nsRange.h
+++ b/dom/base/nsRange.h
@@ -207,16 +207,18 @@ public:
   void SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr);
   void SetStartAfter(nsINode& aNode, ErrorResult& aErr);
   void SetStartBefore(nsINode& aNode, ErrorResult& aErr);
   void SurroundContents(nsINode& aNode, ErrorResult& aErr);
   already_AddRefed<DOMRect> GetBoundingClientRect(bool aClampToEdge = true,
                                                   bool aFlushLayout = true);
   already_AddRefed<DOMRectList> GetClientRects(bool aClampToEdge = true,
                                                bool aFlushLayout = true);
+  void GetInnerTextNoFlush(mozilla::dom::DOMString& aValue,
+                           mozilla::ErrorResult& aError);
 
   nsINode* GetParentObject() const { return mOwner; }
   virtual JSObject* WrapObject(JSContext* cx, JS::Handle<JSObject*> aGivenProto) override final;
 
 private:
   // no copy's or assigns
   nsRange(const nsRange&);
   nsRange& operator=(const nsRange&);
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -79,16 +79,18 @@
 #include "nsIEditor.h"
 #include "nsIEditorIMESupport.h"
 #include "nsLayoutUtils.h"
 #include "mozAutoDocUpdate.h"
 #include "nsHtml5Module.h"
 #include "nsITextControlElement.h"
 #include "mozilla/dom/Element.h"
 #include "HTMLFieldSetElement.h"
+#include "nsTextNode.h"
+#include "HTMLBRElement.h"
 #include "HTMLMenuElement.h"
 #include "nsDOMMutationObserver.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/dom/FromParser.h"
 #include "mozilla/dom/Link.h"
 #include "mozilla/dom/UndoManager.h"
 #include "mozilla/BloomFilter.h"
 
@@ -99,16 +101,17 @@
 #include "nsTextFragment.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/TouchEvent.h"
 #include "mozilla/ErrorResult.h"
 #include "nsHTMLDocument.h"
 #include "nsGlobalWindow.h"
 #include "mozilla/dom/HTMLBodyElement.h"
 #include "imgIContainer.h"
+#include "nsComputedDOMStyle.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 /**
  * nsAutoFocusEvent is used to dispatch a focus event when a
  * nsGenericHTMLFormElement is binded to the tree with the autofocus attribute
  * enabled.
@@ -3289,8 +3292,110 @@ nsGenericHTMLElement::NewURIFromString(c
     // document. Fail here instead of returning the recursive URI
     // and waiting for the subsequent load to fail.
     NS_RELEASE(*aURI);
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
   return NS_OK;
 }
+
+void
+nsGenericHTMLElement::GetInnerText(mozilla::dom::DOMString& aValue,
+                                   mozilla::ErrorResult& aError)
+{
+  if (!GetPrimaryFrame(Flush_Layout)) {
+    RefPtr<nsStyleContext> sc =
+        nsComputedDOMStyle::GetStyleContextForElementNoFlush(this, nullptr, nullptr);
+    if (!sc || sc->StyleDisplay()->mDisplay == NS_STYLE_DISPLAY_NONE ||
+        !GetCurrentDoc()) {
+      GetTextContentInternal(aValue, aError);
+      return;
+    }
+  }
+
+  RefPtr<nsRange> range;
+  nsresult rv = nsRange::CreateRange(static_cast<nsINode*>(this), 0, this,
+      GetChildCount(), getter_AddRefs(range));
+  if (NS_FAILED(rv)) {
+    aError.Throw(rv);
+    return;
+  }
+
+  range->GetInnerTextNoFlush(aValue, aError);
+}
+
+void
+nsGenericHTMLElement::SetInnerText(const nsAString& aValue)
+{
+  // Fire DOMNodeRemoved mutation events before we do anything else.
+  nsCOMPtr<nsIContent> kungFuDeathGrip;
+
+  // Batch possible DOMSubtreeModified events.
+  mozAutoSubtreeModified subtree(nullptr, nullptr);
+
+  // Scope firing mutation events so that we don't carry any state that
+  // might be stale
+  {
+    // We're relying on mozAutoSubtreeModified to keep a strong reference if
+    // needed.
+    nsIDocument* doc = OwnerDoc();
+
+    // Optimize the common case of there being no observers
+    if (nsContentUtils::HasMutationListeners(doc, NS_EVENT_BITS_MUTATION_NODEREMOVED)) {
+      subtree.UpdateTarget(doc, nullptr);
+      // Keep "this" alive during mutation listener firing
+      kungFuDeathGrip = this;
+      nsCOMPtr<nsINode> child;
+      for (child = nsINode::GetFirstChild();
+           child && child->GetParentNode() == this;
+           child = child->GetNextSibling()) {
+        nsContentUtils::MaybeFireNodeRemoved(child, this, doc);
+      }
+    }
+  }
+
+  // Might as well stick a batch around this since we're performing several
+  // mutations.
+  mozAutoDocUpdate updateBatch(GetComposedDoc(),
+    UPDATE_CONTENT_MODEL, true);
+  nsAutoMutationBatch mb;
+
+  uint32_t childCount = GetChildCount();
+
+  mb.Init(this, true, false);
+  for (uint32_t i = 0; i < childCount; ++i) {
+    RemoveChildAt(0, true);
+  }
+  mb.RemovalDone();
+
+  nsString str;
+  const char16_t* s = aValue.BeginReading();
+  const char16_t* end = aValue.EndReading();
+  while (true) {
+    if (s != end && *s == '\r' && s + 1 != end && s[1] == '\n') {
+      // a \r\n pair should only generate one <br>, so just skip the \r
+      ++s;
+    }
+    if (s == end || *s == '\r' || *s == '\n') {
+      if (!str.IsEmpty()) {
+        RefPtr<nsTextNode> textContent =
+            new nsTextNode(NodeInfo()->NodeInfoManager());
+        textContent->SetText(str, true);
+        AppendChildTo(textContent, true);
+      }
+      if (s == end) {
+        break;
+      }
+      str.SetLength(0);
+      already_AddRefed<mozilla::dom::NodeInfo> ni =
+          NodeInfo()->NodeInfoManager()->GetNodeInfo(nsGkAtoms::br,
+                      nullptr, kNameSpaceID_XHTML, nsIDOMNode::ELEMENT_NODE);
+      RefPtr<HTMLBRElement> br = new HTMLBRElement(ni);
+      AppendChildTo(br, true);
+    } else {
+      str.Append(*s);
+    }
+    ++s;
+  }
+
+  mb.NodesAdded();
+}
--- a/dom/html/nsGenericHTMLElement.h
+++ b/dom/html/nsGenericHTMLElement.h
@@ -236,16 +236,19 @@ public:
   {
     return mScrollgrab;
   }
   void SetScrollgrab(bool aValue)
   {
     mScrollgrab = aValue;
   }
 
+  void GetInnerText(mozilla::dom::DOMString& aValue, mozilla::ErrorResult& aError);
+  void SetInnerText(const nsAString& aValue);
+
   /**
    * Determine whether an attribute is an event (onclick, etc.)
    * @param aName the attribute
    * @return whether the name is an event handler name
    */
   virtual bool IsEventAttributeName(nsIAtom* aName) override;
 
 #define EVENT(name_, id_, type_, struct_) /* nothing; handled by nsINode */
--- a/dom/webidl/HTMLElement.webidl
+++ b/dom/webidl/HTMLElement.webidl
@@ -17,16 +17,19 @@ interface HTMLElement : Element {
            attribute DOMString title;
            attribute DOMString lang;
   //         attribute boolean translate;
   [SetterThrows, Pure]
            attribute DOMString dir;
   [Constant]
   readonly attribute DOMStringMap dataset;
 
+  [GetterThrows, Pure]
+           attribute DOMString innerText;
+
   // microdata 
   [SetterThrows, Pure]
            attribute boolean itemScope;
   [PutForwards=value,Constant] readonly attribute DOMSettableTokenList itemType;
   [SetterThrows, Pure]
            attribute DOMString itemId;
   [PutForwards=value,Constant] readonly attribute DOMSettableTokenList itemRef;
   [PutForwards=value,Constant] readonly attribute DOMSettableTokenList itemProp;
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -18709,16 +18709,24 @@
         "path": "http/content_length.html",
         "url": "/http/content_length.html"
       },
       {
         "path": "infrastructure/failing-test.html",
         "url": "/infrastructure/failing-test.html"
       },
       {
+        "path": "innerText/getter.html",
+        "url": "/innerText/getter.html"
+      },
+      {
+        "path": "innerText/setter.html",
+        "url": "/innerText/setter.html"
+      },
+      {
         "path": "js/behaviours/SetPrototypeOf-window.html",
         "url": "/js/behaviours/SetPrototypeOf-window.html"
       },
       {
         "path": "js/builtins/Array.DefineOwnProperty.html",
         "url": "/js/builtins/Array.DefineOwnProperty.html"
       },
       {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/innerText/getter-tests.js
@@ -0,0 +1,322 @@
+testText("<div>abc", "abc", "Simplest possible test");
+
+/**** white-space:normal ****/
+
+testText("<div> abc", "abc", "Leading whitespace removed");
+testText("<div>abc ", "abc", "Trailing whitespace removed");
+testText("<div>abc  def", "abc def", "Internal whitespace compressed");
+testText("<div>abc\ndef", "abc def", "\\n converted to space");
+testText("<div>abc\rdef", "abc def", "\\r converted to space");
+testText("<div>abc\tdef", "abc def", "\\t converted to space");
+testText("<div>abc <br>def", "abc\ndef", "Trailing whitespace before hard line break removed");
+
+/**** <pre> ****/
+
+testText("<pre> abc", " abc", "Leading whitespace preserved");
+testText("<pre>abc ", "abc ", "Trailing whitespace preserved");
+testText("<pre>abc  def", "abc  def", "Internal whitespace preserved");
+testText("<pre>abc\ndef", "abc\ndef", "\\n preserved");
+testText("<pre>abc\rdef", "abc\ndef", "\\r converted to newline");
+testText("<pre>abc\tdef", "abc\tdef", "\\t preserved");
+
+/**** <div style="white-space:pre"> ****/
+
+testText("<div style='white-space:pre'> abc", " abc", "Leading whitespace preserved");
+testText("<div style='white-space:pre'>abc ", "abc ", "Trailing whitespace preserved");
+testText("<div style='white-space:pre'>abc  def", "abc  def", "Internal whitespace preserved");
+testText("<div style='white-space:pre'>abc\ndef", "abc\ndef", "\\n preserved");
+testText("<div style='white-space:pre'>abc\rdef", "abc\ndef", "\\r converted to newline");
+testText("<div style='white-space:pre'>abc\tdef", "abc\tdef", "\\t preserved");
+
+/**** <span style="white-space:pre"> ****/
+
+testText("<span style='white-space:pre'> abc", " abc", "Leading whitespace preserved");
+testText("<span style='white-space:pre'>abc ", "abc ", "Trailing whitespace preserved");
+testText("<span style='white-space:pre'>abc  def", "abc  def", "Internal whitespace preserved");
+testText("<span style='white-space:pre'>abc\ndef", "abc\ndef", "\\n preserved");
+testText("<span style='white-space:pre'>abc\rdef", "abc\ndef", "\\r converted to newline");
+testText("<span style='white-space:pre'>abc\tdef", "abc\tdef", "\\t preserved");
+
+/**** <div style="white-space:pre-line"> ****/
+
+testText("<div style='white-space:pre-line'> abc", "abc", "Leading whitespace removed");
+testText("<div style='white-space:pre-line'>abc ", "abc", "Trailing whitespace removed");
+testText("<div style='white-space:pre-line'>abc  def", "abc def", "Internal whitespace collapsed");
+testText("<div style='white-space:pre-line'>abc\ndef", "abc\ndef", "\\n preserved");
+testText("<div style='white-space:pre-line'>abc\rdef", "abc\ndef", "\\r converted to newline");
+testText("<div style='white-space:pre-line'>abc\tdef", "abc def", "\\t converted to space");
+
+/**** Collapsing whitespace across element boundaries ****/
+
+testText("<div><span>abc </span> def", "abc def", "Whitespace collapses across element boundaries");
+testText("<div><span>abc </span><span></span> def", "abc def", "Whitespace collapses across element boundaries");
+testText("<div><span>abc </span><span style='white-space:pre'></span> def", "abc def", "Whitespace collapses across element boundaries");
+
+/**** Soft line breaks ****/
+
+testText("<div style='width:0'>abc def", "abc def", "Soft line breaks ignored");
+
+/**** first-line/first-letter ****/
+
+testText("<div class='first-line-uppercase' style='width:0'>abc def", "ABC def", "::first-line styles applied");
+testText("<div class='first-letter-uppercase' style='width:0'>abc def", "Abc def", "::first-letter styles applied");
+testText("<div class='first-letter-float' style='width:0'>abc def", "abc def", "::first-letter float ignored");
+
+/**** &nbsp; ****/
+
+testText("<div>&nbsp;", "\xA0", "&nbsp; preserved");
+
+/**** display:none ****/
+
+testText("<div style='display:none'>abc", "abc", "display:none container");
+testText("<div style='display:none'>abc  def", "abc  def", "No whitespace compression in display:none container");
+testText("<div style='display:none'> abc def ", " abc def ", "No removal of leading/trailing whitespace in display:none container");
+testText("<div>123<span style='display:none'>abc", "123", "display:none child not rendered");
+
+/**** display:contents ****/
+
+if (CSS.supports("display", "contents")) {
+  testText("<div style='display:contents'>abc", "abc", "display:contents container");
+  testText("<div><div style='display:contents'>abc", "abc", "display:contents container");
+  testText("<div>123<span style='display:contents'>abc", "123abc", "display:contents rendered");
+  testText("<div>123<span style='display:contents'>abc", "123abc", "display:contents rendered");
+  testText("<div style='display:contents'>   ", "", "display:contents not processed via textContent");
+  testText("<div><div style='display:contents'>   ", "", "display:contents not processed via textContent");
+}
+
+/**** visibility:hidden ****/
+
+testText("<div style='visibility:hidden'>abc", "", "visibility:hidden container");
+testText("<div>123<span style='visibility:hidden'>abc", "123", "visibility:hidden child not rendered");
+testText("<div style='visibility:hidden'>123<span style='visibility:visible'>abc", "abc", "visibility:visible child rendered");
+
+/**** visibility:collapse ****/
+
+testText("<table><tbody style='visibility:collapse'><tr><td>abc", "", "visibility:collapse row-group");
+testText("<table><tr style='visibility:collapse'><td>abc", "", "visibility:collapse row");
+testText("<table><tr><td style='visibility:collapse'>abc", "", "visibility:collapse cell");
+testText("<table><tbody style='visibility:collapse'><tr><td style='visibility:visible'>abc", "abc",
+         "visibility:collapse row-group with visible cell");
+testText("<table><tr style='visibility:collapse'><td style='visibility:visible'>abc", "abc",
+         "visibility:collapse row with visible cell");
+testText("<div style='display:flex'><span style='visibility:collapse'>1</span><span>2</span></div>",
+         "2", "visibility:collapse honored on flex item");
+testText("<div style='display:grid'><span style='visibility:collapse'>1</span><span>2</span></div>",
+         "2", "visibility:collapse honored on grid item");
+
+/**** opacity:0 ****/
+
+testText("<div style='opacity:0'>abc", "abc", "opacity:0 container");
+testText("<div style='opacity:0'>abc  def", "abc def", "Whitespace compression in opacity:0 container");
+testText("<div style='opacity:0'> abc def ", "abc def", "Remove leading/trailing whitespace in opacity:0 container");
+testText("<div>123<span style='opacity:0'>abc", "123abc", "opacity:0 child rendered");
+
+/**** generated content ****/
+
+testText("<div class='before'>", "", "Generated content not included");
+testText("<div><div class='before'>", "", "Generated content on child not included");
+
+/**** innerText on replaced elements ****/
+
+testText("<button>abc", "abc", "<button> contents preserved");
+testText("<fieldset>abc", "abc", "<fieldset> contents preserved");
+testText("<fieldset><legend>abc", "abc", "<fieldset> <legend> contents preserved");
+testText("<input type='text' value='abc'>", "", "<input> contents ignored");
+testText("<textarea>abc", "", "<textarea> contents ignored");
+testText("<select size='1'><option>abc</option><option>def", "", "<select size='1'> contents ignored");
+testText("<select size='2'><option>abc</option><option>def", "", "<select size='2'> contents ignored");
+testText("<select size='1'><option id='target'>abc</option><option>def", "", "<select size='1'> contents ignored");
+testText("<select size='2'><option id='target'>abc</option><option>def", "", "<select size='2'> contents ignored");
+testText("<iframe>abc", "", "<iframe> contents ignored");
+testText("<iframe><div id='target'>abc", "", "<iframe> contents ignored");
+testText("<iframe src='data:text/html,abc'>", "","<iframe> subdocument ignored");
+testText("<audio style='display:block'>abc", "", "<audio> contents ignored");
+testText("<audio style='display:block'><source id='target' class='poke' style='display:block'>", "", "<audio> contents ignored");
+testText("<audio style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<audio> contents ok if display:none");
+testText("<video>abc", "", "<video> contents ignored");
+testText("<video style='display:block'><source id='target' class='poke' style='display:block'>", "", "<video> contents ignored");
+testText("<video style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<video> contents ok if display:none");
+testText("<canvas>abc", "", "<canvas> contents ignored");
+testText("<canvas><div id='target'>abc", "", "<canvas><div id='target'> contents ignored");
+testText("<img alt='abc'>", "", "<img> alt text ignored");
+testText("<img src='about:blank' class='poke'>", "", "<img> contents ignored");
+
+/**** innerText on replaced element children ****/
+
+testText("<div><button>abc", "abc", "<button> contents preserved");
+testText("<div><fieldset>abc", "abc", "<fieldset> contents preserved");
+testText("<div><fieldset><legend>abc", "abc", "<fieldset> <legend> contents preserved");
+testText("<div><input type='text' value='abc'>", "", "<input> contents ignored");
+testText("<div><textarea>abc", "", "<textarea> contents ignored");
+testText("<div><select size='1'><option>abc</option><option>def", "", "<select size='1'> contents ignored");
+testText("<div><select size='2'><option>abc</option><option>def", "", "<select size='2'> contents ignored");
+testText("<div><iframe>abc", "", "<iframe> contents ignored");
+testText("<div><iframe src='data:text/html,abc'>", ""," <iframe> subdocument ignored");
+testText("<div><audio>abc", "", "<audio> contents ignored");
+testText("<div><video>abc", "", "<video> contents ignored");
+testText("<div><canvas>abc", "", "<canvas> contents ignored");
+testText("<div><img alt='abc'>", "", "<img> alt text ignored");
+
+/**** Lines around blocks ****/
+
+testText("<div>123<div>abc</div>def", "123\nabc\ndef", "Newline at block boundary");
+testText("<div>123<span style='display:block'>abc</span>def", "123\nabc\ndef", "Newline at display:block boundary");
+testText("<div>abc<div></div>def", "abc\ndef", "Empty block induces single line break");
+testText("<div>abc<div></div><div></div>def", "abc\ndef", "Consecutive empty blocks ignored");
+testText("<div><p>abc", "abc", "No blank lines around <p> alone");
+testText("<div><p>abc</p> ", "abc", "No blank lines around <p> followed by only collapsible whitespace");
+testText("<div> <p>abc</p>", "abc", "No blank lines around <p> preceded by only collapsible whitespace");
+testText("<div><p>abc<p>def", "abc\n\ndef", "Blank line between consecutive <p>s");
+testText("<div><p>abc</p> <p>def", "abc\n\ndef", "Blank line between consecutive <p>s separated only by collapsible whitespace");
+testText("<div><p>abc</p><div></div><p>def", "abc\n\ndef", "Blank line between consecutive <p>s separated only by empty block");
+testText("<div><p>abc</p><div>123</div><p>def", "abc\n\n123\n\ndef", "Blank lines between <p>s separated by non-empty block");
+testText("<div>abc<div><p>123</p></div>def", "abc\n\n123\n\ndef", "Blank lines around a <p> in its own block");
+testText("<div>abc<p>def", "abc\n\ndef", "Blank line before <p>");
+testText("<div><p>abc</p>def", "abc\n\ndef", "Blank line after <p>");
+testText("<div><p>abc<p></p><p></p><p>def", "abc\n\ndef", "One blank line between <p>s, ignoring empty <p>s");
+testText("<div style='visibility:hidden'><p><span style='visibility:visible'>abc</span></p>\n<div style='visibility:visible'>def</div>",
+     "abc\ndef", "Invisible <p> doesn't induce extra line breaks");
+testText("<div>abc<div style='margin:2em'>def", "abc\ndef", "No blank lines around <div> with margin");
+testText("<div>123<span style='display:inline-block'>abc</span>def", "123abcdef", "No newlines at display:inline-block boundary");
+testText("<div>123<span style='display:inline-block'> abc </span>def", "123abcdef", "Leading/trailing space removal at display:inline-block boundary");
+
+/**** Spans ****/
+
+testText("<div>123<span>abc</span>def", "123abcdef", "<span> boundaries are irrelevant");
+testText("<div>123<em>abc</em>def", "123abcdef", "<em> gets no special treatment");
+testText("<div>123<b>abc</b>def", "123abcdef", "<b> gets no special treatment");
+testText("<div>123<i>abc</i>def", "123abcdef", "<i> gets no special treatment");
+testText("<div>123<strong>abc</strong>def", "123abcdef", "<strong> gets no special treatment");
+testText("<div>123<tt>abc</tt>def", "123abcdef", "<tt> gets no special treatment");
+testText("<div>123<code>abc</code>def", "123abcdef", "<code> gets no special treatment");
+
+/**** Soft hyphen ****/
+
+testText("<div>abc&shy;def", "abc\xADdef", "soft hyphen preserved");
+testText("<div style='width:0'>abc&shy;def", "abc\xADdef", "soft hyphen preserved");
+
+/**** Tables ****/
+
+testText("<div><table style='white-space:pre'>  <td>abc</td>  </table>", "abc", "Ignoring non-rendered table whitespace");
+testText("<div><table><tr><td>abc<td>def</table>", "abc\tdef", "Tab-separated table cells");
+testText("<div><table><tr><td>abc<td><td>def</table>", "abc\t\tdef", "Tab-separated table cells including empty cells");
+testText("<div><table><tr><td>abc<td><td></table>", "abc\t\t", "Tab-separated table cells including trailing empty cells");
+testText("<div><table><tr><td>abc<tr><td>def</table>", "abc\ndef", "Newline-separated table rows");
+testText("<div>abc<table><td>def</table>ghi", "abc\ndef\nghi", "Newlines around table");
+testText("<div><table style='border-collapse:collapse'><tr><td>abc<td>def</table>", "abc\tdef",
+         "Tab-separated table cells in a border-collapse table");
+testText("<div><table><tfoot>x</tfoot><tbody>y</tbody></table>", "xy", "tfoot not reordered");
+testText("<table><tfoot><tr><td>footer</tfoot><thead><tr><td style='visibility:collapse'>thead</thead><tbody><tr><td>tbody</tbody></table>",
+         "footer\n\ntbody", "");
+
+/**** Table captions ****/
+
+testText("<div><table><tr><td>abc<caption>def</caption></table>", "abc\ndef", "Newline between cells and caption");
+
+/**** display:table ****/
+
+testText("<div><div class='table'><span class='cell'>abc</span>\n<span class='cell'>def</span></div>",
+         "abc\tdef", "Tab-separated table cells");
+testText("<div><div class='table'><span class='row'><span class='cell'>abc</span></span>\n<span class='row'><span class='cell'>def</span></span></div>",
+         "abc\ndef", "Newline-separated table rows");
+testText("<div>abc<div class='table'><span class='cell'>def</span></div>ghi", "abc\ndef\nghi", "Newlines around table");
+
+/**** display:inline-table ****/
+
+testText("<div><div class='itable'><span class='cell'>abc</span>\n<span class='cell'>def</span></div>", "abc\tdef", "Tab-separated table cells");
+testText("<div><div class='itable'><span class='row'><span class='cell'>abc</span></span>\n<span class='row'><span class='cell'>def</span></span></div>",
+         "abc\ndef", "Newline-separated table rows");
+testText("<div>abc<div class='itable'><span class='cell'>def</span></div>ghi", "abcdefghi", "No newlines around inline-table");
+testText("<div>abc<div class='itable'><span class='row'><span class='cell'>def</span></span>\n<span class='row'><span class='cell'>123</span></span></div>ghi",
+         "abcdef\n123ghi", "Single newline in two-row inline-table");
+
+/**** Lists ****/
+
+testText("<div><ol><li>abc", "abc", "<ol> list items get no special treatment");
+testText("<div><ul><li>abc", "abc", "<ul> list items get no special treatment");
+
+/**** Misc elements ****/
+
+testText("<div><script style='display:block'>abc", "abc", "display:block <script> is rendered");
+testText("<div><style style='display:block'>abc", "abc", "display:block <style> is rendered");
+testText("<div><noscript style='display:block'>abc", "", "display:block <noscript> is not rendered (it's not parsed!)");
+testText("<div><template style='display:block'>abc", "",
+         "display:block <template> contents are not rendered (the contents are in a different document)");
+testText("<div>abc<br>def", "abc\ndef", "<br> induces line break");
+testText("<div>abc<br>", "abc\n", "<br> induces line break even at end of block");
+testText("<div><br class='poke'>", "\n", "<br> content ignored");
+testText("<div>abc<hr>def", "abc\ndef", "<hr> induces line break");
+testText("<div>abc<hr><hr>def", "abc\ndef", "<hr><hr> induces just one line break");
+testText("<div>abc<hr><hr><hr>def", "abc\ndef", "<hr><hr><hr> induces just one line break");
+testText("<div><hr class='poke'>", "abc", "<hr> content rendered");
+testText("<div>abc<!--comment-->def", "abcdef", "comment ignored");
+
+/**** text-transform ****/
+
+testText("<div><div style='text-transform:uppercase'>abc", "ABC", "text-transform is applied");
+
+/**** block-in-inline ****/
+
+testText("<div>abc<span>123<div>456</div>789</span>def", "abc123\n456\n789def", "block-in-inline doesn't add unnecessary newlines");
+
+/**** floats ****/
+
+testText("<div>abc<div style='float:left'>123</div>def", "abc\n123\ndef", "floats induce a block boundary");
+testText("<div>abc<span style='float:left'>123</span>def", "abc\n123\ndef", "floats induce a block boundary");
+
+/**** position ****/
+
+testText("<div>abc<div style='position:absolute'>123</div>def", "abc\n123\ndef", "position:absolute induces a block boundary");
+testText("<div>abc<span style='position:absolute'>123</span>def", "abc\n123\ndef", "position:absolute induces a block boundary");
+testText("<div>abc<div style='position:relative'>123</div>def", "abc\n123\ndef", "position:relative has no effect");
+testText("<div>abc<span style='position:relative'>123</span>def", "abc123def", "position:relative has no effect");
+
+/**** text-overflow:ellipsis ****/
+
+testText("<div style='overflow:hidden'>abc", "abc", "overflow:hidden ignored");
+// XXX Chrome skips content with width:0 or height:0 and overflow:hidden;
+// should we spec that?
+testText("<div style='width:0; overflow:hidden'>abc", "abc", "overflow:hidden ignored even with zero width");
+testText("<div style='height:0; overflow:hidden'>abc", "abc", "overflow:hidden ignored even with zero height");
+testText("<div style='width:0; overflow:hidden; text-overflow:ellipsis'>abc", "abc", "text-overflow:ellipsis ignored");
+
+/**** Support on non-HTML elements ****/
+
+testText("<svg>abc", undefined, "innerText not supported on SVG elements");
+testText("<math>abc", undefined, "innerText not supported on MathML elements");
+
+/**** Ruby ****/
+
+testText("<div><ruby>abc<rp>(</rp><rt>def</rt><rp>)</rp></ruby>", "abc(def)", "<rp> rendered");
+testText("<div><rp>abc</rp>", "abc", "Lone <rp> rendered");
+testText("<div><rp style='visibility:hidden'>abc</rp>", "", "visibility:hidden <rp> not rendered");
+testText("<div><rp> abc </rp>", " abc ", "Lone <rp> rendered without whitespace trimming");
+testText("<div><rp style='display:block'>abc</rp>def", "abc\ndef", "display:block <rp> induces line breaks");
+testText("<div><rp style='display:block'> abc </rp>def", " abc \ndef", "display:block <rp> induces line breaks but doesn't trim whitespace");
+// XXX this is not desirable but the spec currently requires it.
+testText("<div><select class='poke-rp'></select>", "abc", "<rp> in a replaced element still renders");
+
+/**** Shadow DOM ****/
+
+if ("createShadowRoot" in document.body) {
+  testText("<div class='shadow'>", "", "Shadow DOM contents ignored");
+  testText("<div><div class='shadow'>", "", "Shadow DOM contents ignored");
+}
+
+/**** Flexbox ****/
+
+if (CSS.supports('display', 'flex')) {
+  testText("<div style='display:flex'><div style='order:1'>1</div><div>2</div></div>",
+           "1\n2", "CSS 'order' property ignored");
+  testText("<div style='display:flex'><span>1</span><span>2</span></div>",
+           "1\n2", "Flex items blockified");
+}
+
+/**** Grid ****/
+
+if (CSS.supports('display', 'grid')) {
+  testText("<div style='display:grid'><div style='order:1'>1</div><div>2</div></div>",
+           "1\n2", "CSS 'order' property ignored");
+  testText("<div style='display:grid'><span>1</span><span>2</span></div>",
+           "1\n2", "Grid items blockified");
+}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/innerText/getter.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>innerText getter test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+.before::before { content:'abc'; }
+.table { display:table; }
+.itable { display:inline-table; }
+.row { display:table-row; }
+.cell { display:table-cell; }
+.first-line-uppercase::first-line { text-transform:uppercase; }
+.first-letter-uppercase::first-letter { text-transform:uppercase; }
+.first-letter-float::first-letter { float:left; }
+</style>
+<div id="container"></div>
+<script>
+function testText(html, expectedPlain, msg) {
+  test(function() {
+    container.innerHTML = html;
+    var e = document.getElementById('target');
+    if (!e) {
+      e = container.firstChild;
+    }
+    var pokes = document.getElementsByClassName('poke');
+    for (var i = 0; i < pokes.length; ++i) {
+      pokes[i].textContent = 'abc';
+    }
+    pokes = document.getElementsByClassName('poke-rp');
+    for (var i = 0; i < pokes.length; ++i) {
+      var rp = document.createElement("rp");
+      rp.textContent = "abc";
+      pokes[i].appendChild(rp);
+    }
+    var shadows = document.getElementsByClassName('shadow');
+    for (var i = 0; i < shadows.length; ++i) {
+      var s = shadows[i].createShadowRoot();
+      s.textContent = 'abc';
+    }
+    while (e && e.nodeType != Node.ELEMENT_NODE) {
+      e = e.nextSibling;
+    }
+    assert_equals(e.innerText, expectedPlain);
+  }, msg);
+}
+</script>
+<script src="getter-tests.js"></script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/innerText/setter-tests.js
@@ -0,0 +1,24 @@
+testText("<div>", "abc", "abc", "Simplest possible test");
+testHTML("<div>", "abc\ndef", "abc<br>def", "Newlines convert to <br> in non-white-space:pre elements");
+testHTML("<pre>", "abc\ndef", "abc<br>def", "Newlines convert to <br> in <pre> element");
+testHTML("<div style='white-space:pre'>", "abc\ndef", "abc<br>def", "Newlines convert to <br> in white-space:pre element");
+testHTML("<div>", "abc\rdef", "abc<br>def", "CRs convert to <br> in non-white-space:pre elements");
+testHTML("<pre>", "abc\rdef", "abc<br>def", "CRs convert to <br> in <pre> element");
+testHTML("<div>", "abc\r\ndef", "abc<br>def", "Newline/CR pair converts to <br> in non-white-space:pre element");
+testHTML("<div>", "abc\n\ndef", "abc<br><br>def", "Newline/newline pair converts to two <br>s in non-white-space:pre element");
+testHTML("<div>", "abc\r\rdef", "abc<br><br>def", "CR/CR pair converts to two <br>s in non-white-space:pre element");
+testHTML("<div style='white-space:pre'>", "abc\rdef", "abc<br>def", "CRs convert to <br> in white-space:pre element");
+testText("<div>", "abc<def", "abc<def", "< preserved");
+testText("<div>", "abc>def", "abc>def", "> preserved");
+testText("<div>", "abc&", "abc&", "& preserved");
+testText("<div>", "abc\"def", "abc\"def", "\" preserved");
+testText("<div>", "abc\'def", "abc\'def", "\' preserved");
+testHTML("<svg>", "abc", "", "innerText not supported on SVG elements");
+testHTML("<math>", "abc", "", "innerText not supported on MathML elements");
+testText("<div>", "abc\0def", "abc\0def", "Null characters preserved");
+testText("<div>", "abc\tdef", "abc\tdef", "Tabs preserved");
+testText("<div>", " abc", " abc", "Leading whitespace preserved");
+testText("<div>", "abc ", "abc ", "Trailing whitespace preserved");
+testText("<div>", "abc  def", "abc  def", "Whitespace not compressed");
+testHTML("<div>abc\n\n", "abc", "abc", "Existing text deleted");
+testHTML("<div><br>", "abc", "abc", "Existing <br> deleted");
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/innerText/setter.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>innerText setter test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="container"></div>
+<script>
+function setupTest(context, plain) {
+  container.innerHTML = context;
+  var e = container.firstChild;
+  while (e && e.nodeType != Node.ELEMENT_NODE) {
+    e = e.nextSibling;
+  }
+  e.innerText = plain;
+  return e;
+}
+function testText(context, plain, expectedText, msg) {
+  test(function(){
+    var e = setupTest(context, plain);
+    assert_not_equals(e.firstChild, null, "Should have a child");
+    assert_equals(e.firstChild.nodeType, Node.TEXT_NODE, "Child should be a text node");
+    assert_equals(e.firstChild.nextSibling, null, "Should have only one child");
+    assert_equals(e.firstChild.data, expectedText);
+  }, msg);
+}
+function testHTML(context, plain, expectedHTML, msg) {
+  test(function(){
+    var e = setupTest(context, plain);
+    assert_equals(e.innerHTML, expectedHTML);
+  }, msg);
+}
+</script>
+<script src="setter-tests.js"></script>