chromium/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_keystroke_selection_test.js

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

GEN_INCLUDE(['select_to_speak_e2e_test_base.js']);
GEN_INCLUDE(['../common/testing/mock_tts.js']);

/**
 * Browser tests for select-to-speak's feature to speak text
 * at the press of a keystroke.
 */
SelectToSpeakKeystrokeSelectionTest = class extends SelectToSpeakE2ETest {
  constructor() {
    super();
    this.mockTts = new MockTts();
    chrome.tts = this.mockTts;
  }

  /** @override */
  async setUpDeferred() {
    await super.setUpDeferred();

    await new Promise(resolve => {
      chrome.settingsPrivate.setPref(
          PrefsManager.ENHANCED_VOICES_DIALOG_SHOWN_KEY, true,
          '' /* unused, see crbug.com/866161 */, () => resolve());
    });

    if (!selectToSpeak.prefsManager_.enhancedVoicesDialogShown()) {
      // TODO(b/267705784): This shouldn't happen, but sometimes the
      // setPref call above does not cause PrefsManager.updateSettingsPrefs_ to
      // be called (test: listen to updateSettingsPrefsCallbackForTest_, never
      // called).
      selectToSpeak.prefsManager_.enhancedVoicesDialogShown_ = true;
    }
  }

  /**
   * Function to load a simple webpage, select some of the single text
   * node, and trigger Select-to-Speak to read that partial node. Tests
   * that the selected region creates tts output that matches the expected
   * output.
   * @param {string} text The text to load on the simple webpage.
   * @param {number} anchorOffset The offset into the text node where
   *     focus starts.
   * @param {number} focusOffset The offset into the text node where
   *     focus ends.
   * @param {string} expected The expected string that will be read, ignoring
   *     extra whitespace, after this selection is triggered.
   */
  async testSimpleTextAtKeystroke(text, anchorOffset, focusOffset, expected) {
    await this.testReadTextAtKeystroke(
        '<p>' + text + '</p>', async function(root) {
          // Set the document selection. This will fire the changed event
          // above, allowing us to do the keystroke and test that speech
          // occurred properly.
          const textNode = this.findTextNode(root, text);
          chrome.automation.setDocumentSelection({
            anchorObject: textNode,
            anchorOffset,
            focusObject: textNode,
            focusOffset,
          });
        }, expected);
  }

  /**
   * Function to load given html using a data url, have the caller set a
   * selection on that page, and then trigger select-to-speak to read
   * the selected text. Tests that the tts output matches the expected
   * output.
   * @param {string} contents The web contents to load
   * @param {function(AutomationNode)} setFocusCallback Callback
   *     to take the root node and set the selection appropriately. Once
   *     selection is set, the test will listen for the focus set event and
   *     trigger select-to-speak, comparing the resulting tts output to what
   *     was expected.
   *     will trigger select-to-speak to speak any selected text
   * @param {string} expected The expected string that will be read, ignoring
   *     extra whitespace, after this selection is triggered.
   */
  async testReadTextAtKeystroke(contents, setFocusCallback, expected) {
    setFocusCallback = this.newCallback(setFocusCallback);
    const root = await this.runWithLoadedTree(contents);
    // Set the selection.
    setFocusCallback(root);
    // Wait for Automation to update.
    await this.waitForEvent(
        root, 'documentSelectionChanged', /*capture=*/ false);
    // Speak selected text.
    await this.triggerReadSelectedText(root);
    await this.waitForSpeech();
    assertEquals(this.mockTts.pendingUtterances().length, 1);
    this.assertEqualsCollapseWhitespace(
        this.mockTts.pendingUtterances()[0], expected);
  }

  generateHtmlWithSelection(selectionCode, bodyHtml) {
    return '<script type="text/javascript">' +
        'function doSelection() {' +
        'let selection = window.getSelection();' +
        'let range = document.createRange();' +
        'selection.removeAllRanges();' + selectionCode +
        'selection.addRange(range);}' +
        '</script>' +
        '<body onload="doSelection()">' + bodyHtml + '</body>';
  }

  /**
   * Function to set the value property and the text selection properties of
   * the given node using a text value, a start index, and an end index. It
   * keeps trying to set and wait for textSelStart and textSelEnd until these
   * text selection properties are set with the given indices, respectively.
   * @param {AutomationNode} node The automation node to be set.
   * @param {string} text The text to be set to the node's value property.
   * @param {number} startIndex The index in the text field where focus starts.
   * @param {number} endIndex The index in the text field where focus ends.
   */
  async setValueAndTextSelection(node, value, startIndex, endIndex) {
    node.setValue(value);
    await this.waitForEvent(node, 'valueChanged');

    while (node.textSelStart !== startIndex || node.textSelEnd !== endIndex) {
      node.setSelection(startIndex, endIndex);
      await this.waitForEvent(node, 'textSelectionChanged');
    }
  }
};

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'SpeaksTextAtKeystrokeFullText',
    async function() {
      await this.testSimpleTextAtKeystroke(
          'This is some text', 0, 17, 'This is some text');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'SpeaksTextAtKeystrokePartialText',
    async function() {
      await this.testSimpleTextAtKeystroke(
          'This is some text', 0, 12, 'This is some');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'SpeaksTextAtKeystrokeSingleWord',
    async function() {
      await this.testSimpleTextAtKeystroke('This is some text', 8, 12, 'some');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'SpeaksTextAtKeystrokePartialWord',
    async function() {
      await this.testSimpleTextAtKeystroke('This is some text', 8, 10, 'so');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'SpeaksAcrossNodesAtKeystroke',
    async function() {
      await this.testReadTextAtKeystroke(
          '<p>This is some <b>bold</b> text</p><p>Second paragraph</p>',
          function(root) {
            const firstNode = this.findTextNode(root, 'This is some ');
            const lastNode = this.findTextNode(root, ' text');
            chrome.automation.setDocumentSelection({
              anchorObject: firstNode,
              anchorOffset: 0,
              focusObject: lastNode,
              focusOffset: 5,
            });
          },
          'This is some bold text');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest',
    'SpeaksAcrossNodesSelectedBackwardsAtKeystroke', async function() {
      await this.testReadTextAtKeystroke(
          '<p>This is some <b>bold</b> text</p><p>Second paragraph</p>',
          function(root) {
            // Set the document selection backwards in page order.
            const lastNode = this.findTextNode(root, 'This is some ');
            const firstNode = this.findTextNode(root, ' text');
            chrome.automation.setDocumentSelection({
              anchorObject: firstNode,
              anchorOffset: 5,
              focusObject: lastNode,
              focusOffset: 0,
            });
          },
          'This is some bold text');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'SpeakTextSurroundedByBrs',
    async function() {
      // If you load this html and double-click on "Selected text", this is the
      // document selection that occurs -- into the second <br/> element.

      let setFocusCallback = function(root) {
        const firstNode = this.findTextNode(root, 'Selected text');
        const lastNode = root.findAll({role: 'lineBreak'})[1];
        chrome.automation.setDocumentSelection({
          anchorObject: firstNode,
          anchorOffset: 0,
          focusObject: lastNode,
          focusOffset: 1,
        });
      };
      setFocusCallback = this.newCallback(setFocusCallback);
      const root =
          await this.runWithLoadedTree('<br/><p>Selected text</p><br/>');
      // Add an event listener that will start the user interaction
      // of the test once the selection is completed.
      root.addEventListener(
          'documentSelectionChanged', this.newCallback(async function(event) {
            await this.triggerReadSelectedText(root);
            assertTrue(this.mockTts.currentlySpeaking());
            this.assertEqualsCollapseWhitespace(
                this.mockTts.pendingUtterances()[0], 'Selected text');

            this.mockTts.finishPendingUtterance();
            if (this.mockTts.pendingUtterances().length === 1) {
              this.assertEqualsCollapseWhitespace(
                  this.mockTts.pendingUtterances()[0], '');
            }
          }),
          false);
      setFocusCallback(root);
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'StartsReadingAtFirstNodeWithText',
    async function() {
      await this.testReadTextAtKeystroke(
          '<div id="empty"></div><div><p>This is some <b>bold</b> text</p></div>',
          function(root) {
            const firstNode =
                this.findTextNode(root, 'This is some ').root.children[0];
            const lastNode = this.findTextNode(root, ' text');
            chrome.automation.setDocumentSelection({
              anchorObject: firstNode,
              anchorOffset: 0,
              focusObject: lastNode,
              focusOffset: 5,
            });
          },
          'This is some bold text');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'IgnoresTextMarkedNotUserSelectable',
    async function() {
      await this.testReadTextAtKeystroke(
          '<div><p>This is some <span style="user-select:none">unselectable</span> text</p></div>',
          function(root) {
            const firstNode =
                this.findTextNode(root, 'This is some ').root.children[0];
            const lastNode = this.findTextNode(root, ' text');
            chrome.automation.setDocumentSelection({
              anchorObject: firstNode,
              anchorOffset: 0,
              focusObject: lastNode,
              focusOffset: 5,
            });
          },
          'This is some text');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest',
    'HandlesSingleImageCorrectlyWithAutomation', async function() {
      await this.testReadTextAtKeystroke(
          '<img src="pipe.jpg" alt="one"/>', function(root) {
            const container = root.findAll({role: 'genericContainer'})[0];
            chrome.automation.setDocumentSelection({
              anchorObject: container,
              anchorOffset: 0,
              focusObject: container,
              focusOffset: 1,
            });
          }, 'one');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest',
    'HandlesMultipleImagesCorrectlyWithAutomation', async function() {
      await this.testReadTextAtKeystroke(
          '<img src="pipe.jpg" alt="one"/>' +
              '<img src="pipe.jpg" alt="two"/><img src="pipe.jpg" alt="three"/>',
          function(root) {
            const container = root.findAll({role: 'genericContainer'})[0];
            chrome.automation.setDocumentSelection({
              anchorObject: container,
              anchorOffset: 1,
              focusObject: container,
              focusOffset: 2,
            });
          },
          'two');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest',
    'HandlesMultipleImagesCorrectlyWithJS1', async function() {
      // Using JS to do the selection instead of Automation, so that we can
      // ensure this is stable against changes in chrome.automation.
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 1);' +
          'range.setEnd(body, 2);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<img id="one" src="pipe.jpg" alt="one"/>' +
              '<img id="two" src="pipe.jpg" alt="two"/>' +
              '<img id="three" src="pipe.jpg" alt="three"/>'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'two');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest',
    'HandlesMultipleImagesCorrectlyWithJS2', async function() {
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 1);' +
          'range.setEnd(body, 3);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<img id="one" src="pipe.jpg" alt="one"/>' +
              '<img id="two" src="pipe.jpg" alt="two"/>' +
              '<img id="three" src="pipe.jpg" alt="three"/>'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'two three');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'TextFieldFullySelected',
    async function() {
      const selectionCode = 'let p = document.getElementsByTagName("p")[0];' +
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(p, 0);' +
          'range.setEnd(body, 2);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<p>paragraph</p>' +
              '<input type="text" value="text field">'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'paragraph');

      this.mockTts.finishPendingUtterance();
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'text field');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'TwoTextFieldsFullySelected',
    async function() {
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 0);' +
          'range.setEnd(body, 2);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<input type="text" value="one"></input>' +
              '<textarea cols="5">two three</textarea>'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'one');

      this.mockTts.finishPendingUtterance();
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'two three');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'TextInputPartiallySelected',
    async function() {
      const html = '<script type="text/javascript">' +
          'function doSelection() {' +
          'let input = document.getElementById("input");' +
          'input.focus();' +
          'input.setSelectionRange(5, 10);' +
          '}' +
          '</script>' +
          '<body onload="doSelection()">' +
          '<input id="input" type="text" value="text field"></input>' +
          '</body>';
      const root = await this.runWithLoadedTree(html);
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'field');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'TextAreaPartiallySelected',
    async function() {
      const html = '<script type="text/javascript">' +
          'function doSelection() {' +
          'let input = document.getElementById("input");' +
          'input.focus();' +
          'input.setSelectionRange(6, 17);' +
          '}' +
          '</script>' +
          '<body onload="doSelection()">' +
          '<textarea id="input" type="text" cols="10">first line second line</textarea>' +
          '</body>';
      const root = await this.runWithLoadedTree(html);
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'line second');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBr',
    async function() {
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 0);' +
          'range.setEnd(body, 3);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode, 'Test<br/><br/>Unread'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Test');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBrComplex',
    async function() {
      const selectionCode = 'let p = document.getElementsByTagName("p")[0];' +
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(p, 0);' +
          'range.setEnd(body, 2);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode, '<p>Some text</p><br/><br/>Unread'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Some text');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBrAfterText1',
    async function() {
      // A bug was that if the selection was on the rootWebArea, paragraphs were
      // not counted correctly. The more divs and paragraphs before the
      // selection, the further off it got.
      const selectionCode = 'let p = document.getElementsByTagName("p")[0];' +
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(p, 1);' +
          'range.setEnd(body, 2);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode, '<p>Unread</p><p>Some text</p><br/>Unread'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Some text');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBrAfterText2',
    async function() {
      // A bug was that if the selection was on the rootWebArea, paragraphs were
      // not counted correctly. The more divs and paragraphs before the
      // selection, the further off it got.
      const selectionCode = 'let p = document.getElementsByTagName("p")[0];' +
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(p, 1);' +
          'range.setEnd(body, 3);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode, '<p>Unread</p><p>Some text</p><br/>Unread'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertTrue(this.mockTts.pendingUtterances().length > 0);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Some text');

      this.mockTts.finishPendingUtterance();
      if (this.mockTts.pendingUtterances().length > 0) {
        this.assertEqualsCollapseWhitespace(
            this.mockTts.pendingUtterances()[0], '');
      }
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextAreaAndBrs',
    async function() {
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 1);' +
          'range.setEnd(body, 4);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<br/><br/><textarea>Some text</textarea><br/><br/>Unread'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Some text');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'textFieldWithComboBoxSimple',
    async function() {
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 0);' +
          'range.setEnd(body, 1);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<input list="list" value="one"></label><datalist id="list">' +
              '<option value="one"></datalist>'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'one');
    });
// TODO(katie): It doesn't seem possible to programatically specify a range that
// selects only part of the text in a combo box.

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'ContentEditableInternallySelected',
    async function() {
      const html = '<script type="text/javascript">' +
          'function doSelection() {' +
          'let input = document.getElementById("input");' +
          'input.focus();' +
          'let selection = window.getSelection();' +
          'let range = document.createRange();' +
          'selection.removeAllRanges();' +
          'let p1 = document.getElementsByTagName("p")[0];' +
          'let p2 = document.getElementsByTagName("p")[1];' +
          'range.setStart(p1.firstChild, 1);' +
          'range.setEnd(p2.firstChild, 3);' +
          'selection.addRange(range);' +
          '}' +
          '</script>' +
          '<body onload="doSelection()">' +
          '<div id="input" contenteditable><p>a b c</p><p>d e f</p></div>' +
          '</body>';
      const root = await this.runWithLoadedTree(html);
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'b c');

      this.mockTts.finishPendingUtterance();
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'd e');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'ContentEditableExternallySelected',
    async function() {
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 1);' +
          'range.setEnd(body, 2);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          'Unread <div id="input" contenteditable><p>a b c</p><p>d e f</p>' +
              '</div> Unread'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'a b c');

      this.mockTts.finishPendingUtterance();
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'd e f');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'ReordersSvgSingleLine',
    async function() {
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 0);' +
          'range.setEnd(body, 1);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
              '  <text x="65" y="55">Grumpy!</text>' +
              '  <text x="20" y="35">My</text>' +
              '  <text x="40" y="35">cat</text>' +
              '  <text x="55" y="55">is</text>' +
              '</svg>'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'My cat is Grumpy!');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'ReordersSvgWithGroups',
    async function() {
      const selectionCode =
          'let body = document.getElementsByTagName("body")[0];' +
          'range.setStart(body, 0);' +
          'range.setEnd(body, 1);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
              '  <g>' +
              '    <text x="65" y="0">Column 2, Text 1</text>' +
              '    <text x="65" y="50">Column 2, Text 2</text>' +
              '  </g>' +
              '  <g>' +
              '    <text x="0" y="50">Column 1, Text 2</text>' +
              '    <text x="0" y="0">Column 1, Text 1</text>' +
              '  </g>' +
              '</svg>'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Column 1, Text 1');

      this.mockTts.finishPendingUtterance();
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Column 1, Text 2');

      this.mockTts.finishPendingUtterance();
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Column 2, Text 1');

      this.mockTts.finishPendingUtterance();
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Column 2, Text 2');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest',
    'NonReorderedSvgPreservesSelectionStartEnd', async function() {
      const selectionCode = 'const t1 = document.getElementById("t1");' +
          'const t2 = document.getElementById("t2");' +
          'range.setStart(t1.childNodes[0], 3);' +
          'range.setEnd(t2.childNodes[0], 2);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
              '  <text id="t1" x="0" y="55">My cat</text>' +
              '  <text id="t2" x="100" y="55">is Grumpy!</text>' +
              '</svg>'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'cat is');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest',
    'ReorderedSvgIgnoresSelectionStartEnd', async function() {
      const selectionCode = 'const t1 = document.getElementById("t1");' +
          'const t2 = document.getElementById("t2");' +
          'range.setStart(t1.childNodes[0], 3);' +
          'range.setEnd(t2.childNodes[0], 2);';
      const root = await this.runWithLoadedTree(this.generateHtmlWithSelection(
          selectionCode,
          '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
              '  <text id="t1" x="100" y="55">is Grumpy!</text>' +
              '  <text id="t2" x="0" y="55">My cat</text>' +
              '</svg>'));
      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'My cat is Grumpy!');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'OmniboxFullySelected',
    async function() {
      let omnibox;
      await this.runWithLoadedDesktop(desktop => {
        omnibox = desktop.find({attributes: {className: 'OmniboxViewViews'}});
      });

      await this.setValueAndTextSelection(
          omnibox, 'Hello, Chromium a11y', 0, 20);
      assertEquals('Hello, Chromium a11y', omnibox.value);
      assertEquals(0, omnibox.textSelStart);
      assertEquals(20, omnibox.textSelEnd);

      await this.triggerReadSelectedText();
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Hello, Chromium a11y');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'OmniboxPartiallySelectedFromStart',
    async function() {
      let omnibox;
      await this.runWithLoadedDesktop(desktop => {
        omnibox = desktop.find({attributes: {className: 'OmniboxViewViews'}});
      });

      await this.setValueAndTextSelection(
          omnibox, 'Hello, Chromium a11y', 0, 5);
      assertEquals('Hello, Chromium a11y', omnibox.value);
      assertEquals(0, omnibox.textSelStart);
      assertEquals(5, omnibox.textSelEnd);

      await this.triggerReadSelectedText();
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Hello');
    });


AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'OmniboxPartiallySelectedToEnd',
    async function() {
      let omnibox;
      const root = await this.runWithLoadedDesktop(desktop => {
        omnibox = desktop.find({attributes: {className: 'OmniboxViewViews'}});
      });

      await this.setValueAndTextSelection(
          omnibox, 'Hello, Chromium a11y', 7, 20);
      assertEquals('Hello, Chromium a11y', omnibox.value);
      assertEquals(7, omnibox.textSelStart);
      assertEquals(20, omnibox.textSelEnd);

      await this.triggerReadSelectedText(root);
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Chromium a11y');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'OmniboxPartiallySelectedInMid',
    async function() {
      let omnibox;
      await this.runWithLoadedDesktop(desktop => {
        omnibox = desktop.find({attributes: {className: 'OmniboxViewViews'}});
      });

      await this.setValueAndTextSelection(
          omnibox, 'Hello, Chromium a11y', 7, 15);
      assertEquals('Hello, Chromium a11y', omnibox.value);
      assertEquals(7, omnibox.textSelStart);
      assertEquals(15, omnibox.textSelEnd);

      await this.triggerReadSelectedText();
      assertTrue(this.mockTts.currentlySpeaking());
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Chromium');
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'OmniboxNoneSelected',
    async function() {
      let omnibox;
      await this.runWithLoadedDesktop(desktop => {
        omnibox = desktop.find({attributes: {className: 'OmniboxViewViews'}});
      });

      await this.setValueAndTextSelection(
          omnibox, 'Hello, Chromium a11y', 0, 0);
      assertEquals('Hello, Chromium a11y', omnibox.value);
      assertEquals(0, omnibox.textSelStart);
      assertEquals(0, omnibox.textSelEnd);

      await this.triggerReadSelectedText();
      assertEquals(false, this.mockTts.currentlySpeaking());
    });

AX_TEST_F(
    'SelectToSpeakKeystrokeSelectionTest', 'SearchUpBeforeS', async function() {
      // SelectToSpeakE2ETest.triggerReadSelectedText releases the 'S' key
      // before the 'SEARCH' key.
      // This test releases 'SEARCH' before 'S' to ensure that speech is still
      // started.
      const setFocusCallback = this.newCallback(async function(root) {
        // Set the document selection. This will fire the changed event
        // above, allowing us to do the keystroke and test that speech
        // occurred properly.
        const textNode = this.findTextNode(root, 'This is some text');
        chrome.automation.setDocumentSelection({
          anchorObject: textNode,
          anchorOffset: 0,
          focusObject: textNode,
          focusOffset: 12,
        });
      });

      const root = await this.runWithLoadedTree('<p>This is some text</p>');
      // Set the selection.
      setFocusCallback(root);
      // Wait for Automation to update.
      await this.waitForEvent(
          root, 'documentSelectionChanged', /*capture=*/ false);
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Speak selected text lifting the 'search' key before the 's' key.
      selectToSpeak.sendMockSelectToSpeakKeysPressedChanged(
          [SelectToSpeakConstants.SEARCH_KEY_CODE]);
      selectToSpeak.sendMockSelectToSpeakKeysPressedChanged([
        SelectToSpeakConstants.SEARCH_KEY_CODE,
        SelectToSpeakConstants.READ_SELECTION_KEY_CODE,
      ]);
      assertTrue(selectToSpeak.inputHandler_.isSelectionKeyDown_);

      // Release the SEARCH_KEY_CODE.
      selectToSpeak.sendMockSelectToSpeakKeysPressedChanged(
          [SelectToSpeakConstants.READ_SELECTION_KEY_CODE]);
      selectToSpeak.sendMockSelectToSpeakKeysPressedChanged([]);

      await this.waitForSpeech();
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'This is some');
    });