m87: Add test coverage for editable events

This change precisely stipulates the events handled for each type of editable
encountered through tests.

TBR=​[email protected]

pressing 'e' would also cause sticky mode to turn off.

(cherry picked from commit 788cc609e1a8f386aec22cfbf8d06fe8dc984a5f)

Fixed: 1135118
AX-Relnotes: fixes an issue in Smart Sticky mode where jumping to a text field by
Change-Id: I56de4cebc0a1a942d99c5d0c9445b5b33f9d4e00
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2448053
Reviewed-by: Akihiro Ota <[email protected]>
Commit-Queue: David Tseng <[email protected]>
Cr-Original-Commit-Position: refs/heads/master@{#814056}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2461607
Reviewed-by: David Tseng <[email protected]>
Cr-Commit-Position: refs/branch-heads/4280@{#148}
Cr-Branched-From: ea420fb963f9658c9969b6513c56b8f47efa1a2a-refs/heads/master@{#812852}
diff --git a/chrome/browser/chromeos/accessibility/spoken_feedback_browsertest.cc b/chrome/browser/chromeos/accessibility/spoken_feedback_browsertest.cc
index 8a574a4..7b55449 100644
--- a/chrome/browser/chromeos/accessibility/spoken_feedback_browsertest.cc
+++ b/chrome/browser/chromeos/accessibility/spoken_feedback_browsertest.cc
@@ -871,6 +871,26 @@
   sm_.ExpectSpeech("Sticky mode enabled");
   sm_.ExpectSpeech("start");
 
+  // Try a few jump commands and linear nav with no Search modifier. We never
+  // leave sticky mode.
+  sm_.Call([this]() { SendKeyPress(ui::VKEY_E); });
+  sm_.ExpectSpeech("Edit text");
+
+  sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_F); });
+  sm_.ExpectSpeech("Edit text");
+
+  sm_.Call([this]() { SendKeyPress(ui::VKEY_RIGHT); });
+  sm_.ExpectSpeech("end");
+
+  sm_.Call([this]() { SendKeyPress(ui::VKEY_F); });
+  sm_.ExpectSpeech("Edit text");
+
+  sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_E); });
+  sm_.ExpectSpeech("Edit text");
+
+  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_LEFT); });
+  sm_.ExpectSpeech("start");
+
   // Now, navigate with sticky mode off.
   sm_.Call([this]() { SendStickyKeyCommand(); });
   sm_.ExpectSpeech("Sticky mode disabled");
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
index 3ffa8ee5..5380e31 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
@@ -361,14 +361,37 @@
    * @private
    */
   onEditableChanged_(evt) {
-    // Document selections only apply to rich editables, text selections to
-    // non-rich editables.
-    if (evt.type != EventType.DOCUMENT_SELECTION_CHANGED &&
-        (evt.target.state[StateType.RICHLY_EDITABLE] ||
-         evt.target.htmlTag === 'textarea')) {
+    if (!evt.target.state.editable) {
       return;
     }
 
+    const isInput = evt.target.htmlTag == 'input';
+    const isTextArea = evt.target.htmlTag == 'textarea';
+    const isContentEditable = evt.target.state[StateType.RICHLY_EDITABLE];
+
+    switch (evt.type) {
+      case EventType.TEXT_CHANGED:
+      case EventType.TEXT_SELECTION_CHANGED:
+      case EventType.VALUE_CHANGED:
+        // Known to be duplicated by document selection changes for content
+        // editables and text areas.
+        if (isContentEditable || isTextArea) {
+          return;
+        }
+        break;
+      case EventType.DOCUMENT_SELECTION_CHANGED:
+        // Known to be duplicated by text selection changes.
+        if (isInput) {
+          return;
+        }
+        break;
+      case EventType.FOCUS:
+        // Allowed no matter what.
+        break;
+      default:
+        return;
+    }
+
     if (!this.createTextEditHandlerIfNeeded_(evt.target)) {
       return;
     }
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing.js
index d83d976..ccaf98f 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing.js
@@ -87,14 +87,7 @@
    * @param {!ChromeVoxEvent} evt
    */
   onEvent(evt) {
-    if (evt.type !== EventType.TEXT_CHANGED &&
-        evt.type !== EventType.TEXT_SELECTION_CHANGED &&
-        evt.type !== EventType.DOCUMENT_SELECTION_CHANGED &&
-        evt.type !== EventType.VALUE_CHANGED && evt.type !== EventType.FOCUS) {
-      return;
-    }
-    if (!evt.target.state.focused || !evt.target.state.editable ||
-        evt.target != this.node_) {
+    if (!evt.target.state.focused || evt.target != this.node_) {
       return;
     }
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing_test.js
index 32f381d..3adaf17 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing_test.js
@@ -21,6 +21,13 @@
       EventGenerator.sendKeyPress(keyCode, modifiers);
     };
   }
+
+  waitForEditableEvent() {
+    return new Promise(resolve => {
+      DesktopAutomationHandler.instance.textEditHandler_.onEvent = (e) =>
+          resolve(e);
+    });
+  }
 };
 
 
@@ -96,7 +103,7 @@
           if (input.selectionStart == 0) {
             return;
           }
-          console.log('TIM' + 'ER');
+
           input.value = 'text2';
           window.clearInterval(timer);
         }
@@ -1470,3 +1477,111 @@
         input.focus();
       });
 });
+
+TEST_F('ChromeVoxEditingTest', 'InputEvents', function() {
+  const site = `<input type="text"></input>`;
+  this.runWithLoadedTree(site, async function(root) {
+    const input = root.find({role: RoleType.TEXT_FIELD});
+    input.focus();
+    await new Promise(resolve => {
+      this.listenOnce(input, 'focus', resolve);
+    });
+
+    let event = await this.waitForEditableEvent();
+    assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
+    assertEquals(input, event.target);
+    assertEquals('', input.value);
+
+    this.press(KeyCode.A)();
+
+    // Important to note that there's no document selection changes below.
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.VALUE_CHANGED, event.type);
+    assertEquals(input, event.target);
+    assertEquals('a', input.value);
+
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
+    assertEquals(input, event.target);
+    assertEquals('a', input.value);
+
+    // TODO(accessibility): this extra value change shouldn't happen.
+    // http://crbug.com/1135249.
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.VALUE_CHANGED, event.type);
+    assertEquals(input, event.target);
+    assertEquals('a', input.value);
+
+    this.press(KeyCode.B)();
+
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.VALUE_CHANGED, event.type);
+    assertEquals(input, event.target);
+    assertEquals('ab', input.value);
+
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
+    assertEquals(input, event.target);
+    assertEquals('ab', input.value);
+  });
+});
+
+TEST_F('ChromeVoxEditingTest', 'TextAreaEvents', function() {
+  const site = `<textarea></textarea>`;
+  this.runWithLoadedTree(site, async function(root) {
+    const textArea = root.find({role: RoleType.TEXT_FIELD});
+    textArea.focus();
+    await new Promise(resolve => {
+      this.listenOnce(textArea, 'focus', resolve);
+    });
+
+    let event = await this.waitForEditableEvent();
+    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+    assertEquals(textArea, event.target);
+    assertEquals('', textArea.value);
+
+    this.press(KeyCode.A)();
+
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+    assertEquals(textArea, event.target);
+    assertEquals('a', textArea.value);
+
+    this.press(KeyCode.B)();
+
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+    assertEquals(textArea, event.target);
+    assertEquals('ab', textArea.value);
+  });
+});
+
+TEST_F('ChromeVoxEditingTest', 'ContentEditableEvents', function() {
+  const site = `<div role="textbox" contenteditable></div>`;
+  this.runWithLoadedTree(site, async function(root) {
+    const contentEditable = root.find({role: RoleType.TEXT_FIELD});
+    contentEditable.focus();
+    await new Promise(resolve => {
+      this.listenOnce(contentEditable, 'focus', resolve);
+    });
+
+    let event = await this.waitForEditableEvent();
+    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+    assertEquals(contentEditable, event.target);
+    assertEquals('', contentEditable.value);
+
+    this.press(KeyCode.A)();
+
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+    assertEquals(contentEditable, event.target);
+    assertEquals('a', contentEditable.value);
+
+    this.press(KeyCode.B)();
+
+    event = await this.waitForEditableEvent();
+    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+    assertEquals(contentEditable, event.target);
+    assertEquals('ab', contentEditable.value);
+  });
+});