Fix end of line announcements

- fix Cursor.deepEquivalent to account for end of line selection offset.
- add Cursor.deepEquivalent test coverage.
- add spoken feedback for end of line.
- add test coverage for end of line output.

BUG=719654
CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.linux:closure_compilation

Review-Url: https://codereview.chromium.org/2948173004
Cr-Commit-Position: refs/heads/master@{#482021}
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors.js
index 7142a0e7..1f3f614f 100644
--- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors.js
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors.js
@@ -445,9 +445,14 @@
         var target = newNode.firstChild;
         var length = 0;
         while (target && length < newIndex) {
-          if (length <= newIndex && newIndex < (length + target.name.length))
+          var newLength = length + target.name.length;
+
+          // Either |newIndex| falls between target's text or |newIndex| is the
+          // total length of all sibling text content.
+          if ((length <= newIndex && newIndex < newLength) ||
+              (newIndex == newLength && !target.nextSibling))
             break;
-          length += target.name.length;
+          length = newLength;
           target = target.nextSibling;
         }
         if (target) {
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors_test.extjs b/chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors_test.extjs
index fd8243ce..f0b8a9e 100644
--- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors_test.extjs
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors_test.extjs
@@ -462,3 +462,54 @@
     assertFalse(inlineTextBoxRange.contentEquals(rootRange));
   });
 });
+
+TEST_F('CursorsTest', 'DeepEquivalency', function() {
+  this.runWithLoadedTree(function() {/*!
+    <p style="word-spacing:100000px">this is a test</p>
+  */}, function(root) {
+    var textNode = root.find({role: RoleType.STATIC_TEXT});
+
+    var text = new cursors.Cursor(textNode, 2);
+    deep = text.deepEquivalent;
+    assertEquals('this ', deep.node.name);
+    assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+    assertEquals(2, deep.index);
+
+    text = new cursors.Cursor(textNode, 5);
+    deep = text.deepEquivalent;
+    assertEquals('is ', deep.node.name);
+    assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+    assertEquals(0, deep.index);
+
+    text = new cursors.Cursor(textNode, 7);
+    deep = text.deepEquivalent;
+    assertEquals('is ', deep.node.name);
+    assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+    assertEquals(2, deep.index);
+
+    text = new cursors.Cursor(textNode, 8);
+    deep = text.deepEquivalent;
+    assertEquals('a ', deep.node.name);
+    assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+    assertEquals(0, deep.index);
+
+    text = new cursors.Cursor(textNode, 11);
+    deep = text.deepEquivalent;
+    assertEquals('test', deep.node.name);
+    assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+    assertEquals(1, deep.index);
+
+    // This is the only selection that can be placed at the length of the node's
+    // text. This only happens at the end of a line.
+    text = new cursors.Cursor(textNode, 14);
+    deep = text.deepEquivalent;
+    assertEquals('test', deep.node.name);
+    assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+    assertEquals(4, deep.index);
+
+    // However, any offset larger is invalid.
+    text = new cursors.Cursor(textNode, 15);
+    deep = text.deepEquivalent;
+    assertTrue(text.equals(deep));
+  });
+});
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js
index 4a729cb..fdaae352 100644
--- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js
@@ -391,9 +391,13 @@
 
   /** @override */
   describeSelectionChanged: function(evt) {
-    // Ignore end of text announcements.
-    if ((this.start + 1) == evt.start && evt.start == this.value.length)
+    // Note that since Chrome allows for selection to be placed immediately at
+    // the end of a line (i.e. end == value.length) and since we try to describe
+    // the character to the right, just describe it as a new line.
+    if ((this.start + 1) == evt.start && evt.start == this.value.length) {
+      this.speak('\n', evt.triggeredByUser);
       return;
+    }
 
     cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged.call(
         this, evt);
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing_test.extjs b/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing_test.extjs
index d743aa2..23bc7d14 100644
--- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing_test.extjs
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing_test.extjs
@@ -373,6 +373,43 @@
   });
 });
 
+TEST_F('EditingTest', 'RichTextMoveByCharacterEndOfLine', function() {
+  editing.useRichText = true;
+  var mockFeedback = this.createMockFeedback();
+  this.runWithLoadedTree(function() {/*!
+    <div id="go" role="textbox" contenteditable>Test</div>
+
+    <script>
+      document.getElementById('go').addEventListener('click', function() {
+        var sel = getSelection();
+        sel.modify('move', 'forward', 'character');
+      }, true);
+    </script>
+  */}, function(root) {
+    var input = root.find({role: RoleType.TEXT_FIELD});
+    var moveByChar = input.doDefault.bind(input);
+    var lineText = 'Test';
+
+    this.listenOnce(input, 'focus', function() {
+      mockFeedback.call(moveByChar)
+          .expectSpeech('e')
+          .expectBraille(lineText, { startIndex: 1, endIndex: 1 })
+          .call(moveByChar)
+          .expectSpeech('s')
+          .expectBraille(lineText, { startIndex: 2, endIndex: 2 })
+          .call(moveByChar)
+          .expectSpeech('t')
+          .expectBraille(lineText, { startIndex: 3, endIndex: 3 })
+          .call(moveByChar)
+          .expectSpeech('\n')
+          .expectBraille(lineText, { startIndex: 4, endIndex: 4 })
+
+          .replay();
+    });
+    input.focus();
+  });
+});
+
 TEST_F('EditingTest', 'EditableLineOneStaticText', function() {
   this.runWithLoadedTree(function() {/*!
     <p contenteditable style="word-spacing:100000px">this is a test</p>