chromium/chrome/browser/resources/chromeos/accessibility/common/node_navigation_utils_unittest.js

// Copyright 2022 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/select_to_speak_e2e_test_base.js']);

/**
 * Test fixture for navigation_utils.js.
 */
SelectToSpeakNodeNavigationUtilsUnitTest = class extends SelectToSpeakE2ETest {
  /** @override */
  async setUpDeferred() {
    await super.setUpDeferred();
    await Promise.all([
      importModule('NodeNavigationUtils', '/common/node_navigation_utils.js'),
      importModule('ParagraphUtils', '/common/paragraph_utils.js'),
      importModule(
          ['createMockNode', 'generateTestNodeGroup'],
          '/common/testing/test_node_generator.js'),
    ]);
  }
};

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest', 'GetNodesForNextParagraph',
    function() {
      const root = createMockNode({role: 'rootWebArea'});
      const paragraph1 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'Line 1'});
      const text2 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'Line 2'});
      const paragraph2 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text3 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 3'});

      const nodeGroupForParagraph1 = ParagraphUtils.buildNodeGroup(
          [text1, text2], 0 /* index */, {splitOnLanguage: false});
      const nodeGroupForParagraph2 = ParagraphUtils.buildNodeGroup(
          [text3], 0 /* index */, {splitOnLanguage: false});

      // Navigating forward from paragraph 1 returns the nodes of paragraph 2.
      let result = NodeNavigationUtils.getNodesForNextParagraph(
          nodeGroupForParagraph1, constants.Dir.FORWARD,
          () => true /* does not filter any paragraph */);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);

      // Navigating backward from paragraph 1 returns no nodes.
      result = NodeNavigationUtils.getNodesForNextParagraph(
          nodeGroupForParagraph1, constants.Dir.BACKWARD,
          () => true /* does not filter any paragraph */);
      assertEquals(result.length, 0);

      // Navigating backward from paragraph 2 returns the nodes of paragraph 1.
      result = NodeNavigationUtils.getNodesForNextParagraph(
          nodeGroupForParagraph2, constants.Dir.BACKWARD,
          () => true /* does not filter any paragraph */);
      assertEquals(result.length, 2);
      assertEquals(result[0], text1);
      assertEquals(result[1], text2);

      // Navigating forward from paragraph 2 returns no nodes.
      result = NodeNavigationUtils.getNodesForNextParagraph(
          nodeGroupForParagraph2, constants.Dir.FORWARD,
          () => true /* does not filter any paragraph */);
      assertEquals(result.length, 0);

      // Navigates forward from paragraph 1 with a pred that filters out
      // paragraph 2.
      result = NodeNavigationUtils.getNodesForNextParagraph(
          nodeGroupForParagraph1, constants.Dir.FORWARD,
          nodes => !(nodes.find(
              n => n.parent ===
                  paragraph2) /* filter out nodes belong to paragraph 2 */));
      assertEquals(result.length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest', 'GetNextParagraphWithNode',
    function() {
      const root = createMockNode({role: 'rootWebArea'});
      const paragraph1 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'Line 1'});
      const text2 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'Line 2'});
      const paragraph2 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text3 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 3'});

      // Forward from first text node of paragraph 1 gives paragraph 2 nodes.
      let result = NodeNavigationUtils.getNextParagraphWithNode_(
          text1, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);

      // Forward from second text node of paragraph 1 gives paragraph 2 nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text2, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);

      // Forward from paragraph 1 node gives paragraph 2 nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          paragraph1, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);

      // Forward from text node of paragraph 2 returns no nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text3, constants.Dir.FORWARD);
      assertEquals(result.length, 0);

      // Forward from paragraph 2 returns no nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          paragraph2, constants.Dir.FORWARD);
      assertEquals(result.length, 0);

      // Backward from text node of paragraph 2 returns paragraph 1 nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text3, constants.Dir.BACKWARD);
      assertEquals(result.length, 2);
      assertEquals(result[0], text1);
      assertEquals(result[1], text2);

      // Backward from paragraph 2 node returns paragraph 1 nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          paragraph2, constants.Dir.BACKWARD);
      assertEquals(result.length, 2);
      assertEquals(result[0], text1);
      assertEquals(result[1], text2);

      // Backward from text node of paragraph 1 returns no nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text2, constants.Dir.BACKWARD);
      assertEquals(result.length, 0);

      // Backward from text node of paragraph 1 returns no nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text1, constants.Dir.BACKWARD);
      assertEquals(result.length, 0);

      // Backward from paragraph 1 node returns no nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          paragraph1, constants.Dir.BACKWARD);
      assertEquals(result.length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextParagraphWithNodeContainedWithinRoot', function() {
      const desktop = createMockNode({role: 'desktop'});

      const root = createMockNode({role: 'rootWebArea', parent: desktop});
      const paragraph1 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'Line 1'});
      const otherRoot = createMockNode({role: 'rootWebArea', parent: desktop});
      const paragraph2 = createMockNode(
          {role: 'paragraph', display: 'block', parent: otherRoot, root});
      createMockNode({
        role: 'staticText',
        parent: paragraph2,
        root: otherRoot,
        name: 'Line 2',
      });

      let result = NodeNavigationUtils.getNextParagraphWithNode_(
          text1, constants.Dir.FORWARD);
      assertEquals(result.length, 0);

      NodeNavigationUtils.getNextParagraphWithNode_(
          text1, constants.Dir.BACKWARD);
      assertEquals(result.length, 0);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          paragraph1, constants.Dir.FORWARD);
      assertEquals(result.length, 0);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          paragraph1, constants.Dir.BACKWARD);
      assertEquals(result.length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextParagraphWithNodeThroughIframe', function() {
      const desktop = createMockNode({role: 'desktop'});

      const root =
          createMockNode({role: 'rootWebArea', parent: desktop, root: desktop});
      const paragraph1 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'Line 1'});
      const iframeRoot =
          createMockNode({role: 'rootWebArea', parent: root, root});
      const paragraph2 = createMockNode({
        role: 'paragraph',
        display: 'block',
        parent: iframeRoot,
        root: iframeRoot,
      });
      const text2 = createMockNode({
        role: 'staticText',
        parent: paragraph2,
        root: iframeRoot,
        name: 'Line 2',
      });
      const paragraph3 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text3 = createMockNode(
          {role: 'staticText', parent: paragraph3, root, name: 'Line 3'});

      let result = NodeNavigationUtils.getNextParagraphWithNode_(
          text1, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text2);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text2, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextParagraphWithNodeNonBlockNodes', function() {
      /**
       * Example below is roughly similar to:
       * <p>Line 1</p>
       * <span>Line 2</span>
       * <p><span>Line 2</span><span>Line 3</span></p>
       */
      const root = createMockNode({role: 'rootWebArea'});
      const paragraph1 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'Line 1'});
      const text2 = createMockNode(
          {role: 'staticText', parent: root, root, name: 'Line 2'});
      const paragraph2 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text3 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 3'});
      const text4 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 4'});

      // Forward from first text node of paragraph 1 gives inline text node.
      let result = NodeNavigationUtils.getNextParagraphWithNode_(
          text1, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text2);

      // Forward from inline text node gives paragraph 2 nodes.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text2, constants.Dir.FORWARD);
      assertEquals(result.length, 2);
      assertEquals(result[0], text3);
      assertEquals(result[1], text4);

      // Backwards from paragraph 2 inline text node.
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          paragraph2, constants.Dir.BACKWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text2);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextParagraphWithNodeNestedBlocks', function() {
      const root = createMockNode({role: 'rootWebArea'});
      const paragraph1 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'Before text'});
      const nestedParagraph = createMockNode(
          {role: 'paragraph', display: 'block', parent: paragraph1, root});
      const text2 = createMockNode({
        role: 'staticText',
        parent: nestedParagraph,
        root,
        name: 'Middle text',
      });
      const text3 = createMockNode(
          {role: 'staticText', parent: paragraph1, root, name: 'After text'});

      // Getting next paragraph from nested paragraph only includes nodes in
      // the forward direction (does not include itself)
      let result = NodeNavigationUtils.getNextParagraphWithNode_(
          text2, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);

      // Getting next paragraph from nested paragraph only includes nodes in
      // the backward direction (does not include itself)
      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text2, constants.Dir.BACKWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text1);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextParagraphWithNodeAndroid', function() {
      const root = createMockNode({role: 'application'});
      const container1 =
          createMockNode({role: 'genericContainer', parent: root, root});
      const text1 = createMockNode(
          {role: 'staticText', parent: container1, root, name: 'Line 1'});
      const text2 = createMockNode(
          {role: 'staticText', parent: container1, root, name: 'Line 2'});
      const container2 =
          createMockNode({role: 'genericContainer', parent: root, root});
      const text3 = createMockNode(
          {role: 'staticText', parent: container2, root, name: 'Line 3'});

      // Without paragraphs, navigate node to node.
      let result = NodeNavigationUtils.getNextParagraphWithNode_(
          text1, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text2);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text2, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          container1, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text3, constants.Dir.FORWARD);
      assertEquals(result.length, 0);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          container2, constants.Dir.FORWARD);
      assertEquals(result.length, 0);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text3, constants.Dir.BACKWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text2);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          container2, constants.Dir.BACKWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text2);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text2, constants.Dir.BACKWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text1);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          text1, constants.Dir.BACKWARD);
      assertEquals(result.length, 0);

      result = NodeNavigationUtils.getNextParagraphWithNode_(
          container1, constants.Dir.BACKWARD);
      assertEquals(result.length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextNodesInParagraphFromNodeGroupEmptyNodeGroup', function() {
      const nodeGroup = {nodes: []};
      const result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 0 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 0);
      assertEquals(result.offset, -1);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextNodesInParagraphFromNodeGroupForward', function() {
      // The nodeGroup has four inline text nodes and one static text node.
      // Their starting indexes are 0, 9, 20, 30, and 51.
      const nodeGroup = generateTestNodeGroup();

      let result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 0 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 5);
      assertEquals(result.offset, 0);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 5 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 5);
      assertEquals(result.offset, 5);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 9 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 4);
      assertEquals(result.offset, 0);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 25 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 3);
      assertEquals(result.offset, 5);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 51 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 1);
      assertEquals(result.offset, 0);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 100 /* charIndex */,
          constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextNodesInParagraphFromNodeGroupBackward', function() {
      // The nodeGroup has four inline text nodes and one static text node.
      // Their starting indexes are 0, 9, 20, 30, and 51.
      const nodeGroup = generateTestNodeGroup();

      let result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 0 /* charIndex */, constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 0);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 5 /* charIndex */, constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 1);
      assertEquals(result.offset, 5);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 9 /* charIndex */, constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 1);
      assertEquals(result.offset, 9);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 25 /* charIndex */,
          constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 3);
      assertEquals(result.offset, 5);

      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 51 /* charIndex */,
          constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 4);
      assertEquals(result.offset, 20);

      // The charIndex is out of the range of nodeGroup. It will try to get all
      // the content before the nodeGroup. In this case, there is nothing.
      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 100 /* charIndex */,
          constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextNodesInParagraphFromNodeGroupForwardWithEmptyTail', function() {
      // The nodeGroup consists of three inline text nodes: "Hello", "world ",
      // and " ".
      const nodeGroup = generateTestNodeGroupWithEmptyTail();

      // We can find the non-empty node at this charIndex but there is actually
      // no text content afterwards.
      const nodeWithOffset = ParagraphUtils.findNodeFromNodeGroupByCharIndex(
          nodeGroup, 11 /* charIndex */);
      assertEquals(nodeWithOffset.node.name, 'world ');
      assertEquals(nodeWithOffset.offset, 5);

      let result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 11 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 0);

      // If we decrease the charIndex, we will get the node with 'world ' but
      // not any empty node.
      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 10 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 1);
      assertEquals(result.nodes[0].name, 'world ');
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextNodesInParagraphFromNodeGroupBackwardWithEmptyHeads', function() {
      // The nodeGroup consists of three inline text nodes: " ", " Hello",
      // "world".
      const nodeGroup = generateTestNodeGroupWithEmptyHead();

      // We can find the non-empty node at this charIndex but there is actually
      // no text content before this position.
      const nodeWithOffset = ParagraphUtils.findNodeFromNodeGroupByCharIndex(
          nodeGroup, 2 /* charIndex */);
      assertEquals(nodeWithOffset.node.name, ' Hello');
      assertEquals(nodeWithOffset.offset, 1);

      let result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 2 /* charIndex */, constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 0);

      // If we increase the charIndex, we will get the node with ' Hello' but
      // not any empty node.
      result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 3 /* charIndex */, constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 1);
      assertEquals(result.nodes[0].name, ' Hello');
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextNodesInParagraphFromNodeGroupForwardFromPartialParagraph',
    function() {
      // The nodeGroup consists only one static text node, which is "one". The
      // entire paragraph has three static text nodes: "Sentence", "one",
      // "here".
      const nodeGroup = generateTestNodeGroupFromPartialParagraph();

      // After reading "one", the TTS events will set charIndex to 3. Finding
      // the static text node from the node group will return null.
      const nodeWithOffset = ParagraphUtils.findNodeFromNodeGroupByCharIndex(
          nodeGroup, 3 /* charIndex */);
      assertEquals(nodeWithOffset.node, null);

      const result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 3 /* charIndex */, constants.Dir.FORWARD /* direction */);
      assertEquals(result.nodes.length, 1);
      assertEquals(result.offset, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNextNodesInParagraphFromNodeGroupBackwardFromPartialParagraph',
    function() {
      // The nodeGroup consists only one static text node, which is "one". The
      // entire paragraph has three static text nodes: "Sentence", "one",
      // "here".
      const nodeGroup = generateTestNodeGroupFromPartialParagraph();

      // After reading "one", the TTS events will set charIndex to 3. Finding
      // the static text node from the node group will return null.
      const nodeWithOffset = ParagraphUtils.findNodeFromNodeGroupByCharIndex(
          nodeGroup, 3 /* charIndex */);
      assertEquals(nodeWithOffset.node, null);

      const result = NodeNavigationUtils.getNextNodesInParagraphFromNodeGroup(
          nodeGroup, 3 /* charIndex */, constants.Dir.BACKWARD /* direction */);
      assertEquals(result.nodes.length, 1);
      assertEquals(result.nodes[0].name, 'Sentence');
      assertEquals(result.offset, 8);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest', 'GetNextNodesInParagraph',
    function() {
      const root = createMockNode({role: 'rootWebArea'});
      createMockNode({role: 'paragraph', display: 'block', parent: root, root});
      const paragraph2 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 1'});
      const text2 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 2'});
      const text3 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 3'});
      createMockNode({role: 'paragraph', display: 'block', parent: root, root});

      let result = NodeNavigationUtils.getNextNodesInParagraph_(
          text2, constants.Dir.FORWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text3);

      result = NodeNavigationUtils.getNextNodesInParagraph_(
          text1, constants.Dir.FORWARD);
      assertEquals(result.length, 2);
      assertEquals(result[0], text2);
      assertEquals(result[1], text3);

      result = NodeNavigationUtils.getNextNodesInParagraph_(
          text3, constants.Dir.FORWARD);
      assertEquals(result.length, 0);

      result = NodeNavigationUtils.getNextNodesInParagraph_(
          text3, constants.Dir.BACKWARD);
      assertEquals(result.length, 2);
      assertEquals(result[0], text1);
      assertEquals(result[1], text2);

      result = NodeNavigationUtils.getNextNodesInParagraph_(
          text2, constants.Dir.BACKWARD);
      assertEquals(result.length, 1);
      assertEquals(result[0], text1);

      result = NodeNavigationUtils.getNextNodesInParagraph_(
          text1, constants.Dir.BACKWARD);
      assertEquals(result.length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest', 'GetNodesForNextSentence',
    function() {
      const root = createMockNode({role: 'rootWebArea'});
      const paragraph1 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode({
        role: 'staticText',
        parent: paragraph1,
        root,
        name: 'Line 1.',
        sentenceStarts: [0],
      });
      const text2 = createMockNode({
        role: 'staticText',
        parent: paragraph1,
        root,
        name: 'Line 2.',
        sentenceStarts: [0],
      });
      const paragraph2 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text3 = createMockNode({
        role: 'staticText',
        parent: paragraph2,
        root,
        name: 'Line 3.',
        sentenceStarts: [0],
      });
      const text4 = createMockNode({
        role: 'staticText',
        parent: paragraph2,
        root,
        name: 'Line 4.',
        sentenceStarts: [0],
      });
      const nodeGroupForParagraph1 = ParagraphUtils.buildNodeGroup(
          [text1, text2], 0 /* index */, {splitOnLanguage: false});
      const nodeGroupForParagraph2 = ParagraphUtils.buildNodeGroup(
          [text3, text4], 0 /* index */, {splitOnLanguage: false});

      // First paragraph has two sentences.
      assertEquals(nodeGroupForParagraph1.text, 'Line 1. Line 2. ');
      // Second paragraph has another two sentences.
      assertEquals(nodeGroupForParagraph2.text, 'Line 3. Line 4. ');

      let nodes;
      let offset;
      // Navigating forward from the first sentence returns the second sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph1, 0 /* currentCharIndex */,
           constants.Dir.FORWARD,
           () => true /* does not filter any paragraph */));
      assertEquals(nodes.length, 1);
      assertEquals(nodes[0], text2);
      assertEquals(offset, 0);

      // Navigating forward from the second sentence returns the second
      // paragraph. Index 8 points to the word "Line" in the second sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph1, 8 /* currentCharIndex */,
           constants.Dir.FORWARD,
           () => true /* does not filter any paragraph */));
      assertEquals(nodes.length, 2);
      assertEquals(nodes[0], text3);
      assertEquals(nodes[1], text4);
      assertEquals(offset, undefined);

      // Navigating forward from the third sentence returns the fourth
      // sentence. Index 4 points to the word "3" in the third sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph2, 4 /* currentCharIndex */,
           constants.Dir.FORWARD,
           () => true /* does not filter any paragraph */));
      assertEquals(nodes.length, 1);
      assertEquals(nodes[0], text4);
      assertEquals(offset, 0);

      // Navigating forward from the fourth sentence returns an empty result.
      // Index 8 points to the word "Line" in the fourth sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph2, 8 /* currentCharIndex */,
           constants.Dir.FORWARD,
           () => true /* does not filter any paragraph */));
      assertEquals(nodes.length, 0);

      // Navigates forward from second sentence with a pred that filters out
      // paragraph 2. Index 8 points to the word "Line" in the second sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph1, 8 /* currentCharIndex */,
           constants.Dir.FORWARD,
           nodes => !(nodes.find(
               n => n.parent ===
                   paragraph2) /* filter out nodes belong to paragraph 2 */)));
      assertEquals(nodes.length, 0);

      // Navigating backward from the first sentence returns an empty result.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph1, 0 /* currentCharIndex */,
           constants.Dir.BACKWARD,
           () => true /* does not filter any paragraph */));
      assertEquals(nodes.length, 0);

      // Navigating backward from the second sentence returns the first
      // paragraph. Index 8 points to the word "Line" in the second sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph1, 8 /* currentCharIndex */,
           constants.Dir.BACKWARD,
           () => true /* does not filter any paragraph */));
      assertEquals(nodes.length, 2);
      assertEquals(nodes[0], text1);
      assertEquals(nodes[1], text2);
      assertEquals(offset, 0);

      // Navigating backward from the third sentence returns the second
      // sentence. Index 4 points to the word "3" in the third sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph2, 4 /* currentCharIndex */,
           constants.Dir.BACKWARD,
           () => true /* does not filter any paragraph */));
      assertEquals(nodes.length, 1);
      assertEquals(nodes[0], text2);
      assertEquals(offset, 0);

      // Navigating backward from the fourth sentence returns the second
      // paragraph. Index 8 points to the word "Line" in the fourth sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph2, 8 /* currentCharIndex */,
           constants.Dir.BACKWARD,
           () => true /* does not filter any paragraph */));
      assertEquals(nodes.length, 2);
      assertEquals(nodes[0], text3);
      assertEquals(nodes[1], text4);
      assertEquals(offset, 0);

      // Navigates backward from third sentence with a pred that filters out
      // paragraph 1.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroupForParagraph2, 0 /* currentCharIndex */,
           constants.Dir.BACKWARD,
           nodes => !(nodes.find(
               n => n.parent ===
                   paragraph1) /* filter out nodes belong to paragraph 1 */)));
      assertEquals(nodes.length, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeNavigationUtilsUnitTest',
    'GetNodesForNextSentenceWithChoppedNodes', function() {
      const root = createMockNode({role: 'rootWebArea'});
      const paragraph1 = createMockNode(
          {role: 'paragraph', display: 'block', parent: root, root});
      const text1 = createMockNode({
        role: 'staticText',
        parent: paragraph1,
        root,
        name: 'Line 1.',
        sentenceStarts: [0],
      });
      const text2 = createMockNode({
        role: 'staticText',
        parent: paragraph1,
        root,
        name: 'This sentence',
        sentenceStarts: [0],
      });
      const text3 = createMockNode({
        role: 'staticText',
        parent: paragraph1,
        root,
        name: 'is chopped. Another',
        sentenceStarts: [12],
      });
      const text4 = createMockNode({
        role: 'staticText',
        parent: paragraph1,
        root,
        name: 'chopped',
        sentenceStarts: [],
      });
      const text5 = createMockNode({
        role: 'staticText',
        parent: paragraph1,
        root,
        name: 'sentence.',
        sentenceStarts: [],
      });
      const nodeGroup = ParagraphUtils.buildNodeGroup(
          [text1, text2, text3, text4, text5], 0 /* index */,
          {splitOnLanguage: false});

      // One paragraph has three sentences.
      assertEquals(
          nodeGroup.text,
          'Line 1. This sentence is chopped. Another chopped sentence. ');

      let nodes;
      let offset;
      let result;
      // Navigating forward from the first word returns the content starting
      // from the second sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroup, 0 /* currentCharIndex */, constants.Dir.FORWARD,
           () => true /* does not filter any paragraph */));
      result = ParagraphUtils.buildNodeGroup(
          nodes, 0 /* index */, {splitOnLanguage: false});
      assertEquals(nodes.length, 4);
      assertEquals(offset, 0);
      assertEquals(
          result.text, 'This sentence is chopped. Another chopped sentence. ');

      // Navigating forward from the third word returns the content starting
      // from the third sentence. Index 8 points to the word "This" in the
      // second sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroup, 8 /* currentCharIndex */, constants.Dir.FORWARD,
           () => true /* does not filter any paragraph */));
      result = ParagraphUtils.buildNodeGroup(
          nodes, 0 /* index */, {splitOnLanguage: false});
      assertEquals(nodes.length, 3);
      assertEquals(offset, 12);
      assertEquals(result.text, 'is chopped. Another chopped sentence. ');

      // Navigating backward from the third sentence returns the content
      // starting from the second sentence. Index 42 points to the word
      // "chopped" in the third sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroup, 42 /* currentCharIndex */, constants.Dir.BACKWARD,
           () => true /* does not filter any paragraph */));
      result = ParagraphUtils.buildNodeGroup(
          nodes, 0 /* index */, {splitOnLanguage: false});
      assertEquals(nodes.length, 4);
      assertEquals(offset, 0);
      assertEquals(
          result.text, 'This sentence is chopped. Another chopped sentence. ');

      // Navigating backward from the third sentence returns the content
      // starting from the second sentence. Index 34 points to the word
      // "Another" in the third sentence.
      ({nodes, offset} = NodeNavigationUtils.getNodesForNextSentence(
           nodeGroup, 34 /* currentCharIndex */, constants.Dir.BACKWARD,
           () => true /* does not filter any paragraph */));
      result = ParagraphUtils.buildNodeGroup(
          nodes, 0 /* index */, {splitOnLanguage: false});
      assertEquals(nodes.length, 4);
      assertEquals(offset, 0);
      assertEquals(
          result.text, 'This sentence is chopped. Another chopped sentence. ');
    });

/**
 * Creates a nodeGroup that has an empty tail (i.e., "Hello world  ").
 * @return {!ParagraphUtils.NodeGroup}
 */
function generateTestNodeGroupWithEmptyTail() {
  const root = createMockNode({role: 'rootWebArea'});
  const paragraph =
      createMockNode({role: 'paragraph', display: 'block', parent: root, root});
  const text1 =
      createMockNode({name: 'Hello', role: 'staticText', parent: paragraph});
  const inlineText1 = createMockNode(
      {role: 'inlineTextBox', name: 'Hello', indexInParent: 0, parent: text1});

  const text2 =
      createMockNode({name: 'world  ', role: 'staticText', parent: paragraph});
  const inlineText2 = createMockNode(
      {role: 'inlineTextBox', name: 'world ', indexInParent: 0, parent: text2});
  const inlineText3 = createMockNode(
      {role: 'inlineTextBox', name: ' ', indexInParent: 1, parent: text2});

  return ParagraphUtils.buildNodeGroup(
      [inlineText1, inlineText2, inlineText3], 0,
      false /* do not split on language */);
}

/**
 * Creates a nodeGroup that has an empty head (i.e., "  Hello world").
 * @return {!ParagraphUtils.NodeGroup}
 */
function generateTestNodeGroupWithEmptyHead() {
  const root = createMockNode({role: 'rootWebArea'});
  const paragraph =
      createMockNode({role: 'paragraph', display: 'block', parent: root, root});
  const text1 =
      createMockNode({name: '  Hello', role: 'staticText', parent: paragraph});
  const inlineText1 = createMockNode(
      {role: 'inlineTextBox', name: ' ', indexInParent: 0, parent: text1});
  const inlineText2 = createMockNode(
      {role: 'inlineTextBox', name: ' Hello', indexInParent: 1, parent: text1});

  const text2 =
      createMockNode({name: 'world  ', role: 'staticText', parent: paragraph});
  const inlineText3 = createMockNode(
      {role: 'inlineTextBox', name: 'world', indexInParent: 1, parent: text2});

  return ParagraphUtils.buildNodeGroup(
      [inlineText1, inlineText2, inlineText3], 0,
      false /* do not split on language */);
}

/**
 * Creates a nodeGroup that only has a part of the paragraph (e.g., the "one"
 * in <p>Sentence <span>one</span> here</p>).
 * @return {!ParagraphUtils.NodeGroup}
 */
function generateTestNodeGroupFromPartialParagraph() {
  const root = createMockNode({role: 'rootWebArea'});
  const paragraph =
      createMockNode({role: 'paragraph', display: 'block', parent: root, root});
  const text1 =
      createMockNode({name: 'Sentence', role: 'staticText', parent: paragraph});

  const text2 =
      createMockNode({name: 'one', role: 'staticText', parent: paragraph});

  const text3 =
      createMockNode({name: 'here', role: 'staticText', parent: paragraph});

  return ParagraphUtils.buildNodeGroup(
      [text2], 0, false /* do not split on language */);
}