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

// Copyright 2020 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 navigation control features.
 */
SelectToSpeakNavigationControlTest = class extends SelectToSpeakE2ETest {
  constructor() {
    super();
    this.mockTts = new MockTts();
    chrome.tts = this.mockTts;

    // Save original updateSelectToSpeakPanel function so we can override it in
    // tests, then later restore the original implementation.
    this.updateSelectToSpeakPanel =
        chrome.accessibilityPrivate.updateSelectToSpeakPanel;
  }

  /** @override */
  async setUpDeferred() {
    await super.setUpDeferred();
    window.EventType = chrome.automation.EventType;
    window.RoleType = chrome.automation.RoleType;
    window.SelectToSpeakState = chrome.accessibilityPrivate.SelectToSpeakState;
    chrome.accessibilityPrivate.updateSelectToSpeakPanel =
        this.updateSelectToSpeakPanel;

    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;
    }
  }

  generateHtmlWithSelectedElement(elementId, bodyHtml) {
    return `
    <script type="text/javascript">
      function doSelection() {
        let selection = window.getSelection();
        let range = document.createRange();
        selection.removeAllRanges();
        let node = document.getElementById("${elementId}");
        range.selectNodeContents(node);
        selection.addRange(range);
      }
    </script>
    <body onload="doSelection()">${bodyHtml}</body>`;
  }

  isNodeWithinPanel(node) {
    const windowParent =
        AutomationUtil.getFirstAncestorWithRole(node, RoleType.WINDOW);
    return windowParent.className === 'TrayBubbleView' &&
        windowParent.children.length === 1 &&
        windowParent.children[0].className === 'SelectToSpeakMenuView';
  }

  waitForPanelFocus(root, callback) {
    callback = this.newCallback(callback);
    const focusCallback = () => {
      chrome.automation.getFocus(node => {
        if (!this.isNodeWithinPanel(node)) {
          return;
        }
        root.removeEventListener(EventType.FOCUS, focusCallback);
        callback(node);
      });
    };
    root.addEventListener(EventType.FOCUS, focusCallback);
  }
};

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'NavigatesToNextParagraph',
    async function() {
      const bodyHtml = `
    <p id="p1">Paragraph 1</p>
    <p id="p2">Paragraph 2</p>'
  `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks first paragraph
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph 1');

      // TODO([email protected]): Figure out a better way to trigger
      // the actual floating panel button rather than calling private
      // method directly.
      selectToSpeak.onNextParagraphRequested();

      // Speaks second paragraph
      this.waitOneEventLoop(() => {
        assertTrue(this.mockTts.currentlySpeaking());
        assertEquals(this.mockTts.pendingUtterances().length, 1);
        this.assertEqualsCollapseWhitespace(
            this.mockTts.pendingUtterances()[0], 'Paragraph 2');
      });
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'NavigatesToPreviousParagraph',
    async function() {
      const bodyHtml = `
    <p id="p1">Paragraph 1</p>
    <p id="p2">Paragraph 2</p>'
  `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p2', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks first paragraph
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph 2');

      // TODO([email protected]): Figure out a better way to trigger
      // the actual floating panel button rather than calling private
      // method directly.
      selectToSpeak.onPreviousParagraphRequested();

      // Speaks second paragraph
      this.waitOneEventLoop(() => {
        assertTrue(this.mockTts.currentlySpeaking());
        assertEquals(this.mockTts.pendingUtterances().length, 1);
        this.assertEqualsCollapseWhitespace(
            this.mockTts.pendingUtterances()[0], 'Paragraph 1');
      });
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'ReadsParagraphOnClick',
    async function() {
      const bodyHtml = `
      <p id="p1">Sentence <span>one</span>. Sentence two.</p>
      <p id="p2">Paragraph <span>two</span></p>'
    `;
      const root = await this.runWithLoadedTree(bodyHtml);
      this.mockTts.setOnSpeechCallbacks([
        this.newCallback(utterance => {
          // Speech for first click.
          assertTrue(this.mockTts.currentlySpeaking());
          assertEquals(this.mockTts.pendingUtterances().length, 1);
          this.assertEqualsCollapseWhitespace(
              this.mockTts.pendingUtterances()[0],
              'Sentence one . Sentence two.');

          this.mockTts.setOnSpeechCallbacks([this.newCallback(utterance => {
            // Speech for second click.
            assertTrue(this.mockTts.currentlySpeaking());
            assertEquals(this.mockTts.pendingUtterances().length, 1);
            this.assertEqualsCollapseWhitespace(
                this.mockTts.pendingUtterances()[0], 'Paragraph two');
          })]);

          // Click on node in second paragraph.
          const textNode2 = this.findTextNode(root, 'two');
          const mouseEvent2 = {
            screenX: textNode2.location.left + 1,
            screenY: textNode2.location.top + 1,
          };
          this.triggerReadMouseSelectedText(mouseEvent2, mouseEvent2);
        }),
      ]);

      // Click on node in first paragraph.
      const textNode1 = this.findTextNode(root, 'one');
      const event1 = {
        screenX: textNode1.location.left + 1,
        screenY: textNode1.location.top + 1,
      };
      this.triggerReadMouseSelectedText(event1, event1);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PauseResumeWithinTheSentence',
    async function() {
      const bodyHtml = `
      <p id="p1">First sentence. Second sentence. Third sentence.</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks until the second word of the second sentence.
      this.mockTts.speakUntilCharIndex(23);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0],
          'First sentence. Second sentence. Third sentence.');

      // Hitting pause will stop the current TTS.
      selectToSpeak.onPauseRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Hitting resume will start from the remaining content of the
      // second sentence.
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'sentence. Third sentence.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PauseResumeAtTheBeginningOfSentence',
    async function() {
      const bodyHtml = `
      <p id="p1">First sentence. Second sentence. Third sentence.</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks until the third sentence.
      this.mockTts.speakUntilCharIndex(33);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0],
          'First sentence. Second sentence. Third sentence.');

      // Hitting pause will stop the current TTS.
      selectToSpeak.onPauseRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Hitting resume will start from the beginning of the third
      // sentence.
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Third sentence.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest',
    'PauseResumeAtTheBeginningOfParagraph', async function() {
      const bodyHtml = `
      <p id="p1">first sentence.</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks until the second word.
      this.mockTts.speakUntilCharIndex(6);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'first sentence.');

      // Hitting pause will stop the current TTS.
      selectToSpeak.onPauseRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Hitting resume will start from the remaining content of the
      // paragraph.
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'sentence.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest',
    'PauseResumeInTheMiddleOfMultiParagraphs', async function() {
      const bodyHtml = `
      <span id='s1'>
        <p>Paragraph one.</p>
        <p>Paragraph two.</p>
        <p>Paragraph three.</p>
      </span>'
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('s1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks until the second word.
      this.mockTts.speakUntilCharIndex(10);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph one.');

      // Hitting pause will stop the current TTS.
      selectToSpeak.onPauseRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Hitting resume will start from the remaining content of the
      // paragraph.
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'one.');

      // Keep reading will finish all the content.
      this.mockTts.finishPendingUtterance();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph two.');
      this.mockTts.finishPendingUtterance();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph three.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PauseResumeAfterParagraphNavigation',
    async function() {
      const bodyHtml = `
      <span id='s1'>
        <p>Paragraph one.</p>
        <p>Paragraph two.</p>
        <p>Paragraph three.</p>
      </span>'
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('s1', bodyHtml));
      this.triggerReadSelectedText();

      // Navigates to the next paragraph and speaks until the second word.
      await selectToSpeak.onNextParagraphRequested();
      this.mockTts.speakUntilCharIndex(10);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph two.');

      // Hitting pause and resume will start reading the remaining content
      // in the second paragraph.
      selectToSpeak.onPauseRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'two.');

      // Should not keep reading beyond the second paragraph.
      this.mockTts.finishPendingUtterance();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PauseResumeAfterSentenceNavigation',
    async function() {
      const bodyHtml = `
      <span id='s1'>
        <p>Sentence one. Sentence two.</p>
        <p>Paragraph two.</p>
      </span>'
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('s1', bodyHtml));
      this.triggerReadSelectedText();
      // Navigates to the next sentence and speaks until the last word
      // (i.e., "two") in the first pargraph.
      await selectToSpeak.onNextSentenceRequested();
      this.mockTts.speakUntilCharIndex(23);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Sentence two.');

      // Hitting pause and resume will start reading the remaining content
      // in the first paragraph.
      selectToSpeak.onPauseRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'two.');

      // Should not keep reading beyond the first paragraph.
      this.mockTts.finishPendingUtterance();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PauseResumeAtTheEndOfNodeGroupItem',
    async function() {
      const bodyHtml = `
        <p id="p1">Sentence <span>one</span>. Sentence two.</p>
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Finishes the second word.
      this.mockTts.speakUntilCharIndex(13);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Sentence one . Sentence two.');

      // Hitting pause will stop the current TTS.
      selectToSpeak.onPauseRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Hitting resume will start from the remaining content of the
      // paragraph.
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], '. Sentence two.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PauseResumeFromKeystrokeSelection',
    async function() {
      const bodyHtml =
          '<p>This is some <b>bold</b> text</p><p>Second paragraph</p>';
      const setFocusCallback = this.newCallback(root => {
        const firstNode = this.findTextNode(root, 'This is some ');
        const lastNode = this.findTextNode(root, 'Second paragraph');
        // Sets the selection from "is some" to "Second".
        chrome.automation.setDocumentSelection({
          anchorObject: firstNode,
          anchorOffset: 5,
          focusObject: lastNode,
          focusOffset: 6,
        });
      });
      const root = await this.runWithLoadedTree(bodyHtml);
      root.addEventListener(
          'documentSelectionChanged', this.newCallback(function(event) {
            this.triggerReadSelectedText();

            // Speaks the first word 'is', the char index will count from the
            // beginning of the node (i.e., from "This").
            this.mockTts.speakUntilCharIndex(8);
            assertTrue(this.mockTts.currentlySpeaking());
            assertEquals(this.mockTts.pendingUtterances().length, 1);
            this.assertEqualsCollapseWhitespace(
                this.mockTts.pendingUtterances()[0], 'is some bold text');

            // Hitting pause will stop the current TTS.
            selectToSpeak.onPauseRequested();
            assertFalse(this.mockTts.currentlySpeaking());
            assertEquals(this.mockTts.pendingUtterances().length, 0);

            // Hitting resume will start from the remaining content of the
            // paragraph.
            selectToSpeak.onResumeRequested();
            assertTrue(this.mockTts.currentlySpeaking());
            assertEquals(this.mockTts.pendingUtterances().length, 1);
            this.assertEqualsCollapseWhitespace(
                this.mockTts.pendingUtterances()[0], 'some bold text');

            // Keep reading will finish all the content.
            this.mockTts.finishPendingUtterance();
            assertTrue(this.mockTts.currentlySpeaking());
            assertEquals(this.mockTts.pendingUtterances().length, 1);
            this.assertEqualsCollapseWhitespace(
                this.mockTts.pendingUtterances()[0], 'Second');
          }),
          false);
      setFocusCallback(root);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'NextSentence', async function() {
      const bodyHtml = `
      <p id="p1">This is the first. This is the second.</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks the first word.
      this.mockTts.speakUntilCharIndex(5);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0],
          'This is the first. This is the second.');

      // Hitting next sentence will start another TTS.
      await selectToSpeak.onNextSentenceRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'This is the second.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'NextSentenceWithinParagraph',
    async function() {
      const bodyHtml = `
        <p id="p1">Sent 1. <span id="s1">Sent 2.</span> Sent 3. Sent 4.</p>
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('s1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks the first word.
      this.mockTts.speakUntilCharIndex(5);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Sent 2.');

      // Hitting next sentence will start from the next sentence.
      selectToSpeak.onNextSentenceRequested();
      this.waitOneEventLoop(() => {
        assertTrue(this.mockTts.currentlySpeaking());
        assertEquals(this.mockTts.pendingUtterances().length, 1);
        this.assertEqualsCollapseWhitespace(
            this.mockTts.pendingUtterances()[0], 'Sent 3. Sent 4.');
      });
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'NextSentenceAcrossParagraph',
    async function() {
      const bodyHtml = `
        <p id="p1">Sent 1.</p>
        <p id="p2">Sent 2. Sent 3.</p>'
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks the first word.
      this.mockTts.speakUntilCharIndex(5);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Sent 1.');

      // Hitting next sentence will star from the next paragraph as there
      // is no more sentence in the current paragraph.
      selectToSpeak.onNextSentenceRequested();
      this.waitOneEventLoop(() => {
        assertTrue(this.mockTts.currentlySpeaking());
        assertEquals(this.mockTts.pendingUtterances().length, 1);
        this.assertEqualsCollapseWhitespace(
            this.mockTts.pendingUtterances()[0], 'Sent 2. Sent 3.');
      });
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PrevSentence', async function() {
      const bodyHtml = `
      <p id="p1">First sentence. Second sentence. Third sentence.</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks util the start of the second sentence.
      this.mockTts.speakUntilCharIndex(33);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0],
          'First sentence. Second sentence. Third sentence.');

      // Hitting prev sentence will start another TTS.
      await selectToSpeak.onPreviousSentenceRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0],
          'Second sentence. Third sentence.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PrevSentenceFromMiddleOfSentence',
    async function() {
      const bodyHtml = `
      <p id="p1">First sentence. Second sentence. Third sentence.</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks util the start of "sentence" in "Second sentence".
      this.mockTts.speakUntilCharIndex(23);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0],
          'First sentence. Second sentence. Third sentence.');

      // Hitting prev sentence will start another TTS.
      await selectToSpeak.onPreviousSentenceRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0],
          'First sentence. Second sentence. Third sentence.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PrevSentenceWithinParagraph',
    async function() {
      const bodyHtml = `
      <p id="p1">Sent 0. Sent 1. <span id="s1">Sent 2.</span> Sent 3.</p>
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('s1', bodyHtml));
      this.triggerReadSelectedText();

      // Supposing we are at the start of the sentence.
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Sent 2.');

      // Hitting previous sentence will start from the previous sentence.
      selectToSpeak.onPreviousSentenceRequested();
      this.waitOneEventLoop(() => {
        assertTrue(this.mockTts.currentlySpeaking());
        assertEquals(this.mockTts.pendingUtterances().length, 1);
        this.assertEqualsCollapseWhitespace(
            this.mockTts.pendingUtterances()[0], 'Sent 1. Sent 2. Sent 3.');
      });
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'PrevSentenceAcrossParagraph',
    async function() {
      const bodyHtml = `
      <p id="p1">Sent 1. Sent 2.</p>
      <p id="p2">Sent 3.</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p2', bodyHtml));
      this.triggerReadSelectedText();

      // We are at the start of the sentence.
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Sent 3.');

      // Hitting previous sentence will start from the last sentence in
      // the previous paragraph as there is no more sentence in the
      // current paragraph.
      selectToSpeak.onPreviousSentenceRequested();
      this.waitOneEventLoop(() => {
        assertTrue(this.mockTts.currentlySpeaking());
        assertEquals(this.mockTts.pendingUtterances().length, 1);
        this.assertEqualsCollapseWhitespace(
            this.mockTts.pendingUtterances()[0], 'Sent 2.');
      });
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'ChangeSpeedWhilePlaying',
    async function() {
      chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.2);
      const bodyHtml = `
      <p id="p1">Paragraph 1</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks the first word.
      this.mockTts.speakUntilCharIndex(10);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph 1');
      assertEquals(this.mockTts.getOptions().rate, 1.2);

      // Changing speed will resume with the remaining content of the
      // current sentence.
      selectToSpeak.onChangeSpeedRequested(1.5);
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Wait an event loop so all pending promises are resolved prior to
      // asserting that TTS resumed with the proper rate.
      setTimeout(
          this.newCallback(() => {
            // Should resume TTS with the remaining content with adjusted
            // rate.
            assertTrue(this.mockTts.currentlySpeaking());
            assertEquals(this.mockTts.getOptions().rate, 1.8);
            assertEquals(this.mockTts.pendingUtterances().length, 1);
            this.assertEqualsCollapseWhitespace(
                this.mockTts.pendingUtterances()[0], '1');
          }),
          0);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'RetainsSpeedChange',
    async function() {
      chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.0);
      const bodyHtml = `
    <p id="p1">Paragraph 1</p>'
  `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Changing speed then exit.
      selectToSpeak.onChangeSpeedRequested(1.5);
      selectToSpeak.onExitRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Next TTS session should remember previous rate.
      this.triggerReadSelectedText();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.getOptions().rate, 1.5);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'ChangeSpeedWhilePaused',
    async function() {
      chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.2);
      const bodyHtml = `
      <p id="p1">Paragraph 1</p>'
    `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks the first word.
      this.mockTts.speakUntilCharIndex(10);
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph 1');
      assertEquals(this.mockTts.getOptions().rate, 1.2);

      // User-intiated pause.
      selectToSpeak.onPauseRequested();
      assertFalse(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 0);

      // Changing speed will remain paused.
      selectToSpeak.onChangeSpeedRequested(1.5);

      // Wait an event loop so all pending promises are resolved prior to
      // asserting that TTS remains paused.
      setTimeout(this.newCallback(() => {
        assertFalse(this.mockTts.currentlySpeaking());
        assertEquals(this.mockTts.pendingUtterances().length, 0);
      }, 0));
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'ResumeAtTheEndOfParagraph',
    async function() {
      const bodyHtml = `
        <p id="p1">Paragraph 1</p>
        <p id="p2">Paragraph 2</p>
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      this.triggerReadSelectedText();

      // Finishes the current utterance.
      this.mockTts.finishPendingUtterance();

      // Hitting resume will start the next paragraph.
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'Paragraph 2');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'ResumeAtTheEndOfUserSelection',
    async function() {
      const bodyHtml = `
        <p id="p1">Sentence <span id="s1">one</span>. Sentence two.</p>
        <p id="p2">Paragraph 2</p>
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('s1', bodyHtml));
      this.triggerReadSelectedText();

      // Finishes the current utterance.
      this.mockTts.finishPendingUtterance();

      // Hitting resume will start the remaining content.
      selectToSpeak.onResumeRequested();
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], '. Sentence two.');
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'ResumeFromSelectionEndingInSpace',
    async function() {
      const bodyHtml = '<p>This is some text with space.</p>';
      const setFocusCallback = this.newCallback(root => {
        const node = this.findTextNode(root, 'This is some text with space.');
        // Sets the selection to "This ".
        chrome.automation.setDocumentSelection({
          anchorObject: node,
          anchorOffset: 0,
          focusObject: node,
          focusOffset: 5,
        });
      });
      const root = await this.runWithLoadedTree(bodyHtml);
      root.addEventListener(
          'documentSelectionChanged', this.newCallback(event => {
            this.triggerReadSelectedText();

            assertTrue(this.mockTts.currentlySpeaking());
            assertEquals(this.mockTts.pendingUtterances().length, 1);
            this.assertEqualsCollapseWhitespace(
                this.mockTts.pendingUtterances()[0], 'This');

            // Finishes the current utterance.
            this.mockTts.finishPendingUtterance();

            // Hitting resume will start from the remaining content of the
            // paragraph.
            selectToSpeak.onResumeRequested();
            assertTrue(this.mockTts.currentlySpeaking());
            assertEquals(this.mockTts.pendingUtterances().length, 1);
            this.assertEqualsCollapseWhitespace(
                this.mockTts.pendingUtterances()[0],
                'is some text with space.');
          }),
          false);
      setFocusCallback(root);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'ResizeWhilePlaying',
    async function() {
      const longLine =
          'Second paragraph is longer than 300 pixels and will wrap when' +
          'resized';
      const bodyHtml = `
          <script type="text/javascript">
            function doResize() {
              document.getElementById('resize').style.width = '100px';
            }
          </script>
          <div id="content">
            <p>First paragraph</p>
            <p id='resize' style='width:300px; font-size: 1em'>
              ${longLine}
            </p>
          </div>
          <button onclick="doResize()">Resize</button>
        `;
      const root = await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('content', bodyHtml));
      this.triggerReadSelectedText();

      // Speaks the first paragraph.
      assertTrue(this.mockTts.currentlySpeaking());
      assertEquals(this.mockTts.pendingUtterances().length, 1);
      this.assertEqualsCollapseWhitespace(
          this.mockTts.pendingUtterances()[0], 'First paragraph');

      const resizeButton =
          root.find({role: 'button', attributes: {name: 'Resize'}});

      // Wait for click event, at which point the automation tree should
      // be updated from the resize.
      resizeButton.addEventListener(EventType.CLICKED, this.newCallback(() => {
        // Trigger next node group by completing first TTS request.
        this.mockTts.finishPendingUtterance();

        // Should still read second paragraph, even though some nodes
        // were invalided from the resize.
        assertTrue(this.mockTts.currentlySpeaking());
        assertEquals(this.mockTts.pendingUtterances().length, 1);
        this.assertEqualsCollapseWhitespace(
            this.mockTts.pendingUtterances()[0], longLine);
      }));

      // Perform resize.
      resizeButton.doDefault();
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest',
    'RemainsActiveAfterCompletingUtterance', async function() {
      const bodyHtml = '<p id="p1">Paragraph 1</p>';
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      // Simulate starting and completing TTS.
      this.triggerReadSelectedText();
      this.mockTts.finishPendingUtterance();

      // Should remain in speaking state.
      assertEquals(selectToSpeak.state_, SelectToSpeakState.SPEAKING);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest',
    'AutoDismissesIfNavigationControlsDisabled', async function() {
      const bodyHtml = '<p id="p1">Paragraph 1</p>';
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));

      // Disable navigation controls.
      selectToSpeak.prefsManager_.navigationControlsEnabled_ = false;

      // Simulate starting and completing TTS.
      this.triggerReadSelectedText();
      this.mockTts.finishPendingUtterance();

      // Should auto-dismiss.
      assertEquals(selectToSpeak.state_, SelectToSpeakState.INACTIVE);
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'NavigatesToNextParagraphQuickly',
    async function() {
      const bodyHtml = `
        <p id="p1">Paragraph 1</p>
        <p id="p2">Paragraph 2</p>'
      `;
      await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      // Have mock TTS engine wait to send events so we can simulate a
      // delayed 'start' event.
      this.mockTts.setWaitToSendEvents(true);
      this.triggerReadSelectedText();
      const speakOptions = this.mockTts.getOptions();

      // Navigate to next paragraph before speech begins.
      selectToSpeak.onNextParagraphRequested();

      this.waitOneEventLoop(() => {
        // Manually triggered delayed events.
        this.mockTts.sendPendingEvents();

        // Should remain in speaking state.
        assertEquals(selectToSpeak.state_, SelectToSpeakState.SPEAKING);
      });
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'SetsInitialFocusToPanel',
    async function() {
      const bodyHtml = '<p id="p1">Sample text</p>';
      const root = await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      const desktop = root.parent.root;

      // Wait for button in STS panel to be focused.
      // Test will fail if panel is never focused.
      this.waitForPanelFocus(desktop, () => {});

      // Trigger STS, which will initially set focus to the panel.
      this.triggerReadSelectedText();
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'KeyboardShortcutKeepsFocusInPanel',
    async function() {
      const bodyHtml = '<p id="p1">Sample text</p>';
      const root = await this.runWithLoadedTree(
          this.generateHtmlWithSelectedElement('p1', bodyHtml));
      const desktop = root.parent.root;

      // Wait for button within STS panel is focused.
      this.waitForPanelFocus(desktop, () => {
        // Remove text selection.
        const textNode = this.findTextNode(root, 'Sample text');
        chrome.automation.setDocumentSelection({
          anchorObject: textNode,
          anchorOffset: 0,
          focusObject: textNode,
          focusOffset: 0,
        });

        // Perform Search key + S, which should restore focus to
        // panel.
        selectToSpeak.sendMockSelectToSpeakKeysPressedChanged(
            [SelectToSpeakConstants.SEARCH_KEY_CODE]);
        selectToSpeak.sendMockSelectToSpeakKeysPressedChanged([
          SelectToSpeakConstants.SEARCH_KEY_CODE,
          SelectToSpeakConstants.READ_SELECTION_KEY_CODE,
        ]);
        selectToSpeak.sendMockSelectToSpeakKeysPressedChanged(
            [SelectToSpeakConstants.SEARCH_KEY_CODE]);
        selectToSpeak.sendMockSelectToSpeakKeysPressedChanged([]);

        // Verify focus is still on button within panel.
        chrome.automation.getFocus(this.newCallback(focusedNode => {
          assertEquals(focusedNode.role, RoleType.TOGGLE_BUTTON);
          assertTrue(this.isNodeWithinPanel(focusedNode));
        }));
      });

      // Trigger STS, which will initially set focus to the panel.
      this.triggerReadSelectedText();
    });

AX_TEST_F(
    'SelectToSpeakNavigationControlTest', 'SelectingWindowDoesNotShowPanel',
    async function() {
      const bodyHtml = `
        <title>Test</title>
        <div style="position: absolute; top: 300px;">
          Hello
        </div>
      `;
      const root = await this.runWithLoadedTree(bodyHtml);
      // Expect call to updateSelectToSpeakPanel to set panel to be hidden.
      chrome.accessibilityPrivate.updateSelectToSpeakPanel =
          this.newCallback(visible => {
            assertFalse(visible);
          });

      // Trigger mouse selection on a part of the page where no text nodes
      // exist, should select entire page.
      const mouseEvent = {
        screenX: root.location.left + 1,
        screenY: root.location.top + 1,
      };
      this.triggerReadMouseSelectedText(mouseEvent, mouseEvent);
    });